feat: search

This commit is contained in:
jelveh
2024-11-12 20:20:43 -08:00
parent b589512c9d
commit 55d2af189e
9 changed files with 387 additions and 3 deletions
@@ -20,7 +20,7 @@ class HLNameSearch extends HLFilesystemOperation {
const results = await db.read(
`SELECT uuid FROM fsentries WHERE name LIKE ? AND ` +
`user_id = ?`,
`user_id = ? LIMIT 50`,
[term, actor.type.user.id]
);
+8
View File
@@ -41,6 +41,7 @@ import UINotification from "./UINotification.js"
import UIWindowWelcome from "./UIWindowWelcome.js"
import launch_app from "../helpers/launch_app.js"
import item_icon from "../helpers/item_icon.js"
import UIWindowSearch from "./UIWindowSearch.js"
async function UIDesktop(options){
let h = '';
@@ -1032,6 +1033,9 @@ async function UIDesktop(options){
if(!window.is_embedded)
ht += `<div class="toolbar-btn qr-btn" title="QR code" style="background-image:url(${window.icons['qr.svg']})"></div>`;
// search button
ht += `<div class="toolbar-btn search-btn" title="Search" style="background-image:url(${window.icons['search.svg']})"></div>`;
// user options menu
ht += `<div class="toolbar-btn user-options-menu-btn" style="background-image:url(${window.icons['profile.svg']})">`;
h += `<span class="user-options-menu-username">${window.user.username}</span>`;
@@ -1498,6 +1502,10 @@ $(document).on('click', '.close-launch-popover', function(){
});
});
$(document).on('click', '.search-btn', function(){
UIWindowSearch();
})
$(document).on('click', '.toolbar-puter-logo', function(){
UIWindowSettings();
})
+4 -1
View File
@@ -3449,7 +3449,10 @@ $.fn.focusWindow = function(event) {
if(this.hasClass('window')){
const $app_iframe = $(this).find('.window-app-iframe');
const win_id = $(this).attr('data-id');
// remove active class from all windows, except for this window
$('.window').not(this).removeClass('window-active');
// add active class to this window
$(this).addClass('window-active');
// disable pointer events on all windows' iframes, except for this window's iframe
$('.window-app-iframe').not($app_iframe).css('pointer-events', 'none');
@@ -3509,7 +3512,7 @@ $.fn.focusWindow = function(event) {
window.history.replaceState({window_id: $(this).attr('data-id')}, '', '/app/'+url_app_name+$(this).attr('data-user_set_url_params'));
document.title = $(this).attr('data-name');
}
$(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).addClass('taskbar-item-active');
$(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).addClass('taskbar-item-active');
}else{
$('.window').find('.item-selected').addClass('item-blurred');
$('.desktop').find('.item-blurred').removeClass('item-blurred');
+259
View File
@@ -0,0 +1,259 @@
/**
* 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/>.
*/
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'
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;">`;
const el_window = await UIWindow({
icon: null,
single_instance: true,
app: 'search',
uid: null,
is_dir: false,
body_content: h,
has_head: false,
selectable_body: false,
draggable_body: true,
allow_context_menu: false,
is_draggable: false,
is_resizable: false,
is_droppable: false,
init_center: true,
allow_native_ctxmenu: true,
allow_user_select: true,
window_class: 'window-search',
onAppend: function(el_window){
},
width: 500,
dominant: true,
window_css: {
height: 'initial',
padding: '0',
},
body_css: {
width: 'initial',
'max-height': 'calc(100vh - 200px)',
'background-color': 'rgb(241 246 251)',
'backdrop-filter': 'blur(3px)',
'padding': '0',
'height': 'initial',
'overflow': 'hidden',
'min-height': '65px',
'padding-bottom': '10px',
}
});
$(el_window).find('.search-input').focus();
// Debounce function to limit rate of API calls
function debounce(func, wait) {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 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
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}`
},
body: JSON.stringify({ text: searchInput.val() })
});
results = await results.json();
// Hide results if there are none
if(results.length === 0)
resultsContainer.hide();
else
resultsContainer.show();
// Build results HTML
let h = '';
for(let i = 0; i < results.length; i++){
const result = results[i];
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
// Event binding
$(el_window).find('.search-input').on('input', function(e) {
const searchInput = $(this);
const resultsContainer = $(el_window).find('.search-results');
performSearch(searchInput, resultsContainer);
});
}
$(document).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();
return;
}
// get all info needed to open an item
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,
}),
headers: {
"Authorization": "Bearer "+window.auth_token
},
statusCode: {
401: function () {
window.logout();
},
},
});
}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]);
}
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,
});
}
// close
$(this).closest('.window').close();
})
export default UIWindowSearch
+47 -1
View File
@@ -4230,4 +4230,50 @@ fieldset[name=number-code] {
.welcome-window-footer a:hover{
color: #1d1e23;
}
}
/*
* ------------------------------------
* Search
* ------------------------------------
*/
.search-input-wrapper{
width: 100%;
border-radius: 5px;
padding-bottom: 10px;
padding-top: 20px;
position: absolute;
padding-left: 15px;
padding-right: 15px;
box-sizing: border-box;
background: #f1f6fc;
}
.search-input{
padding-left: 33px !important;
background-repeat: no-repeat;
background-position: 5px center;
background-size: 20px;
}
.search-results{
padding-right: 15px;
margin-top: 70px;
padding-left: 15px;
padding-right: 15px;
padding-bottom: 5px;
display: none;
}
.search-result{
padding: 10px; cursor: pointer;
font-size: 13px;
display: flex;
align-items: center;
}
.search-result-active{
background-color: #4092da;
color: #fff;
border-radius: 5px;
}
.search-results .search-result:last-child {
margin-bottom: 0;
}
+4
View File
@@ -51,6 +51,10 @@ const item_icon = async (fsentry)=>{
// thumbnail
// --------------------------------------------------
if(fsentry.thumbnail){
// if thumbnail but a directory under AppData, then it's a thumbnail for an app and must be treated as an icon
if(fsentry.path.startsWith(window.appdata_path + '/'))
return {image: fsentry.thumbnail, type: 'icon'};
// otherwise, it's a thumbnail for a file
return {image: fsentry.thumbnail, type: 'thumb'};
}
// --------------------------------------------------
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
<path d="M 11.742 5.658 C 14.7 9.694 12.179 15.418 7.205 15.962 C 2.231 16.505 -1.466 11.46 0.55 6.881 C 2.226 3.076 6.993 1.8 10.345 4.26 L 10.344 4.26 C 10.373 4.22 10.406 4.182 10.442 4.145 L 14.292 0.295 C 14.836 -0.25 15.766 -0.001 15.966 0.743 C 16.058 1.088 15.96 1.456 15.707 1.709 L 11.857 5.559 C 11.821 5.595 11.783 5.629 11.742 5.659 L 11.742 5.658 Z M 12 9.502 C 12 5.268 7.417 2.622 3.75 4.739 C 2.048 5.721 1 7.537 1 9.502 C 1 13.736 5.583 16.382 9.25 14.265 C 10.952 13.283 12 11.467 12 9.502" style="transform-box: fill-box; transform-origin: 50% 50%; fill: rgb(255, 255, 255);" transform="matrix(-1, 0, 0, -1, 0, 0.000001)"/>
</svg>

After

Width:  |  Height:  |  Size: 816 B

+6
View File
@@ -1420,6 +1420,12 @@ $(document).on('click', '.generic-close-window-button', function(e){
$(this).closest('.window').close();
});
$(document).on('click', function(e){
if(!$(e.target).hasClass('window-search') && $(e.target).closest('.window-search').length === 0 && !$(e.target).is('.toolbar-btn.search-btn')){
$('.window-search').close();
}
})
// Re-calculate desktop height and width on window resize and re-position the login and signup windows
$(window).on("resize", function () {
// If host env is popup, don't continue because the popup window has its own resize requirements.
+54
View File
@@ -18,11 +18,23 @@
*/
import UIAlert from './UI/UIAlert.js';
import UIWindowSearch from './UI/UIWindowSearch.js';
import launch_app from './helpers/launch_app.js';
import open_item from './helpers/open_item.js';
$(document).bind('keydown', async function(e){
const focused_el = document.activeElement;
//-----------------------------------------------------------------------------
// Search
// ctrl/command + f, will open UIWindowSearch
//-----------------------------------------------------------------------------
if((e.ctrlKey || e.metaKey) && e.which === 70 && !$(focused_el).is('input') && !$(focused_el).is('textarea')){
e.preventDefault();
e.stopPropagation();
UIWindowSearch();
return false;
}
//-----------------------------------------------------------------------
// ← ↑ → ↓: an arrow key is pressed
//-----------------------------------------------------------------------
@@ -290,6 +302,32 @@ $(document).bind('keydown', async function(e){
next_item.scrollIntoView(false);
}
}
// ----------------------------------------------
// Navigate search results in the search window
// ----------------------------------------------
else if($('.window-search').length > 0){
let selected_item = $('.window-search .search-result-active').get(0);
let selected_item_index = selected_item ? $('.window-search .search-result').index(selected_item) : -1;
let new_selected_item_index = selected_item_index;
let new_selected_item;
// if up arrow is pressed
if(e.which === 38){
new_selected_item_index = selected_item_index - 1;
if(new_selected_item_index < 0)
new_selected_item_index = $('.window-search .search-result').length - 1;
}
// if down arrow is pressed
else if(e.which === 40){
new_selected_item_index = selected_item_index + 1;
if(new_selected_item_index >= $('.window-search .search-result').length)
new_selected_item_index = 0;
}
new_selected_item = $('.window-search .search-result').get(new_selected_item_index);
$(selected_item).removeClass('search-result-active');
$(new_selected_item).addClass('search-result-active');
new_selected_item.scrollIntoView(false);
}
}
//-----------------------------------------------------------------------
// if the Esc key is pressed on a FileDialog/Alert, close that FileDialog/Alert
@@ -313,6 +351,12 @@ $(document).bind('keydown', async function(e){
$(focused_el).val($(focused_el).closest('.window').attr('data-path'));
$(focused_el).attr('data-path', $(focused_el).closest('.window').attr('data-path'));
}
//-----------------------------------------------------------------------
// if the Esc key is pressed on a Search Window, close the Search Window
//-----------------------------------------------------------------------
else if( e.which === 27 && $('.window-search').length > 0){
$('.window-search').close();
}
//-----------------------------------------------------------------------
// Esc key should:
@@ -612,6 +656,16 @@ $(document).bind("keyup keydown", async function(e){
return false;
}
//-----------------------------------------------------------------------
// Enter key on a search window result
//-----------------------------------------------------------------------
if(e.which === 13 && $('.window-search').length > 0
// prevent firing twice, because this will be fired on both keyup and keydown
&& e.type === 'keydown'){
$('.window-search .search-result-active').trigger('click');
return false;
}
//-----------------------------------------------------------------------
// Open
// Enter key on a selected item will open it
//-----------------------------------------------------------------------