Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:Add context menu to search results for better file navigation (#… #1049

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
243 changes: 120 additions & 123 deletions src/gui/src/UI/UIWindowSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

import UIWindow from './UIWindow.js'
import path from "../lib/path.js"
import UIAlert from './UIAlert.js'
import launch_app from '../helpers/launch_app.js'
import item_icon from '../helpers/item_icon.js'
import UIWindow from './UIWindow.js';
import path from "../lib/path.js";
import UIAlert from './UIAlert.js';
import launch_app from '../helpers/launch_app.js';
import item_icon from '../helpers/item_icon.js';
import UIContextMenu from './UIContextMenu.js';

async function UIWindowSearch(options){
async function UIWindowSearch(options) {
let h = '';

h += `<div class="search-input-wrapper">`;
h += `<input type="text" class="search-input" placeholder="Search" style="background-image:url('${window.icons['magnifier-outline.svg']}');">`;
h += `<input type="text" class="search-input" placeholder="Search" style="background-image:url('${window.icons['magnifier-outline.svg']}');">`;
h += `</div>`;
h += `<div class="search-results" style="overflow-y: auto; max-height: 300px;">`;
h += `<div class="search-results" style="overflow-y: auto; max-height: 300px;"></div>`;

const el_window = await UIWindow({
icon: null,
Expand All @@ -49,13 +54,8 @@ async function UIWindowSearch(options){
allow_native_ctxmenu: true,
allow_user_select: true,
window_class: 'window-search',
backdrop: true,
center: isMobile.phone,
onAppend: function(el_window){
},
width: 500,
dominant: true,

window_css: {
height: 'initial',
padding: '0',
Expand All @@ -70,12 +70,12 @@ async function UIWindowSearch(options){
'overflow': 'hidden',
'min-height': '65px',
'padding-bottom': '10px',
}
},
});

$(el_window).find('.search-input').focus();

// Debounce function to limit rate of API calls
// Fonction debounce pour limiter les appels API
function debounce(func, wait) {
let timeout;
return function (...args) {
Expand All @@ -87,176 +87,173 @@ async function UIWindowSearch(options){
};
}

// State for managing loading indicator
let isSearching = false;

// Debounced search function
const performSearch = debounce(async function(searchInput, resultsContainer) {
// Don't search if input is empty
const performSearch = debounce(async function (searchInput, resultsContainer) {
if (searchInput.val() === '') {
resultsContainer.html('');
resultsContainer.hide();
return;
}

// Set loading state
if (!isSearching) {
isSearching = true;
}

try {
// Perform the search
let results = await fetch(window.api_origin + '/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${puter.authToken}`
'Authorization': `Bearer ${puter.authToken}`,
},
body: JSON.stringify({ text: searchInput.val() })
body: JSON.stringify({ text: searchInput.val() }),
});

results = await results.json();

// Hide results if there are none
if(results.length === 0)
if (results.length === 0) {
resultsContainer.hide();
else
resultsContainer.show();
return;
}

// Build results HTML
resultsContainer.show();
let h = '';

for(let i = 0; i < results.length; i++){
const result = results[i];
for (let result of results) {
h += `<div
class="search-result"
data-path="${html_encode(result.path)}"
data-uid="${html_encode(result.uid)}"
data-is_dir="${html_encode(result.is_dir)}"
>`;
// icon
h += `<img src="${(await item_icon(result)).image}" style="width: 20px; height: 20px; margin-right: 6px;">`;
h += html_encode(result.name);
h += `</div>`;
}

resultsContainer.html(h);
} catch (error) {
resultsContainer.html('<div class="search-error">Search failed. Please try again.</div>');
console.error('Search error:', error);
} finally {
isSearching = false;
}
}, 300); // Wait 300ms after last keystroke before searching
}, 300);

// Event binding
$(el_window).find('.search-input').on('input', function(e) {
$(el_window).find('.search-input').on('input', function () {
const searchInput = $(this);
const resultsContainer = $(el_window).find('.search-results');
performSearch(searchInput, resultsContainer);
});
}

$(document).on('click', '.search-result', async function(e){
// Handle clicks on search results

$(document).off('click', '.search-result').on('click', '.search-result', async function(e) {
const fspath = $(this).data('path');
const fsuid = $(this).data('uid');
const is_dir = $(this).attr('data-is_dir') === 'true' || $(this).data('is_dir') === '1';
let open_item_meta;

if(is_dir){
UIWindow({
path: fspath,
title: path.basename(fspath),
icon: await item_icon({is_dir: true, path: fspath}),
uid: fsuid,
is_dir: is_dir,
app: 'explorer',
// top: options.maximized ? 0 : undefined,
// left: options.maximized ? 0 : undefined,
// height: options.maximized ? `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)` : undefined,
// width: options.maximized ? `100%` : undefined,
});

// close search window
$(this).closest('.window').close();
if (is_dir) {
try {
UIWindow({
path: fspath,
title: path.basename(fspath),
icon: await item_icon({ is_dir: true, path: fspath }),
uid: fsuid,
is_dir: true,
app: 'explorer',
});

return;
// Close the search window
$(this).closest('.window').close();
} catch (error) {
console.error('Error opening directory:', error);
}
} else {
openFile($(this));
}
});

// Handle right-click with context menu
$(document).off('contextmenu', '.search-result').on('contextmenu', '.search-result', async function (e) {
e.preventDefault(); // Prevents the default context menu

// get all info needed to open an item
try{
const item = $(this);
const isDir = item.attr('data-is_dir') === 'true' || item.data('is_dir') === '1';
$('.context-menu').remove();
// Create the context menu with specific options
UIContextMenu({
parent_element: $(this),
event: e,
items: isDir
? [
{
html: "Open Containing Folder",
onClick: async function () {
const dirPath = item.data('path');
const dirUid = item.data('uid');

// Opens the directory
try {
UIWindow({
path: dirPath,
title: path.basename(dirPath),
icon: await item_icon({ is_dir: true, path: dirPath }),
uid: dirUid,
is_dir: true,
app: 'explorer',
});
} catch (error) {
console.error('Error opening directory:', error);
}
clearCache();
},
},
]
: [
{
html: "Open File",
onClick: async function () {
openFile(item); // Calls the function to open the file

clearCache();
},
},
],
});
});

// Function to open a file
async function openFile(item) {
const filePath = item.data('path');
const fileUid = item.data('uid');
let open_item_meta;

try {
open_item_meta = await $.ajax({
url: window.api_origin + "/open_item",
type: 'POST',
contentType: "application/json",
data: JSON.stringify({
uid: fsuid ?? undefined,
path: fspath ?? undefined,
uid: fileUid ?? undefined,
path: filePath ?? undefined,
}),
headers: {
"Authorization": "Bearer "+window.auth_token
},
statusCode: {
401: function () {
window.logout();
},
"Authorization": "Bearer " + window.auth_token,
},
});
}catch(err){
// Ignored
}

// get a list of suggested apps for this file type.
let suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({uid: fsuid, path: fspath});

//---------------------------------------------
// No suitable apps, ask if user would like to
// download
//---------------------------------------------
if(suggested_apps.length === 0){
//---------------------------------------------
// If .zip file, unzip it
//---------------------------------------------
if(path.extname(fspath) === '.zip'){
window.unzipItem(fspath);
return;
}
const alert_resp = await UIAlert(
'Found no suitable apps to open this file with. Would you like to download it instead?',
[
{
label: i18n('download_file'),
value: 'download_file',
type: 'primary',

},
{
label: i18n('cancel')
}
])
if(alert_resp === 'download_file'){
window.trigger_download([fspath]);
const suggested_apps = open_item_meta?.suggested_apps ?? [];
if (suggested_apps.length > 0) {
launch_app({
name: suggested_apps[0].name,
token: open_item_meta.token,
file_path: filePath,
app_obj: suggested_apps[0],
window_title: path.basename(filePath),
file_uid: fileUid,
});
} else {
console.log("No suitable app to open the file.");
}
return;
}
//---------------------------------------------
// First suggested app is default app to open this item
//---------------------------------------------
else{
launch_app({
name: suggested_apps[0].name,
token: open_item_meta.token,
file_path: fspath,
app_obj: suggested_apps[0],
window_title: path.basename(fspath),
file_uid: fsuid,
// maximized: options.maximized,
file_signature: open_item_meta.signature,
});
}

} catch (error) {
console.error('Error opening file:', error);
}
}

// close
$(this).closest('.window').close();
})
}

export default UIWindowSearch
export default UIWindowSearch;
10 changes: 10 additions & 0 deletions src/gui/src/keyboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ $(document).bind('keydown', async function(e){
$(selected_item).removeClass('search-result-active');
$(new_selected_item).addClass('search-result-active');
new_selected_item.scrollIntoView(false);

}
}
//-----------------------------------------------------------------------
Expand Down Expand Up @@ -761,4 +762,13 @@ $(document).bind("keyup keydown", async function(e){
window.undo_last_action();
return false;
}
// When hovering over a search result element
$(document).on('mouseenter', '.search-result', function () {
$(this).addClass('search-result-active'); // Adds the 'hover' class on hover
});

// When leaving a search result element
$(document).on('mouseleave', '.search-result', function () {
$(this).removeClass('search-result-active'); // Removes the 'hover' class when leaving
});
});
Loading