fish-dock-plugin-filemgr 1.0.6 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin.json +9 -6
- package/plugins/filemgr/backend.mjs +167 -6
- package/plugins/filemgr/filemgr.css +13 -0
- package/plugins/filemgr/page.html +4 -0
- package/plugins/filemgr/page.js +89 -0
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"id": "jassdingk.filemgr",
|
|
4
4
|
"name": "文件管理",
|
|
5
|
-
"version": "1.0.
|
|
5
|
+
"version": "1.0.7",
|
|
6
6
|
"hostApi": 1,
|
|
7
7
|
"platforms": [
|
|
8
8
|
"windows-x86_64"
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
],
|
|
28
28
|
"files": {
|
|
29
29
|
"bridge.js": "f38f93994e3cb2b71f2ff1810b3f68fb42f4261550f8ab01b0bfa3eecd5f94ee",
|
|
30
|
-
"package.json": "
|
|
31
|
-
"plugins/filemgr/backend.mjs": "
|
|
30
|
+
"package.json": "330dcaaf54a8291462d17ef6a28d7b49465d7a603add478f03fb35feb49a4bee",
|
|
31
|
+
"plugins/filemgr/backend.mjs": "7cd37f01f6d4ae8b9693d2b1642f5078855fd405dae1e1cf9a6c10d7a8c15b15",
|
|
32
32
|
"plugins/filemgr/clip.ps1": "b0a32fca35cf2d25133338f46e20a0db20d78c5bb6b5c2aab899998bfbd6c6ab",
|
|
33
33
|
"plugins/filemgr/dock.css": "e795214dd39de3b473ac0b1a7b79f6922a88c761fca4d3ea4da8fd9b60188bcf",
|
|
34
|
-
"plugins/filemgr/filemgr.css": "
|
|
34
|
+
"plugins/filemgr/filemgr.css": "c6d6e4041901e37d023205d8d7025b597294a8503bb74be74ed774eeb86fe1e3",
|
|
35
35
|
"plugins/filemgr/FolderPicker.ps1": "4b7f86c543e06fd8f93fa5d8002997d944c8189cb0ae031bedf864ea89196427",
|
|
36
36
|
"plugins/filemgr/page.css": "0801a05b5148cd091a68957beac21cc982a7818ef4707cd06fd11b20adbc7765",
|
|
37
|
-
"plugins/filemgr/page.html": "
|
|
38
|
-
"plugins/filemgr/page.js": "
|
|
37
|
+
"plugins/filemgr/page.html": "415e9ae0d7ed85040974498d4d5581d707a668d55c917181025d4fee4dda67de",
|
|
38
|
+
"plugins/filemgr/page.js": "ab67c2a1f5e4a48c1912b630a712524ed3a65a592ae1a6a69d9a12058ca84ee4",
|
|
39
39
|
"plugins/filemgr/pick-files.ps1": "88bff92d9a2e5b69e9411bb372441ff0b86899b7f5b31f5ed8d154c4f6fc2f37",
|
|
40
40
|
"plugins/filemgr/pick-folder.ps1": "ff031c14df32149c1c80c7ab6a21710c1a68e823e96e3068c458a60902ae3b6f",
|
|
41
41
|
"plugins/filemgr/preview.mjs": "a5596e77180fe7928443244a0148b6d3ea8381a1f886ee01f9a08be038c7ee80",
|
|
@@ -43,5 +43,8 @@
|
|
|
43
43
|
"plugins/filemgr/stash.css": "fce2aaa13a86dbd7675b685c533f0bb916c7e610fc612a3ade660c67069f5662",
|
|
44
44
|
"plugins/filemgr/stash.mjs": "c33cea6b845a3847950d26a10b9aba1ef68d39b1f9fc423b23055dead7638866",
|
|
45
45
|
"README.md": "e9fb4ca9831b778a1d530b67518fcaf2b42cf02494b9d68deaad16bf2b947687"
|
|
46
|
+
},
|
|
47
|
+
"behaviors": {
|
|
48
|
+
"autostart": true
|
|
46
49
|
}
|
|
47
50
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// 所有业务方法都返回 {ok:true,...} 或 {ok:false,error},便于页面显示真实原因。
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import fs from 'node:fs'
|
|
5
|
+
import http from 'node:http'
|
|
5
6
|
import { spawn } from 'node:child_process'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
7
8
|
import { createPlugin } from './sdk.mjs'
|
|
@@ -21,12 +22,6 @@ function logCrash(where, error) {
|
|
|
21
22
|
process.on('uncaughtException', error => logCrash('uncaughtException', error))
|
|
22
23
|
process.on('unhandledRejection', error => logCrash('unhandledRejection', error))
|
|
23
24
|
|
|
24
|
-
// [诊断探针] 宿主每次 spawn 本后端都会留下这行痕迹,用于判定"宿主到底有没有拉起插件"。
|
|
25
|
-
try {
|
|
26
|
-
const fs = await import('node:fs')
|
|
27
|
-
fs.appendFileSync(path.join(HERE, '_spawn-probe.log'),
|
|
28
|
-
`${new Date().toISOString()} pid=${process.pid} argv=${JSON.stringify(process.argv)} cwd=${process.cwd()} node=${process.execPath}\n`)
|
|
29
|
-
} catch { /* 探针失败不影响业务 */ }
|
|
30
25
|
|
|
31
26
|
// 宿主对插件方法的单次调用有 15 秒硬超时(plugin_runtime.rs:42),
|
|
32
27
|
// 而用户挑文件夹可能盯着一分钟。所以选择操作拆成两步:
|
|
@@ -165,6 +160,161 @@ function guard(fn) {
|
|
|
165
160
|
|
|
166
161
|
function activeRoots() { return roots(extraRoots) }
|
|
167
162
|
|
|
163
|
+
// ---- 全局文件搜索:遍历「位置」里所有根目录,按文件名匹配关键字 ----
|
|
164
|
+
// 宿主对单次插件调用有 15 秒硬超时,这里用 8 秒 deadline + 扫描/命中上限主动收敛,
|
|
165
|
+
// 并把系统/缓存等超大类目录跳过,避免全盘扫描拖死页面。
|
|
166
|
+
const SEARCH_HIDDEN = new Set([
|
|
167
|
+
'node_modules', '.git', '.svn', '.hg', '__pycache__', '.idea', '.vscode', 'target', 'dist', '.next', '.cache',
|
|
168
|
+
'$RECYCLE.BIN', 'System Volume Information', 'AppData', 'Windows', 'Program Files', 'Program Files (x86)',
|
|
169
|
+
'ProgramData', 'Recovery', 'PerfLogs', '$WinREAgent', 'OneDriveTemp',
|
|
170
|
+
])
|
|
171
|
+
async function searchFiles(query, list) {
|
|
172
|
+
const raw = String(query || '').trim()
|
|
173
|
+
const needle = raw.toLowerCase()
|
|
174
|
+
if (!needle) return { query: raw, results: [], scanned: 0, dirs: 0, truncated: false, timedOut: false }
|
|
175
|
+
const MAX_RESULTS = 300
|
|
176
|
+
const MAX_SCAN = 80000
|
|
177
|
+
const MAX_DEPTH = 14
|
|
178
|
+
const deadline = Date.now() + 8000
|
|
179
|
+
const results = []
|
|
180
|
+
let scanned = 0
|
|
181
|
+
let dirs = 0
|
|
182
|
+
let truncated = false
|
|
183
|
+
let timedOut = false
|
|
184
|
+
const stack = list.map(r => ({ dir: r.path, depth: 0, root: r.label }))
|
|
185
|
+
while (stack.length) {
|
|
186
|
+
if (results.length >= MAX_RESULTS || scanned >= MAX_SCAN || Date.now() > deadline) { truncated = true; timedOut = Date.now() > deadline; break }
|
|
187
|
+
const cur = stack.pop()
|
|
188
|
+
let ents
|
|
189
|
+
try { ents = await fs.promises.readdir(cur.dir, { withFileTypes: true }) } catch { continue }
|
|
190
|
+
dirs++
|
|
191
|
+
for (const e of ents) {
|
|
192
|
+
if (results.length >= MAX_RESULTS || scanned >= MAX_SCAN || Date.now() > deadline) { truncated = true; timedOut = Date.now() > deadline; break }
|
|
193
|
+
const name = e.name
|
|
194
|
+
if (!name || name.startsWith('.')) continue
|
|
195
|
+
if (e.isSymbolicLink && e.isSymbolicLink()) continue
|
|
196
|
+
const isDir = e.isDirectory()
|
|
197
|
+
if (isDir && SEARCH_HIDDEN.has(name)) continue
|
|
198
|
+
const full = path.join(cur.dir, name)
|
|
199
|
+
scanned++
|
|
200
|
+
if (!isDir) {
|
|
201
|
+
if (name.toLowerCase().includes(needle)) {
|
|
202
|
+
let size = 0, mtime = 0
|
|
203
|
+
try { const s = await fs.promises.stat(full); size = s.size; mtime = s.mtimeMs } catch { /* 用 0 兜底 */ }
|
|
204
|
+
results.push({ name, path: full, dir: cur.dir, root: cur.root, size, mtime })
|
|
205
|
+
}
|
|
206
|
+
} else if (cur.depth < MAX_DEPTH) {
|
|
207
|
+
stack.push({ dir: full, depth: cur.depth + 1, root: cur.root })
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
results.sort((a, b) => {
|
|
212
|
+
const ea = a.name.toLowerCase() === needle ? 1 : 0
|
|
213
|
+
const eb = b.name.toLowerCase() === needle ? 1 : 0
|
|
214
|
+
if (ea !== eb) return eb - ea
|
|
215
|
+
return (b.mtime || 0) - (a.mtime || 0)
|
|
216
|
+
})
|
|
217
|
+
return { query: raw, results, scanned, dirs, truncated, timedOut }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ---- Everything 索引搜索(优先):调用本机 Everything 的 HTTP JSON 接口 ----
|
|
221
|
+
// Everything 用 NTFS USN/MFT 索引,全盘搜索是毫秒级;没装/没开则返回 null,让上层回退到 searchFiles 遍历。
|
|
222
|
+
// 端口优先 8090(本机已配),再试默认 80 / 8080;命中后缓存端口。
|
|
223
|
+
const EVERYTHING_PORTS = [8090, 80, 8080]
|
|
224
|
+
let everythingPort = null
|
|
225
|
+
|
|
226
|
+
function httpGetJson(url, timeoutMs) {
|
|
227
|
+
return new Promise(resolve => {
|
|
228
|
+
let req
|
|
229
|
+
try {
|
|
230
|
+
req = http.get(url, res => {
|
|
231
|
+
let body = ''
|
|
232
|
+
res.on('data', d => { body += d })
|
|
233
|
+
res.on('end', () => { try { resolve(JSON.parse(body)) } catch { resolve(null) } })
|
|
234
|
+
res.on('error', () => resolve(null))
|
|
235
|
+
})
|
|
236
|
+
} catch { return resolve(null) }
|
|
237
|
+
req.on('error', () => resolve(null))
|
|
238
|
+
req.setTimeout(timeoutMs, () => { try { req.destroy() } catch { /* ignore */ } resolve(null) })
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function probeEverything(requireHits) {
|
|
243
|
+
// 缓存端口也必须验证存活:用户可能随时关掉 Everything,不能盲信缓存否则不会触发重新拉起
|
|
244
|
+
if (everythingPort) {
|
|
245
|
+
const j = await httpGetJson(`http://127.0.0.1:${everythingPort}/?json=1&search=&count=1`, 1500)
|
|
246
|
+
if (j && typeof j.totalResults === 'number' && (!requireHits || j.totalResults > 0)) return everythingPort
|
|
247
|
+
everythingPort = null // 已失效,清缓存,下面重新探测(必要时由 searchViaEverything 拉起)
|
|
248
|
+
}
|
|
249
|
+
for (const p of EVERYTHING_PORTS) {
|
|
250
|
+
const j = await httpGetJson(`http://127.0.0.1:${p}/?json=1&search=&count=1`, 1500)
|
|
251
|
+
if (j && typeof j.totalResults === 'number' && (!requireHits || j.totalResults > 0)) { everythingPort = p; return p }
|
|
252
|
+
}
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 首次启用 / 用户没开 Everything 时:后台拉起主程序,不弹出窗口(-minimized + windowsHide)。
|
|
257
|
+
function findEverythingExe() {
|
|
258
|
+
const cands = [
|
|
259
|
+
process.env.EVERYTHING_EXE,
|
|
260
|
+
'D:\\Everything\\Everything.exe',
|
|
261
|
+
'C:\\Program Files\\Everything\\Everything.exe',
|
|
262
|
+
'C:\\Program Files (x86)\\Everything\\Everything.exe',
|
|
263
|
+
]
|
|
264
|
+
for (const p of cands) { try { if (p && fs.existsSync(p)) return p } catch { /* ignore */ } }
|
|
265
|
+
return null
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function spawnEverythingBackground() {
|
|
269
|
+
const exe = findEverythingExe()
|
|
270
|
+
if (!exe) return false
|
|
271
|
+
try {
|
|
272
|
+
// -startup:按 ini 的 minimize_to_tray=1 隐藏到托盘(-minimized 只是最小化到任务栏,仍占任务栏)
|
|
273
|
+
const child = spawn(exe, ['-startup'], { detached: true, stdio: 'ignore', windowsHide: true })
|
|
274
|
+
child.unref()
|
|
275
|
+
return true
|
|
276
|
+
} catch { return false }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function waitForEverything(maxWaitMs) {
|
|
280
|
+
const deadline = Date.now() + maxWaitMs
|
|
281
|
+
while (Date.now() < deadline) {
|
|
282
|
+
const p = await probeEverything(true) // 等到索引真的有命中,而不只是 HTTP 通了
|
|
283
|
+
if (p) return p
|
|
284
|
+
await new Promise(r => setTimeout(r, 800))
|
|
285
|
+
}
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function filetimeToMs(ft) {
|
|
290
|
+
const n = Number(ft)
|
|
291
|
+
return Number.isFinite(n) && n > 0 ? Math.round(n / 10000 - 11644473600000) : 0
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function searchViaEverything(query, maxResults) {
|
|
295
|
+
let port = await probeEverything()
|
|
296
|
+
if (!port) {
|
|
297
|
+
// 首次启用 / 用户没开 Everything:后台拉起(-minimized + windowsHide,不显示窗口),最多等 10 秒索引就绪
|
|
298
|
+
if (spawnEverythingBackground()) port = await waitForEverything(10000)
|
|
299
|
+
if (!port) return null
|
|
300
|
+
}
|
|
301
|
+
// 用双引号包裹用户输入,避免 . * ? 空格等被 Everything 当作搜索语法解析
|
|
302
|
+
const wrapped = '"' + String(query).replace(/"/g, '') + '"'
|
|
303
|
+
const count = Math.max(1, Math.min(maxResults || 300, 1000))
|
|
304
|
+
const url = `http://127.0.0.1:${port}/?json=1&search=${encodeURIComponent(wrapped)}&count=${count}&path_column=1&size_column=1&date_modified_column=1`
|
|
305
|
+
const j = await httpGetJson(url, 8000)
|
|
306
|
+
if (!j || !Array.isArray(j.results)) return null
|
|
307
|
+
const results = j.results.map(x => {
|
|
308
|
+
const parent = x.path || ''
|
|
309
|
+
const full = parent ? path.join(parent, x.name || '') : (x.name || '')
|
|
310
|
+
return {
|
|
311
|
+
name: x.name || '', path: full, dir: parent,
|
|
312
|
+
isDir: x.type === 'folder', size: Number(x.size) || 0, mtime: filetimeToMs(x.date_modified),
|
|
313
|
+
}
|
|
314
|
+
})
|
|
315
|
+
return { query, results, total: j.totalResults || results.length, source: 'everything' }
|
|
316
|
+
}
|
|
317
|
+
|
|
168
318
|
async function loadConfig() {
|
|
169
319
|
const raw = await context.storage.read('config', 'prefs.json').catch(() => null)
|
|
170
320
|
if (!raw) return { roots: [], layout, sideW, recent, favorites, pinned, stash: undefined }
|
|
@@ -338,6 +488,17 @@ createPlugin({
|
|
|
338
488
|
return { name: row.name, ext: row.ext, kind: row.isDir ? 'dir' : kindOf(row.ext), size: row.size, mtime: row.mtime }
|
|
339
489
|
}),
|
|
340
490
|
|
|
491
|
+
// 全局文件搜索:有 Everything 走它的索引(秒级全盘),没有则回退 searchFiles 遍历
|
|
492
|
+
'preview.search': guard(async params => {
|
|
493
|
+
const q = String(params.query || '').trim()
|
|
494
|
+
if (q) {
|
|
495
|
+
const via = await searchViaEverything(q, 300)
|
|
496
|
+
if (via) return { ...via, scanned: null, dirs: null, truncated: via.total > via.results.length, timedOut: false }
|
|
497
|
+
}
|
|
498
|
+
const r = await searchFiles(q, activeRoots())
|
|
499
|
+
return { ...r, source: 'scan' }
|
|
500
|
+
}),
|
|
501
|
+
|
|
341
502
|
// ── stash 命令 ──────────────────────────────────────────────
|
|
342
503
|
|
|
343
504
|
'stash.list': guard(async () => stash.list()),
|
|
@@ -11,6 +11,19 @@ body.fm-layout, main.fm{height:820px;display:flex;flex-direction:column;padding:
|
|
|
11
11
|
.fm-head .fm-meta{color:var(--dock-muted);font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;max-width:52vw}
|
|
12
12
|
.fm-head .fm-meta:empty{display:none}
|
|
13
13
|
|
|
14
|
+
/* 预览工具栏右侧的全局搜索框(在「📌 固定当前文件夹」右边) */
|
|
15
|
+
.search-box{display:flex;align-items:center;gap:4px;margin-left:auto;min-width:0;max-width:260px;flex:0 1 auto}
|
|
16
|
+
.search-box input[type=search]{width:180px;min-width:80px;height:26px;padding:0 9px;font:inherit;font-size:12px;color:var(--dock-fg);background:var(--dock-control);border:1px solid var(--dock-line);border-radius:999px;outline:none}
|
|
17
|
+
.search-box input[type=search]:focus{border-color:var(--dock-accent)}
|
|
18
|
+
.search-box #search-clear{margin:0;padding:2px 7px;border-radius:999px;flex:none}
|
|
19
|
+
.search-head{padding:6px 2px 8px;color:var(--dock-muted);font-size:12px}
|
|
20
|
+
/* 搜索结果行用独立类 srow,彻底避开 .filelist .row 的多套列宽/隐藏规则冲突 */
|
|
21
|
+
.search-list .srow{display:grid;grid-template-columns:minmax(0,1.6fr) minmax(0,0.9fr) 74px;align-items:center;gap:8px;width:100%;text-align:left;border:0;background:transparent;border-radius:9px;padding:5px 9px;min-height:28px;cursor:pointer;font:inherit;color:inherit}
|
|
22
|
+
.search-list .srow:hover{background:var(--dock-control)}
|
|
23
|
+
.search-list .srow .nm{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;text-align:left;color:var(--dock-fg);font-size:13px}
|
|
24
|
+
.search-list .srow .dt{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;color:var(--dock-muted);font-size:11px;text-align:left}
|
|
25
|
+
.search-list .srow .sz{text-align:right;color:var(--dock-muted);font-size:11px;font-variant-numeric:tabular-nums;white-space:nowrap}
|
|
26
|
+
|
|
14
27
|
/* 错误条跨全宽 */
|
|
15
28
|
main.fm > #error{margin:0 8px 8px;flex:none}
|
|
16
29
|
|
|
@@ -76,6 +76,10 @@
|
|
|
76
76
|
<button id="refresh" title="刷新">刷新</button>
|
|
77
77
|
<button id="fav-open" title="常用文件夹" class="ghost">★</button>
|
|
78
78
|
<button id="fav-pin-btn" title="固定当前文件夹" class="ghost" hidden>📌</button>
|
|
79
|
+
<div id="search-box" class="search-box">
|
|
80
|
+
<input id="search-input" type="search" placeholder="搜索所有磁盘…" autocomplete="off" spellcheck="false" aria-label="搜索所有磁盘里的文件" title="输入关键字后停顿或回车,检索「位置」里所有磁盘/根目录下的文件">
|
|
81
|
+
<button id="search-clear" class="ghost tiny" type="button" title="清除搜索" hidden>✕</button>
|
|
82
|
+
</div>
|
|
79
83
|
<!-- 浮层必须放在 .preview-toolbar 里面:它是 position:relative 的定位祖先,
|
|
80
84
|
放到 nav 外面会以 #preview-col 为基准、top:100% 直接掉到整个预览列下面看不见。 -->
|
|
81
85
|
<div id="fav-pop" class="fav-pop" hidden>
|
package/plugins/filemgr/page.js
CHANGED
|
@@ -1311,6 +1311,94 @@
|
|
|
1311
1311
|
guard(() => openPath(value))()
|
|
1312
1312
|
}
|
|
1313
1313
|
|
|
1314
|
+
// ---------------------------------------------------------------- 全局文件搜索
|
|
1315
|
+
// 固定当前文件夹按钮右侧的搜索框:停止输入 350ms 或回车后,检索所有已添加磁盘/根目录的文件。
|
|
1316
|
+
let searchTimer = null
|
|
1317
|
+
let searchSeq = 0
|
|
1318
|
+
|
|
1319
|
+
function twoUp(dir) {
|
|
1320
|
+
const parts = String(dir || '').split(/[\\/]+/).filter(Boolean)
|
|
1321
|
+
if (parts.length <= 2) return parts.join('\\')
|
|
1322
|
+
return '…\\' + parts.slice(-2).join('\\')
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
function drawSearchResults(query, row) {
|
|
1326
|
+
const view = clear($('view'))
|
|
1327
|
+
const count = row ? row.results.length : 0
|
|
1328
|
+
let headText
|
|
1329
|
+
if (row && row.source === 'everything') {
|
|
1330
|
+
headText = `搜索「${query}」:找到 ${count} 项(Everything 索引,共匹配 ${row.total} 项)`
|
|
1331
|
+
if (row.truncated) headText += `,仅显示前 ${count} 项`
|
|
1332
|
+
} else {
|
|
1333
|
+
headText = `搜索「${query}」:找到 ${count} 项(扫描 ${row ? row.scanned : 0} 个文件)`
|
|
1334
|
+
if (row && row.truncated) headText += row.timedOut ? ',已达时间上限,结果可能不全' : ',已达数量上限,结果可能不全'
|
|
1335
|
+
}
|
|
1336
|
+
view.append(el('div', { class: 'search-head' }, [el('span', { text: headText })]))
|
|
1337
|
+
if (!row || !count) {
|
|
1338
|
+
view.append(el('p', { class: 'empty', text: row && row.scanned ? '没有匹配的文件' : '没有可搜索的磁盘或目录' }))
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1341
|
+
const list = el('div', { class: 'filelist search-list' })
|
|
1342
|
+
for (const item of row.results) {
|
|
1343
|
+
list.append(el('button', {
|
|
1344
|
+
type: 'button', class: 'srow',
|
|
1345
|
+
title: item.path,
|
|
1346
|
+
onclick: guard(async () => {
|
|
1347
|
+
$('search-input').value = ''
|
|
1348
|
+
$('search-clear').hidden = true
|
|
1349
|
+
await openPath(item.path)
|
|
1350
|
+
}),
|
|
1351
|
+
}, [
|
|
1352
|
+
el('span', { class: 'nm', text: item.name }),
|
|
1353
|
+
el('span', { class: 'dt', text: twoUp(item.dir) }),
|
|
1354
|
+
el('span', { class: 'sz', text: bytes(item.size) }),
|
|
1355
|
+
]))
|
|
1356
|
+
}
|
|
1357
|
+
view.append(list)
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function wireSearch() {
|
|
1361
|
+
const box = $('search-input')
|
|
1362
|
+
const clearBtn = $('search-clear')
|
|
1363
|
+
if (!box) return
|
|
1364
|
+
const run = async () => {
|
|
1365
|
+
const query = box.value.trim()
|
|
1366
|
+
if (clearBtn) clearBtn.hidden = !query
|
|
1367
|
+
if (!query) {
|
|
1368
|
+
// 清空搜索:恢复当前目录/文件视图
|
|
1369
|
+
if (state.file) guard(() => openFile(state.file.path))()
|
|
1370
|
+
else if (state.path) guard(() => openDir(state.path))()
|
|
1371
|
+
else drawDir()
|
|
1372
|
+
return
|
|
1373
|
+
}
|
|
1374
|
+
const seq = ++searchSeq
|
|
1375
|
+
say('正在搜索…')
|
|
1376
|
+
bad('')
|
|
1377
|
+
try {
|
|
1378
|
+
const row = await call('preview.search', { query })
|
|
1379
|
+
if (seq !== searchSeq) return
|
|
1380
|
+
say('')
|
|
1381
|
+
drawSearchResults(query, row)
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (seq !== searchSeq) return
|
|
1384
|
+
say('')
|
|
1385
|
+
bad(error && error.message ? error.message : String(error))
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
box.addEventListener('input', () => {
|
|
1389
|
+
const query = box.value.trim()
|
|
1390
|
+
if (clearBtn) clearBtn.hidden = !query
|
|
1391
|
+
if (searchTimer) { clearTimeout(searchTimer); searchTimer = null }
|
|
1392
|
+
if (!query) { run(); return }
|
|
1393
|
+
searchTimer = setTimeout(() => { searchTimer = null; run() }, 350)
|
|
1394
|
+
})
|
|
1395
|
+
box.addEventListener('keydown', event => {
|
|
1396
|
+
if (event.key === 'Enter') { event.preventDefault(); if (searchTimer) { clearTimeout(searchTimer); searchTimer = null } run() }
|
|
1397
|
+
else if (event.key === 'Escape') { box.value = ''; if (clearBtn) clearBtn.hidden = true; run() }
|
|
1398
|
+
})
|
|
1399
|
+
if (clearBtn) clearBtn.onclick = () => { box.value = ''; clearBtn.hidden = true; run() }
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1314
1402
|
// ---------------------------------------------------------------- 启动
|
|
1315
1403
|
|
|
1316
1404
|
async function bootPreview() {
|
|
@@ -2046,6 +2134,7 @@
|
|
|
2046
2134
|
wireFmSplit()
|
|
2047
2135
|
wireSplitter()
|
|
2048
2136
|
wireBrowseSelect()
|
|
2137
|
+
wireSearch()
|
|
2049
2138
|
drawSide()
|
|
2050
2139
|
// 两套 boot 只读数据、不互斥;不能用 guard(它会因共享 state.busy 把第二个启动挡掉)。
|
|
2051
2140
|
const bootSafe = fn => Promise.resolve().then(fn).catch(error => bad(error && error.message ? error.message : String(error)))
|