Skip to content

Commit

Permalink
feat:Add context menu to search results for better file navigation (H…
Browse files Browse the repository at this point in the history
  • Loading branch information
mariatouil committed Dec 11, 2024
1 parent badc654 commit a9b13e1
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 142 deletions.
268 changes: 126 additions & 142 deletions src/gui/src/UI/UIWindowSearch.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,24 @@
/**
* Copyright (C) 2024 Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* 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/>.
* Gestion du double-clic et du clic droit
* - Double-clic ouvre les fichiers et les dossiers.
* - Clic droit affiche un menu contextuel avec "Open Dir" pour les dossiers et "Open File" pour les fichiers.
* - Le menu contextuel disparaît et le cache est effacé après l'exécution de l'action.
*/

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 += `</div>`;
h += `<div class="search-results" style="overflow-y: auto; max-height: 300px;">`;
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 += `</div>`;
h += `<div class="search-results" style="overflow-y: auto; max-height: 300px;"></div>`;

const el_window = await UIWindow({
icon: null,
Expand All @@ -49,8 +38,6 @@ async function UIWindowSearch(options){
allow_native_ctxmenu: true,
allow_user_select: true,
window_class: 'window-search',
onAppend: function(el_window){
},
width: 500,
dominant: true,
window_css: {
Expand All @@ -67,12 +54,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 @@ -84,176 +71,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

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();
},
},
],
});
});

// get all info needed to open an item
try{
// 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 @@ -327,6 +327,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 @@ -760,4 +761,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
});
});

0 comments on commit a9b13e1

Please sign in to comment.