@yuuichi/openlist-assets 4.2.6-ym.2 → 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 +1 -1
- package/dist/assets/previews-legacy-DOedQYss.js +1 -1
- 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 +540 -0
- package/dist/brand/music-player.js +135 -525
- package/dist/brand/upload-add.js +164 -100
- package/dist/brand/ym-bundle.516f792dd3.js +1733 -0
- package/dist/brand/ym-bundle.js +815 -598
- package/dist/brand/ym-bundle.manifest.json +4 -0
- package/dist/esa-entry.js +104 -103
- package/package.json +1 -1
|
@@ -1,554 +1,164 @@
|
|
|
1
1
|
/* =============================================================================
|
|
2
|
-
* Yuuichi 资源站 ·
|
|
2
|
+
* Yuuichi 资源站 · 背景音乐(与 blog.yuuichi.me / www.yuuichi.me 统一的 bgm-dock)
|
|
3
3
|
* -----------------------------------------------------------------------------
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 每首歌的直链 / 封面 / 歌词由 metingApi 按 id 实时解析(公益服务,失效可换其它 Meting 兼容地址)。
|
|
14
|
-
* 改完保存,在 openlist 目录执行:
|
|
15
|
-
* node scripts/sync-branding.mjs
|
|
16
|
-
* pnpm exec wrangler deploy
|
|
4
|
+
* 统一播放器:左下角圆形按钮(半吸附边缘 / 可拖动 / 可缩放),点击展开面板,
|
|
5
|
+
* 内置 Spotify 官方嵌入 + 网易云(Meting/APlayer,含封面/歌词/列表循环/倍速)。
|
|
6
|
+
*
|
|
7
|
+
* 想换歌单,修改下面 MUSIC_CONFIG 的 id,然后:
|
|
8
|
+
* node scripts/sync-branding.mjs && pnpm exec wrangler deploy
|
|
9
|
+
* 也可以在 OpenList 管理台设置项 music_config 在线覆盖(无需重新部署)。
|
|
10
|
+
*
|
|
11
|
+
* 资源站资源全部同源托管在 /brand/bgm/ 下;网易云歌单/直链/歌词走本站
|
|
12
|
+
* /api/music/meting(Worker 服务端代理网易云),浏览器不直接访问 workers.dev。
|
|
17
13
|
* ============================================================================= */
|
|
18
14
|
(function () {
|
|
19
15
|
"use strict";
|
|
20
16
|
|
|
17
|
+
var ASSET_BASE = "/brand/bgm";
|
|
18
|
+
var METING_API = "/api/music/meting?server=:server&type=:type&id=:id&r=:r";
|
|
19
|
+
|
|
21
20
|
var MUSIC_CONFIG = {
|
|
22
|
-
netease: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
autoplay: false, // 网易云是否自动播放(多数浏览器会拦截,建议 false)
|
|
30
|
-
fabHint: "音乐"
|
|
21
|
+
netease: [{ id: "18475904830", name: "网易云" }],
|
|
22
|
+
spotify: [{ id: "3bePpYfOLDwFtVVs6cqwyh", name: "Spotify" }],
|
|
23
|
+
defaultTab: "spotify", // "spotify" 或 "netease"
|
|
24
|
+
autoplay: false,
|
|
25
|
+
theme: "#0ea5e9",
|
|
26
|
+
volume: 0.6,
|
|
27
|
+
order: "list" // list / random
|
|
31
28
|
};
|
|
32
29
|
|
|
33
|
-
//
|
|
34
|
-
// Worker 会在本脚本前注入 window.__YUUICHI_MUSIC_CONFIG_OVERLAY__。
|
|
30
|
+
// 管理台 music_config 在线覆盖
|
|
35
31
|
try {
|
|
36
32
|
var __ov = window.__YUUICHI_MUSIC_CONFIG_OVERLAY__;
|
|
37
33
|
if (__ov && typeof __ov === "object") {
|
|
38
34
|
["netease", "spotify"].forEach(function (k) {
|
|
39
35
|
if (Array.isArray(__ov[k])) MUSIC_CONFIG[k] = __ov[k];
|
|
40
36
|
});
|
|
41
|
-
["defaultTab", "
|
|
37
|
+
["defaultTab", "theme", "order"].forEach(function (k) {
|
|
42
38
|
if (typeof __ov[k] === "string" && __ov[k]) MUSIC_CONFIG[k] = __ov[k];
|
|
43
39
|
});
|
|
44
|
-
|
|
40
|
+
["autoplay"].forEach(function (k) {
|
|
41
|
+
if (typeof __ov[k] === "boolean") MUSIC_CONFIG[k] = __ov[k];
|
|
42
|
+
});
|
|
43
|
+
if (typeof __ov.volume === "number") MUSIC_CONFIG.volume = __ov.volume;
|
|
45
44
|
}
|
|
46
45
|
} catch (e) {}
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
" border-radius:999px; padding:4px 12px; font-size:12px; cursor:pointer; }",
|
|
97
|
-
".tab.active { background:linear-gradient(135deg,#38bdf8,#0284c7); color:#fff; border-color:transparent; }",
|
|
98
|
-
".sel { flex:1; min-width:0; border:1px solid var(--ym-line); background:transparent;",
|
|
99
|
-
" color:var(--ym-txt); border-radius:8px; font-size:12px; padding:4px 6px; outline:none; }",
|
|
100
|
-
".sel option { color:#0f172a; }",
|
|
101
|
-
".close { border:none; background:transparent; color:var(--ym-txt); font-size:18px; line-height:1; cursor:pointer; padding:2px 7px; border-radius:8px; }",
|
|
102
|
-
".close:hover { background:var(--ym-soft); }",
|
|
103
|
-
".refresh { display:none; border:none; background:transparent; color:var(--ym-txt); cursor:pointer; padding:2px 6px; border-radius:8px; align-items:center; }",
|
|
104
|
-
".refresh.show { display:inline-flex; }",
|
|
105
|
-
".refresh:hover { background:var(--ym-soft); }",
|
|
106
|
-
".refresh svg { width:16px; height:16px; }",
|
|
107
|
-
".refresh.spin svg { animation:ym-rspin .8s linear infinite; }",
|
|
108
|
-
"@keyframes ym-rspin { to { transform:rotate(360deg); } }",
|
|
109
|
-
".ctrl { display:none; align-items:center; gap:6px; padding:5px 10px; flex:0 0 auto;",
|
|
110
|
-
" border-bottom:1px solid var(--ym-line); }",
|
|
111
|
-
".ctrl.show { display:flex; }",
|
|
112
|
-
".cbtn { border:1px solid var(--ym-line); background:transparent; color:var(--ym-txt); cursor:pointer;",
|
|
113
|
-
" border-radius:999px; font-size:12px; line-height:1; padding:4px 10px; display:inline-flex; align-items:center; gap:5px; }",
|
|
114
|
-
".cbtn:hover { background:var(--ym-soft); }",
|
|
115
|
-
".cbtn svg { width:14px; height:14px; }",
|
|
116
|
-
".stages { flex:1; min-height:0; position:relative; }",
|
|
117
|
-
".stage { position:absolute; inset:0; display:none; }",
|
|
118
|
-
".stage.active { display:block; }",
|
|
119
|
-
".stage iframe { width:100%; height:100%; border:0; display:block; background:#fff; }",
|
|
120
|
-
".ap-wrap { position:absolute; inset:0; overflow:auto; }",
|
|
121
|
-
".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; }",
|
|
122
|
-
".status button { border:none; cursor:pointer; color:#fff; background:linear-gradient(135deg,#38bdf8,#0284c7); border-radius:999px; padding:6px 18px; font-size:13px; }",
|
|
123
|
-
".resizer { position:absolute; right:0; bottom:0; width:22px; height:22px; cursor:nwse-resize; z-index:5; }",
|
|
124
|
-
".resizer::after { content:''; position:absolute; right:5px; bottom:5px; width:10px; height:10px;",
|
|
125
|
-
" border-right:2px solid var(--ym-line); border-bottom:2px solid var(--ym-line); }",
|
|
126
|
-
/* APlayer 皮肤适配(透明、贴合面板、青蓝主题) */
|
|
127
|
-
".aplayer { width:100%; margin:0; background:transparent !important; box-shadow:none !important; border-radius:0; color:var(--ym-txt); }",
|
|
128
|
-
".aplayer .aplayer-info { border-top:1px solid var(--ym-line); }",
|
|
129
|
-
".aplayer .aplayer-title, .aplayer .aplayer-list ol li .aplayer-list-index, .aplayer .aplayer-time { color:var(--ym-sub); }",
|
|
130
|
-
".aplayer .aplayer-list ol li { border-top:1px solid var(--ym-line); }",
|
|
131
|
-
".aplayer .aplayer-list ol li.aplayer-list-light { background:var(--ym-soft); }",
|
|
132
|
-
".aplayer .aplayer-list ol li:hover { background:var(--ym-soft); }",
|
|
133
|
-
".aplayer .aplayer-list ol li .aplayer-list-author { color:var(--ym-sub); }",
|
|
134
|
-
".aplayer .aplayer-lrc { text-shadow:0 1px 4px rgba(0,0,0,.35); }",
|
|
135
|
-
".aplayer .aplayer-lrc p.aplayer-lrc-current { color:#38bdf8; }",
|
|
136
|
-
".aplayer .aplayer-miniswitcher { background:var(--ym-soft); }",
|
|
137
|
-
":host([data-dark]) .aplayer svg, :host([data-dark]) .aplayer .aplayer-icon { filter:invert(1) hue-rotate(180deg); }",
|
|
138
|
-
"@media (max-width:600px) {",
|
|
139
|
-
" .fab { left:14px; bottom:14px; width:50px; height:50px; }",
|
|
140
|
-
" .panel { left:12px !important; bottom:76px !important; width:calc(100vw - 24px) !important; height:min(76vh,600px) !important; }",
|
|
141
|
-
" .resizer { display:none; }",
|
|
142
|
-
"}"
|
|
143
|
-
].join("\n");
|
|
144
|
-
shadow.appendChild(style);
|
|
145
|
-
|
|
146
|
-
/* ---------------- DOM 骨架 ---------------- */
|
|
147
|
-
var wrap = document.createElement("div");
|
|
148
|
-
wrap.className = "wrap";
|
|
149
|
-
shadow.appendChild(wrap);
|
|
150
|
-
|
|
151
|
-
var fab = document.createElement("button");
|
|
152
|
-
fab.className = "fab";
|
|
153
|
-
fab.title = MUSIC_CONFIG.fabHint || "音乐";
|
|
154
|
-
fab.setAttribute("aria-label", "音乐播放器");
|
|
155
|
-
fab.innerHTML =
|
|
156
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
157
|
-
'<path d="M9 18V5l10-2v13"></path><circle cx="6" cy="18" r="3"></circle><circle cx="16" cy="16" r="3"></circle></svg>';
|
|
158
|
-
wrap.appendChild(fab);
|
|
159
|
-
|
|
160
|
-
var panel = document.createElement("div");
|
|
161
|
-
panel.className = "panel";
|
|
162
|
-
panel.innerHTML =
|
|
163
|
-
'<div class="bar"></div>' +
|
|
164
|
-
'<div class="ctrl">' +
|
|
165
|
-
'<button class="cbtn js-speed" type="button" title="播放倍速"><span class="speed-label">1.0x</span></button>' +
|
|
166
|
-
'<button class="cbtn js-order" type="button" title="播放顺序"></button>' +
|
|
167
|
-
'</div>' +
|
|
168
|
-
'<div class="stages"></div>' +
|
|
169
|
-
'<div class="resizer"></div>';
|
|
170
|
-
wrap.appendChild(panel);
|
|
171
|
-
|
|
172
|
-
var bar = panel.querySelector(".bar");
|
|
173
|
-
var stages = panel.querySelector(".stages");
|
|
174
|
-
var ctrlBar = panel.querySelector(".ctrl");
|
|
175
|
-
var speedBtn = panel.querySelector(".js-speed");
|
|
176
|
-
var speedLabel = panel.querySelector(".speed-label");
|
|
177
|
-
var orderBtn = panel.querySelector(".js-order");
|
|
178
|
-
|
|
179
|
-
var tabBtns = {};
|
|
180
|
-
platforms.forEach(function (p) {
|
|
181
|
-
var b = document.createElement("button");
|
|
182
|
-
b.className = "tab";
|
|
183
|
-
b.textContent = p.label;
|
|
184
|
-
b.dataset.key = p.key;
|
|
185
|
-
b.addEventListener("click", function () { selectTab(p.key); });
|
|
186
|
-
bar.appendChild(b);
|
|
187
|
-
tabBtns[p.key] = b;
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
var sel = document.createElement("select");
|
|
191
|
-
sel.className = "sel";
|
|
192
|
-
bar.appendChild(sel);
|
|
193
|
-
|
|
194
|
-
var refreshBtn = document.createElement("button");
|
|
195
|
-
refreshBtn.className = "refresh";
|
|
196
|
-
refreshBtn.title = "重新拉取最新歌单";
|
|
197
|
-
refreshBtn.setAttribute("aria-label", "刷新歌单");
|
|
198
|
-
refreshBtn.innerHTML =
|
|
199
|
-
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' +
|
|
200
|
-
'<path d="M21 12a9 9 0 1 1-2.64-6.36"></path><polyline points="21 3 21 9 15 9"></polyline></svg>';
|
|
201
|
-
refreshBtn.addEventListener("click", function () {
|
|
202
|
-
if (state.tab === "netease") {
|
|
203
|
-
refreshBtn.classList.add("spin");
|
|
204
|
-
setupNetease(true).finally(function () { refreshBtn.classList.remove("spin"); });
|
|
205
|
-
}
|
|
47
|
+
/* 收起态关键 CSS(与博客 #bgm-dock-critical 保持一致),内联防止展开前闪烁 */
|
|
48
|
+
var CRITICAL_CSS =
|
|
49
|
+
".bgm-dock{--bgm-accent:" + (MUSIC_CONFIG.theme || "#0ea5e9") + ";" +
|
|
50
|
+
"position:fixed;left:16px;bottom:16px;z-index:99999;width:52px;max-width:calc(100vw - 24px);" +
|
|
51
|
+
"font-size:13px;line-height:1.4;color:#222;}" +
|
|
52
|
+
".bgm-dock.is-open{width:320px;max-width:calc(100vw - 24px);}" +
|
|
53
|
+
".bgm-dock__fab{position:relative;width:52px;height:52px;border:0;border-radius:50%;" +
|
|
54
|
+
"background:var(--bgm-accent);color:#fff;font-size:20px;cursor:pointer;" +
|
|
55
|
+
"box-shadow:0 8px 22px rgba(0,0,0,.28);display:inline-flex;align-items:center;justify-content:center;touch-action:none;}" +
|
|
56
|
+
".bgm-dock__fab.is-dragging{cursor:grabbing;opacity:.92;}" +
|
|
57
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab{--bgm-peek:18px;transition:transform .28s cubic-bezier(.22,.61,.36,1);will-change:transform;}" +
|
|
58
|
+
// 收起态半吸附到屏幕边缘:默认只露出一小截(半圆即可 hover),鼠标移上 /
|
|
59
|
+
// 键盘聚焦 / 拖拽 / 已全露 时整个滑回屏幕内,保证桌面鼠标始终点得到。
|
|
60
|
+
// 触摸设备点一下全露、再点展开(见 bgm-dock.js 的 is-revealed 逻辑)。
|
|
61
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-left{transform:translateX(calc(var(--bgm-peek) - 100%));}" +
|
|
62
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-right{transform:translateX(calc(100% - var(--bgm-peek)));}" +
|
|
63
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab:hover," +
|
|
64
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab:focus-visible," +
|
|
65
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-revealed," +
|
|
66
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-dragging{transform:none;}" +
|
|
67
|
+
// 触摸(无 hover)首次点按仅全露:未全露时不要因 hover 规则干扰(触摸无 hover,
|
|
68
|
+
// 该媒体查询下默认半藏即可)。
|
|
69
|
+
"@media (hover:none),(pointer:coarse){" +
|
|
70
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-left{transform:translateX(calc(var(--bgm-peek) - 100%));}" +
|
|
71
|
+
".bgm-dock:not(.is-open) .bgm-dock__fab.is-peek-right{transform:translateX(calc(100% - var(--bgm-peek)));}}" +
|
|
72
|
+
".bgm-dock__fab svg{width:22px;height:22px;fill:currentColor;display:block;}" +
|
|
73
|
+
".bgm-dock__fab-bar{position:absolute;right:9px;top:9px;width:9px;height:9px;border-radius:50%;" +
|
|
74
|
+
"background:#2ecc71;border:2px solid #fff;opacity:0;}" +
|
|
75
|
+
".bgm-dock.is-playing .bgm-dock__fab-bar{opacity:1;}" +
|
|
76
|
+
".bgm-dock__panel{display:none;}" +
|
|
77
|
+
".bgm-dock.is-open .bgm-dock__panel{display:flex;}" +
|
|
78
|
+
".bgm-dock.is-open .bgm-dock__fab{display:none;}" +
|
|
79
|
+
".bgm-dock__overlay{position:fixed;inset:0;z-index:999999;cursor:grabbing;user-select:none;}" +
|
|
80
|
+
"@media (max-width:768px){.bgm-dock{left:14px;bottom:14px;width:52px;}" +
|
|
81
|
+
".bgm-dock.is-open{width:auto;left:0;right:0;bottom:0;max-width:none;}}";
|
|
82
|
+
|
|
83
|
+
function buildSources() {
|
|
84
|
+
var sources = [];
|
|
85
|
+
var i = 0;
|
|
86
|
+
(MUSIC_CONFIG.spotify || []).forEach(function (s) {
|
|
87
|
+
if (!s || !s.id) return;
|
|
88
|
+
sources.push({
|
|
89
|
+
name: s.name || "Spotify",
|
|
90
|
+
type: "spotify",
|
|
91
|
+
resourceType: "playlist",
|
|
92
|
+
id: String(s.id),
|
|
93
|
+
key: "spotify-" + i++
|
|
94
|
+
});
|
|
206
95
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
var stageEl = {};
|
|
217
|
-
platforms.forEach(function (p) {
|
|
218
|
-
var d = document.createElement("div");
|
|
219
|
-
d.className = "stage";
|
|
220
|
-
d.dataset.key = p.key;
|
|
221
|
-
stages.appendChild(d);
|
|
222
|
-
stageEl[p.key] = d;
|
|
96
|
+
(MUSIC_CONFIG.netease || []).forEach(function (s) {
|
|
97
|
+
if (!s || !s.id) return;
|
|
98
|
+
sources.push({
|
|
99
|
+
name: s.name || "网易云",
|
|
100
|
+
type: "netease",
|
|
101
|
+
resourceType: "playlist",
|
|
102
|
+
id: String(s.id),
|
|
103
|
+
key: "netease-" + i++
|
|
104
|
+
});
|
|
223
105
|
});
|
|
106
|
+
// 默认源标记
|
|
107
|
+
var wantDefault = MUSIC_CONFIG.defaultTab === "netease" ? "netease" : "spotify";
|
|
108
|
+
var def = sources.filter(function (s) { return s.type === wantDefault; })[0] || sources[0];
|
|
109
|
+
if (def) def.default = true;
|
|
110
|
+
return sources;
|
|
111
|
+
}
|
|
224
112
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
113
|
+
function init() {
|
|
114
|
+
if (window.__yuuichiBgmInited) return;
|
|
115
|
+
if (document.getElementById("bgm-dock")) return;
|
|
116
|
+
window.__yuuichiBgmInited = true;
|
|
117
|
+
|
|
118
|
+
var sources = buildSources();
|
|
119
|
+
if (!sources.length) return;
|
|
120
|
+
|
|
121
|
+
var styleEl = document.createElement("style");
|
|
122
|
+
styleEl.id = "bgm-dock-critical";
|
|
123
|
+
styleEl.textContent = CRITICAL_CSS;
|
|
124
|
+
document.head.appendChild(styleEl);
|
|
125
|
+
|
|
126
|
+
var dock = document.createElement("div");
|
|
127
|
+
dock.id = "bgm-dock";
|
|
128
|
+
dock.className = "bgm-dock";
|
|
129
|
+
dock.setAttribute(
|
|
130
|
+
"data-bgm",
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
sources: sources,
|
|
133
|
+
autoplay: MUSIC_CONFIG.autoplay === true,
|
|
134
|
+
order: MUSIC_CONFIG.order === "random" ? "random" : "list",
|
|
135
|
+
volume: typeof MUSIC_CONFIG.volume === "number" ? MUSIC_CONFIG.volume : 0.6,
|
|
136
|
+
theme: MUSIC_CONFIG.theme || "#0ea5e9",
|
|
137
|
+
assetBase: ASSET_BASE,
|
|
138
|
+
metingApi: METING_API
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
document.body.appendChild(dock);
|
|
142
|
+
|
|
143
|
+
// 收起态只依赖内联 CRITICAL_CSS,主脚本 bgm-dock.js(~28KB)推迟到浏览器
|
|
144
|
+
// 空闲 / load 后再拉,避免与首屏 JS、目录列表、背景图争抢带宽与主线程。
|
|
145
|
+
var s = document.createElement("script");
|
|
146
|
+
s.src = ASSET_BASE + "/js/bgm-dock.js";
|
|
147
|
+
s.defer = true;
|
|
148
|
+
s.onerror = function () {
|
|
149
|
+
// 加载失败时至少保证按钮还能看到,避免完全无反应
|
|
150
|
+
try { dock.style.display = "none"; } catch (e) {}
|
|
232
151
|
};
|
|
233
|
-
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
{ 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>' },
|
|
242
|
-
{ 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>' }
|
|
243
|
-
];
|
|
244
|
-
var orderIdx = 0;
|
|
245
|
-
try {
|
|
246
|
-
var sv = parseFloat(localStorage.getItem("ym-speed"));
|
|
247
|
-
var si = SPEEDS.indexOf(sv);
|
|
248
|
-
if (si >= 0) speedIdx = si;
|
|
249
|
-
var oi = parseInt(localStorage.getItem("ym-order"), 10);
|
|
250
|
-
if (!isNaN(oi) && ORDER_MODES[oi]) orderIdx = oi;
|
|
251
|
-
} catch (e) {}
|
|
252
|
-
|
|
253
|
-
function applySpeed() {
|
|
254
|
-
var rate = SPEEDS[speedIdx];
|
|
255
|
-
speedLabel.textContent = (rate % 1 === 0 ? rate.toFixed(1) : String(rate)) + "x";
|
|
256
|
-
try {
|
|
257
|
-
if (state.ap && state.ap.audio) state.ap.audio.playbackRate = rate;
|
|
258
|
-
localStorage.setItem("ym-speed", String(rate));
|
|
259
|
-
} catch (e) {}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function applyOrder() {
|
|
263
|
-
var mode = ORDER_MODES[orderIdx];
|
|
264
|
-
orderBtn.innerHTML = mode.icon + '<span class="order-label">' + mode.label + '</span>';
|
|
265
|
-
orderBtn.title = "播放顺序:" + mode.label;
|
|
266
|
-
try {
|
|
267
|
-
if (state.ap && state.ap.options) {
|
|
268
|
-
state.ap.options.loop = mode.loop;
|
|
269
|
-
state.ap.options.order = mode.order;
|
|
270
|
-
}
|
|
271
|
-
localStorage.setItem("ym-order", String(orderIdx));
|
|
272
|
-
} catch (e) {}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
speedBtn.addEventListener("click", function () {
|
|
276
|
-
speedIdx = (speedIdx + 1) % SPEEDS.length;
|
|
277
|
-
applySpeed();
|
|
278
|
-
});
|
|
279
|
-
orderBtn.addEventListener("click", function () {
|
|
280
|
-
orderIdx = (orderIdx + 1) % ORDER_MODES.length;
|
|
281
|
-
applyOrder();
|
|
282
|
-
});
|
|
283
|
-
applySpeed();
|
|
284
|
-
applyOrder();
|
|
285
|
-
|
|
286
|
-
function currentPlatform() {
|
|
287
|
-
for (var i = 0; i < platforms.length; i++) if (platforms[i].key === state.tab) return platforms[i];
|
|
288
|
-
return platforms[0];
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function loadScriptOnce(attr, src) {
|
|
292
|
-
return new Promise(function (resolve, reject) {
|
|
293
|
-
var existing = document.querySelector("script[" + attr + "]");
|
|
294
|
-
if (existing) {
|
|
295
|
-
if (existing.dataset.loaded === "1") return resolve();
|
|
296
|
-
existing.addEventListener("load", function () { resolve(); });
|
|
297
|
-
existing.addEventListener("error", reject);
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
var s = document.createElement("script");
|
|
301
|
-
s.async = true;
|
|
302
|
-
s.setAttribute(attr, "1");
|
|
303
|
-
s.onload = function () { s.dataset.loaded = "1"; resolve(); };
|
|
304
|
-
s.onerror = reject;
|
|
305
|
-
s.src = src;
|
|
306
|
-
document.head.appendChild(s);
|
|
152
|
+
var load = function () { document.body.appendChild(s); };
|
|
153
|
+
if (document.readyState === "complete") {
|
|
154
|
+
var ric = window.requestIdleCallback || function (cb) { return setTimeout(cb, 200); };
|
|
155
|
+
ric(load, { timeout: 2500 });
|
|
156
|
+
} else {
|
|
157
|
+
window.addEventListener("load", function () {
|
|
158
|
+
var ric = window.requestIdleCallback || function (cb) { return setTimeout(cb, 300); };
|
|
159
|
+
ric(load, { timeout: 3000 });
|
|
307
160
|
});
|
|
308
161
|
}
|
|
309
|
-
|
|
310
|
-
function loadScript(src) {
|
|
311
|
-
return loadScriptOnce("data-ym-aplayer", src);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
/* 实时同源接口:Worker 服务端优先走自建 Meting、失败回退网易云官方,无 CORS */
|
|
315
|
-
function fetchWithTimeout(url, ms) {
|
|
316
|
-
var ctrl = ("AbortController" in window) ? new AbortController() : null;
|
|
317
|
-
var timer = ctrl ? setTimeout(function () { ctrl.abort(); }, ms) : null;
|
|
318
|
-
return fetch(url, { cache: "no-store", signal: ctrl ? ctrl.signal : undefined })
|
|
319
|
-
.finally(function () { if (timer) clearTimeout(timer); });
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
async function fetchPlaylist(item, force) {
|
|
323
|
-
var url = "/api/music/netease?id=" + encodeURIComponent(item.id) + (force ? "&refresh=1" : "");
|
|
324
|
-
var res = await fetchWithTimeout(url, 15000);
|
|
325
|
-
var j = await res.json();
|
|
326
|
-
if (!j || j.code !== 200 || !Array.isArray(j.data && j.data.songs) || !j.data.songs.length) {
|
|
327
|
-
throw new Error("empty");
|
|
328
|
-
}
|
|
329
|
-
return j.data.songs;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function statusEl(el, msg, withRetry) {
|
|
333
|
-
el.innerHTML = "";
|
|
334
|
-
var d = document.createElement("div");
|
|
335
|
-
d.className = "status";
|
|
336
|
-
var span = document.createElement("span");
|
|
337
|
-
span.textContent = msg;
|
|
338
|
-
d.appendChild(span);
|
|
339
|
-
if (withRetry) {
|
|
340
|
-
var btn = document.createElement("button");
|
|
341
|
-
btn.textContent = "重试";
|
|
342
|
-
btn.addEventListener("click", function () { setupNetease(true); });
|
|
343
|
-
d.appendChild(btn);
|
|
344
|
-
}
|
|
345
|
-
el.appendChild(d);
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
async function setupNetease(force) {
|
|
349
|
-
var el = stageEl.netease;
|
|
350
|
-
if (state.ready.netease && !force) return;
|
|
351
|
-
if (state.loading) return;
|
|
352
|
-
state.loading = true;
|
|
353
|
-
statusEl(el, "正在加载网易云歌单…", false);
|
|
354
|
-
try {
|
|
355
|
-
var item = MUSIC_CONFIG.netease[state.idx.netease || 0];
|
|
356
|
-
var list = await fetchPlaylist(item, !!force);
|
|
357
|
-
state.tracks = list;
|
|
358
|
-
|
|
359
|
-
if (state.ap) { try { state.ap.destroy(); } catch (e) {} state.ap = null; }
|
|
360
|
-
el.innerHTML = '<div class="ap-wrap"></div>';
|
|
361
|
-
ensureVendorCss();
|
|
362
|
-
await loadScript("/brand/vendor/APlayer.min.js");
|
|
363
|
-
|
|
364
|
-
var mode = ORDER_MODES[orderIdx];
|
|
365
|
-
var ap = new window.APlayer({
|
|
366
|
-
container: el.querySelector(".ap-wrap"),
|
|
367
|
-
fixed: false,
|
|
368
|
-
mini: false,
|
|
369
|
-
autoplay: !!MUSIC_CONFIG.autoplay,
|
|
370
|
-
theme: "#0ea5e9",
|
|
371
|
-
loop: mode.loop,
|
|
372
|
-
order: mode.order,
|
|
373
|
-
listMaxHeight: "999px",
|
|
374
|
-
lrcType: list[0].lrc ? 3 : 0,
|
|
375
|
-
audio: list.map(function (t) {
|
|
376
|
-
return { name: t.name, artist: t.artist, url: t.url, cover: t.pic, lrc: t.lrc, theme: "#0ea5e9" };
|
|
377
|
-
})
|
|
378
|
-
});
|
|
379
|
-
state.ap = ap;
|
|
380
|
-
var audio = ap.audio;
|
|
381
|
-
if (audio) {
|
|
382
|
-
audio.playbackRate = SPEEDS[speedIdx];
|
|
383
|
-
audio.addEventListener("play", function () { audio.playbackRate = SPEEDS[speedIdx]; });
|
|
384
|
-
}
|
|
385
|
-
state.ready.netease = true;
|
|
386
|
-
} catch (e) {
|
|
387
|
-
statusEl(el, "网易云歌单加载失败(解析服务可能暂时不可用)", true);
|
|
388
|
-
} finally {
|
|
389
|
-
state.loading = false;
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function spotifySrc(item) {
|
|
394
|
-
var dark = document.body.classList.contains("hope-ui-dark");
|
|
395
|
-
return "https://open.spotify.com/embed/playlist/" + encodeURIComponent(item.id) +
|
|
396
|
-
"?utm_source=generator&theme=" + (dark ? "0" : "1");
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function setupSpotify(force) {
|
|
400
|
-
var el = stageEl.spotify;
|
|
401
|
-
if (state.ready.spotify && !force) return;
|
|
402
|
-
var item = MUSIC_CONFIG.spotify[state.idx.spotify || 0];
|
|
403
|
-
el.innerHTML = "";
|
|
404
|
-
var ifr = document.createElement("iframe");
|
|
405
|
-
ifr.loading = "lazy";
|
|
406
|
-
ifr.allow = "autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture";
|
|
407
|
-
ifr.referrerPolicy = "no-referrer";
|
|
408
|
-
ifr.src = spotifySrc(item);
|
|
409
|
-
el.appendChild(ifr);
|
|
410
|
-
state.ready.spotify = true;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
function fillSelect(p) {
|
|
414
|
-
sel.innerHTML = "";
|
|
415
|
-
if (p.items.length <= 1) { sel.style.display = "none"; return; }
|
|
416
|
-
sel.style.display = "";
|
|
417
|
-
p.items.forEach(function (it, i) {
|
|
418
|
-
var o = document.createElement("option");
|
|
419
|
-
o.value = String(i);
|
|
420
|
-
o.textContent = it.name || ("歌单 " + (i + 1));
|
|
421
|
-
sel.appendChild(o);
|
|
422
|
-
});
|
|
423
|
-
sel.value = String(state.idx[p.key] || 0);
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function activateStages(key) {
|
|
427
|
-
Object.keys(stageEl).forEach(function (k) {
|
|
428
|
-
stageEl[k].classList.toggle("active", k === key);
|
|
429
|
-
});
|
|
430
|
-
ctrlBar.classList.toggle("show", key === "netease");
|
|
431
|
-
refreshBtn.classList.toggle("show", key === "netease");
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function selectTab(key) {
|
|
435
|
-
state.tab = key;
|
|
436
|
-
Object.keys(tabBtns).forEach(function (k) { tabBtns[k].classList.toggle("active", k === key); });
|
|
437
|
-
var p = currentPlatform();
|
|
438
|
-
fillSelect(p);
|
|
439
|
-
activateStages(key);
|
|
440
|
-
if (key === "netease") setupNetease(false);
|
|
441
|
-
else setupSpotify(false);
|
|
442
|
-
try { localStorage.setItem("ym-tab", key); } catch (e) {}
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
sel.addEventListener("change", function () {
|
|
446
|
-
var key = state.tab;
|
|
447
|
-
state.idx[key] = parseInt(sel.value, 10) || 0;
|
|
448
|
-
if (key === "netease") setupNetease(true);
|
|
449
|
-
else setupSpotify(true);
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
function open() {
|
|
453
|
-
panel.classList.add("open");
|
|
454
|
-
var key = state.tab;
|
|
455
|
-
Object.keys(tabBtns).forEach(function (k) { tabBtns[k].classList.toggle("active", k === key); });
|
|
456
|
-
fillSelect(currentPlatform());
|
|
457
|
-
activateStages(key);
|
|
458
|
-
if (key === "netease") setupNetease(true);
|
|
459
|
-
else setupSpotify(false);
|
|
460
|
-
try { localStorage.setItem("ym-open", "1"); } catch (e) {}
|
|
461
|
-
}
|
|
462
|
-
function close() {
|
|
463
|
-
panel.classList.remove("open");
|
|
464
|
-
state.ready.netease = false;
|
|
465
|
-
try { localStorage.setItem("ym-open", "0"); } catch (e) {}
|
|
466
|
-
}
|
|
467
|
-
fab.addEventListener("click", function () { panel.classList.contains("open") ? close() : open(); });
|
|
468
|
-
|
|
469
|
-
/* 记忆位置/尺寸/上次标签 */
|
|
470
|
-
try {
|
|
471
|
-
var savedRect = JSON.parse(localStorage.getItem("ym-rect") || "null");
|
|
472
|
-
if (savedRect && savedRect.w && savedRect.h && !window.matchMedia("(max-width:600px)").matches) {
|
|
473
|
-
panel.style.left = savedRect.x + "px";
|
|
474
|
-
panel.style.top = savedRect.y + "px";
|
|
475
|
-
panel.style.bottom = "auto";
|
|
476
|
-
panel.style.width = savedRect.w + "px";
|
|
477
|
-
panel.style.height = savedRect.h + "px";
|
|
478
|
-
}
|
|
479
|
-
var savedTab = localStorage.getItem("ym-tab");
|
|
480
|
-
if (savedTab && tabBtns[savedTab]) state.tab = savedTab;
|
|
481
|
-
} catch (e) {}
|
|
482
|
-
|
|
483
|
-
function isMobile() { return window.matchMedia("(max-width:600px)").matches; }
|
|
484
|
-
function saveRect() {
|
|
485
|
-
if (isMobile()) return;
|
|
486
|
-
var r = panel.getBoundingClientRect();
|
|
487
|
-
try { localStorage.setItem("ym-rect", JSON.stringify({ x: r.left, y: r.top, w: r.width, h: r.height })); } catch (e) {}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/* 拖动 */
|
|
491
|
-
var drag = null;
|
|
492
|
-
function startDrag(ev) {
|
|
493
|
-
if (ev.target.closest(".tab,.close,.sel")) return;
|
|
494
|
-
var pt = ev.touches ? ev.touches[0] : ev;
|
|
495
|
-
var r = panel.getBoundingClientRect();
|
|
496
|
-
drag = { x: pt.clientX, y: pt.clientY, left: r.left, top: r.top };
|
|
497
|
-
panel.style.left = r.left + "px";
|
|
498
|
-
panel.style.top = r.top + "px";
|
|
499
|
-
panel.style.bottom = "auto";
|
|
500
|
-
ev.preventDefault();
|
|
501
|
-
}
|
|
502
|
-
function moveDrag(ev) {
|
|
503
|
-
if (!drag) return;
|
|
504
|
-
var pt = ev.touches ? ev.touches[0] : ev;
|
|
505
|
-
var nx = drag.left + (pt.clientX - drag.x);
|
|
506
|
-
var ny = drag.top + (pt.clientY - drag.y);
|
|
507
|
-
var r = panel.getBoundingClientRect();
|
|
508
|
-
nx = Math.max(0, Math.min(window.innerWidth - r.width, nx));
|
|
509
|
-
ny = Math.max(0, Math.min(window.innerHeight - 60, ny));
|
|
510
|
-
panel.style.left = nx + "px";
|
|
511
|
-
panel.style.top = ny + "px";
|
|
512
|
-
}
|
|
513
|
-
function endDrag() { if (drag) { drag = null; saveRect(); } }
|
|
514
|
-
bar.addEventListener("mousedown", startDrag);
|
|
515
|
-
window.addEventListener("mousemove", moveDrag);
|
|
516
|
-
window.addEventListener("mouseup", endDrag);
|
|
517
|
-
bar.addEventListener("touchstart", startDrag, { passive: false });
|
|
518
|
-
window.addEventListener("touchmove", moveDrag, { passive: false });
|
|
519
|
-
window.addEventListener("touchend", endDrag);
|
|
520
|
-
|
|
521
|
-
/* 缩放(桌面) */
|
|
522
|
-
var resizer = panel.querySelector(".resizer");
|
|
523
|
-
var rsz = null;
|
|
524
|
-
resizer.addEventListener("mousedown", function (ev) {
|
|
525
|
-
var r = panel.getBoundingClientRect();
|
|
526
|
-
rsz = { x: ev.clientX, y: ev.clientY, w: r.width, h: r.height, left: r.left, top: r.top };
|
|
527
|
-
panel.style.left = r.left + "px";
|
|
528
|
-
panel.style.top = r.top + "px";
|
|
529
|
-
panel.style.bottom = "auto";
|
|
530
|
-
ev.preventDefault();
|
|
531
|
-
ev.stopPropagation();
|
|
532
|
-
});
|
|
533
|
-
window.addEventListener("mousemove", function (ev) {
|
|
534
|
-
if (!rsz) return;
|
|
535
|
-
var w = Math.max(300, rsz.w + (ev.clientX - rsz.x));
|
|
536
|
-
var h = Math.max(360, rsz.h + (ev.clientY - rsz.y));
|
|
537
|
-
w = Math.min(w, window.innerWidth - rsz.left - 8);
|
|
538
|
-
panel.style.width = w + "px";
|
|
539
|
-
panel.style.height = h + "px";
|
|
540
|
-
});
|
|
541
|
-
window.addEventListener("mouseup", function () { if (rsz) { rsz = null; saveRect(); } });
|
|
542
|
-
|
|
543
|
-
/* 明暗主题:同步宿主属性;Spotify 已初始化时重建一次(仅换主题,曲目由其自身续播) */
|
|
544
|
-
function syncDark() {
|
|
545
|
-
var dark = document.body.classList.contains("hope-ui-dark");
|
|
546
|
-
if (dark) host.setAttribute("data-dark", "");
|
|
547
|
-
else host.removeAttribute("data-dark");
|
|
548
|
-
}
|
|
549
|
-
syncDark();
|
|
550
|
-
var mo = new MutationObserver(syncDark);
|
|
551
|
-
mo.observe(document.body, { attributes: true, attributeFilter: ["class"] });
|
|
552
162
|
}
|
|
553
163
|
|
|
554
164
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", init);
|