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
0 commit comments