@yuuichi/openlist-assets 4.2.6-ym.3 → 4.2.6-ym.5
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/dist/assets/previews-CoVCUlRx.js +2 -2
- package/dist/assets/previews-legacy-DOedQYss.js +2 -2
- package/dist/brand/bgm/css/bgm-dock.css +350 -0
- package/dist/brand/bgm/js/bgm-dock.js +876 -0
- package/dist/brand/bgm/js/lib/aplayer/APlayer.min.css +3 -0
- package/dist/brand/bgm/js/lib/aplayer/APlayer.min.js +2 -0
- package/dist/brand/bgm/js/lib/aplayer/Meting.min.js +1 -0
- package/dist/brand/global-search.js +489 -69
- package/dist/brand/music-player.js +135 -542
- package/dist/brand/ym-bundle.516f792dd3.js +1733 -0
- package/dist/brand/ym-bundle.js +624 -611
- package/dist/brand/ym-bundle.manifest.json +4 -0
- package/dist/esa-entry.js +104 -103
- package/package.json +1 -1
|
@@ -1,118 +1,538 @@
|
|
|
1
1
|
(function () {
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/* =============================================================================
|
|
5
|
+
* 全局实时搜索
|
|
6
|
+
* - 去掉“全局”勾选框:搜索永远从根目录全局递归(在任何文件夹都能搜到全盘)。
|
|
7
|
+
* - 输入即搜:300ms 防抖,旧请求自动取消,结果逐个增量渲染。
|
|
8
|
+
* - 每条结果始终显示完整所在路径(parent),点击进入对应目录/文件。
|
|
9
|
+
* 实现方式:劫持官方搜索弹窗,隐藏其原生结果区与类型下拉,注入自定义结果层;
|
|
10
|
+
* 网络仍走同源 /api/fs/search(parent 固定 "/"),复用登录态。
|
|
11
|
+
* ============================================================================= */
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
var DEBOUNCE_MS = 300;
|
|
14
|
+
var state = {
|
|
15
|
+
modal: null,
|
|
16
|
+
input: null,
|
|
17
|
+
listEl: null,
|
|
18
|
+
statusEl: null,
|
|
19
|
+
scopeRow: null,
|
|
20
|
+
nativeList: null,
|
|
21
|
+
nativePagination: null,
|
|
22
|
+
nativeEmpty: null,
|
|
23
|
+
nativeSpinner: null,
|
|
24
|
+
seq: 0,
|
|
25
|
+
timer: null,
|
|
26
|
+
abort: null,
|
|
27
|
+
lastKeyword: "",
|
|
28
|
+
lastNodes: [],
|
|
29
|
+
scope: 0
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/* ---------- 工具 ---------- */
|
|
33
|
+
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
34
|
+
function $all(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
|
|
35
|
+
|
|
36
|
+
function isDark() {
|
|
37
|
+
return document.documentElement.getAttribute("data-theme") === "dark" ||
|
|
38
|
+
document.body.classList.contains("hope-ui-dark") ||
|
|
39
|
+
document.documentElement.classList.contains("dark");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function joinPath(parent, name) {
|
|
43
|
+
var p = (parent || "/").replace(/\/+$/, "");
|
|
44
|
+
if (!p) p = "/";
|
|
45
|
+
if (p === "/") return "/" + name;
|
|
46
|
+
return p + "/" + name;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 后端返回的 parent 是 URL 编码的虚拟路径(中文显示成 %E4%B8…)。
|
|
50
|
+
// 链接必须保留编码(路由按编码路径解析),仅展示文本逐段解码。
|
|
51
|
+
function decodePath(p) {
|
|
52
|
+
if (!p) return "/";
|
|
10
53
|
try {
|
|
11
|
-
return
|
|
54
|
+
return p.split("/").map(function (seg) {
|
|
55
|
+
if (!seg) return seg;
|
|
56
|
+
try { return decodeURIComponent(seg); } catch (e) { return seg; }
|
|
57
|
+
}).join("/");
|
|
58
|
+
} catch (e) { return p; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fmtSize(n) {
|
|
62
|
+
if (!n || n <= 0) return "";
|
|
63
|
+
var units = ["B", "KB", "MB", "GB", "TB"];
|
|
64
|
+
var i = 0;
|
|
65
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
66
|
+
return (i === 0 ? n : n.toFixed(1)) + " " + units[i];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
var FILE_ICON = {
|
|
70
|
+
video: "M8 5v14l11-7z",
|
|
71
|
+
audio: "M12 3v10.55A4 4 0 1 0 14 17V7h4V3z",
|
|
72
|
+
image: "M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5z",
|
|
73
|
+
doc: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM6 9V4h7v5h5v11H6z",
|
|
74
|
+
archive: "M21 8v13H3V8M1 3h22v5H1zM10 12h4"
|
|
75
|
+
};
|
|
76
|
+
var DIR_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="#f0b429"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8z"/></svg>';
|
|
77
|
+
var FILE_SVG = '<svg viewBox="0 0 24 24" width="18" height="18" fill="#7dd3fc"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM6 9V4h7v5h5v11H6z"/></svg>';
|
|
78
|
+
|
|
79
|
+
function highlight(text, kw) {
|
|
80
|
+
var safe = String(text == null ? "" : text);
|
|
81
|
+
if (!kw) return safe;
|
|
82
|
+
var idx = safe.toLowerCase().indexOf(kw.toLowerCase());
|
|
83
|
+
if (idx < 0) return safe;
|
|
84
|
+
return safe.slice(0, idx) +
|
|
85
|
+
'<mark class="ym-hl">' + safe.slice(idx, idx + kw.length) + "</mark>" +
|
|
86
|
+
safe.slice(idx + kw.length);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function iconFor(node) {
|
|
90
|
+
if (node.is_dir) return DIR_SVG;
|
|
91
|
+
var ext = String(node.name || "").split(".").pop().toLowerCase();
|
|
92
|
+
if (/\b(mp4|mkv|avi|mov|flv|webm|ts|m4v|rmvb)$/.test(ext)) return '<svg viewBox="0 0 24 24" width="18" height="18" fill="#f472b6"><path d="M8 5v14l11-7z"/></svg>';
|
|
93
|
+
if (/\b(mp3|flac|wav|aac|ogg|m4a|ape)$/.test(ext)) return '<svg viewBox="0 0 24 24" width="18" height="18" fill="#c084fc"><path d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3z"/></svg>';
|
|
94
|
+
if (/\b(jpg|jpeg|png|gif|webp|bmp|svg)$/.test(ext)) return '<svg viewBox="0 0 24 24" width="18" height="18" fill="#34d399"><path d="M21 19V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5z"/></svg>';
|
|
95
|
+
if (/\b(zip|rar|7z|tar|gz)$/.test(ext)) return '<svg viewBox="0 0 24 24" width="18" height="18" fill="#fbbf24"><path d="M21 8v13H3V8M1 3h22v5H1zM10 12h4"/></svg>';
|
|
96
|
+
return FILE_SVG;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* ---------- 样式 ---------- */
|
|
100
|
+
function injectCss() {
|
|
101
|
+
if (document.getElementById("ym-global-search-css")) return;
|
|
102
|
+
var st = document.createElement("style");
|
|
103
|
+
st.id = "ym-global-search-css";
|
|
104
|
+
st.textContent = [
|
|
105
|
+
"#ym-search-results{max-height:min(62vh,560px);overflow-y:auto;padding:4px 2px;}",
|
|
106
|
+
".ym-sr{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:10px;cursor:pointer;text-decoration:none;color:inherit;}",
|
|
107
|
+
".ym-sr:hover,.ym-sr:focus{background:rgba(14,165,233,.12);outline:none;}",
|
|
108
|
+
".ym-sr__ic{flex:0 0 auto;margin-top:2px;display:flex;}",
|
|
109
|
+
".ym-sr__main{flex:1;min-width:0;}",
|
|
110
|
+
".ym-sr__name{font-size:14px;font-weight:600;line-height:1.35;word-break:break-all;}",
|
|
111
|
+
".ym-sr__path{font-size:12px;opacity:.7;margin-top:2px;word-break:break-all;display:flex;align-items:center;gap:4px;}",
|
|
112
|
+
".ym-sr__size{font-size:11px;opacity:.6;margin-left:6px;}",
|
|
113
|
+
".ym-hl{background:#fde68a;color:#92400e;border-radius:3px;padding:0 1px;font-weight:700;}",
|
|
114
|
+
".ym-sstatus{padding:18px 10px;text-align:center;font-size:13px;opacity:.65;}",
|
|
115
|
+
"[role=dialog][data-ym-search] [data-ym-native-block]{display:none !important;}",
|
|
116
|
+
// 永远全局:隐藏官方“搜索范围”下拉(全部/文件夹/文件所在的整行控件)。
|
|
117
|
+
"[role=dialog][data-ym-search] [data-ym-scope-select]{display:none !important;}",
|
|
118
|
+
"[role=dialog][data-ym-search] [data-ym-empty-hint]{display:none !important;}",
|
|
119
|
+
".ym-sspinner{display:inline-block;width:16px;height:16px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;animation:ym-sspin .7s linear infinite;vertical-align:-3px;margin-right:8px;}",
|
|
120
|
+
"@keyframes ym-sspin{to{transform:rotate(360deg)}}",
|
|
121
|
+
].join("\n");
|
|
122
|
+
document.head.appendChild(st);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* ---------- 网络请求(同源,复用 cookie/token) ---------- */
|
|
126
|
+
function getToken() {
|
|
127
|
+
try {
|
|
128
|
+
var raw = localStorage.getItem("token");
|
|
129
|
+
if (!raw) return "";
|
|
130
|
+
raw = raw.trim();
|
|
131
|
+
// 官方在 localStorage.token 里存的是【裸 token】,axios 拦截器再补
|
|
132
|
+
// "Bearer " 前缀(见 store chunk:Authorization = localStorage.token)。
|
|
133
|
+
// 若历史数据里已带 Bearer 前缀,剥掉它,由 doSearch 统一补,避免出现
|
|
134
|
+
// "Bearer Bearer xxx" 导致 401。
|
|
135
|
+
var m = raw.match(/^Bearer\s+(.+)$/i);
|
|
136
|
+
if (m) raw = m[1].trim();
|
|
137
|
+
// 兼容 { token: "xxx" | "Bearer xxx" } 形态
|
|
138
|
+
try {
|
|
139
|
+
var t = JSON.parse(raw);
|
|
140
|
+
if (t && typeof t.token === "string") {
|
|
141
|
+
var mm = t.token.match(/^Bearer\s+(.+)$/i);
|
|
142
|
+
return mm ? mm[1].trim() : t.token;
|
|
143
|
+
}
|
|
144
|
+
} catch (e) {}
|
|
145
|
+
return raw;
|
|
12
146
|
} catch (e) {
|
|
13
|
-
return
|
|
147
|
+
return "";
|
|
14
148
|
}
|
|
15
149
|
}
|
|
16
150
|
|
|
17
|
-
function
|
|
18
|
-
|
|
151
|
+
async function doSearch(keyword, signal) {
|
|
152
|
+
var headers = { "Content-Type": "application/json" };
|
|
153
|
+
var token = getToken();
|
|
154
|
+
if (token) headers.Authorization = "Bearer " + token;
|
|
155
|
+
var res = await fetch("/api/fs/search", {
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: headers,
|
|
158
|
+
signal: signal,
|
|
159
|
+
body: JSON.stringify({
|
|
160
|
+
parent: "/", // 永远全局
|
|
161
|
+
keywords: keyword,
|
|
162
|
+
scope: 0, // 永远全局(官方范围下拉已隐藏);0 = 全部
|
|
163
|
+
page: 1,
|
|
164
|
+
per_page: 100,
|
|
165
|
+
password: ""
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
var j = await res.json();
|
|
169
|
+
if (j.code !== 200) throw new Error(j.message || "search failed");
|
|
170
|
+
return j.data || { content: [], total: 0 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* ---------- 渲染 ---------- */
|
|
174
|
+
function setStatus(html) {
|
|
175
|
+
state.statusEl.innerHTML = html;
|
|
176
|
+
state.statusEl.style.display = html ? "block" : "none";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function renderRows(nodes, kw, append) {
|
|
180
|
+
if (!append) {
|
|
181
|
+
state.listEl.innerHTML = "";
|
|
182
|
+
state.lastNodes = [];
|
|
183
|
+
}
|
|
184
|
+
var frag = document.createDocumentFragment();
|
|
185
|
+
nodes.forEach(function (node) {
|
|
186
|
+
var fullPath = joinPath(node.parent, node.name);
|
|
187
|
+
var a = document.createElement("a");
|
|
188
|
+
a.className = "ym-sr";
|
|
189
|
+
a.href = fullPath + "?from=search";
|
|
190
|
+
a.setAttribute("role", "button");
|
|
191
|
+
if (node.is_dir) a.setAttribute("data-dir", "1");
|
|
192
|
+
a.innerHTML =
|
|
193
|
+
'<span class="ym-sr__ic">' + iconFor(node) + "</span>" +
|
|
194
|
+
'<span class="ym-sr__main">' +
|
|
195
|
+
'<span class="ym-sr__name">' + highlight(node.name, kw) +
|
|
196
|
+
(!node.is_dir && node.size ? '<span class="ym-sr__size">' + fmtSize(node.size) + "</span>" : "") +
|
|
197
|
+
"</span>" +
|
|
198
|
+
'<span class="ym-sr__path">' +
|
|
199
|
+
'<svg viewBox="0 0 24 24" width="12" height="12" fill="currentColor" style="opacity:.7"><path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8z"/></svg>' +
|
|
200
|
+
"<span>" + highlight(decodePath(node.parent || "/"), kw) + "</span>" +
|
|
201
|
+
"</span>" +
|
|
202
|
+
"</span>";
|
|
203
|
+
frag.appendChild(a);
|
|
204
|
+
});
|
|
205
|
+
state.listEl.appendChild(frag);
|
|
206
|
+
state.lastNodes = state.lastNodes.concat(nodes);
|
|
19
207
|
}
|
|
20
208
|
|
|
21
|
-
function
|
|
209
|
+
async function runSearch(keyword) {
|
|
210
|
+
var mySeq = ++state.seq;
|
|
211
|
+
if (state.abort) { try { state.abort.abort(); } catch (e) {} }
|
|
212
|
+
var ctrl = ("AbortController" in window) ? new AbortController() : null;
|
|
213
|
+
state.abort = ctrl;
|
|
214
|
+
|
|
215
|
+
state.listEl.innerHTML = "";
|
|
216
|
+
state.lastNodes = [];
|
|
217
|
+
setStatus('<span class="ym-sspinner"></span>搜索中…');
|
|
218
|
+
|
|
219
|
+
if (!keyword) {
|
|
220
|
+
setStatus("");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
22
224
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
225
|
+
var data = await doSearch(keyword, ctrl ? ctrl.signal : undefined);
|
|
226
|
+
if (mySeq !== state.seq) return; // 已被更新的输入取代
|
|
227
|
+
var nodes = data.content || [];
|
|
228
|
+
renderRows(nodes, keyword, false);
|
|
229
|
+
// 我们已渲染结果(或明确空态):隐藏官方弹窗里的“暂无结果”原生文案
|
|
230
|
+
try { hideNativeEmptyHints(); } catch (e) {}
|
|
231
|
+
if (!nodes.length) {
|
|
232
|
+
setStatus("没有找到与 “" + keyword + "” 相关的内容");
|
|
233
|
+
} else {
|
|
234
|
+
setStatus("");
|
|
235
|
+
}
|
|
27
236
|
} catch (e) {
|
|
28
|
-
return
|
|
237
|
+
if (mySeq !== state.seq) return;
|
|
238
|
+
if (e && e.name === "AbortError") return;
|
|
239
|
+
setStatus("搜索失败:" + (e.message || "网络错误"));
|
|
29
240
|
}
|
|
30
241
|
}
|
|
31
242
|
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
243
|
+
function scheduleSearch() {
|
|
244
|
+
var kw = (state.input.value || "").trim();
|
|
245
|
+
state.lastKeyword = kw;
|
|
246
|
+
if (state.timer) clearTimeout(state.timer);
|
|
247
|
+
if (!kw) {
|
|
248
|
+
state.seq++;
|
|
249
|
+
if (state.abort) { try { state.abort.abort(); } catch (e) {} }
|
|
250
|
+
state.listEl.innerHTML = "";
|
|
251
|
+
setStatus("");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
state.timer = setTimeout(function () { runSearch(kw); }, DEBOUNCE_MS);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/* ---------- 劫持官方弹窗 ---------- */
|
|
258
|
+
function ensureOverlay() {
|
|
259
|
+
if (!state.modal) return;
|
|
260
|
+
// 幂等:重复 setAttribute 会触发属性观察器,必须避免无谓写操作
|
|
261
|
+
if (state.modal.getAttribute("data-ym-search") !== "1") {
|
|
262
|
+
state.modal.setAttribute("data-ym-search", "1");
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (state.listEl) return;
|
|
266
|
+
var box = document.createElement("div");
|
|
267
|
+
box.id = "ym-search-results";
|
|
268
|
+
var status = document.createElement("div");
|
|
269
|
+
status.className = "ym-sstatus";
|
|
270
|
+
status.style.display = "none";
|
|
271
|
+
|
|
272
|
+
// 插入到 modal body:该容器的直接子节点 = 输入卡片 + 原生结果块 + 我们两层。
|
|
273
|
+
var body = state.modal.querySelector(".hope-modal__body, [class*=modal__body]");
|
|
274
|
+
var mount = body || state.modal;
|
|
275
|
+
mount.appendChild(status);
|
|
276
|
+
mount.appendChild(box);
|
|
277
|
+
|
|
278
|
+
function hideScopeSelect() {
|
|
279
|
+
// 官方“搜索范围”下拉(全部/文件夹/文件)在输入卡片左侧,宽度 $32。
|
|
280
|
+
// 找到 #search-input 所在行内的 Hope Select 触发器并隐藏整行容器。
|
|
281
|
+
var input = state.modal.querySelector("#search-input");
|
|
282
|
+
if (!input) return;
|
|
283
|
+
var sel = state.modal.querySelector('[role="combobox"]');
|
|
284
|
+
if (!sel) return;
|
|
285
|
+
var holder = sel.closest(".hope-select, [class*=select]") || sel;
|
|
286
|
+
if (holder.getAttribute("data-ym-scope-select") !== "1") {
|
|
287
|
+
holder.setAttribute("data-ym-scope-select", "1");
|
|
288
|
+
}
|
|
289
|
+
// 从触发器向上,标记“不包含搜索框”的最外层包裹节点,即下拉独占的整行容器。
|
|
290
|
+
var wrap = holder;
|
|
291
|
+
for (var i = 0; i < 6 && wrap.parentElement; i++) {
|
|
292
|
+
var p = wrap.parentElement;
|
|
293
|
+
if (p.querySelector("#search-input")) break; // 再往上就会连带隐藏输入框
|
|
294
|
+
wrap = p;
|
|
295
|
+
}
|
|
296
|
+
if (wrap.getAttribute("data-ym-scope-select") !== "1") {
|
|
297
|
+
wrap.setAttribute("data-ym-scope-select", "1");
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function hideNativeEmptyHints() {
|
|
302
|
+
// 原生空态文案“暂无结果”是 <p class="hope-text">。在不同构建版本里它可能
|
|
303
|
+
// 位于输入卡片内,也可能与输入卡片平级(modal__body 直接子节点),因此在整个
|
|
304
|
+
// 弹窗范围内查找,但绝不标记包含 #search-input 的节点(输入框/按钮)。
|
|
305
|
+
if (!state.modal) return;
|
|
306
|
+
var ps = state.modal.querySelectorAll("p.hope-text, .hope-text");
|
|
307
|
+
Array.prototype.forEach.call(ps, function (p) {
|
|
308
|
+
if (p.querySelector && p.querySelector("#search-input")) return;
|
|
309
|
+
if (p.closest && p.closest("#ym-search-results,.ym-sstatus")) return;
|
|
310
|
+
var txt = (p.textContent || "").trim();
|
|
311
|
+
if (/暂无|没有|无结果|no\s*result|nothing|empty/i.test(txt)
|
|
312
|
+
&& p.getAttribute("data-ym-empty-hint") !== "1") {
|
|
313
|
+
p.setAttribute("data-ym-empty-hint", "1");
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function syncNative() {
|
|
319
|
+
Array.prototype.forEach.call(mount.children, function (ch) {
|
|
320
|
+
if (ch === box || ch === status) return;
|
|
321
|
+
if (ch.querySelector && ch.querySelector("#search-input")) {
|
|
322
|
+
// 输入卡片保留,但其内的原生“暂无结果”空态要隐藏(我们自己渲染结果)。
|
|
323
|
+
hideNativeEmptyHints();
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (ch.getAttribute("data-ym-native-block") !== "1") {
|
|
327
|
+
ch.setAttribute("data-ym-native-block", "1");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
hideScopeSelect();
|
|
331
|
+
hideNativeEmptyHints();
|
|
332
|
+
}
|
|
333
|
+
syncNative();
|
|
334
|
+
var snQueued = false, snRunning = false;
|
|
335
|
+
function snFlush() {
|
|
336
|
+
snQueued = false;
|
|
337
|
+
if (snRunning) return;
|
|
338
|
+
snRunning = true;
|
|
339
|
+
try { syncNative(); } finally { snRunning = false; }
|
|
340
|
+
}
|
|
341
|
+
new MutationObserver(function () {
|
|
342
|
+
if (snQueued) return;
|
|
343
|
+
snQueued = true;
|
|
344
|
+
(window.requestAnimationFrame || function (f) { setTimeout(f, 16); })(snFlush);
|
|
345
|
+
}).observe(mount, { childList: true, subtree: true });
|
|
346
|
+
|
|
347
|
+
state.listEl = box;
|
|
348
|
+
state.statusEl = status;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function bindInput() {
|
|
352
|
+
if (!state.input || state.input.__ymBound) return;
|
|
353
|
+
state.input.__ymBound = true;
|
|
354
|
+
state.input.setAttribute("placeholder", "全局搜索文件 / 文件夹…");
|
|
355
|
+
state.input.addEventListener("input", function () {
|
|
356
|
+
scheduleSearch();
|
|
357
|
+
});
|
|
358
|
+
// 回车立即搜(不等防抖)
|
|
359
|
+
state.input.addEventListener("keydown", function (e) {
|
|
360
|
+
if (e.key === "Enter") {
|
|
361
|
+
e.preventDefault();
|
|
362
|
+
e.stopPropagation();
|
|
363
|
+
if (state.timer) clearTimeout(state.timer);
|
|
364
|
+
runSearch((state.input.value || "").trim());
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* 拦截官方 /fs/search:官方前端用 axios(XHR),不是 fetch。
|
|
370
|
+
重写请求体里的 parent 为 "/",保证即便走官方自己的搜索通道也是全局。 */
|
|
371
|
+
function rewriteSearchBody(bodyText) {
|
|
372
|
+
try {
|
|
373
|
+
var data = JSON.parse(bodyText);
|
|
374
|
+
if (data && typeof data === "object" && "keywords" in data) {
|
|
375
|
+
data.parent = "/"; // 永远全局
|
|
376
|
+
return JSON.stringify(data);
|
|
377
|
+
}
|
|
378
|
+
} catch (e) {}
|
|
379
|
+
return bodyText;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function patchFetch() {
|
|
383
|
+
if (window.__ymGlobalFetchPatched) return;
|
|
384
|
+
window.__ymGlobalFetchPatched = true;
|
|
35
385
|
var ofetch = window.fetch;
|
|
36
386
|
window.fetch = function (input, init) {
|
|
37
387
|
try {
|
|
38
388
|
var url = typeof input === "string" ? input : input && input.url;
|
|
39
389
|
if (url && url.indexOf("/fs/search") !== -1 && init && typeof init.body === "string") {
|
|
40
|
-
|
|
390
|
+
var nb = rewriteSearchBody(init.body);
|
|
391
|
+
if (nb !== init.body) init = Object.assign({}, init, { body: nb });
|
|
41
392
|
}
|
|
42
393
|
} catch (e) {}
|
|
43
394
|
return ofetch.apply(this, arguments);
|
|
44
395
|
};
|
|
45
396
|
}
|
|
46
397
|
|
|
47
|
-
function
|
|
48
|
-
if (window.
|
|
49
|
-
window.
|
|
50
|
-
var
|
|
51
|
-
var
|
|
398
|
+
function patchXhr() {
|
|
399
|
+
if (window.__ymGlobalXhrPatched) return;
|
|
400
|
+
window.__ymGlobalXhrPatched = true;
|
|
401
|
+
var OOpen = XMLHttpRequest.prototype.open;
|
|
402
|
+
var OSend = XMLHttpRequest.prototype.send;
|
|
52
403
|
XMLHttpRequest.prototype.open = function (method, url) {
|
|
53
|
-
this.
|
|
54
|
-
return
|
|
404
|
+
this.__ymIsSearch = typeof url === "string" && url.indexOf("/fs/search") !== -1;
|
|
405
|
+
return OOpen.apply(this, arguments);
|
|
55
406
|
};
|
|
56
407
|
XMLHttpRequest.prototype.send = function (body) {
|
|
57
408
|
try {
|
|
58
|
-
if (this.
|
|
59
|
-
|
|
409
|
+
if (this.__ymIsSearch && typeof body === "string") {
|
|
410
|
+
var nb = rewriteSearchBody(body);
|
|
411
|
+
if (nb !== body) body = nb;
|
|
60
412
|
}
|
|
61
413
|
} catch (e) {}
|
|
62
|
-
return
|
|
414
|
+
return OSend.call(this, body);
|
|
63
415
|
};
|
|
64
416
|
}
|
|
65
417
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
418
|
+
// 弹窗关闭 / SPA 路由切换(点搜索结果进文件夹)后,Solid 会把旧弹窗移出文档。
|
|
419
|
+
// 此时必须重置缓存的 modal/input,否则下次打开的是【新弹窗节点】,而我们仍持有
|
|
420
|
+
// 游离的旧节点,自定义结果层不会挂回去,看起来“变回了原来的搜索界面”。
|
|
421
|
+
function resetModalState() {
|
|
422
|
+
state.modal = null;
|
|
423
|
+
state.input = null;
|
|
424
|
+
state.listEl = null;
|
|
425
|
+
state.statusEl = null;
|
|
426
|
+
state.lastKeyword = "";
|
|
427
|
+
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
|
|
428
|
+
if (state.abort) { try { state.abort.abort(); } catch (e) {} state.abort = null; }
|
|
429
|
+
state.seq++;
|
|
78
430
|
}
|
|
79
431
|
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return null;
|
|
432
|
+
function isDetached(el) {
|
|
433
|
+
if (!el) return true;
|
|
434
|
+
if (!el.isConnected) return true;
|
|
435
|
+
// 旧弹窗节点:仍在 DOM 中但已不可见(Solid 复用/卸载),也视为失效
|
|
436
|
+
try { if (el.getBoundingClientRect().width === 0) return true; } catch (e) {}
|
|
437
|
+
return false;
|
|
87
438
|
}
|
|
88
439
|
|
|
89
|
-
function
|
|
90
|
-
|
|
91
|
-
if (
|
|
440
|
+
function hookModal() {
|
|
441
|
+
// 已缓存的弹窗已脱离文档:SPA 导航后清理,确保重新 hook 新弹窗
|
|
442
|
+
if (state.modal && isDetached(state.modal)) {
|
|
443
|
+
resetModalState();
|
|
444
|
+
}
|
|
445
|
+
var modal = null;
|
|
446
|
+
var dialogs = $all('[role="dialog"]');
|
|
447
|
+
for (var i = 0; i < dialogs.length; i++) {
|
|
448
|
+
if (dialogs[i].querySelector("#search-input") && dialogs[i].getBoundingClientRect().width > 0 && dialogs[i].isConnected) {
|
|
449
|
+
modal = dialogs[i];
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (!modal) return;
|
|
454
|
+
|
|
92
455
|
var input = modal.querySelector("#search-input");
|
|
93
456
|
if (!input) return;
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
457
|
+
|
|
458
|
+
var isNewModal = state.modal !== modal;
|
|
459
|
+
state.modal = modal;
|
|
460
|
+
state.input = input;
|
|
461
|
+
// 弹窗 DOM 在 Solid 中复用;overlay 只建一次并缓存在节点上,避免重复插入。
|
|
462
|
+
if (modal.__ymListEl) {
|
|
463
|
+
state.listEl = modal.__ymListEl;
|
|
464
|
+
state.statusEl = modal.__ymStatusEl;
|
|
465
|
+
} else {
|
|
466
|
+
state.listEl = null;
|
|
467
|
+
state.statusEl = null;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
ensureOverlay();
|
|
471
|
+
modal.__ymListEl = state.listEl;
|
|
472
|
+
modal.__ymStatusEl = state.statusEl;
|
|
473
|
+
bindInput();
|
|
474
|
+
|
|
475
|
+
if (isNewModal) {
|
|
476
|
+
// 弹窗打开时聚焦并按当前内容立即搜
|
|
477
|
+
setTimeout(function () {
|
|
478
|
+
try { input.focus(); } catch (e) {}
|
|
479
|
+
var kw = (input.value || "").trim();
|
|
480
|
+
if (kw) runSearch(kw);
|
|
481
|
+
}, 50);
|
|
482
|
+
}
|
|
107
483
|
}
|
|
108
484
|
|
|
109
485
|
function boot() {
|
|
110
486
|
injectCss();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
487
|
+
patchFetch();
|
|
488
|
+
patchXhr();
|
|
489
|
+
// 只监听节点增删(不监听 attributes,避免我们自己 setAttribute 引发观察器回调风暴);
|
|
490
|
+
// 用 rAF 合并高频回调,并防重入。
|
|
491
|
+
var queued = false, running = false;
|
|
492
|
+
function flush() {
|
|
493
|
+
queued = false;
|
|
494
|
+
if (running) return;
|
|
495
|
+
if (!document.querySelector("#search-input")) return;
|
|
496
|
+
running = true;
|
|
497
|
+
try { hookModal(); } finally { running = false; }
|
|
498
|
+
}
|
|
499
|
+
var mo = new MutationObserver(function () {
|
|
500
|
+
if (queued) return;
|
|
501
|
+
queued = true;
|
|
502
|
+
(window.requestAnimationFrame || function (f) { setTimeout(f, 16); })(flush);
|
|
503
|
+
});
|
|
504
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
505
|
+
|
|
506
|
+
// SPA 路由切换(点搜索结果进文件夹):旧弹窗被卸载,复位缓存,使下次打开
|
|
507
|
+
// 搜索时重新 hook 新弹窗,而不是停留在旧的原生搜索界面直到整页刷新。
|
|
508
|
+
// 用【只读轮询 pathname】而非包装 history.pushState/replaceState —— 后者可能
|
|
509
|
+
// 干扰 Solid 路由器(曾导致导航后转圈/卡死)。轮询零副作用、最稳。
|
|
510
|
+
var lastPath = location.pathname + location.search;
|
|
511
|
+
function onRouteChange() {
|
|
512
|
+
lastPath = location.pathname + location.search;
|
|
513
|
+
setTimeout(function () {
|
|
514
|
+
if (isDetached(state.modal)) resetModalState();
|
|
515
|
+
}, 0);
|
|
516
|
+
}
|
|
517
|
+
setInterval(function () {
|
|
518
|
+
var cur = location.pathname + location.search;
|
|
519
|
+
if (cur !== lastPath) {
|
|
520
|
+
lastPath = cur;
|
|
521
|
+
setTimeout(function () {
|
|
522
|
+
if (isDetached(state.modal)) resetModalState();
|
|
523
|
+
}, 0);
|
|
524
|
+
}
|
|
525
|
+
}, 300);
|
|
526
|
+
window.addEventListener("popstate", onRouteChange);
|
|
527
|
+
window.addEventListener("hashchange", onRouteChange);
|
|
528
|
+
|
|
529
|
+
// 弹窗关闭时复位
|
|
530
|
+
document.addEventListener("click", function (e) {
|
|
531
|
+
var t = e.target;
|
|
532
|
+
if (t && t.closest && t.closest(".hope-modal__overlay, [class*=modal__overlay]")) {
|
|
533
|
+
state.seq++;
|
|
534
|
+
}
|
|
535
|
+
}, true);
|
|
116
536
|
}
|
|
117
537
|
|
|
118
538
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|