anshdadhich commited on
Commit
cff0bdd
·
verified ·
1 Parent(s): ae9710e

Upload fastsearch-tauri/ui/app.js

Browse files
Files changed (1) hide show
  1. fastsearch-tauri/ui/app.js +318 -0
fastsearch-tauri/ui/app.js ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // ── DOM Elements ───────────────────────────────────────────────────
2
+ const searchInput = document.getElementById('searchInput');
3
+ const loadingSpinner = document.getElementById('loadingSpinner');
4
+ const resultsContainer = document.getElementById('resultsContainer');
5
+ const resultsList = document.getElementById('resultsList');
6
+ const resultsFooter = document.getElementById('resultsFooter');
7
+ const footerStats = document.getElementById('footerStats');
8
+ const emptyState = document.getElementById('emptyState');
9
+ const noResults = document.getElementById('noResults');
10
+
11
+ // ── State ──────────────────────────────────────────────────────────
12
+ let results = [];
13
+ let selectedIndex = -1;
14
+ let searchTimeout = null;
15
+ let totalIndexed = 0;
16
+
17
+ // ── Icon helpers ───────────────────────────────────────────────────
18
+ function getFileIcon(name, isDir) {
19
+ if (isDir) return '📁';
20
+
21
+ const ext = name.split('.').pop()?.toLowerCase() || '';
22
+
23
+ const iconMap = {
24
+ exe: '⚙️',
25
+ lnk: '🔗',
26
+ pdf: '📄',
27
+ doc: '📝', docx: '📝',
28
+ xls: '📊', xlsx: '📊',
29
+ ppt: '📊', pptx: '📊',
30
+ jpg: '🖼️', jpeg: '🖼️', png: '🖼️', gif: '🖼️', bmp: '🖼️', svg: '🖼️', webp: '🖼️',
31
+ mp4: '🎬', mov: '🎬', avi: '🎬', mkv: '🎬',
32
+ mp3: '🎵', wav: '🎵', flac: '🎵', aac: '🎵',
33
+ zip: '📦', rar: '📦', '7z': '📦', tar: '📦', gz: '📦',
34
+ js: '📜', ts: '📜', jsx: '📜', tsx: '📜',
35
+ html: '🌐', css: '🎨', json: '📋', xml: '📋',
36
+ py: '🐍', rs: '🦀', go: '🐹', java: '☕', cpp: '⚙️', c: '⚙️',
37
+ txt: '📃', md: '📃', log: '📃',
38
+ db: '🗄️', sql: '🗄️',
39
+ ini: '⚙️', cfg: '⚙️', conf: '⚙️',
40
+ };
41
+
42
+ return iconMap[ext] || '📄';
43
+ }
44
+
45
+ function getFileClass(name, isDir) {
46
+ if (isDir) return 'folder';
47
+ const ext = name.split('.').pop()?.toLowerCase() || '';
48
+ if (['exe', 'lnk', 'msi', 'bat', 'cmd'].includes(ext)) return 'app';
49
+ return 'file';
50
+ }
51
+
52
+ // ── Highlight query in result name ────────────────────────────────
53
+ function highlightMatch(name, query) {
54
+ if (!query) return name;
55
+ const q = query.toLowerCase();
56
+ const n = name.toLowerCase();
57
+ const idx = n.indexOf(q);
58
+ if (idx === -1) return name;
59
+ return (
60
+ name.slice(0, idx) +
61
+ '<span class="highlight">' +
62
+ name.slice(idx, idx + query.length) +
63
+ '</span>' +
64
+ name.slice(idx + query.length)
65
+ );
66
+ }
67
+
68
+ // ── Render results ────────────────────────────────────────────────
69
+ function renderResults() {
70
+ resultsList.innerHTML = '';
71
+
72
+ results.forEach((item, idx) => {
73
+ const el = document.createElement('div');
74
+ el.className = 'result-item' + (idx === selectedIndex ? ' selected' : '');
75
+ el.dataset.index = idx;
76
+ el.addEventListener('click', () => selectAndOpen(idx));
77
+ el.addEventListener('dblclick', () => openItem(idx, true));
78
+
79
+ const iconClass = getFileClass(item.name, item.is_dir);
80
+ const icon = getFileIcon(item.name, item.is_dir);
81
+
82
+ const query = searchInput.value.trim();
83
+ const highlightedName = highlightMatch(item.name, query);
84
+
85
+ // Format path - shorten if too long
86
+ let displayPath = item.path;
87
+ if (displayPath.length > 80) {
88
+ displayPath = '...' + displayPath.slice(-77);
89
+ }
90
+
91
+ el.innerHTML = `
92
+ <div class="result-icon ${iconClass}">${icon}</div>
93
+ <div class="result-info">
94
+ <div class="result-name">${highlightedName}</div>
95
+ <div class="result-path">${escapeHtml(displayPath)}</div>
96
+ </div>
97
+ <div class="result-meta">${item.is_dir ? 'DIR' : 'FILE'}</div>
98
+ `;
99
+
100
+ resultsList.appendChild(el);
101
+ });
102
+ }
103
+
104
+ function escapeHtml(text) {
105
+ const div = document.createElement('div');
106
+ div.textContent = text;
107
+ return div.innerHTML;
108
+ }
109
+
110
+ // ── Update selection ────────────────────────────────────────────
111
+ function updateSelection() {
112
+ const items = resultsList.querySelectorAll('.result-item');
113
+ items.forEach((el, idx) => {
114
+ el.classList.toggle('selected', idx === selectedIndex);
115
+ });
116
+
117
+ // Scroll into view
118
+ if (selectedIndex >= 0) {
119
+ const selected = items[selectedIndex];
120
+ if (selected) {
121
+ selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
122
+ }
123
+ }
124
+ }
125
+
126
+ function selectNext() {
127
+ if (results.length === 0) return;
128
+ selectedIndex = (selectedIndex + 1) % results.length;
129
+ updateSelection();
130
+ }
131
+
132
+ function selectPrev() {
133
+ if (results.length === 0) return;
134
+ selectedIndex = (selectedIndex - 1 + results.length) % results.length;
135
+ updateSelection();
136
+ }
137
+
138
+ function selectAndOpen(index) {
139
+ selectedIndex = index;
140
+ updateSelection();
141
+ openItem(index, false);
142
+ }
143
+
144
+ // ── Open item ────────────────────────────────────────────────────
145
+ function openItem(index, reveal) {
146
+ if (index < 0 || index >= results.length) return;
147
+ const item = results[index];
148
+
149
+ if (typeof window.__TAURI__ !== 'undefined') {
150
+ if (reveal || item.is_dir) {
151
+ window.__TAURI__.invoke('cmd_open_folder', { path: item.path });
152
+ } else {
153
+ window.__TAURI__.invoke('cmd_open_file', { path: item.path });
154
+ }
155
+ } else {
156
+ // Fallback for dev
157
+ console.log('Open:', item.path);
158
+ }
159
+
160
+ // Hide window after opening
161
+ setTimeout(() => {
162
+ if (typeof window.__TAURI__ !== 'undefined') {
163
+ window.__TAURI__.invoke('cmd_toggle_window');
164
+ }
165
+ }, 100);
166
+ }
167
+
168
+ // ── Perform search ───────────────────────────────────────────────
169
+ function performSearch(query) {
170
+ if (!query) {
171
+ showEmptyState();
172
+ return;
173
+ }
174
+
175
+ loadingSpinner.classList.add('active');
176
+
177
+ if (typeof window.__TAURI__ !== 'undefined') {
178
+ window.__TAURI__.invoke('cmd_search', {
179
+ request: {
180
+ query: query,
181
+ limit: 50,
182
+ case_sensitive: false
183
+ }
184
+ }).then(response => {
185
+ loadingSpinner.classList.remove('active');
186
+ results = response.results || [];
187
+ totalIndexed = response.total_indexed;
188
+
189
+ if (results.length > 0) {
190
+ showResults();
191
+ selectedIndex = 0;
192
+ renderResults();
193
+ updateFooter(response.elapsed_ms);
194
+ } else {
195
+ showNoResults();
196
+ }
197
+ }).catch(err => {
198
+ loadingSpinner.classList.remove('active');
199
+ console.error('Search error:', err);
200
+ });
201
+ } else {
202
+ // Dev mode - show empty
203
+ loadingSpinner.classList.remove('active');
204
+ showEmptyState();
205
+ }
206
+ }
207
+
208
+ // ── View states ───────────────────────────────────────────────────
209
+ function showResults() {
210
+ resultsContainer.classList.add('visible');
211
+ emptyState.classList.add('hidden');
212
+ noResults.classList.remove('visible');
213
+ }
214
+
215
+ function showEmptyState() {
216
+ resultsContainer.classList.remove('visible');
217
+ emptyState.classList.remove('hidden');
218
+ noResults.classList.remove('visible');
219
+ results = [];
220
+ selectedIndex = -1;
221
+ }
222
+
223
+ function showNoResults() {
224
+ resultsContainer.classList.remove('visible');
225
+ emptyState.classList.add('hidden');
226
+ noResults.classList.add('visible');
227
+ results = [];
228
+ selectedIndex = -1;
229
+ }
230
+
231
+ function updateFooter(elapsedMs) {
232
+ footerStats.textContent = `${results.length} of ${totalIndexed.toLocaleString()} files · ${elapsedMs.toFixed(2)}ms`;
233
+ }
234
+
235
+ // ── Search input handler ─────────────────────────────────────────
236
+ searchInput.addEventListener('input', (e) => {
237
+ const query = e.target.value.trim();
238
+
239
+ clearTimeout(searchTimeout);
240
+
241
+ if (!query) {
242
+ showEmptyState();
243
+ return;
244
+ }
245
+
246
+ searchTimeout = setTimeout(() => {
247
+ performSearch(query);
248
+ }, 50); // 50ms debounce for instant feel
249
+ });
250
+
251
+ // ── Keyboard navigation ──────────────────────────────────────────
252
+ document.addEventListener('keydown', (e) => {
253
+ // ESC - close window
254
+ if (e.key === 'Escape') {
255
+ if (typeof window.__TAURI__ !== 'undefined') {
256
+ window.__TAURI__.invoke('cmd_toggle_window');
257
+ }
258
+ return;
259
+ }
260
+
261
+ // Arrow down - next result
262
+ if (e.key === 'ArrowDown') {
263
+ e.preventDefault();
264
+ selectNext();
265
+ return;
266
+ }
267
+
268
+ // Arrow up - previous result
269
+ if (e.key === 'ArrowUp') {
270
+ e.preventDefault();
271
+ selectPrev();
272
+ return;
273
+ }
274
+
275
+ // Enter - open selected
276
+ if (e.key === 'Enter') {
277
+ e.preventDefault();
278
+ const reveal = e.ctrlKey || e.metaKey;
279
+ openItem(selectedIndex, reveal);
280
+ return;
281
+ }
282
+
283
+ // Tab - focus search input (if not focused)
284
+ if (e.key === 'Tab') {
285
+ e.preventDefault();
286
+ searchInput.focus();
287
+ return;
288
+ }
289
+
290
+ // Any printable key - focus search input
291
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
292
+ if (document.activeElement !== searchInput) {
293
+ searchInput.focus();
294
+ }
295
+ }
296
+ });
297
+
298
+ // ── Focus search on show ──────────────────────────────────────────
299
+ function resetSearch() {
300
+ searchInput.value = '';
301
+ searchInput.focus();
302
+ showEmptyState();
303
+ }
304
+
305
+ // ── Listen for toggle event from Rust ──────────────────��─────────
306
+ if (typeof window.__TAURI__ !== 'undefined') {
307
+ window.__TAURI__.event.listen('toggle-search', () => {
308
+ resetSearch();
309
+ });
310
+
311
+ // Load stats on startup
312
+ window.__TAURI__.invoke('cmd_get_stats').then(stats => {
313
+ totalIndexed = stats.total_indexed;
314
+ });
315
+ }
316
+
317
+ // Focus input on load
318
+ searchInput.focus();