@yuuichi/openlist-assets 4.2.6-ym.3 → 4.2.6-ym.4
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
package/dist/brand/ym-bundle.js
CHANGED
|
@@ -1024,118 +1024,538 @@
|
|
|
1024
1024
|
(function () {
|
|
1025
1025
|
"use strict";
|
|
1026
1026
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1027
|
+
/* =============================================================================
|
|
1028
|
+
* 全局实时搜索
|
|
1029
|
+
* - 去掉“全局”勾选框:搜索永远从根目录全局递归(在任何文件夹都能搜到全盘)。
|
|
1030
|
+
* - 输入即搜:300ms 防抖,旧请求自动取消,结果逐个增量渲染。
|
|
1031
|
+
* - 每条结果始终显示完整所在路径(parent),点击进入对应目录/文件。
|
|
1032
|
+
* 实现方式:劫持官方搜索弹窗,隐藏其原生结果区与类型下拉,注入自定义结果层;
|
|
1033
|
+
* 网络仍走同源 /api/fs/search(parent 固定 "/"),复用登录态。
|
|
1034
|
+
* ============================================================================= */
|
|
1035
|
+
|
|
1036
|
+
var DEBOUNCE_MS = 300;
|
|
1037
|
+
var state = {
|
|
1038
|
+
modal: null,
|
|
1039
|
+
input: null,
|
|
1040
|
+
listEl: null,
|
|
1041
|
+
statusEl: null,
|
|
1042
|
+
scopeRow: null,
|
|
1043
|
+
nativeList: null,
|
|
1044
|
+
nativePagination: null,
|
|
1045
|
+
nativeEmpty: null,
|
|
1046
|
+
nativeSpinner: null,
|
|
1047
|
+
seq: 0,
|
|
1048
|
+
timer: null,
|
|
1049
|
+
abort: null,
|
|
1050
|
+
lastKeyword: "",
|
|
1051
|
+
lastNodes: [],
|
|
1052
|
+
scope: 0
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
/* ---------- 工具 ---------- */
|
|
1056
|
+
function $(sel, root) { return (root || document).querySelector(sel); }
|
|
1057
|
+
function $all(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); }
|
|
1058
|
+
|
|
1059
|
+
function isDark() {
|
|
1060
|
+
return document.documentElement.getAttribute("data-theme") === "dark" ||
|
|
1061
|
+
document.body.classList.contains("hope-ui-dark") ||
|
|
1062
|
+
document.documentElement.classList.contains("dark");
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function joinPath(parent, name) {
|
|
1066
|
+
var p = (parent || "/").replace(/\/+$/, "");
|
|
1067
|
+
if (!p) p = "/";
|
|
1068
|
+
if (p === "/") return "/" + name;
|
|
1069
|
+
return p + "/" + name;
|
|
1070
|
+
}
|
|
1031
1071
|
|
|
1032
|
-
|
|
1072
|
+
// 后端返回的 parent 是 URL 编码的虚拟路径(中文显示成 %E4%B8…)。
|
|
1073
|
+
// 链接必须保留编码(路由按编码路径解析),仅展示文本逐段解码。
|
|
1074
|
+
function decodePath(p) {
|
|
1075
|
+
if (!p) return "/";
|
|
1033
1076
|
try {
|
|
1034
|
-
return
|
|
1077
|
+
return p.split("/").map(function (seg) {
|
|
1078
|
+
if (!seg) return seg;
|
|
1079
|
+
try { return decodeURIComponent(seg); } catch (e) { return seg; }
|
|
1080
|
+
}).join("/");
|
|
1081
|
+
} catch (e) { return p; }
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function fmtSize(n) {
|
|
1085
|
+
if (!n || n <= 0) return "";
|
|
1086
|
+
var units = ["B", "KB", "MB", "GB", "TB"];
|
|
1087
|
+
var i = 0;
|
|
1088
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
1089
|
+
return (i === 0 ? n : n.toFixed(1)) + " " + units[i];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
var FILE_ICON = {
|
|
1093
|
+
video: "M8 5v14l11-7z",
|
|
1094
|
+
audio: "M12 3v10.55A4 4 0 1 0 14 17V7h4V3z",
|
|
1095
|
+
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",
|
|
1096
|
+
doc: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM6 9V4h7v5h5v11H6z",
|
|
1097
|
+
archive: "M21 8v13H3V8M1 3h22v5H1zM10 12h4"
|
|
1098
|
+
};
|
|
1099
|
+
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>';
|
|
1100
|
+
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>';
|
|
1101
|
+
|
|
1102
|
+
function highlight(text, kw) {
|
|
1103
|
+
var safe = String(text == null ? "" : text);
|
|
1104
|
+
if (!kw) return safe;
|
|
1105
|
+
var idx = safe.toLowerCase().indexOf(kw.toLowerCase());
|
|
1106
|
+
if (idx < 0) return safe;
|
|
1107
|
+
return safe.slice(0, idx) +
|
|
1108
|
+
'<mark class="ym-hl">' + safe.slice(idx, idx + kw.length) + "</mark>" +
|
|
1109
|
+
safe.slice(idx + kw.length);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
function iconFor(node) {
|
|
1113
|
+
if (node.is_dir) return DIR_SVG;
|
|
1114
|
+
var ext = String(node.name || "").split(".").pop().toLowerCase();
|
|
1115
|
+
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>';
|
|
1116
|
+
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>';
|
|
1117
|
+
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>';
|
|
1118
|
+
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>';
|
|
1119
|
+
return FILE_SVG;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
/* ---------- 样式 ---------- */
|
|
1123
|
+
function injectCss() {
|
|
1124
|
+
if (document.getElementById("ym-global-search-css")) return;
|
|
1125
|
+
var st = document.createElement("style");
|
|
1126
|
+
st.id = "ym-global-search-css";
|
|
1127
|
+
st.textContent = [
|
|
1128
|
+
"#ym-search-results{max-height:min(62vh,560px);overflow-y:auto;padding:4px 2px;}",
|
|
1129
|
+
".ym-sr{display:flex;align-items:flex-start;gap:10px;padding:9px 10px;border-radius:10px;cursor:pointer;text-decoration:none;color:inherit;}",
|
|
1130
|
+
".ym-sr:hover,.ym-sr:focus{background:rgba(14,165,233,.12);outline:none;}",
|
|
1131
|
+
".ym-sr__ic{flex:0 0 auto;margin-top:2px;display:flex;}",
|
|
1132
|
+
".ym-sr__main{flex:1;min-width:0;}",
|
|
1133
|
+
".ym-sr__name{font-size:14px;font-weight:600;line-height:1.35;word-break:break-all;}",
|
|
1134
|
+
".ym-sr__path{font-size:12px;opacity:.7;margin-top:2px;word-break:break-all;display:flex;align-items:center;gap:4px;}",
|
|
1135
|
+
".ym-sr__size{font-size:11px;opacity:.6;margin-left:6px;}",
|
|
1136
|
+
".ym-hl{background:#fde68a;color:#92400e;border-radius:3px;padding:0 1px;font-weight:700;}",
|
|
1137
|
+
".ym-sstatus{padding:18px 10px;text-align:center;font-size:13px;opacity:.65;}",
|
|
1138
|
+
"[role=dialog][data-ym-search] [data-ym-native-block]{display:none !important;}",
|
|
1139
|
+
// 永远全局:隐藏官方“搜索范围”下拉(全部/文件夹/文件所在的整行控件)。
|
|
1140
|
+
"[role=dialog][data-ym-search] [data-ym-scope-select]{display:none !important;}",
|
|
1141
|
+
"[role=dialog][data-ym-search] [data-ym-empty-hint]{display:none !important;}",
|
|
1142
|
+
".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;}",
|
|
1143
|
+
"@keyframes ym-sspin{to{transform:rotate(360deg)}}",
|
|
1144
|
+
].join("\n");
|
|
1145
|
+
document.head.appendChild(st);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/* ---------- 网络请求(同源,复用 cookie/token) ---------- */
|
|
1149
|
+
function getToken() {
|
|
1150
|
+
try {
|
|
1151
|
+
var raw = localStorage.getItem("token");
|
|
1152
|
+
if (!raw) return "";
|
|
1153
|
+
raw = raw.trim();
|
|
1154
|
+
// 官方在 localStorage.token 里存的是【裸 token】,axios 拦截器再补
|
|
1155
|
+
// "Bearer " 前缀(见 store chunk:Authorization = localStorage.token)。
|
|
1156
|
+
// 若历史数据里已带 Bearer 前缀,剥掉它,由 doSearch 统一补,避免出现
|
|
1157
|
+
// "Bearer Bearer xxx" 导致 401。
|
|
1158
|
+
var m = raw.match(/^Bearer\s+(.+)$/i);
|
|
1159
|
+
if (m) raw = m[1].trim();
|
|
1160
|
+
// 兼容 { token: "xxx" | "Bearer xxx" } 形态
|
|
1161
|
+
try {
|
|
1162
|
+
var t = JSON.parse(raw);
|
|
1163
|
+
if (t && typeof t.token === "string") {
|
|
1164
|
+
var mm = t.token.match(/^Bearer\s+(.+)$/i);
|
|
1165
|
+
return mm ? mm[1].trim() : t.token;
|
|
1166
|
+
}
|
|
1167
|
+
} catch (e) {}
|
|
1168
|
+
return raw;
|
|
1035
1169
|
} catch (e) {
|
|
1036
|
-
return
|
|
1170
|
+
return "";
|
|
1037
1171
|
}
|
|
1038
1172
|
}
|
|
1039
1173
|
|
|
1040
|
-
function
|
|
1041
|
-
|
|
1174
|
+
async function doSearch(keyword, signal) {
|
|
1175
|
+
var headers = { "Content-Type": "application/json" };
|
|
1176
|
+
var token = getToken();
|
|
1177
|
+
if (token) headers.Authorization = "Bearer " + token;
|
|
1178
|
+
var res = await fetch("/api/fs/search", {
|
|
1179
|
+
method: "POST",
|
|
1180
|
+
headers: headers,
|
|
1181
|
+
signal: signal,
|
|
1182
|
+
body: JSON.stringify({
|
|
1183
|
+
parent: "/", // 永远全局
|
|
1184
|
+
keywords: keyword,
|
|
1185
|
+
scope: 0, // 永远全局(官方范围下拉已隐藏);0 = 全部
|
|
1186
|
+
page: 1,
|
|
1187
|
+
per_page: 100,
|
|
1188
|
+
password: ""
|
|
1189
|
+
})
|
|
1190
|
+
});
|
|
1191
|
+
var j = await res.json();
|
|
1192
|
+
if (j.code !== 200) throw new Error(j.message || "search failed");
|
|
1193
|
+
return j.data || { content: [], total: 0 };
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/* ---------- 渲染 ---------- */
|
|
1197
|
+
function setStatus(html) {
|
|
1198
|
+
state.statusEl.innerHTML = html;
|
|
1199
|
+
state.statusEl.style.display = html ? "block" : "none";
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function renderRows(nodes, kw, append) {
|
|
1203
|
+
if (!append) {
|
|
1204
|
+
state.listEl.innerHTML = "";
|
|
1205
|
+
state.lastNodes = [];
|
|
1206
|
+
}
|
|
1207
|
+
var frag = document.createDocumentFragment();
|
|
1208
|
+
nodes.forEach(function (node) {
|
|
1209
|
+
var fullPath = joinPath(node.parent, node.name);
|
|
1210
|
+
var a = document.createElement("a");
|
|
1211
|
+
a.className = "ym-sr";
|
|
1212
|
+
a.href = fullPath + "?from=search";
|
|
1213
|
+
a.setAttribute("role", "button");
|
|
1214
|
+
if (node.is_dir) a.setAttribute("data-dir", "1");
|
|
1215
|
+
a.innerHTML =
|
|
1216
|
+
'<span class="ym-sr__ic">' + iconFor(node) + "</span>" +
|
|
1217
|
+
'<span class="ym-sr__main">' +
|
|
1218
|
+
'<span class="ym-sr__name">' + highlight(node.name, kw) +
|
|
1219
|
+
(!node.is_dir && node.size ? '<span class="ym-sr__size">' + fmtSize(node.size) + "</span>" : "") +
|
|
1220
|
+
"</span>" +
|
|
1221
|
+
'<span class="ym-sr__path">' +
|
|
1222
|
+
'<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>' +
|
|
1223
|
+
"<span>" + highlight(decodePath(node.parent || "/"), kw) + "</span>" +
|
|
1224
|
+
"</span>" +
|
|
1225
|
+
"</span>";
|
|
1226
|
+
frag.appendChild(a);
|
|
1227
|
+
});
|
|
1228
|
+
state.listEl.appendChild(frag);
|
|
1229
|
+
state.lastNodes = state.lastNodes.concat(nodes);
|
|
1042
1230
|
}
|
|
1043
1231
|
|
|
1044
|
-
function
|
|
1232
|
+
async function runSearch(keyword) {
|
|
1233
|
+
var mySeq = ++state.seq;
|
|
1234
|
+
if (state.abort) { try { state.abort.abort(); } catch (e) {} }
|
|
1235
|
+
var ctrl = ("AbortController" in window) ? new AbortController() : null;
|
|
1236
|
+
state.abort = ctrl;
|
|
1237
|
+
|
|
1238
|
+
state.listEl.innerHTML = "";
|
|
1239
|
+
state.lastNodes = [];
|
|
1240
|
+
setStatus('<span class="ym-sspinner"></span>搜索中…');
|
|
1241
|
+
|
|
1242
|
+
if (!keyword) {
|
|
1243
|
+
setStatus("");
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1045
1247
|
try {
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1248
|
+
var data = await doSearch(keyword, ctrl ? ctrl.signal : undefined);
|
|
1249
|
+
if (mySeq !== state.seq) return; // 已被更新的输入取代
|
|
1250
|
+
var nodes = data.content || [];
|
|
1251
|
+
renderRows(nodes, keyword, false);
|
|
1252
|
+
// 我们已渲染结果(或明确空态):隐藏官方弹窗里的“暂无结果”原生文案
|
|
1253
|
+
try { hideNativeEmptyHints(); } catch (e) {}
|
|
1254
|
+
if (!nodes.length) {
|
|
1255
|
+
setStatus("没有找到与 “" + keyword + "” 相关的内容");
|
|
1256
|
+
} else {
|
|
1257
|
+
setStatus("");
|
|
1258
|
+
}
|
|
1050
1259
|
} catch (e) {
|
|
1051
|
-
return
|
|
1260
|
+
if (mySeq !== state.seq) return;
|
|
1261
|
+
if (e && e.name === "AbortError") return;
|
|
1262
|
+
setStatus("搜索失败:" + (e.message || "网络错误"));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
function scheduleSearch() {
|
|
1267
|
+
var kw = (state.input.value || "").trim();
|
|
1268
|
+
state.lastKeyword = kw;
|
|
1269
|
+
if (state.timer) clearTimeout(state.timer);
|
|
1270
|
+
if (!kw) {
|
|
1271
|
+
state.seq++;
|
|
1272
|
+
if (state.abort) { try { state.abort.abort(); } catch (e) {} }
|
|
1273
|
+
state.listEl.innerHTML = "";
|
|
1274
|
+
setStatus("");
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
state.timer = setTimeout(function () { runSearch(kw); }, DEBOUNCE_MS);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/* ---------- 劫持官方弹窗 ---------- */
|
|
1281
|
+
function ensureOverlay() {
|
|
1282
|
+
if (!state.modal) return;
|
|
1283
|
+
// 幂等:重复 setAttribute 会触发属性观察器,必须避免无谓写操作
|
|
1284
|
+
if (state.modal.getAttribute("data-ym-search") !== "1") {
|
|
1285
|
+
state.modal.setAttribute("data-ym-search", "1");
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
if (state.listEl) return;
|
|
1289
|
+
var box = document.createElement("div");
|
|
1290
|
+
box.id = "ym-search-results";
|
|
1291
|
+
var status = document.createElement("div");
|
|
1292
|
+
status.className = "ym-sstatus";
|
|
1293
|
+
status.style.display = "none";
|
|
1294
|
+
|
|
1295
|
+
// 插入到 modal body:该容器的直接子节点 = 输入卡片 + 原生结果块 + 我们两层。
|
|
1296
|
+
var body = state.modal.querySelector(".hope-modal__body, [class*=modal__body]");
|
|
1297
|
+
var mount = body || state.modal;
|
|
1298
|
+
mount.appendChild(status);
|
|
1299
|
+
mount.appendChild(box);
|
|
1300
|
+
|
|
1301
|
+
function hideScopeSelect() {
|
|
1302
|
+
// 官方“搜索范围”下拉(全部/文件夹/文件)在输入卡片左侧,宽度 $32。
|
|
1303
|
+
// 找到 #search-input 所在行内的 Hope Select 触发器并隐藏整行容器。
|
|
1304
|
+
var input = state.modal.querySelector("#search-input");
|
|
1305
|
+
if (!input) return;
|
|
1306
|
+
var sel = state.modal.querySelector('[role="combobox"]');
|
|
1307
|
+
if (!sel) return;
|
|
1308
|
+
var holder = sel.closest(".hope-select, [class*=select]") || sel;
|
|
1309
|
+
if (holder.getAttribute("data-ym-scope-select") !== "1") {
|
|
1310
|
+
holder.setAttribute("data-ym-scope-select", "1");
|
|
1311
|
+
}
|
|
1312
|
+
// 从触发器向上,标记“不包含搜索框”的最外层包裹节点,即下拉独占的整行容器。
|
|
1313
|
+
var wrap = holder;
|
|
1314
|
+
for (var i = 0; i < 6 && wrap.parentElement; i++) {
|
|
1315
|
+
var p = wrap.parentElement;
|
|
1316
|
+
if (p.querySelector("#search-input")) break; // 再往上就会连带隐藏输入框
|
|
1317
|
+
wrap = p;
|
|
1318
|
+
}
|
|
1319
|
+
if (wrap.getAttribute("data-ym-scope-select") !== "1") {
|
|
1320
|
+
wrap.setAttribute("data-ym-scope-select", "1");
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function hideNativeEmptyHints() {
|
|
1325
|
+
// 原生空态文案“暂无结果”是 <p class="hope-text">。在不同构建版本里它可能
|
|
1326
|
+
// 位于输入卡片内,也可能与输入卡片平级(modal__body 直接子节点),因此在整个
|
|
1327
|
+
// 弹窗范围内查找,但绝不标记包含 #search-input 的节点(输入框/按钮)。
|
|
1328
|
+
if (!state.modal) return;
|
|
1329
|
+
var ps = state.modal.querySelectorAll("p.hope-text, .hope-text");
|
|
1330
|
+
Array.prototype.forEach.call(ps, function (p) {
|
|
1331
|
+
if (p.querySelector && p.querySelector("#search-input")) return;
|
|
1332
|
+
if (p.closest && p.closest("#ym-search-results,.ym-sstatus")) return;
|
|
1333
|
+
var txt = (p.textContent || "").trim();
|
|
1334
|
+
if (/暂无|没有|无结果|no\s*result|nothing|empty/i.test(txt)
|
|
1335
|
+
&& p.getAttribute("data-ym-empty-hint") !== "1") {
|
|
1336
|
+
p.setAttribute("data-ym-empty-hint", "1");
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
function syncNative() {
|
|
1342
|
+
Array.prototype.forEach.call(mount.children, function (ch) {
|
|
1343
|
+
if (ch === box || ch === status) return;
|
|
1344
|
+
if (ch.querySelector && ch.querySelector("#search-input")) {
|
|
1345
|
+
// 输入卡片保留,但其内的原生“暂无结果”空态要隐藏(我们自己渲染结果)。
|
|
1346
|
+
hideNativeEmptyHints();
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (ch.getAttribute("data-ym-native-block") !== "1") {
|
|
1350
|
+
ch.setAttribute("data-ym-native-block", "1");
|
|
1351
|
+
}
|
|
1352
|
+
});
|
|
1353
|
+
hideScopeSelect();
|
|
1354
|
+
hideNativeEmptyHints();
|
|
1052
1355
|
}
|
|
1356
|
+
syncNative();
|
|
1357
|
+
var snQueued = false, snRunning = false;
|
|
1358
|
+
function snFlush() {
|
|
1359
|
+
snQueued = false;
|
|
1360
|
+
if (snRunning) return;
|
|
1361
|
+
snRunning = true;
|
|
1362
|
+
try { syncNative(); } finally { snRunning = false; }
|
|
1363
|
+
}
|
|
1364
|
+
new MutationObserver(function () {
|
|
1365
|
+
if (snQueued) return;
|
|
1366
|
+
snQueued = true;
|
|
1367
|
+
(window.requestAnimationFrame || function (f) { setTimeout(f, 16); })(snFlush);
|
|
1368
|
+
}).observe(mount, { childList: true, subtree: true });
|
|
1369
|
+
|
|
1370
|
+
state.listEl = box;
|
|
1371
|
+
state.statusEl = status;
|
|
1053
1372
|
}
|
|
1054
1373
|
|
|
1055
|
-
function
|
|
1056
|
-
if (
|
|
1057
|
-
|
|
1374
|
+
function bindInput() {
|
|
1375
|
+
if (!state.input || state.input.__ymBound) return;
|
|
1376
|
+
state.input.__ymBound = true;
|
|
1377
|
+
state.input.setAttribute("placeholder", "全局搜索文件 / 文件夹…");
|
|
1378
|
+
state.input.addEventListener("input", function () {
|
|
1379
|
+
scheduleSearch();
|
|
1380
|
+
});
|
|
1381
|
+
// 回车立即搜(不等防抖)
|
|
1382
|
+
state.input.addEventListener("keydown", function (e) {
|
|
1383
|
+
if (e.key === "Enter") {
|
|
1384
|
+
e.preventDefault();
|
|
1385
|
+
e.stopPropagation();
|
|
1386
|
+
if (state.timer) clearTimeout(state.timer);
|
|
1387
|
+
runSearch((state.input.value || "").trim());
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
/* 拦截官方 /fs/search:官方前端用 axios(XHR),不是 fetch。
|
|
1393
|
+
重写请求体里的 parent 为 "/",保证即便走官方自己的搜索通道也是全局。 */
|
|
1394
|
+
function rewriteSearchBody(bodyText) {
|
|
1395
|
+
try {
|
|
1396
|
+
var data = JSON.parse(bodyText);
|
|
1397
|
+
if (data && typeof data === "object" && "keywords" in data) {
|
|
1398
|
+
data.parent = "/"; // 永远全局
|
|
1399
|
+
return JSON.stringify(data);
|
|
1400
|
+
}
|
|
1401
|
+
} catch (e) {}
|
|
1402
|
+
return bodyText;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function patchFetch() {
|
|
1406
|
+
if (window.__ymGlobalFetchPatched) return;
|
|
1407
|
+
window.__ymGlobalFetchPatched = true;
|
|
1058
1408
|
var ofetch = window.fetch;
|
|
1059
1409
|
window.fetch = function (input, init) {
|
|
1060
1410
|
try {
|
|
1061
1411
|
var url = typeof input === "string" ? input : input && input.url;
|
|
1062
1412
|
if (url && url.indexOf("/fs/search") !== -1 && init && typeof init.body === "string") {
|
|
1063
|
-
|
|
1413
|
+
var nb = rewriteSearchBody(init.body);
|
|
1414
|
+
if (nb !== init.body) init = Object.assign({}, init, { body: nb });
|
|
1064
1415
|
}
|
|
1065
1416
|
} catch (e) {}
|
|
1066
1417
|
return ofetch.apply(this, arguments);
|
|
1067
1418
|
};
|
|
1068
1419
|
}
|
|
1069
1420
|
|
|
1070
|
-
function
|
|
1071
|
-
if (window.
|
|
1072
|
-
window.
|
|
1073
|
-
var
|
|
1074
|
-
var
|
|
1421
|
+
function patchXhr() {
|
|
1422
|
+
if (window.__ymGlobalXhrPatched) return;
|
|
1423
|
+
window.__ymGlobalXhrPatched = true;
|
|
1424
|
+
var OOpen = XMLHttpRequest.prototype.open;
|
|
1425
|
+
var OSend = XMLHttpRequest.prototype.send;
|
|
1075
1426
|
XMLHttpRequest.prototype.open = function (method, url) {
|
|
1076
|
-
this.
|
|
1077
|
-
return
|
|
1427
|
+
this.__ymIsSearch = typeof url === "string" && url.indexOf("/fs/search") !== -1;
|
|
1428
|
+
return OOpen.apply(this, arguments);
|
|
1078
1429
|
};
|
|
1079
1430
|
XMLHttpRequest.prototype.send = function (body) {
|
|
1080
1431
|
try {
|
|
1081
|
-
if (this.
|
|
1082
|
-
|
|
1432
|
+
if (this.__ymIsSearch && typeof body === "string") {
|
|
1433
|
+
var nb = rewriteSearchBody(body);
|
|
1434
|
+
if (nb !== body) body = nb;
|
|
1083
1435
|
}
|
|
1084
1436
|
} catch (e) {}
|
|
1085
|
-
return
|
|
1437
|
+
return OSend.call(this, body);
|
|
1086
1438
|
};
|
|
1087
1439
|
}
|
|
1088
1440
|
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1441
|
+
// 弹窗关闭 / SPA 路由切换(点搜索结果进文件夹)后,Solid 会把旧弹窗移出文档。
|
|
1442
|
+
// 此时必须重置缓存的 modal/input,否则下次打开的是【新弹窗节点】,而我们仍持有
|
|
1443
|
+
// 游离的旧节点,自定义结果层不会挂回去,看起来“变回了原来的搜索界面”。
|
|
1444
|
+
function resetModalState() {
|
|
1445
|
+
state.modal = null;
|
|
1446
|
+
state.input = null;
|
|
1447
|
+
state.listEl = null;
|
|
1448
|
+
state.statusEl = null;
|
|
1449
|
+
state.lastKeyword = "";
|
|
1450
|
+
if (state.timer) { clearTimeout(state.timer); state.timer = null; }
|
|
1451
|
+
if (state.abort) { try { state.abort.abort(); } catch (e) {} state.abort = null; }
|
|
1452
|
+
state.seq++;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function isDetached(el) {
|
|
1456
|
+
if (!el) return true;
|
|
1457
|
+
if (!el.isConnected) return true;
|
|
1458
|
+
// 旧弹窗节点:仍在 DOM 中但已不可见(Solid 复用/卸载),也视为失效
|
|
1459
|
+
try { if (el.getBoundingClientRect().width === 0) return true; } catch (e) {}
|
|
1460
|
+
return false;
|
|
1101
1461
|
}
|
|
1102
1462
|
|
|
1103
|
-
function
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
if (n.querySelector("#search-input") && n.getBoundingClientRect().width > 0) return n;
|
|
1463
|
+
function hookModal() {
|
|
1464
|
+
// 已缓存的弹窗已脱离文档:SPA 导航后清理,确保重新 hook 新弹窗
|
|
1465
|
+
if (state.modal && isDetached(state.modal)) {
|
|
1466
|
+
resetModalState();
|
|
1108
1467
|
}
|
|
1109
|
-
|
|
1110
|
-
|
|
1468
|
+
var modal = null;
|
|
1469
|
+
var dialogs = $all('[role="dialog"]');
|
|
1470
|
+
for (var i = 0; i < dialogs.length; i++) {
|
|
1471
|
+
if (dialogs[i].querySelector("#search-input") && dialogs[i].getBoundingClientRect().width > 0 && dialogs[i].isConnected) {
|
|
1472
|
+
modal = dialogs[i];
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (!modal) return;
|
|
1111
1477
|
|
|
1112
|
-
function ensureToggle() {
|
|
1113
|
-
var modal = findModal();
|
|
1114
|
-
if (!modal || modal.querySelector(".ym-search-scope")) return;
|
|
1115
1478
|
var input = modal.querySelector("#search-input");
|
|
1116
1479
|
if (!input) return;
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1480
|
+
|
|
1481
|
+
var isNewModal = state.modal !== modal;
|
|
1482
|
+
state.modal = modal;
|
|
1483
|
+
state.input = input;
|
|
1484
|
+
// 弹窗 DOM 在 Solid 中复用;overlay 只建一次并缓存在节点上,避免重复插入。
|
|
1485
|
+
if (modal.__ymListEl) {
|
|
1486
|
+
state.listEl = modal.__ymListEl;
|
|
1487
|
+
state.statusEl = modal.__ymStatusEl;
|
|
1488
|
+
} else {
|
|
1489
|
+
state.listEl = null;
|
|
1490
|
+
state.statusEl = null;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
ensureOverlay();
|
|
1494
|
+
modal.__ymListEl = state.listEl;
|
|
1495
|
+
modal.__ymStatusEl = state.statusEl;
|
|
1496
|
+
bindInput();
|
|
1497
|
+
|
|
1498
|
+
if (isNewModal) {
|
|
1499
|
+
// 弹窗打开时聚焦并按当前内容立即搜
|
|
1500
|
+
setTimeout(function () {
|
|
1501
|
+
try { input.focus(); } catch (e) {}
|
|
1502
|
+
var kw = (input.value || "").trim();
|
|
1503
|
+
if (kw) runSearch(kw);
|
|
1504
|
+
}, 50);
|
|
1505
|
+
}
|
|
1130
1506
|
}
|
|
1131
1507
|
|
|
1132
1508
|
function boot() {
|
|
1133
1509
|
injectCss();
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1510
|
+
patchFetch();
|
|
1511
|
+
patchXhr();
|
|
1512
|
+
// 只监听节点增删(不监听 attributes,避免我们自己 setAttribute 引发观察器回调风暴);
|
|
1513
|
+
// 用 rAF 合并高频回调,并防重入。
|
|
1514
|
+
var queued = false, running = false;
|
|
1515
|
+
function flush() {
|
|
1516
|
+
queued = false;
|
|
1517
|
+
if (running) return;
|
|
1518
|
+
if (!document.querySelector("#search-input")) return;
|
|
1519
|
+
running = true;
|
|
1520
|
+
try { hookModal(); } finally { running = false; }
|
|
1521
|
+
}
|
|
1522
|
+
var mo = new MutationObserver(function () {
|
|
1523
|
+
if (queued) return;
|
|
1524
|
+
queued = true;
|
|
1525
|
+
(window.requestAnimationFrame || function (f) { setTimeout(f, 16); })(flush);
|
|
1526
|
+
});
|
|
1527
|
+
mo.observe(document.body, { childList: true, subtree: true });
|
|
1528
|
+
|
|
1529
|
+
// SPA 路由切换(点搜索结果进文件夹):旧弹窗被卸载,复位缓存,使下次打开
|
|
1530
|
+
// 搜索时重新 hook 新弹窗,而不是停留在旧的原生搜索界面直到整页刷新。
|
|
1531
|
+
// 用【只读轮询 pathname】而非包装 history.pushState/replaceState —— 后者可能
|
|
1532
|
+
// 干扰 Solid 路由器(曾导致导航后转圈/卡死)。轮询零副作用、最稳。
|
|
1533
|
+
var lastPath = location.pathname + location.search;
|
|
1534
|
+
function onRouteChange() {
|
|
1535
|
+
lastPath = location.pathname + location.search;
|
|
1536
|
+
setTimeout(function () {
|
|
1537
|
+
if (isDetached(state.modal)) resetModalState();
|
|
1538
|
+
}, 0);
|
|
1539
|
+
}
|
|
1540
|
+
setInterval(function () {
|
|
1541
|
+
var cur = location.pathname + location.search;
|
|
1542
|
+
if (cur !== lastPath) {
|
|
1543
|
+
lastPath = cur;
|
|
1544
|
+
setTimeout(function () {
|
|
1545
|
+
if (isDetached(state.modal)) resetModalState();
|
|
1546
|
+
}, 0);
|
|
1547
|
+
}
|
|
1548
|
+
}, 300);
|
|
1549
|
+
window.addEventListener("popstate", onRouteChange);
|
|
1550
|
+
window.addEventListener("hashchange", onRouteChange);
|
|
1551
|
+
|
|
1552
|
+
// 弹窗关闭时复位
|
|
1553
|
+
document.addEventListener("click", function (e) {
|
|
1554
|
+
var t = e.target;
|
|
1555
|
+
if (t && t.closest && t.closest(".hope-modal__overlay, [class*=modal__overlay]")) {
|
|
1556
|
+
state.seq++;
|
|
1557
|
+
}
|
|
1558
|
+
}, true);
|
|
1139
1559
|
}
|
|
1140
1560
|
|
|
1141
1561
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
@@ -1145,573 +1565,166 @@
|
|
|
1145
1565
|
/* ===== music-player.js ===== */
|
|
1146
1566
|
;(function(){
|
|
1147
1567
|
/* =============================================================================
|
|
1148
|
-
* Yuuichi 资源站 ·
|
|
1568
|
+
* Yuuichi 资源站 · 背景音乐(与 blog.yuuichi.me / www.yuuichi.me 统一的 bgm-dock)
|
|
1149
1569
|
* -----------------------------------------------------------------------------
|
|
1150
|
-
*
|
|
1151
|
-
*
|
|
1152
|
-
*
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
* 每首歌的直链 / 封面 / 歌词由 metingApi 按 id 实时解析(公益服务,失效可换其它 Meting 兼容地址)。
|
|
1160
|
-
* 改完保存,在 openlist 目录执行:
|
|
1161
|
-
* node scripts/sync-branding.mjs
|
|
1162
|
-
* pnpm exec wrangler deploy
|
|
1570
|
+
* 统一播放器:左下角圆形按钮(半吸附边缘 / 可拖动 / 可缩放),点击展开面板,
|
|
1571
|
+
* 内置 Spotify 官方嵌入 + 网易云(Meting/APlayer,含封面/歌词/列表循环/倍速)。
|
|
1572
|
+
*
|
|
1573
|
+
* 想换歌单,修改下面 MUSIC_CONFIG 的 id,然后:
|
|
1574
|
+
* node scripts/sync-branding.mjs && pnpm exec wrangler deploy
|
|
1575
|
+
* 也可以在 OpenList 管理台设置项 music_config 在线覆盖(无需重新部署)。
|
|
1576
|
+
*
|
|
1577
|
+
* 资源站资源全部同源托管在 /brand/bgm/ 下;网易云歌单/直链/歌词走本站
|
|
1578
|
+
* /api/music/meting(Worker 服务端代理网易云),浏览器不直接访问 workers.dev。
|
|
1163
1579
|
* ============================================================================= */
|
|
1164
1580
|
(function () {
|
|
1165
1581
|
"use strict";
|
|
1166
1582
|
|
|
1583
|
+
var ASSET_BASE = "/brand/bgm";
|
|
1584
|
+
var METING_API = "/api/music/meting?server=:server&type=:type&id=:id&r=:r";
|
|
1585
|
+
|
|
1167
1586
|
var MUSIC_CONFIG = {
|
|
1168
|
-
netease: [
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
autoplay: false, // 网易云是否自动播放(多数浏览器会拦截,建议 false)
|
|
1176
|
-
fabHint: "音乐"
|
|
1587
|
+
netease: [{ id: "18475904830", name: "网易云" }],
|
|
1588
|
+
spotify: [{ id: "3bePpYfOLDwFtVVs6cqwyh", name: "Spotify" }],
|
|
1589
|
+
defaultTab: "spotify", // "spotify" 或 "netease"
|
|
1590
|
+
autoplay: false,
|
|
1591
|
+
theme: "#0ea5e9",
|
|
1592
|
+
volume: 0.6,
|
|
1593
|
+
order: "list" // list / random
|
|
1177
1594
|
};
|
|
1178
1595
|
|
|
1179
|
-
//
|
|
1180
|
-
// Worker 会在本脚本前注入 window.__YUUICHI_MUSIC_CONFIG_OVERLAY__。
|
|
1596
|
+
// 管理台 music_config 在线覆盖
|
|
1181
1597
|
try {
|
|
1182
1598
|
var __ov = window.__YUUICHI_MUSIC_CONFIG_OVERLAY__;
|
|
1183
1599
|
if (__ov && typeof __ov === "object") {
|
|
1184
1600
|
["netease", "spotify"].forEach(function (k) {
|
|
1185
1601
|
if (Array.isArray(__ov[k])) MUSIC_CONFIG[k] = __ov[k];
|
|
1186
1602
|
});
|
|
1187
|
-
["defaultTab", "
|
|
1603
|
+
["defaultTab", "theme", "order"].forEach(function (k) {
|
|
1188
1604
|
if (typeof __ov[k] === "string" && __ov[k]) MUSIC_CONFIG[k] = __ov[k];
|
|
1189
1605
|
});
|
|
1190
|
-
|
|
1606
|
+
["autoplay"].forEach(function (k) {
|
|
1607
|
+
if (typeof __ov[k] === "boolean") MUSIC_CONFIG[k] = __ov[k];
|
|
1608
|
+
});
|
|
1609
|
+
if (typeof __ov.volume === "number") MUSIC_CONFIG.volume = __ov.volume;
|
|
1191
1610
|
}
|
|
1192
1611
|
} catch (e) {}
|
|
1193
1612
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
".tab { border:1px solid var(--ym-line); background:transparent; color:var(--ym-txt);",
|
|
1243
|
-
" border-radius:999px; padding:4px 12px; font-size:12px; cursor:pointer; }",
|
|
1244
|
-
".tab.active { background:linear-gradient(135deg,#38bdf8,#0284c7); color:#fff; border-color:transparent; }",
|
|
1245
|
-
".sel { flex:1; min-width:0; border:1px solid var(--ym-line); background:transparent;",
|
|
1246
|
-
" color:var(--ym-txt); border-radius:8px; font-size:12px; padding:4px 6px; outline:none; }",
|
|
1247
|
-
".sel option { color:#0f172a; }",
|
|
1248
|
-
".close { border:none; background:transparent; color:var(--ym-txt); font-size:18px; line-height:1; cursor:pointer; padding:2px 7px; border-radius:8px; }",
|
|
1249
|
-
".close:hover { background:var(--ym-soft); }",
|
|
1250
|
-
".refresh { display:none; border:none; background:transparent; color:var(--ym-txt); cursor:pointer; padding:2px 6px; border-radius:8px; align-items:center; }",
|
|
1251
|
-
".refresh.show { display:inline-flex; }",
|
|
1252
|
-
".refresh:hover { background:var(--ym-soft); }",
|
|
1253
|
-
".refresh svg { width:16px; height:16px; }",
|
|
1254
|
-
".refresh.spin svg { animation:ym-rspin .8s linear infinite; }",
|
|
1255
|
-
"@keyframes ym-rspin { to { transform:rotate(360deg); } }",
|
|
1256
|
-
".ctrl { display:none; align-items:center; gap:6px; padding:5px 10px; flex:0 0 auto;",
|
|
1257
|
-
" border-bottom:1px solid var(--ym-line); }",
|
|
1258
|
-
".ctrl.show { display:flex; }",
|
|
1259
|
-
".cbtn { border:1px solid var(--ym-line); background:transparent; color:var(--ym-txt); cursor:pointer;",
|
|
1260
|
-
" border-radius:999px; font-size:12px; line-height:1; padding:4px 10px; display:inline-flex; align-items:center; gap:5px; }",
|
|
1261
|
-
".cbtn:hover { background:var(--ym-soft); }",
|
|
1262
|
-
".cbtn svg { width:14px; height:14px; }",
|
|
1263
|
-
".stages { flex:1; min-height:0; position:relative; }",
|
|
1264
|
-
".stage { position:absolute; inset:0; display:none; }",
|
|
1265
|
-
".stage.active { display:block; }",
|
|
1266
|
-
".stage iframe { width:100%; height:100%; border:0; display:block; background:#fff; }",
|
|
1267
|
-
".ap-wrap { position:absolute; inset:0; overflow:auto; }",
|
|
1268
|
-
".status { position:absolute; inset:0; display:flex; flex-direction:column; gap:10px; align-items:center; justify-content:center; text-align:center; color:var(--ym-sub); font-size:13px; padding:24px; }",
|
|
1269
|
-
".status button { border:none; cursor:pointer; color:#fff; background:linear-gradient(135deg,#38bdf8,#0284c7); border-radius:999px; padding:6px 18px; font-size:13px; }",
|
|
1270
|
-
".resizer { position:absolute; right:0; bottom:0; width:22px; height:22px; cursor:nwse-resize; z-index:5; }",
|
|
1271
|
-
".resizer::after { content:''; position:absolute; right:5px; bottom:5px; width:10px; height:10px;",
|
|
1272
|
-
" border-right:2px solid var(--ym-line); border-bottom:2px solid var(--ym-line); }",
|
|
1273
|
-
/* APlayer 皮肤适配(透明、贴合面板、青蓝主题) */
|
|
1274
|
-
".aplayer { width:100%; margin:0; background:transparent !important; box-shadow:none !important; border-radius:0; color:var(--ym-txt); }",
|
|
1275
|
-
".aplayer .aplayer-info { border-top:1px solid var(--ym-line); }",
|
|
1276
|
-
".aplayer .aplayer-title, .aplayer .aplayer-list ol li .aplayer-list-index, .aplayer .aplayer-time { color:var(--ym-sub); }",
|
|
1277
|
-
".aplayer .aplayer-list ol li { border-top:1px solid var(--ym-line); }",
|
|
1278
|
-
".aplayer .aplayer-list ol li.aplayer-list-light { background:var(--ym-soft); }",
|
|
1279
|
-
".aplayer .aplayer-list ol li:hover { background:var(--ym-soft); }",
|
|
1280
|
-
".aplayer .aplayer-list ol li .aplayer-list-author { color:var(--ym-sub); }",
|
|
1281
|
-
".aplayer .aplayer-lrc { text-shadow:0 1px 4px rgba(0,0,0,.35); }",
|
|
1282
|
-
".aplayer .aplayer-lrc p.aplayer-lrc-current { color:#38bdf8; }",
|
|
1283
|
-
".aplayer .aplayer-miniswitcher { background:var(--ym-soft); }",
|
|
1284
|
-
":host([data-dark]) .aplayer svg, :host([data-dark]) .aplayer .aplayer-icon { filter:invert(1) hue-rotate(180deg); }",
|
|
1285
|
-
"@media (max-width:600px) {",
|
|
1286
|
-
" .fab { left:14px; bottom:14px; width:50px; height:50px; }",
|
|
1287
|
-
" .panel { left:12px !important; bottom:76px !important; width:calc(100vw - 24px) !important; height:min(76vh,600px) !important; }",
|
|
1288
|
-
" .resizer { display:none; }",
|
|
1289
|
-
"}"
|
|
1290
|
-
].join("\n");
|
|
1291
|
-
shadow.appendChild(style);
|
|
1292
|
-
|
|
1293
|
-
/* ---------------- DOM 骨架 ---------------- */
|
|
1294
|
-
var wrap = document.createElement("div");
|
|
1295
|
-
wrap.className = "wrap";
|
|
1296
|
-
shadow.appendChild(wrap);
|
|
1297
|
-
|
|
1298
|
-
var fab = document.createElement("button");
|
|
1299
|
-
fab.className = "fab";
|
|
1300
|
-
fab.title = MUSIC_CONFIG.fabHint || "音乐";
|
|
1301
|
-
fab.setAttribute("aria-label", "音乐播放器");
|
|
1302
|
-
fab.innerHTML =
|
|
1303
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
1304
|
-
'<path d="M9 18V5l10-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="16" cy="16" r="3"></circle></svg>';
|
|
1305
|
-
wrap.appendChild(fab);
|
|
1306
|
-
|
|
1307
|
-
var panel = document.createElement("div");
|
|
1308
|
-
panel.className = "panel";
|
|
1309
|
-
panel.innerHTML =
|
|
1310
|
-
'<div class="bar"></div>' +
|
|
1311
|
-
'<div class="ctrl">' +
|
|
1312
|
-
'<button class="cbtn js-speed" type="button" title="播放倍速"><span class="speed-label">1.0x</span></button>' +
|
|
1313
|
-
'<button class="cbtn js-order" type="button" title="播放顺序"></button>' +
|
|
1314
|
-
'</div>' +
|
|
1315
|
-
'<div class="stages"></div>' +
|
|
1316
|
-
'<div class="resizer"></div>';
|
|
1317
|
-
wrap.appendChild(panel);
|
|
1318
|
-
|
|
1319
|
-
var bar = panel.querySelector(".bar");
|
|
1320
|
-
var stages = panel.querySelector(".stages");
|
|
1321
|
-
var ctrlBar = panel.querySelector(".ctrl");
|
|
1322
|
-
var speedBtn = panel.querySelector(".js-speed");
|
|
1323
|
-
var speedLabel = panel.querySelector(".speed-label");
|
|
1324
|
-
var orderBtn = panel.querySelector(".js-order");
|
|
1325
|
-
|
|
1326
|
-
var tabBtns = {};
|
|
1327
|
-
platforms.forEach(function (p) {
|
|
1328
|
-
var b = document.createElement("button");
|
|
1329
|
-
b.className = "tab";
|
|
1330
|
-
b.textContent = p.label;
|
|
1331
|
-
b.dataset.key = p.key;
|
|
1332
|
-
b.addEventListener("click", function () { selectTab(p.key); });
|
|
1333
|
-
bar.appendChild(b);
|
|
1334
|
-
tabBtns[p.key] = b;
|
|
1335
|
-
});
|
|
1336
|
-
|
|
1337
|
-
var sel = document.createElement("select");
|
|
1338
|
-
sel.className = "sel";
|
|
1339
|
-
bar.appendChild(sel);
|
|
1340
|
-
|
|
1341
|
-
var refreshBtn = document.createElement("button");
|
|
1342
|
-
refreshBtn.className = "refresh";
|
|
1343
|
-
refreshBtn.title = "重新拉取最新歌单";
|
|
1344
|
-
refreshBtn.setAttribute("aria-label", "刷新歌单");
|
|
1345
|
-
refreshBtn.innerHTML =
|
|
1346
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
1347
|
-
'<path d="M21 12a9 9 0 1 1-2.64-6.36"></path><polyline points="21 3 21 9 15 9"></polyline></svg>';
|
|
1348
|
-
refreshBtn.addEventListener("click", function () {
|
|
1349
|
-
if (state.tab === "netease") {
|
|
1350
|
-
refreshBtn.classList.add("spin");
|
|
1351
|
-
setupNetease(true).finally(function () { refreshBtn.classList.remove("spin"); });
|
|
1352
|
-
}
|
|
1613
|
+
/* 收起态关键 CSS(与博客 #bgm-dock-critical 保持一致),内联防止展开前闪烁 */
|
|
1614
|
+
var CRITICAL_CSS =
|
|
1615
|
+
".bgm-dock{--bgm-accent:" + (MUSIC_CONFIG.theme || "#0ea5e9") + ";" +
|
|
1616
|
+
"position:fixed;left:16px;bottom:16px;z-index:99999;width:52px;max-width:calc(100vw - 24px);" +
|
|
1617
|
+
"font-size:13px;line-height:1.4;color:#222;}" +
|
|
1618
|
+
".bgm-dock.is-open{width:320px;max-width:calc(100vw - 24px);}" +
|
|
1619
|
+
".bgm-dock__fab{position:relative;width:52px;height:52px;border:0;border-radius:50%;" +
|
|
1620
|
+
"background:var(--bgm-accent);color:#fff;font-size:20px;cursor:pointer;" +
|
|
1621
|
+
"box-shadow:0 8px 22px rgba(0,0,0,.28);display:inline-flex;align-items:center;justify-content:center;touch-action:none;}" +
|
|
1622
|
+
".bgm-dock__fab.is-dragging{cursor:grabbing;opacity:.92;}" +
|
|
1623
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab{--bgm-peek:18px;transition:transform .28s cubic-bezier(.22,.61,.36,1);will-change:transform;}" +
|
|
1624
|
+
// 收起态半吸附到屏幕边缘:默认只露出一小截(半圆即可 hover),鼠标移上 /
|
|
1625
|
+
// 键盘聚焦 / 拖拽 / 已全露 时整个滑回屏幕内,保证桌面鼠标始终点得到。
|
|
1626
|
+
// 触摸设备点一下全露、再点展开(见 bgm-dock.js 的 is-revealed 逻辑)。
|
|
1627
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-left{transform:translateX(calc(var(--bgm-peek) - 100%));}" +
|
|
1628
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-right{transform:translateX(calc(100% - var(--bgm-peek)));}" +
|
|
1629
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab:hover," +
|
|
1630
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab:focus-visible," +
|
|
1631
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-revealed," +
|
|
1632
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-dragging{transform:none;}" +
|
|
1633
|
+
// 触摸(无 hover)首次点按仅全露:未全露时不要因 hover 规则干扰(触摸无 hover,
|
|
1634
|
+
// 该媒体查询下默认半藏即可)。
|
|
1635
|
+
"@media (hover:none),(pointer:coarse){" +
|
|
1636
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-left{transform:translateX(calc(var(--bgm-peek) - 100%));}" +
|
|
1637
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-right{transform:translateX(calc(100% - var(--bgm-peek)));}}" +
|
|
1638
|
+
".bgm-dock__fab svg{width:22px;height:22px;fill:currentColor;display:block;}" +
|
|
1639
|
+
".bgm-dock__fab-bar{position:absolute;right:9px;top:9px;width:9px;height:9px;border-radius:50%;" +
|
|
1640
|
+
"background:#2ecc71;border:2px solid #fff;opacity:0;}" +
|
|
1641
|
+
".bgm-dock.is-playing .bgm-dock__fab-bar{opacity:1;}" +
|
|
1642
|
+
".bgm-dock__panel{display:none;}" +
|
|
1643
|
+
".bgm-dock.is-open .bgm-dock__panel{display:flex;}" +
|
|
1644
|
+
".bgm-dock.is-open .bgm-dock__fab{display:none;}" +
|
|
1645
|
+
".bgm-dock__overlay{position:fixed;inset:0;z-index:999999;cursor:grabbing;user-select:none;}" +
|
|
1646
|
+
"@media (max-width:768px){.bgm-dock{left:14px;bottom:14px;width:52px;}" +
|
|
1647
|
+
".bgm-dock.is-open{width:auto;left:0;right:0;bottom:0;max-width:none;}}";
|
|
1648
|
+
|
|
1649
|
+
function buildSources() {
|
|
1650
|
+
var sources = [];
|
|
1651
|
+
var i = 0;
|
|
1652
|
+
(MUSIC_CONFIG.spotify || []).forEach(function (s) {
|
|
1653
|
+
if (!s || !s.id) return;
|
|
1654
|
+
sources.push({
|
|
1655
|
+
name: s.name || "Spotify",
|
|
1656
|
+
type: "spotify",
|
|
1657
|
+
resourceType: "playlist",
|
|
1658
|
+
id: String(s.id),
|
|
1659
|
+
key: "spotify-" + i++
|
|
1660
|
+
});
|
|
1353
1661
|
});
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
var stageEl = {};
|
|
1364
|
-
platforms.forEach(function (p) {
|
|
1365
|
-
var d = document.createElement("div");
|
|
1366
|
-
d.className = "stage";
|
|
1367
|
-
d.dataset.key = p.key;
|
|
1368
|
-
stages.appendChild(d);
|
|
1369
|
-
stageEl[p.key] = d;
|
|
1662
|
+
(MUSIC_CONFIG.netease || []).forEach(function (s) {
|
|
1663
|
+
if (!s || !s.id) return;
|
|
1664
|
+
sources.push({
|
|
1665
|
+
name: s.name || "网易云",
|
|
1666
|
+
type: "netease",
|
|
1667
|
+
resourceType: "playlist",
|
|
1668
|
+
id: String(s.id),
|
|
1669
|
+
key: "netease-" + i++
|
|
1670
|
+
});
|
|
1370
1671
|
});
|
|
1672
|
+
// 默认源标记
|
|
1673
|
+
var wantDefault = MUSIC_CONFIG.defaultTab === "netease" ? "netease" : "spotify";
|
|
1674
|
+
var def = sources.filter(function (s) { return s.type === wantDefault; })[0] || sources[0];
|
|
1675
|
+
if (def) def.default = true;
|
|
1676
|
+
return sources;
|
|
1677
|
+
}
|
|
1371
1678
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1679
|
+
function init() {
|
|
1680
|
+
if (window.__yuuichiBgmInited) return;
|
|
1681
|
+
if (document.getElementById("bgm-dock")) return;
|
|
1682
|
+
window.__yuuichiBgmInited = true;
|
|
1683
|
+
|
|
1684
|
+
var sources = buildSources();
|
|
1685
|
+
if (!sources.length) return;
|
|
1686
|
+
|
|
1687
|
+
var styleEl = document.createElement("style");
|
|
1688
|
+
styleEl.id = "bgm-dock-critical";
|
|
1689
|
+
styleEl.textContent = CRITICAL_CSS;
|
|
1690
|
+
document.head.appendChild(styleEl);
|
|
1691
|
+
|
|
1692
|
+
var dock = document.createElement("div");
|
|
1693
|
+
dock.id = "bgm-dock";
|
|
1694
|
+
dock.className = "bgm-dock";
|
|
1695
|
+
dock.setAttribute(
|
|
1696
|
+
"data-bgm",
|
|
1697
|
+
JSON.stringify({
|
|
1698
|
+
sources: sources,
|
|
1699
|
+
autoplay: MUSIC_CONFIG.autoplay === true,
|
|
1700
|
+
order: MUSIC_CONFIG.order === "random" ? "random" : "list",
|
|
1701
|
+
volume: typeof MUSIC_CONFIG.volume === "number" ? MUSIC_CONFIG.volume : 0.6,
|
|
1702
|
+
theme: MUSIC_CONFIG.theme || "#0ea5e9",
|
|
1703
|
+
assetBase: ASSET_BASE,
|
|
1704
|
+
metingApi: METING_API
|
|
1705
|
+
})
|
|
1706
|
+
);
|
|
1707
|
+
document.body.appendChild(dock);
|
|
1708
|
+
|
|
1709
|
+
// 收起态只依赖内联 CRITICAL_CSS,主脚本 bgm-dock.js(~28KB)推迟到浏览器
|
|
1710
|
+
// 空闲 / load 后再拉,避免与首屏 JS、目录列表、背景图争抢带宽与主线程。
|
|
1711
|
+
var s = document.createElement("script");
|
|
1712
|
+
s.src = ASSET_BASE + "/js/bgm-dock.js";
|
|
1713
|
+
s.defer = true;
|
|
1714
|
+
s.onerror = function () {
|
|
1715
|
+
// 加载失败时至少保证按钮还能看到,避免完全无反应
|
|
1716
|
+
try { dock.style.display = "none"; } catch (e) {}
|
|
1379
1717
|
};
|
|
1380
|
-
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
{ loop: "none", order: "random", label: "随机播放", icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 3 21 3 21 8"></polyline><line x1="4" y1="20" x2="21" y2="3"></line><polyline points="21 16 21 21 16 21"></polyline><line x1="15" y1="15" x2="21" y2="21"></line><line x1="4" y1="4" x2="9" y2="9"></line></svg>' },
|
|
1389
|
-
{ loop: "all", order: "list", label: "全部循环", icon: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 2l4 4-4 4"></path><path d="M3 11v-1a4 4 0 0 1 4-4h14"></path><path d="M7 22l-4-4 4-4"></path><path d="M21 13v1a4 4 0 0 1-4 4H3"></path></svg>' }
|
|
1390
|
-
];
|
|
1391
|
-
var orderIdx = 0;
|
|
1392
|
-
try {
|
|
1393
|
-
var sv = parseFloat(localStorage.getItem("ym-speed"));
|
|
1394
|
-
var si = SPEEDS.indexOf(sv);
|
|
1395
|
-
if (si >= 0) speedIdx = si;
|
|
1396
|
-
var oi = parseInt(localStorage.getItem("ym-order"), 10);
|
|
1397
|
-
if (!isNaN(oi) && ORDER_MODES[oi]) orderIdx = oi;
|
|
1398
|
-
} catch (e) {}
|
|
1399
|
-
|
|
1400
|
-
function applySpeed() {
|
|
1401
|
-
var rate = SPEEDS[speedIdx];
|
|
1402
|
-
speedLabel.textContent = (rate % 1 === 0 ? rate.toFixed(1) : String(rate)) + "x";
|
|
1403
|
-
try {
|
|
1404
|
-
if (state.ap && state.ap.audio) state.ap.audio.playbackRate = rate;
|
|
1405
|
-
localStorage.setItem("ym-speed", String(rate));
|
|
1406
|
-
} catch (e) {}
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
function applyOrder() {
|
|
1410
|
-
var mode = ORDER_MODES[orderIdx];
|
|
1411
|
-
orderBtn.innerHTML = mode.icon + '<span class="order-label">' + mode.label + '</span>';
|
|
1412
|
-
orderBtn.title = "播放顺序:" + mode.label;
|
|
1413
|
-
try {
|
|
1414
|
-
if (state.ap && state.ap.options) {
|
|
1415
|
-
state.ap.options.loop = mode.loop;
|
|
1416
|
-
state.ap.options.order = mode.order;
|
|
1417
|
-
}
|
|
1418
|
-
localStorage.setItem("ym-order", String(orderIdx));
|
|
1419
|
-
} catch (e) {}
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
speedBtn.addEventListener("click", function () {
|
|
1423
|
-
speedIdx = (speedIdx + 1) % SPEEDS.length;
|
|
1424
|
-
applySpeed();
|
|
1425
|
-
});
|
|
1426
|
-
orderBtn.addEventListener("click", function () {
|
|
1427
|
-
orderIdx = (orderIdx + 1) % ORDER_MODES.length;
|
|
1428
|
-
applyOrder();
|
|
1429
|
-
});
|
|
1430
|
-
applySpeed();
|
|
1431
|
-
applyOrder();
|
|
1432
|
-
|
|
1433
|
-
function currentPlatform() {
|
|
1434
|
-
for (var i = 0; i < platforms.length; i++) if (platforms[i].key === state.tab) return platforms[i];
|
|
1435
|
-
return platforms[0];
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
function loadScriptOnce(attr, src) {
|
|
1439
|
-
return new Promise(function (resolve, reject) {
|
|
1440
|
-
var existing = document.querySelector("script[" + attr + "]");
|
|
1441
|
-
if (existing) {
|
|
1442
|
-
if (existing.dataset.loaded === "1") return resolve();
|
|
1443
|
-
existing.addEventListener("load", function () { resolve(); });
|
|
1444
|
-
existing.addEventListener("error", reject);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
var s = document.createElement("script");
|
|
1448
|
-
s.async = true;
|
|
1449
|
-
s.setAttribute(attr, "1");
|
|
1450
|
-
s.onload = function () { s.dataset.loaded = "1"; resolve(); };
|
|
1451
|
-
s.onerror = reject;
|
|
1452
|
-
s.src = src;
|
|
1453
|
-
document.head.appendChild(s);
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
function loadScript(src) {
|
|
1458
|
-
return loadScriptOnce("data-ym-aplayer", src);
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
/* 实时同源接口:Worker 服务端优先走自建 Meting、失败回退网易云官方,无 CORS */
|
|
1462
|
-
function fetchWithTimeout(url, ms) {
|
|
1463
|
-
var ctrl = ("AbortController" in window) ? new AbortController() : null;
|
|
1464
|
-
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
|
|
1465
|
-
return fetch(url, { cache: "no-store", signal: ctrl ? ctrl.signal : undefined })
|
|
1466
|
-
.finally(function () { if (timer) clearTimeout(timer); });
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
async function fetchPlaylist(item, force) {
|
|
1470
|
-
var url = "/api/music/netease?id=" + encodeURIComponent(item.id) + (force ? "&refresh=1" : "");
|
|
1471
|
-
var res = await fetchWithTimeout(url, 15000);
|
|
1472
|
-
var j = await res.json();
|
|
1473
|
-
if (!j || j.code !== 200 || !Array.isArray(j.data && j.data.songs) || !j.data.songs.length) {
|
|
1474
|
-
throw new Error("empty");
|
|
1475
|
-
}
|
|
1476
|
-
return j.data.songs;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
function statusEl(el, msg, withRetry) {
|
|
1480
|
-
el.innerHTML = "";
|
|
1481
|
-
var d = document.createElement("div");
|
|
1482
|
-
d.className = "status";
|
|
1483
|
-
var span = document.createElement("span");
|
|
1484
|
-
span.textContent = msg;
|
|
1485
|
-
d.appendChild(span);
|
|
1486
|
-
if (withRetry) {
|
|
1487
|
-
var btn = document.createElement("button");
|
|
1488
|
-
btn.textContent = "重试";
|
|
1489
|
-
btn.addEventListener("click", function () { setupNetease(true); });
|
|
1490
|
-
d.appendChild(btn);
|
|
1491
|
-
}
|
|
1492
|
-
el.appendChild(d);
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
async function setupNetease(force) {
|
|
1496
|
-
var el = stageEl.netease;
|
|
1497
|
-
if (state.ready.netease && !force) return;
|
|
1498
|
-
if (state.loading) return;
|
|
1499
|
-
state.loading = true;
|
|
1500
|
-
statusEl(el, "正在加载网易云歌单…", false);
|
|
1501
|
-
try {
|
|
1502
|
-
var item = MUSIC_CONFIG.netease[state.idx.netease || 0];
|
|
1503
|
-
var list = await fetchPlaylist(item, !!force);
|
|
1504
|
-
state.tracks = list;
|
|
1505
|
-
|
|
1506
|
-
if (state.ap) { try { state.ap.destroy(); } catch (e) {} state.ap = null; }
|
|
1507
|
-
el.innerHTML = '<div class="ap-wrap"></div>';
|
|
1508
|
-
ensureVendorCss();
|
|
1509
|
-
await loadScript("/brand/vendor/APlayer.min.js");
|
|
1510
|
-
|
|
1511
|
-
var mode = ORDER_MODES[orderIdx];
|
|
1512
|
-
var ap = new window.APlayer({
|
|
1513
|
-
container: el.querySelector(".ap-wrap"),
|
|
1514
|
-
fixed: false,
|
|
1515
|
-
mini: false,
|
|
1516
|
-
autoplay: !!MUSIC_CONFIG.autoplay,
|
|
1517
|
-
theme: "#0ea5e9",
|
|
1518
|
-
loop: mode.loop,
|
|
1519
|
-
order: mode.order,
|
|
1520
|
-
listMaxHeight: "999px",
|
|
1521
|
-
lrcType: list[0].lrc ? 3 : 0,
|
|
1522
|
-
audio: list.map(function (t) {
|
|
1523
|
-
return { name: t.name, artist: t.artist, url: t.url, cover: t.pic, lrc: t.lrc, theme: "#0ea5e9" };
|
|
1524
|
-
})
|
|
1525
|
-
});
|
|
1526
|
-
state.ap = ap;
|
|
1527
|
-
var audio = ap.audio;
|
|
1528
|
-
if (audio) {
|
|
1529
|
-
audio.playbackRate = SPEEDS[speedIdx];
|
|
1530
|
-
audio.addEventListener("play", function () { audio.playbackRate = SPEEDS[speedIdx]; });
|
|
1531
|
-
}
|
|
1532
|
-
state.ready.netease = true;
|
|
1533
|
-
} catch (e) {
|
|
1534
|
-
statusEl(el, "网易云歌单加载失败(解析服务可能暂时不可用)", true);
|
|
1535
|
-
} finally {
|
|
1536
|
-
state.loading = false;
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
function spotifySrc(item) {
|
|
1541
|
-
var dark = document.body.classList.contains("hope-ui-dark");
|
|
1542
|
-
return "https://open.spotify.com/embed/playlist/" + encodeURIComponent(item.id) +
|
|
1543
|
-
"?utm_source=generator&theme=" + (dark ? "0" : "1");
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
function setupSpotify(force) {
|
|
1547
|
-
var el = stageEl.spotify;
|
|
1548
|
-
if (state.ready.spotify && !force) return;
|
|
1549
|
-
var item = MUSIC_CONFIG.spotify[state.idx.spotify || 0];
|
|
1550
|
-
el.innerHTML = "";
|
|
1551
|
-
var ifr = document.createElement("iframe");
|
|
1552
|
-
ifr.loading = "lazy";
|
|
1553
|
-
ifr.allow = "autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture";
|
|
1554
|
-
ifr.referrerPolicy = "no-referrer";
|
|
1555
|
-
ifr.src = spotifySrc(item);
|
|
1556
|
-
el.appendChild(ifr);
|
|
1557
|
-
state.ready.spotify = true;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
function fillSelect(p) {
|
|
1561
|
-
sel.innerHTML = "";
|
|
1562
|
-
if (p.items.length <= 1) { sel.style.display = "none"; return; }
|
|
1563
|
-
sel.style.display = "";
|
|
1564
|
-
p.items.forEach(function (it, i) {
|
|
1565
|
-
var o = document.createElement("option");
|
|
1566
|
-
o.value = String(i);
|
|
1567
|
-
o.textContent = it.name || ("歌单 " + (i + 1));
|
|
1568
|
-
sel.appendChild(o);
|
|
1569
|
-
});
|
|
1570
|
-
sel.value = String(state.idx[p.key] || 0);
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
function activateStages(key) {
|
|
1574
|
-
Object.keys(stageEl).forEach(function (k) {
|
|
1575
|
-
stageEl[k].classList.toggle("active", k === key);
|
|
1718
|
+
var load = function () { document.body.appendChild(s); };
|
|
1719
|
+
if (document.readyState === "complete") {
|
|
1720
|
+
var ric = window.requestIdleCallback || function (cb) { return setTimeout(cb, 200); };
|
|
1721
|
+
ric(load, { timeout: 2500 });
|
|
1722
|
+
} else {
|
|
1723
|
+
window.addEventListener("load", function () {
|
|
1724
|
+
var ric = window.requestIdleCallback || function (cb) { return setTimeout(cb, 300); };
|
|
1725
|
+
ric(load, { timeout: 3000 });
|
|
1576
1726
|
});
|
|
1577
|
-
ctrlBar.classList.toggle("show", key === "netease");
|
|
1578
|
-
refreshBtn.classList.toggle("show", key === "netease");
|
|
1579
|
-
}
|
|
1580
|
-
|
|
1581
|
-
function selectTab(key) {
|
|
1582
|
-
state.tab = key;
|
|
1583
|
-
Object.keys(tabBtns).forEach(function (k) { tabBtns[k].classList.toggle("active", k === key); });
|
|
1584
|
-
var p = currentPlatform();
|
|
1585
|
-
fillSelect(p);
|
|
1586
|
-
activateStages(key);
|
|
1587
|
-
if (key === "netease") setupNetease(false);
|
|
1588
|
-
else setupSpotify(false);
|
|
1589
|
-
try { localStorage.setItem("ym-tab", key); } catch (e) {}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
sel.addEventListener("change", function () {
|
|
1593
|
-
var key = state.tab;
|
|
1594
|
-
state.idx[key] = parseInt(sel.value, 10) || 0;
|
|
1595
|
-
if (key === "netease") setupNetease(true);
|
|
1596
|
-
else setupSpotify(true);
|
|
1597
|
-
});
|
|
1598
|
-
|
|
1599
|
-
function open() {
|
|
1600
|
-
panel.classList.add("open");
|
|
1601
|
-
var key = state.tab;
|
|
1602
|
-
Object.keys(tabBtns).forEach(function (k) { tabBtns[k].classList.toggle("active", k === key); });
|
|
1603
|
-
fillSelect(currentPlatform());
|
|
1604
|
-
activateStages(key);
|
|
1605
|
-
if (key === "netease") setupNetease(true);
|
|
1606
|
-
else setupSpotify(false);
|
|
1607
|
-
try { localStorage.setItem("ym-open", "1"); } catch (e) {}
|
|
1608
|
-
}
|
|
1609
|
-
function close() {
|
|
1610
|
-
panel.classList.remove("open");
|
|
1611
|
-
fab.classList.remove("peek");
|
|
1612
|
-
state.ready.netease = false;
|
|
1613
|
-
try { localStorage.setItem("ym-open", "0"); } catch (e) {}
|
|
1614
|
-
}
|
|
1615
|
-
fab.addEventListener("click", function (ev) {
|
|
1616
|
-
// 第一次点击在半露出吸附态时只负责全露出,不立刻打开面板;再次点击才打开。
|
|
1617
|
-
if (!panel.classList.contains("open") && !fab.classList.contains("peek")) {
|
|
1618
|
-
fab.classList.add("peek");
|
|
1619
|
-
ev.stopPropagation();
|
|
1620
|
-
return;
|
|
1621
|
-
}
|
|
1622
|
-
fab.classList.remove("peek");
|
|
1623
|
-
panel.classList.contains("open") ? close() : open();
|
|
1624
|
-
});
|
|
1625
|
-
// 触屏没有 hover:点 FAB 之外恢复半露出;面板打开时不收起,避免遮挡交互。
|
|
1626
|
-
document.addEventListener("pointerdown", function (ev) {
|
|
1627
|
-
if (panel.classList.contains("open")) return;
|
|
1628
|
-
if (wrap.contains(ev.target)) return;
|
|
1629
|
-
fab.classList.remove("peek");
|
|
1630
|
-
}, true);
|
|
1631
|
-
|
|
1632
|
-
/* 记忆位置/尺寸/上次标签 */
|
|
1633
|
-
try {
|
|
1634
|
-
var savedRect = JSON.parse(localStorage.getItem("ym-rect") || "null");
|
|
1635
|
-
if (savedRect && savedRect.w && savedRect.h && !window.matchMedia("(max-width:600px)").matches) {
|
|
1636
|
-
panel.style.left = savedRect.x + "px";
|
|
1637
|
-
panel.style.top = savedRect.y + "px";
|
|
1638
|
-
panel.style.bottom = "auto";
|
|
1639
|
-
panel.style.width = savedRect.w + "px";
|
|
1640
|
-
panel.style.height = savedRect.h + "px";
|
|
1641
|
-
}
|
|
1642
|
-
var savedTab = localStorage.getItem("ym-tab");
|
|
1643
|
-
if (savedTab && tabBtns[savedTab]) state.tab = savedTab;
|
|
1644
|
-
} catch (e) {}
|
|
1645
|
-
|
|
1646
|
-
function isMobile() { return window.matchMedia("(max-width:600px)").matches; }
|
|
1647
|
-
function saveRect() {
|
|
1648
|
-
if (isMobile()) return;
|
|
1649
|
-
var r = panel.getBoundingClientRect();
|
|
1650
|
-
try { localStorage.setItem("ym-rect", JSON.stringify({ x: r.left, y: r.top, w: r.width, h: r.height })); } catch (e) {}
|
|
1651
|
-
}
|
|
1652
|
-
|
|
1653
|
-
/* 拖动 */
|
|
1654
|
-
var drag = null;
|
|
1655
|
-
function startDrag(ev) {
|
|
1656
|
-
if (ev.target.closest(".tab,.close,.sel")) return;
|
|
1657
|
-
var pt = ev.touches ? ev.touches[0] : ev;
|
|
1658
|
-
var r = panel.getBoundingClientRect();
|
|
1659
|
-
drag = { x: pt.clientX, y: pt.clientY, left: r.left, top: r.top };
|
|
1660
|
-
panel.style.left = r.left + "px";
|
|
1661
|
-
panel.style.top = r.top + "px";
|
|
1662
|
-
panel.style.bottom = "auto";
|
|
1663
|
-
ev.preventDefault();
|
|
1664
|
-
}
|
|
1665
|
-
function moveDrag(ev) {
|
|
1666
|
-
if (!drag) return;
|
|
1667
|
-
var pt = ev.touches ? ev.touches[0] : ev;
|
|
1668
|
-
var nx = drag.left + (pt.clientX - drag.x);
|
|
1669
|
-
var ny = drag.top + (pt.clientY - drag.y);
|
|
1670
|
-
var r = panel.getBoundingClientRect();
|
|
1671
|
-
nx = Math.max(0, Math.min(window.innerWidth - r.width, nx));
|
|
1672
|
-
ny = Math.max(0, Math.min(window.innerHeight - 60, ny));
|
|
1673
|
-
panel.style.left = nx + "px";
|
|
1674
|
-
panel.style.top = ny + "px";
|
|
1675
|
-
}
|
|
1676
|
-
function endDrag() { if (drag) { drag = null; saveRect(); } }
|
|
1677
|
-
bar.addEventListener("mousedown", startDrag);
|
|
1678
|
-
window.addEventListener("mousemove", moveDrag);
|
|
1679
|
-
window.addEventListener("mouseup", endDrag);
|
|
1680
|
-
bar.addEventListener("touchstart", startDrag, { passive: false });
|
|
1681
|
-
window.addEventListener("touchmove", moveDrag, { passive: false });
|
|
1682
|
-
window.addEventListener("touchend", endDrag);
|
|
1683
|
-
|
|
1684
|
-
/* 缩放(桌面) */
|
|
1685
|
-
var resizer = panel.querySelector(".resizer");
|
|
1686
|
-
var rsz = null;
|
|
1687
|
-
resizer.addEventListener("mousedown", function (ev) {
|
|
1688
|
-
var r = panel.getBoundingClientRect();
|
|
1689
|
-
rsz = { x: ev.clientX, y: ev.clientY, w: r.width, h: r.height, left: r.left, top: r.top };
|
|
1690
|
-
panel.style.left = r.left + "px";
|
|
1691
|
-
panel.style.top = r.top + "px";
|
|
1692
|
-
panel.style.bottom = "auto";
|
|
1693
|
-
ev.preventDefault();
|
|
1694
|
-
ev.stopPropagation();
|
|
1695
|
-
});
|
|
1696
|
-
window.addEventListener("mousemove", function (ev) {
|
|
1697
|
-
if (!rsz) return;
|
|
1698
|
-
var w = Math.max(300, rsz.w + (ev.clientX - rsz.x));
|
|
1699
|
-
var h = Math.max(360, rsz.h + (ev.clientY - rsz.y));
|
|
1700
|
-
w = Math.min(w, window.innerWidth - rsz.left - 8);
|
|
1701
|
-
panel.style.width = w + "px";
|
|
1702
|
-
panel.style.height = h + "px";
|
|
1703
|
-
});
|
|
1704
|
-
window.addEventListener("mouseup", function () { if (rsz) { rsz = null; saveRect(); } });
|
|
1705
|
-
|
|
1706
|
-
/* 明暗主题:同步宿主属性;Spotify 已初始化时重建一次(仅换主题,曲目由其自身续播) */
|
|
1707
|
-
function syncDark() {
|
|
1708
|
-
var dark = document.body.classList.contains("hope-ui-dark");
|
|
1709
|
-
if (dark) host.setAttribute("data-dark", "");
|
|
1710
|
-
else host.removeAttribute("data-dark");
|
|
1711
1727
|
}
|
|
1712
|
-
syncDark();
|
|
1713
|
-
var mo = new MutationObserver(syncDark);
|
|
1714
|
-
mo.observe(document.body, { attributes: true, attributeFilter: ["class"] });
|
|
1715
1728
|
}
|
|
1716
1729
|
|
|
1717
1730
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
|