Skip to content

Commit 55d2af1

Browse files
committed
feat: search
1 parent b589512 commit 55d2af1

File tree

9 files changed

+387
-3
lines changed

9 files changed

+387
-3
lines changed

src/backend/src/filesystem/hl_operations/hl_name_search.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class HLNameSearch extends HLFilesystemOperation {
2020

2121
const results = await db.read(
2222
`SELECT uuid FROM fsentries WHERE name LIKE ? AND ` +
23-
`user_id = ?`,
23+
`user_id = ? LIMIT 50`,
2424
[term, actor.type.user.id]
2525
);
2626

src/gui/src/UI/UIDesktop.js

+8
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import UINotification from "./UINotification.js"
4141
import UIWindowWelcome from "./UIWindowWelcome.js"
4242
import launch_app from "../helpers/launch_app.js"
4343
import item_icon from "../helpers/item_icon.js"
44+
import UIWindowSearch from "./UIWindowSearch.js"
4445

4546
async function UIDesktop(options){
4647
let h = '';
@@ -1032,6 +1033,9 @@ async function UIDesktop(options){
10321033
if(!window.is_embedded)
10331034
ht += `<div class="toolbar-btn qr-btn" title="QR code" style="background-image:url(${window.icons['qr.svg']})"></div>`;
10341035

1036+
// search button
1037+
ht += `<div class="toolbar-btn search-btn" title="Search" style="background-image:url(${window.icons['search.svg']})"></div>`;
1038+
10351039
// user options menu
10361040
ht += `<div class="toolbar-btn user-options-menu-btn" style="background-image:url(${window.icons['profile.svg']})">`;
10371041
h += `<span class="user-options-menu-username">${window.user.username}</span>`;
@@ -1498,6 +1502,10 @@ $(document).on('click', '.close-launch-popover', function(){
14981502
});
14991503
});
15001504

1505+
$(document).on('click', '.search-btn', function(){
1506+
UIWindowSearch();
1507+
})
1508+
15011509
$(document).on('click', '.toolbar-puter-logo', function(){
15021510
UIWindowSettings();
15031511
})

src/gui/src/UI/UIWindow.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -3449,7 +3449,10 @@ $.fn.focusWindow = function(event) {
34493449
if(this.hasClass('window')){
34503450
const $app_iframe = $(this).find('.window-app-iframe');
34513451
const win_id = $(this).attr('data-id');
3452+
3453+
// remove active class from all windows, except for this window
34523454
$('.window').not(this).removeClass('window-active');
3455+
// add active class to this window
34533456
$(this).addClass('window-active');
34543457
// disable pointer events on all windows' iframes, except for this window's iframe
34553458
$('.window-app-iframe').not($app_iframe).css('pointer-events', 'none');
@@ -3509,7 +3512,7 @@ $.fn.focusWindow = function(event) {
35093512
window.history.replaceState({window_id: $(this).attr('data-id')}, '', '/app/'+url_app_name+$(this).attr('data-user_set_url_params'));
35103513
document.title = $(this).attr('data-name');
35113514
}
3512-
$(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).addClass('taskbar-item-active');
3515+
$(`.taskbar .taskbar-item[data-app="${$(this).attr('data-app')}"]`).addClass('taskbar-item-active');
35133516
}else{
35143517
$('.window').find('.item-selected').addClass('item-blurred');
35153518
$('.desktop').find('.item-blurred').removeClass('item-blurred');

src/gui/src/UI/UIWindowSearch.js

+259
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/**
2+
* Copyright (C) 2024 Puter Technologies Inc.
3+
*
4+
* This file is part of Puter.
5+
*
6+
* Puter is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU Affero General Public License as published
8+
* by the Free Software Foundation, either version 3 of the License, or
9+
* (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU Affero General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Affero General Public License
17+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
18+
*/
19+
20+
import UIWindow from './UIWindow.js'
21+
import path from "../lib/path.js"
22+
import UIAlert from './UIAlert.js'
23+
import launch_app from '../helpers/launch_app.js'
24+
import item_icon from '../helpers/item_icon.js'
25+
26+
async function UIWindowSearch(options){
27+
let h = '';
28+
29+
h += `<div class="search-input-wrapper">`;
30+
h += `<input type="text" class="search-input" placeholder="Search" style="background-image:url(${window.icons['magnifier-outline.svg']});">`;
31+
h += `</div>`;
32+
h += `<div class="search-results" style="overflow-y: auto; max-height: 300px;">`;
33+
34+
const el_window = await UIWindow({
35+
icon: null,
36+
single_instance: true,
37+
app: 'search',
38+
uid: null,
39+
is_dir: false,
40+
body_content: h,
41+
has_head: false,
42+
selectable_body: false,
43+
draggable_body: true,
44+
allow_context_menu: false,
45+
is_draggable: false,
46+
is_resizable: false,
47+
is_droppable: false,
48+
init_center: true,
49+
allow_native_ctxmenu: true,
50+
allow_user_select: true,
51+
window_class: 'window-search',
52+
onAppend: function(el_window){
53+
},
54+
width: 500,
55+
dominant: true,
56+
window_css: {
57+
height: 'initial',
58+
padding: '0',
59+
},
60+
body_css: {
61+
width: 'initial',
62+
'max-height': 'calc(100vh - 200px)',
63+
'background-color': 'rgb(241 246 251)',
64+
'backdrop-filter': 'blur(3px)',
65+
'padding': '0',
66+
'height': 'initial',
67+
'overflow': 'hidden',
68+
'min-height': '65px',
69+
'padding-bottom': '10px',
70+
}
71+
});
72+
73+
$(el_window).find('.search-input').focus();
74+
75+
// Debounce function to limit rate of API calls
76+
function debounce(func, wait) {
77+
let timeout;
78+
return function (...args) {
79+
const context = this;
80+
clearTimeout(timeout);
81+
timeout = setTimeout(() => {
82+
func.apply(context, args);
83+
}, wait);
84+
};
85+
}
86+
87+
// State for managing loading indicator
88+
let isSearching = false;
89+
90+
// Debounced search function
91+
const performSearch = debounce(async function(searchInput, resultsContainer) {
92+
// Don't search if input is empty
93+
if (searchInput.val() === '') {
94+
resultsContainer.html('');
95+
resultsContainer.hide();
96+
return;
97+
}
98+
99+
// Set loading state
100+
if (!isSearching) {
101+
isSearching = true;
102+
}
103+
104+
try {
105+
// Perform the search
106+
let results = await fetch(window.api_origin + '/search', {
107+
method: 'POST',
108+
headers: {
109+
'Content-Type': 'application/json',
110+
'Authorization': `Bearer ${puter.authToken}`
111+
},
112+
body: JSON.stringify({ text: searchInput.val() })
113+
});
114+
115+
results = await results.json();
116+
117+
// Hide results if there are none
118+
if(results.length === 0)
119+
resultsContainer.hide();
120+
else
121+
resultsContainer.show();
122+
123+
// Build results HTML
124+
let h = '';
125+
126+
for(let i = 0; i < results.length; i++){
127+
const result = results[i];
128+
h += `<div
129+
class="search-result"
130+
data-path="${html_encode(result.path)}"
131+
data-uid="${html_encode(result.uid)}"
132+
data-is_dir="${html_encode(result.is_dir)}"
133+
>`;
134+
// icon
135+
h += `<img src="${(await item_icon(result)).image}" style="width: 20px; height: 20px; margin-right: 6px;">`;
136+
h += html_encode(result.name);
137+
h += `</div>`;
138+
}
139+
resultsContainer.html(h);
140+
} catch (error) {
141+
resultsContainer.html('<div class="search-error">Search failed. Please try again.</div>');
142+
console.error('Search error:', error);
143+
} finally {
144+
isSearching = false;
145+
}
146+
}, 300); // Wait 300ms after last keystroke before searching
147+
148+
// Event binding
149+
$(el_window).find('.search-input').on('input', function(e) {
150+
const searchInput = $(this);
151+
const resultsContainer = $(el_window).find('.search-results');
152+
performSearch(searchInput, resultsContainer);
153+
});
154+
}
155+
156+
$(document).on('click', '.search-result', async function(e){
157+
const fspath = $(this).data('path');
158+
const fsuid = $(this).data('uid');
159+
const is_dir = $(this).attr('data-is_dir') === 'true' || $(this).data('is_dir') === '1';
160+
let open_item_meta;
161+
162+
if(is_dir){
163+
UIWindow({
164+
path: fspath,
165+
title: path.basename(fspath),
166+
icon: await item_icon({is_dir: true, path: fspath}),
167+
uid: fsuid,
168+
is_dir: is_dir,
169+
app: 'explorer',
170+
// top: options.maximized ? 0 : undefined,
171+
// left: options.maximized ? 0 : undefined,
172+
// height: options.maximized ? `calc(100% - ${window.taskbar_height + window.toolbar_height + 1}px)` : undefined,
173+
// width: options.maximized ? `100%` : undefined,
174+
});
175+
176+
// close search window
177+
$(this).closest('.window').close();
178+
179+
return;
180+
}
181+
182+
// get all info needed to open an item
183+
try{
184+
open_item_meta = await $.ajax({
185+
url: window.api_origin + "/open_item",
186+
type: 'POST',
187+
contentType: "application/json",
188+
data: JSON.stringify({
189+
uid: fsuid ?? undefined,
190+
path: fspath ?? undefined,
191+
}),
192+
headers: {
193+
"Authorization": "Bearer "+window.auth_token
194+
},
195+
statusCode: {
196+
401: function () {
197+
window.logout();
198+
},
199+
},
200+
});
201+
}catch(err){
202+
// Ignored
203+
}
204+
205+
// get a list of suggested apps for this file type.
206+
let suggested_apps = open_item_meta?.suggested_apps ?? await window.suggest_apps_for_fsentry({uid: fsuid, path: fspath});
207+
208+
//---------------------------------------------
209+
// No suitable apps, ask if user would like to
210+
// download
211+
//---------------------------------------------
212+
if(suggested_apps.length === 0){
213+
//---------------------------------------------
214+
// If .zip file, unzip it
215+
//---------------------------------------------
216+
if(path.extname(fspath) === '.zip'){
217+
window.unzipItem(fspath);
218+
return;
219+
}
220+
const alert_resp = await UIAlert(
221+
'Found no suitable apps to open this file with. Would you like to download it instead?',
222+
[
223+
{
224+
label: i18n('download_file'),
225+
value: 'download_file',
226+
type: 'primary',
227+
228+
},
229+
{
230+
label: i18n('cancel')
231+
}
232+
])
233+
if(alert_resp === 'download_file'){
234+
window.trigger_download([fspath]);
235+
}
236+
return;
237+
}
238+
//---------------------------------------------
239+
// First suggested app is default app to open this item
240+
//---------------------------------------------
241+
else{
242+
launch_app({
243+
name: suggested_apps[0].name,
244+
token: open_item_meta.token,
245+
file_path: fspath,
246+
app_obj: suggested_apps[0],
247+
window_title: path.basename(fspath),
248+
file_uid: fsuid,
249+
// maximized: options.maximized,
250+
file_signature: open_item_meta.signature,
251+
});
252+
}
253+
254+
255+
// close
256+
$(this).closest('.window').close();
257+
})
258+
259+
export default UIWindowSearch

src/gui/src/css/style.css

+47-1
Original file line numberDiff line numberDiff line change
@@ -4230,4 +4230,50 @@ fieldset[name=number-code] {
42304230

42314231
.welcome-window-footer a:hover{
42324232
color: #1d1e23;
4233-
}
4233+
}
4234+
4235+
/*
4236+
* ------------------------------------
4237+
* Search
4238+
* ------------------------------------
4239+
*/
4240+
.search-input-wrapper{
4241+
width: 100%;
4242+
border-radius: 5px;
4243+
padding-bottom: 10px;
4244+
padding-top: 20px;
4245+
position: absolute;
4246+
padding-left: 15px;
4247+
padding-right: 15px;
4248+
box-sizing: border-box;
4249+
background: #f1f6fc;
4250+
}
4251+
.search-input{
4252+
padding-left: 33px !important;
4253+
background-repeat: no-repeat;
4254+
background-position: 5px center;
4255+
background-size: 20px;
4256+
}
4257+
.search-results{
4258+
padding-right: 15px;
4259+
margin-top: 70px;
4260+
padding-left: 15px;
4261+
padding-right: 15px;
4262+
padding-bottom: 5px;
4263+
display: none;
4264+
}
4265+
.search-result{
4266+
padding: 10px; cursor: pointer;
4267+
font-size: 13px;
4268+
display: flex;
4269+
align-items: center;
4270+
}
4271+
.search-result-active{
4272+
background-color: #4092da;
4273+
color: #fff;
4274+
border-radius: 5px;
4275+
}
4276+
.search-results .search-result:last-child {
4277+
margin-bottom: 0;
4278+
}
4279+

src/gui/src/helpers/item_icon.js

+4
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ const item_icon = async (fsentry)=>{
5151
// thumbnail
5252
// --------------------------------------------------
5353
if(fsentry.thumbnail){
54+
// if thumbnail but a directory under AppData, then it's a thumbnail for an app and must be treated as an icon
55+
if(fsentry.path.startsWith(window.appdata_path + '/'))
56+
return {image: fsentry.thumbnail, type: 'icon'};
57+
// otherwise, it's a thumbnail for a file
5458
return {image: fsentry.thumbnail, type: 'thumb'};
5559
}
5660
// --------------------------------------------------

src/gui/src/icons/search.svg

+4
Loading

0 commit comments

Comments
 (0)