@yuuichi/openlist-assets 4.2.6-ym.3 → 4.2.6-ym.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/previews-CoVCUlRx.js +2 -2
- package/dist/assets/previews-legacy-DOedQYss.js +2 -2
- package/dist/brand/bgm/css/bgm-dock.css +350 -0
- package/dist/brand/bgm/js/bgm-dock.js +876 -0
- package/dist/brand/bgm/js/lib/aplayer/APlayer.min.css +3 -0
- package/dist/brand/bgm/js/lib/aplayer/APlayer.min.js +2 -0
- package/dist/brand/bgm/js/lib/aplayer/Meting.min.js +1 -0
- package/dist/brand/global-search.js +489 -69
- package/dist/brand/music-player.js +135 -542
- package/dist/brand/ym-bundle.516f792dd3.js +1733 -0
- package/dist/brand/ym-bundle.js +624 -611
- package/dist/brand/ym-bundle.manifest.json +4 -0
- package/dist/esa-entry.js +104 -103
- package/package.json +1 -1
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
/* 统一背景音乐 Dock:本地 / 网易云 / QQ音乐 / Spotify 多源切换
|
|
2
|
+
双端统一:左下角圆形按钮,点击展开面板(移动端=底部全宽,桌面端=按钮上方浮层) */
|
|
3
|
+
(function () {
|
|
4
|
+
var STORE_POS = 'bgm-dock-pos-v2'
|
|
5
|
+
var STORE_SRC = 'bgm-dock-source'
|
|
6
|
+
var STORE_SIZE = 'bgm-dock-size'
|
|
7
|
+
var DRAG_THRESHOLD = 6
|
|
8
|
+
var MIN_W = 260
|
|
9
|
+
var MAX_W = 560
|
|
10
|
+
var MIN_H = 180
|
|
11
|
+
var MAX_H = 760
|
|
12
|
+
var MOBILE_MAX = 768
|
|
13
|
+
|
|
14
|
+
var root = document.getElementById('bgm-dock')
|
|
15
|
+
if (!root) return
|
|
16
|
+
|
|
17
|
+
var cfg
|
|
18
|
+
try {
|
|
19
|
+
cfg = JSON.parse(root.getAttribute('data-bgm') || '{}')
|
|
20
|
+
} catch (e) {
|
|
21
|
+
cfg = {}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
var sources = Array.isArray(cfg.sources) ? cfg.sources : []
|
|
25
|
+
if (!sources.length) return
|
|
26
|
+
|
|
27
|
+
// 资源基址:blog 本地为同源根路径;welcome 页经 /__blog/ 代理。
|
|
28
|
+
// 优先读取配置中的 assetBase,其次从脚本自身地址推导(…/js/bgm-dock.js -> 前缀)。
|
|
29
|
+
function detectAssetBase() {
|
|
30
|
+
if (cfg.assetBase) return cfg.assetBase.replace(/\/$/, '')
|
|
31
|
+
try {
|
|
32
|
+
var me = document.currentScript && document.currentScript.src
|
|
33
|
+
if (!me) {
|
|
34
|
+
var s = document.querySelector('script[src*="bgm-dock.js"]')
|
|
35
|
+
me = s && s.src
|
|
36
|
+
}
|
|
37
|
+
if (me) {
|
|
38
|
+
// welcome 页:…/__blog/js/bgm-dock.js -> 前缀为 '/__blog'
|
|
39
|
+
var wp = me.match(/^(https?:\/\/[^/]+)\/__blog\/js\/bgm-dock\.js(?:[?#].*)?$/)
|
|
40
|
+
if (wp) return '/__blog'
|
|
41
|
+
// 博客同源:…/js/bgm-dock.js -> 空前缀
|
|
42
|
+
var same = me.match(/^https?:\/\/[^/]+\/js\/bgm-dock\.js(?:[?#].*)?$/)
|
|
43
|
+
if (same) return ''
|
|
44
|
+
}
|
|
45
|
+
} catch (e) {
|
|
46
|
+
// ignore
|
|
47
|
+
}
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
var ASSET_BASE = detectAssetBase()
|
|
51
|
+
var ASSET_V = (typeof window.__ASSET_VERSION__ === 'string' && window.__ASSET_VERSION__) || ''
|
|
52
|
+
function assetUrl(p) {
|
|
53
|
+
return ASSET_BASE + p + (ASSET_V ? (p.indexOf('?') >= 0 ? '&' : '?') + 'v=' + ASSET_V : '')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function loadStyleOnce(href) {
|
|
57
|
+
var existing = document.querySelector('link[data-bgm-asset="' + href + '"]')
|
|
58
|
+
if (existing) return Promise.resolve()
|
|
59
|
+
return new Promise(function (resolve, reject) {
|
|
60
|
+
var link = document.createElement('link')
|
|
61
|
+
link.rel = 'stylesheet'
|
|
62
|
+
link.href = href
|
|
63
|
+
link.dataset.bgmAsset = href
|
|
64
|
+
link.onload = function () { link.dataset.loaded = '1'; resolve() }
|
|
65
|
+
link.onerror = function () { reject(new Error('css load fail: ' + href)) }
|
|
66
|
+
document.head.appendChild(link)
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function loadScriptOnce(src, isReady) {
|
|
71
|
+
var existing = document.querySelector('script[data-bgm-asset="' + src + '"]')
|
|
72
|
+
if (existing) {
|
|
73
|
+
if (existing.dataset.loaded === '1' || (isReady && isReady())) return Promise.resolve()
|
|
74
|
+
return new Promise(function (resolve) {
|
|
75
|
+
function check() {
|
|
76
|
+
if (!isReady || isReady()) {
|
|
77
|
+
existing.dataset.loaded = '1'
|
|
78
|
+
resolve()
|
|
79
|
+
} else {
|
|
80
|
+
setTimeout(check, 60)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
check()
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
return new Promise(function (resolve, reject) {
|
|
87
|
+
var s = document.createElement('script')
|
|
88
|
+
s.src = src
|
|
89
|
+
s.defer = true
|
|
90
|
+
s.dataset.bgmAsset = src
|
|
91
|
+
s.onload = function () { s.dataset.loaded = '1'; resolve() }
|
|
92
|
+
s.onerror = function () { reject(new Error('script load fail: ' + src)) }
|
|
93
|
+
document.body.appendChild(s)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
var dockCssLoaded = null
|
|
98
|
+
function loadDockCss() {
|
|
99
|
+
if (dockCssLoaded) return dockCssLoaded
|
|
100
|
+
if (document.getElementById('bgm-dock-full-css')) return Promise.resolve()
|
|
101
|
+
var href = assetUrl('/css/bgm-dock.css')
|
|
102
|
+
dockCssLoaded = loadStyleOnce(href).then(function () {
|
|
103
|
+
var l = document.querySelector('link[data-bgm-asset="' + href + '"]')
|
|
104
|
+
if (l) l.id = 'bgm-dock-full-css'
|
|
105
|
+
})
|
|
106
|
+
return dockCssLoaded
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var aplayerLoading = null
|
|
110
|
+
function loadAPlayer() {
|
|
111
|
+
if (window.APlayer) return Promise.resolve()
|
|
112
|
+
if (aplayerLoading) return aplayerLoading
|
|
113
|
+
aplayerLoading = Promise.all([
|
|
114
|
+
loadStyleOnce(assetUrl('/js/lib/aplayer/APlayer.min.css')),
|
|
115
|
+
loadScriptOnce(assetUrl('/js/lib/aplayer/APlayer.min.js'), function () { return !!window.APlayer }),
|
|
116
|
+
]).then(function () {
|
|
117
|
+
if (!window.APlayer) throw new Error('aplayer missing after load')
|
|
118
|
+
})
|
|
119
|
+
return aplayerLoading
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
var built = {}
|
|
123
|
+
var instances = []
|
|
124
|
+
var current = null
|
|
125
|
+
var pending = null
|
|
126
|
+
var dragging = false
|
|
127
|
+
var suppressClick = false
|
|
128
|
+
var opened = false
|
|
129
|
+
var mounted = false
|
|
130
|
+
var pendingKey = null
|
|
131
|
+
var peekRevealed = false
|
|
132
|
+
var swallowPeekClick = false
|
|
133
|
+
|
|
134
|
+
// 暴露 Dock 内的 APlayer 实例,便于倍速控制与 PJAX 检测
|
|
135
|
+
window.__bgmDock = { instances: instances }
|
|
136
|
+
|
|
137
|
+
/* ---------- 构建外壳 ---------- */
|
|
138
|
+
root.innerHTML =
|
|
139
|
+
'<button type="button" class="bgm-dock__fab" aria-label="背景音乐">' +
|
|
140
|
+
'<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z"/></svg>' +
|
|
141
|
+
'<span class="bgm-dock__fab-bar"></span>' +
|
|
142
|
+
'</button>' +
|
|
143
|
+
'<div class="bgm-dock__panel">' +
|
|
144
|
+
'<div class="bgm-dock__head">' +
|
|
145
|
+
'<div class="bgm-dock__handle"><svg viewBox="0 0 16 16" aria-hidden="true"><path d="M6 13c0 1.105-1.12 2-2.5 2S1 14.105 1 13s1.12-2 2.5-2 2.5.9 2.5 2zm9-2c0 1.105-1.12 2-2.5 2s-2.5-.9-2.5-2 1.12-2 2.5-2 2.5.9 2.5 2z"/><path fill-rule="evenodd" d="M14 11V2h1v9h-1zM6 3v10H5V3h1z"/><path d="M5 2.905a1 1 0 0 1 .9-.995l9-1a1 1 0 0 1 1.096.995V3L6 4v-1.095z"/></svg><span class="bgm-dock__title">背景音乐</span></div>' +
|
|
146
|
+
'<div class="bgm-dock__tabs"></div>' +
|
|
147
|
+
'<button type="button" class="bgm-dock__toggle" aria-label="折叠">−</button>' +
|
|
148
|
+
'</div>' +
|
|
149
|
+
'<div class="bgm-dock__body"></div>' +
|
|
150
|
+
'<div class="bgm-dock__resize" title="拖动缩放,双击还原"></div>' +
|
|
151
|
+
'</div>'
|
|
152
|
+
|
|
153
|
+
var headEl = root.querySelector('.bgm-dock__head')
|
|
154
|
+
var handleEl = root.querySelector('.bgm-dock__handle')
|
|
155
|
+
var tabsEl = root.querySelector('.bgm-dock__tabs')
|
|
156
|
+
var toggleEl = root.querySelector('.bgm-dock__toggle')
|
|
157
|
+
var bodyEl = root.querySelector('.bgm-dock__body')
|
|
158
|
+
var resizeEl = root.querySelector('.bgm-dock__resize')
|
|
159
|
+
var fabEl = root.querySelector('.bgm-dock__fab')
|
|
160
|
+
var panelEl = root.querySelector('.bgm-dock__panel')
|
|
161
|
+
|
|
162
|
+
/* ---------- 响应式 ---------- */
|
|
163
|
+
var mobileMQ = window.matchMedia
|
|
164
|
+
? window.matchMedia('(max-width:' + MOBILE_MAX + 'px)')
|
|
165
|
+
: { matches: false }
|
|
166
|
+
|
|
167
|
+
function isMobile() {
|
|
168
|
+
return mobileMQ.matches
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function clamp(v, min, max) {
|
|
172
|
+
return Math.min(Math.max(v, min), max)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* ---------- 拖拽时的全屏覆盖层(防 iframe/页面吞事件) ---------- */
|
|
176
|
+
var overlay = document.createElement('div')
|
|
177
|
+
overlay.className = 'bgm-dock__overlay'
|
|
178
|
+
|
|
179
|
+
/* ---------- 圆形按钮定位(双端共用,左下角默认) ---------- */
|
|
180
|
+
// root 本身不占布局尺寸:FAB 与展开面板都是相对它的绝对定位子元素,
|
|
181
|
+
// 因此拖拽边界必须按“当前可见子元素相对 root 的外接盒”计算,
|
|
182
|
+
// 不能直接用 root.offsetWidth/Height(展开时为 0,会导致面板拖不下去)。
|
|
183
|
+
function visibleBounds() {
|
|
184
|
+
var boxes = []
|
|
185
|
+
var children = [fabEl, panelEl]
|
|
186
|
+
for (var i = 0; i < children.length; i++) {
|
|
187
|
+
var c = children[i]
|
|
188
|
+
if (!c || c.offsetParent === null) continue
|
|
189
|
+
var st = c.ownerDocument.defaultView.getComputedStyle(c)
|
|
190
|
+
if (st.display === 'none' || st.visibility === 'hidden') continue
|
|
191
|
+
var rect = c.getBoundingClientRect()
|
|
192
|
+
if (rect.width === 0 && rect.height === 0) continue
|
|
193
|
+
// 用 offset 位置,得到相对 root 的坐标
|
|
194
|
+
boxes.push({
|
|
195
|
+
left: c.offsetLeft,
|
|
196
|
+
top: c.offsetTop,
|
|
197
|
+
right: c.offsetLeft + rect.width,
|
|
198
|
+
bottom: c.offsetTop + rect.height,
|
|
199
|
+
})
|
|
200
|
+
}
|
|
201
|
+
if (!boxes.length) {
|
|
202
|
+
return { left: 0, top: 0, width: fabEl.offsetWidth || 52, height: fabEl.offsetHeight || 52 }
|
|
203
|
+
}
|
|
204
|
+
var minL = Infinity, minT = Infinity, maxR = -Infinity, maxB = -Infinity
|
|
205
|
+
boxes.forEach(function (b) {
|
|
206
|
+
minL = Math.min(minL, b.left)
|
|
207
|
+
minT = Math.min(minT, b.top)
|
|
208
|
+
maxR = Math.max(maxR, b.right)
|
|
209
|
+
maxB = Math.max(maxB, b.bottom)
|
|
210
|
+
})
|
|
211
|
+
return { left: minL, top: minT, width: maxR - minL, height: maxB - minT }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function placeFab(left, top) {
|
|
215
|
+
// 入参为 root 的视口坐标。展开时面板相对 root 为负偏移(root 高度为 0),
|
|
216
|
+
// 故先换算成“可见外接盒”视口位置做边界约束,再还原成 root 的定位坐标。
|
|
217
|
+
var b = visibleBounds()
|
|
218
|
+
var w = b.width || fabEl.offsetWidth || 52
|
|
219
|
+
var h = b.height || fabEl.offsetHeight || 52
|
|
220
|
+
var visLeft = clamp(left + b.left, 0, Math.max(0, window.innerWidth - w))
|
|
221
|
+
var visTop = clamp(top + b.top, 0, Math.max(0, window.innerHeight - h))
|
|
222
|
+
var rootLeft = visLeft - b.left
|
|
223
|
+
var rootTop = visTop - b.top
|
|
224
|
+
root.style.left = rootLeft + 'px'
|
|
225
|
+
root.style.right = 'auto'
|
|
226
|
+
root.style.top = rootTop + 'px'
|
|
227
|
+
root.style.bottom = 'auto'
|
|
228
|
+
root.style.margin = '0'
|
|
229
|
+
return { left: rootLeft, top: rootTop }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function applySavedPosition() {
|
|
233
|
+
try {
|
|
234
|
+
var s = JSON.parse(localStorage.getItem(STORE_POS) || 'null')
|
|
235
|
+
if (s && typeof s.left === 'number') {
|
|
236
|
+
placeFab(s.left, s.top)
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
} catch (e) {
|
|
240
|
+
// ignore
|
|
241
|
+
}
|
|
242
|
+
// 默认左下角
|
|
243
|
+
placeFab(16, window.innerHeight - 68)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* ---------- 收起态半吸附边缘 ---------- */
|
|
247
|
+
// 收起时半吸附到屏幕左/右边缘,只露出一小截;桌面靠 hover、触摸靠首次点按
|
|
248
|
+
// 让整个按钮滑回屏幕内(滑入动画/可点区域由 music-player.js 内联 CSS 保证),
|
|
249
|
+
// 因此圆心即使部分在视口外,鼠标仍可在露出的半圆上 hover/点击。
|
|
250
|
+
function canPeek() {
|
|
251
|
+
return true
|
|
252
|
+
}
|
|
253
|
+
function snapToEdge() {
|
|
254
|
+
if (opened) return
|
|
255
|
+
var fabSize = fabEl.offsetWidth || 52
|
|
256
|
+
var rect = root.getBoundingClientRect()
|
|
257
|
+
var center = rect.left + fabSize / 2
|
|
258
|
+
var toLeft = center < window.innerWidth / 2
|
|
259
|
+
var top = clamp(rect.top, 0, Math.max(0, window.innerHeight - fabSize))
|
|
260
|
+
var left = toLeft ? 0 : window.innerWidth - fabSize
|
|
261
|
+
placeFab(left, top)
|
|
262
|
+
try {
|
|
263
|
+
localStorage.setItem(STORE_POS, JSON.stringify({ left: left, top: top }))
|
|
264
|
+
} catch (e) {
|
|
265
|
+
// ignore
|
|
266
|
+
}
|
|
267
|
+
fabEl.classList.toggle('is-peek-left', toLeft)
|
|
268
|
+
fabEl.classList.toggle('is-peek-right', !toLeft)
|
|
269
|
+
return toLeft
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function revealFab() {
|
|
273
|
+
peekRevealed = true
|
|
274
|
+
fabEl.classList.add('is-revealed')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function peekFab() {
|
|
278
|
+
peekRevealed = false
|
|
279
|
+
fabEl.classList.remove('is-revealed')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function fabDragStart(e) {
|
|
283
|
+
if (opened) return // 展开状态下不让 FAB 拖(桌面拖面板标题栏;移动端面板固定)
|
|
284
|
+
if (e.pointerType === 'mouse' && e.button !== 0) return
|
|
285
|
+
// 触屏(或笔)首次点按仅全露(不展开),并吞掉这次 click;已全露后再点才展开。
|
|
286
|
+
// 鼠标悬停即全露,点击直接展开。
|
|
287
|
+
swallowPeekClick = e.pointerType !== 'mouse' && !peekRevealed
|
|
288
|
+
if (!peekRevealed) revealFab()
|
|
289
|
+
fabDrag = { id: e.pointerId, x0: e.clientX, y0: e.clientY, moved: false }
|
|
290
|
+
try { fabEl.setPointerCapture(e.pointerId) } catch (err) {
|
|
291
|
+
// ignore
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function fabDragMove(e) {
|
|
296
|
+
if (!fabDrag || e.pointerId !== fabDrag.id) return
|
|
297
|
+
if (!fabDrag.moved) {
|
|
298
|
+
if (Math.hypot(e.clientX - fabDrag.x0, e.clientY - fabDrag.y0) < DRAG_THRESHOLD) return
|
|
299
|
+
fabDrag.moved = true
|
|
300
|
+
suppressClick = true
|
|
301
|
+
dragging = true
|
|
302
|
+
fabEl.classList.add('is-dragging')
|
|
303
|
+
document.body.appendChild(overlay)
|
|
304
|
+
}
|
|
305
|
+
var rect = root.getBoundingClientRect()
|
|
306
|
+
placeFab(rect.left + (e.clientX - fabDrag.x0), rect.top + (e.clientY - fabDrag.y0))
|
|
307
|
+
fabDrag.x0 = e.clientX
|
|
308
|
+
fabDrag.y0 = e.clientY
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function fabDragEnd(e) {
|
|
312
|
+
if (!fabDrag || (e && e.pointerId !== fabDrag.id)) return
|
|
313
|
+
if (fabDrag.moved) {
|
|
314
|
+
swallowPeekClick = false
|
|
315
|
+
snapToEdge()
|
|
316
|
+
setTimeout(function () {
|
|
317
|
+
suppressClick = false
|
|
318
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay)
|
|
319
|
+
}, 50)
|
|
320
|
+
}
|
|
321
|
+
fabEl.classList.remove('is-dragging')
|
|
322
|
+
dragging = false
|
|
323
|
+
fabDrag = null
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var fabDrag = null
|
|
327
|
+
fabEl.addEventListener('pointerdown', fabDragStart)
|
|
328
|
+
fabEl.addEventListener('pointermove', fabDragMove)
|
|
329
|
+
fabEl.addEventListener('pointerup', fabDragEnd)
|
|
330
|
+
fabEl.addEventListener('pointercancel', fabDragEnd)
|
|
331
|
+
|
|
332
|
+
/* ---------- 展开 / 收起 ---------- */
|
|
333
|
+
function renderState() {
|
|
334
|
+
root.classList.toggle('is-mobile', isMobile())
|
|
335
|
+
root.classList.toggle('is-open', opened)
|
|
336
|
+
root.classList.toggle('is-collapsed', !opened)
|
|
337
|
+
toggleEl.textContent = opened ? '−' : '+'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
var sizeRestored = false
|
|
341
|
+
function openDock() {
|
|
342
|
+
opened = true
|
|
343
|
+
peekRevealed = true
|
|
344
|
+
fabEl.classList.remove('is-peek-left', 'is-peek-right', 'is-revealed')
|
|
345
|
+
renderState()
|
|
346
|
+
loadDockCss()
|
|
347
|
+
.then(function () {
|
|
348
|
+
if (sizeRestored) return
|
|
349
|
+
sizeRestored = true
|
|
350
|
+
// 强制布局后再读取面板位置,避免 display:none 时 rect 为 0
|
|
351
|
+
void panelEl.offsetHeight
|
|
352
|
+
restoreSize()
|
|
353
|
+
})
|
|
354
|
+
.catch(function () {
|
|
355
|
+
sizeRestored = true
|
|
356
|
+
})
|
|
357
|
+
ensurePlayerMounted()
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function closeDock() {
|
|
361
|
+
opened = false
|
|
362
|
+
renderState()
|
|
363
|
+
peekFab()
|
|
364
|
+
snapToEdge()
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
fabEl.addEventListener('click', function (e) {
|
|
368
|
+
e.stopPropagation()
|
|
369
|
+
if (suppressClick) return
|
|
370
|
+
if (swallowPeekClick) {
|
|
371
|
+
swallowPeekClick = false
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
openDock()
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
// 鼠标靠近全露出,离开恢复半吸附(展开态不处理)
|
|
378
|
+
fabEl.addEventListener('mouseenter', function () {
|
|
379
|
+
if (!opened) revealFab()
|
|
380
|
+
})
|
|
381
|
+
fabEl.addEventListener('mouseleave', function () {
|
|
382
|
+
if (!opened) peekFab()
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// 触屏/鼠标点到按钮以外区域:恢复半吸附
|
|
386
|
+
document.addEventListener('pointerdown', function (e) {
|
|
387
|
+
if (opened || peekRevealed === false) return
|
|
388
|
+
if (root.contains(e.target)) return
|
|
389
|
+
peekFab()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
toggleEl.addEventListener('click', function () {
|
|
393
|
+
if (suppressClick) return
|
|
394
|
+
closeDock()
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
if (mobileMQ.addEventListener) {
|
|
398
|
+
mobileMQ.addEventListener('change', renderState)
|
|
399
|
+
} else if (mobileMQ.addListener) {
|
|
400
|
+
mobileMQ.addListener(renderState)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* ---------- 桌面端面板标题栏拖拽 ---------- */
|
|
404
|
+
handleEl.addEventListener('pointerdown', function (e) {
|
|
405
|
+
if (e.button !== undefined && e.button !== 0) return
|
|
406
|
+
if (isMobile()) return // 移动端面板固定底部
|
|
407
|
+
if (!opened) return
|
|
408
|
+
var rect = root.getBoundingClientRect()
|
|
409
|
+
pending = { x0: e.clientX, y0: e.clientY, dx: e.clientX - rect.left, dy: e.clientY - rect.top }
|
|
410
|
+
e.preventDefault()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
document.addEventListener('pointermove', function (e) {
|
|
414
|
+
if (!pending) return
|
|
415
|
+
if (!dragging) {
|
|
416
|
+
var moved = Math.hypot(e.clientX - pending.x0, e.clientY - pending.y0)
|
|
417
|
+
if (moved < DRAG_THRESHOLD) return
|
|
418
|
+
dragging = true
|
|
419
|
+
suppressClick = true
|
|
420
|
+
document.body.appendChild(overlay)
|
|
421
|
+
}
|
|
422
|
+
// 直接移动 root(FAB 与绝对定位面板一起走)
|
|
423
|
+
placeFab(e.clientX - pending.dx, e.clientY - pending.dy)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
document.addEventListener('pointerup', function () {
|
|
427
|
+
if (!pending) return
|
|
428
|
+
if (dragging) {
|
|
429
|
+
var rect = root.getBoundingClientRect()
|
|
430
|
+
try {
|
|
431
|
+
localStorage.setItem(STORE_POS, JSON.stringify({ left: rect.left, top: rect.top }))
|
|
432
|
+
} catch (e) {
|
|
433
|
+
// ignore
|
|
434
|
+
}
|
|
435
|
+
setTimeout(function () {
|
|
436
|
+
suppressClick = false
|
|
437
|
+
if (overlay.parentNode) overlay.parentNode.removeChild(overlay)
|
|
438
|
+
}, 50)
|
|
439
|
+
}
|
|
440
|
+
pending = null
|
|
441
|
+
dragging = false
|
|
442
|
+
})
|
|
443
|
+
|
|
444
|
+
/* ---------- 缩放(右下角拖柄,仅桌面端展开面板)
|
|
445
|
+
关键:开始缩放时把面板的顶边钉在当前视口位置(top 锚定),
|
|
446
|
+
因此向下拖也能长高,而不是被 bottom:64px 顶得只能向上长。 ---------- */
|
|
447
|
+
function anchorPanel(viewportTop) {
|
|
448
|
+
// panel 为相对 #bgm-dock 的绝对定位,需换算成相对其父容器的偏移
|
|
449
|
+
var rootRect = root.getBoundingClientRect()
|
|
450
|
+
panelEl.style.top = (viewportTop - rootRect.top) + 'px'
|
|
451
|
+
panelEl.style.bottom = 'auto'
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function pinPanelTop() {
|
|
455
|
+
var rect = panelEl.getBoundingClientRect()
|
|
456
|
+
anchorPanel(clamp(rect.top, 0, Math.max(0, window.innerHeight - MIN_H)))
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function unpinPanelTop() {
|
|
460
|
+
panelEl.style.top = ''
|
|
461
|
+
panelEl.style.bottom = ''
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function applySize(w, h, persist) {
|
|
465
|
+
w = clamp(Math.round(w), MIN_W, Math.min(MAX_W, window.innerWidth - 16))
|
|
466
|
+
var curTop = panelEl.getBoundingClientRect().top
|
|
467
|
+
var maxH = Math.min(MAX_H, window.innerHeight - curTop - 8)
|
|
468
|
+
h = clamp(Math.round(h), MIN_H, Math.max(MIN_H, maxH))
|
|
469
|
+
panelEl.style.width = w + 'px'
|
|
470
|
+
panelEl.style.height = h + 'px'
|
|
471
|
+
root.classList.add('is-sized')
|
|
472
|
+
if (persist) {
|
|
473
|
+
try {
|
|
474
|
+
localStorage.setItem(STORE_SIZE, JSON.stringify({ w: w, h: h, top: Math.round(curTop) }))
|
|
475
|
+
} catch (e) {}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function resetSize() {
|
|
480
|
+
panelEl.style.width = ''
|
|
481
|
+
panelEl.style.height = ''
|
|
482
|
+
unpinPanelTop()
|
|
483
|
+
root.classList.remove('is-sized')
|
|
484
|
+
try { localStorage.removeItem(STORE_SIZE) } catch (e) {}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function restoreSize() {
|
|
488
|
+
if (isMobile()) return
|
|
489
|
+
try {
|
|
490
|
+
var s = JSON.parse(localStorage.getItem(STORE_SIZE) || 'null')
|
|
491
|
+
if (s && typeof s.w === 'number' && typeof s.h === 'number') {
|
|
492
|
+
var top = typeof s.top === 'number' ? s.top : panelEl.getBoundingClientRect().top
|
|
493
|
+
top = clamp(top, 0, Math.max(0, window.innerHeight - MIN_H))
|
|
494
|
+
anchorPanel(top)
|
|
495
|
+
applySize(s.w, s.h, false)
|
|
496
|
+
}
|
|
497
|
+
} catch (e) {}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
var resizing = false
|
|
501
|
+
var rStart = null
|
|
502
|
+
var rMoved = false
|
|
503
|
+
var lastTap = 0
|
|
504
|
+
resizeEl.addEventListener('pointerdown', function (e) {
|
|
505
|
+
if (e.button !== undefined && e.button !== 0) return
|
|
506
|
+
if (isMobile()) return
|
|
507
|
+
resizing = true
|
|
508
|
+
rMoved = false
|
|
509
|
+
if (!panelEl.style.top) pinPanelTop()
|
|
510
|
+
rStart = { x: e.clientX, y: e.clientY, w: panelEl.offsetWidth, h: panelEl.offsetHeight }
|
|
511
|
+
document.body.appendChild(overlay)
|
|
512
|
+
overlay.style.cursor = 'nwse-resize'
|
|
513
|
+
e.preventDefault()
|
|
514
|
+
e.stopPropagation()
|
|
515
|
+
})
|
|
516
|
+
|
|
517
|
+
document.addEventListener('pointermove', function (e) {
|
|
518
|
+
if (!resizing) return
|
|
519
|
+
if (Math.abs(e.clientX - rStart.x) > 3 || Math.abs(e.clientY - rStart.y) > 3) rMoved = true
|
|
520
|
+
applySize(rStart.w + (e.clientX - rStart.x), rStart.h + (e.clientY - rStart.y), false)
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
document.addEventListener('pointerup', function () {
|
|
524
|
+
if (!resizing) return
|
|
525
|
+
resizing = false
|
|
526
|
+
overlay.style.cursor = ''
|
|
527
|
+
setTimeout(function () {
|
|
528
|
+
if (overlay.parentNode && !dragging) overlay.parentNode.removeChild(overlay)
|
|
529
|
+
}, 50)
|
|
530
|
+
if (rMoved) {
|
|
531
|
+
try {
|
|
532
|
+
var s = JSON.parse(localStorage.getItem(STORE_SIZE) || 'null') || {}
|
|
533
|
+
s.w = panelEl.offsetWidth
|
|
534
|
+
s.h = panelEl.offsetHeight
|
|
535
|
+
s.top = Math.round(panelEl.getBoundingClientRect().top)
|
|
536
|
+
localStorage.setItem(STORE_SIZE, JSON.stringify(s))
|
|
537
|
+
} catch (e) {}
|
|
538
|
+
} else {
|
|
539
|
+
var now = Date.now()
|
|
540
|
+
if (now - lastTap < 350) {
|
|
541
|
+
resetSize()
|
|
542
|
+
lastTap = 0
|
|
543
|
+
} else {
|
|
544
|
+
lastTap = now
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
var SPEEDS = [0.75, 1, 1.25, 1.5, 2]
|
|
550
|
+
|
|
551
|
+
function addSpeedControl(ap, holder) {
|
|
552
|
+
var time = holder.querySelector('.aplayer-time')
|
|
553
|
+
var audio = ap.audio
|
|
554
|
+
if (!time || !audio || time.querySelector('.bgm-speed')) return
|
|
555
|
+
|
|
556
|
+
var btn = document.createElement('span')
|
|
557
|
+
btn.className = 'aplayer-icon bgm-speed'
|
|
558
|
+
btn.title = '播放倍速'
|
|
559
|
+
btn.textContent = '1x'
|
|
560
|
+
|
|
561
|
+
var menu = document.createElement('div')
|
|
562
|
+
menu.className = 'bgm-speed__menu'
|
|
563
|
+
SPEEDS.forEach(function (rate) {
|
|
564
|
+
var item = document.createElement('button')
|
|
565
|
+
item.type = 'button'
|
|
566
|
+
item.className = 'bgm-speed__item'
|
|
567
|
+
item.dataset.rate = String(rate)
|
|
568
|
+
item.textContent = rate === 1 ? '正常' : rate + 'x'
|
|
569
|
+
item.addEventListener('click', function (e) {
|
|
570
|
+
e.stopPropagation()
|
|
571
|
+
audio.playbackRate = rate
|
|
572
|
+
btn.textContent = rate === 1 ? '1x' : rate + 'x'
|
|
573
|
+
menu.classList.remove('is-open')
|
|
574
|
+
menu.querySelectorAll('.bgm-speed__item').forEach(function (it) {
|
|
575
|
+
it.classList.toggle('is-active', Number(it.dataset.rate) === rate)
|
|
576
|
+
})
|
|
577
|
+
})
|
|
578
|
+
if (rate === 1) item.classList.add('is-active')
|
|
579
|
+
menu.appendChild(item)
|
|
580
|
+
})
|
|
581
|
+
|
|
582
|
+
btn.addEventListener('click', function (e) {
|
|
583
|
+
e.stopPropagation()
|
|
584
|
+
menu.classList.toggle('is-open')
|
|
585
|
+
})
|
|
586
|
+
document.addEventListener('click', function (e) {
|
|
587
|
+
if (!btn.contains(e.target) && !menu.contains(e.target)) menu.classList.remove('is-open')
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
time.insertBefore(menu, time.querySelector('.aplayer-volume-wrap'))
|
|
591
|
+
time.insertBefore(btn, menu)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/* ---------- 各平台播放器构建 ---------- */
|
|
595
|
+
function showLoading(holder) {
|
|
596
|
+
holder.classList.add('is-loading')
|
|
597
|
+
holder.textContent = '播放器加载中…'
|
|
598
|
+
}
|
|
599
|
+
function clearLoading(holder) {
|
|
600
|
+
holder.classList.remove('is-loading')
|
|
601
|
+
holder.textContent = ''
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function attachAutoSkip(ap) {
|
|
605
|
+
if (!ap || ap.__ymAutoSkip) return
|
|
606
|
+
ap.__ymAutoSkip = true
|
|
607
|
+
var tried = 0
|
|
608
|
+
var skip = function () {
|
|
609
|
+
// 会员/版权歌曲网易云会 302 到 /404,APlayer 报 audio error 且不会自动跳。
|
|
610
|
+
// 这里在连续错误时自动切到下一首;遍历一遍仍全失败则停止,避免死循环。
|
|
611
|
+
var list = ap.list
|
|
612
|
+
var total = (list && list.audios && list.audios.length) || 0
|
|
613
|
+
tried += 1
|
|
614
|
+
if (!total || tried > total) return
|
|
615
|
+
try {
|
|
616
|
+
if (typeof ap.skip === 'function') ap.skip()
|
|
617
|
+
else if (list && typeof list.switch === 'function') list.switch((list.index || 0) + 1)
|
|
618
|
+
setTimeout(function () { try { ap.play() } catch (e) {} }, 250)
|
|
619
|
+
} catch (e) {
|
|
620
|
+
// ignore
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
ap.on('error', skip)
|
|
625
|
+
// 兜底:部分 APlayer 版本 audio error 不冒泡到实例事件,直接监听媒体元素
|
|
626
|
+
var bindMedia = function () {
|
|
627
|
+
var audios = ap.container ? ap.container.querySelectorAll('audio') : []
|
|
628
|
+
Array.prototype.forEach.call(audios, function (au) {
|
|
629
|
+
if (au.__ymErrBound) return
|
|
630
|
+
au.__ymErrBound = true
|
|
631
|
+
au.addEventListener('error', function () {
|
|
632
|
+
// 真正无可用音源(404/会员限制)才跳,排除中断切换时的瞬时错误
|
|
633
|
+
var code = au.error && au.error.code
|
|
634
|
+
if (code === 4) skip()
|
|
635
|
+
})
|
|
636
|
+
})
|
|
637
|
+
}
|
|
638
|
+
ap.on('loadeddata', bindMedia)
|
|
639
|
+
setTimeout(bindMedia, 0)
|
|
640
|
+
setInterval(bindMedia, 1500)
|
|
641
|
+
} catch (e) {
|
|
642
|
+
// ignore
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function buildLocal(src) {
|
|
647
|
+
var holder = document.createElement('div')
|
|
648
|
+
holder.className = 'bgm-dock__player bgm-dock__player--aplayer'
|
|
649
|
+
showLoading(holder)
|
|
650
|
+
bodyEl.appendChild(holder)
|
|
651
|
+
|
|
652
|
+
loadAPlayer()
|
|
653
|
+
.then(function () {
|
|
654
|
+
if (!holder.isConnected) return
|
|
655
|
+
clearLoading(holder)
|
|
656
|
+
// eslint-disable-next-line no-new, no-undef
|
|
657
|
+
var ap = new APlayer({
|
|
658
|
+
container: holder,
|
|
659
|
+
fixed: false,
|
|
660
|
+
theme: cfg.theme || '#0078e7',
|
|
661
|
+
autoplay: cfg.autoplay === true,
|
|
662
|
+
loop: 'all',
|
|
663
|
+
order: cfg.order === 'random' ? 'random' : 'list',
|
|
664
|
+
preload: 'auto',
|
|
665
|
+
volume: typeof cfg.volume === 'number' ? cfg.volume : 0.6,
|
|
666
|
+
listFolded: false,
|
|
667
|
+
audio: (src.songs || []).map(function (s) {
|
|
668
|
+
return { name: s.name, artist: s.artist, url: s.url, cover: s.cover }
|
|
669
|
+
}),
|
|
670
|
+
})
|
|
671
|
+
instances.push({ type: 'local', ap: ap, holder: holder })
|
|
672
|
+
addSpeedControl(ap, holder)
|
|
673
|
+
if (holder.classList.contains('is-active')) {
|
|
674
|
+
try { ap.play() } catch (e) {
|
|
675
|
+
// 自动播放可能被浏览器拦截
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
try {
|
|
679
|
+
ap.on('play', function () { root.classList.add('is-playing') })
|
|
680
|
+
ap.on('pause', function () { root.classList.remove('is-playing') })
|
|
681
|
+
} catch (e) {
|
|
682
|
+
// ignore
|
|
683
|
+
}
|
|
684
|
+
attachAutoSkip(ap)
|
|
685
|
+
})
|
|
686
|
+
.catch(function () {
|
|
687
|
+
holder.textContent = '播放器加载失败'
|
|
688
|
+
})
|
|
689
|
+
|
|
690
|
+
return holder
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
var metingLoading = null
|
|
694
|
+
function loadMeting() {
|
|
695
|
+
if (window.Meting || customElements.get('meting-js')) return Promise.resolve()
|
|
696
|
+
if (metingLoading) return metingLoading
|
|
697
|
+
metingLoading = loadAPlayer()
|
|
698
|
+
.then(function () { return loadScriptOnce(assetUrl('/js/lib/aplayer/Meting.min.js')) })
|
|
699
|
+
.then(function () {
|
|
700
|
+
if (!customElements.get('meting-js') && !window.Meting) throw new Error('meting missing')
|
|
701
|
+
})
|
|
702
|
+
.catch(function (e) {
|
|
703
|
+
metingLoading = null
|
|
704
|
+
throw e
|
|
705
|
+
})
|
|
706
|
+
return metingLoading
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function buildMeting(src) {
|
|
710
|
+
var holder = document.createElement('div')
|
|
711
|
+
holder.className = 'bgm-dock__player bgm-dock__player--meting'
|
|
712
|
+
showLoading(holder)
|
|
713
|
+
bodyEl.appendChild(holder)
|
|
714
|
+
|
|
715
|
+
function createEl() {
|
|
716
|
+
if (holder.querySelector('meting-js')) return
|
|
717
|
+
clearLoading(holder)
|
|
718
|
+
var el = document.createElement('meting-js')
|
|
719
|
+
el.setAttribute('server', src.type === 'tencent' ? 'tencent' : 'netease')
|
|
720
|
+
el.setAttribute('type', src.resourceType || 'playlist')
|
|
721
|
+
el.setAttribute('id', src.id)
|
|
722
|
+
el.setAttribute('api', cfg.metingApi)
|
|
723
|
+
el.setAttribute('fixed', 'false')
|
|
724
|
+
el.setAttribute('theme', cfg.theme || '#0078e7')
|
|
725
|
+
el.setAttribute('autoplay', cfg.autoplay === true ? 'true' : 'false')
|
|
726
|
+
el.setAttribute('loop', 'all')
|
|
727
|
+
el.setAttribute('order', cfg.order === 'random' ? 'random' : 'list')
|
|
728
|
+
el.setAttribute('list-folded', 'false')
|
|
729
|
+
el.setAttribute('volume', String(typeof cfg.volume === 'number' ? cfg.volume : 0.6))
|
|
730
|
+
holder.appendChild(el)
|
|
731
|
+
|
|
732
|
+
setTimeout(function () {
|
|
733
|
+
var ap = holder.querySelector('.aplayer') && el.aplayer
|
|
734
|
+
if (ap) {
|
|
735
|
+
instances.push({ type: 'meting', ap: ap, holder: holder })
|
|
736
|
+
try {
|
|
737
|
+
ap.on('play', function () { root.classList.add('is-playing') })
|
|
738
|
+
ap.on('pause', function () { root.classList.remove('is-playing') })
|
|
739
|
+
} catch (e) {
|
|
740
|
+
// ignore
|
|
741
|
+
}
|
|
742
|
+
attachAutoSkip(ap)
|
|
743
|
+
}
|
|
744
|
+
}, 1500)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
loadMeting().then(createEl).catch(function () {
|
|
748
|
+
holder.textContent = '播放器加载失败'
|
|
749
|
+
})
|
|
750
|
+
return holder
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function buildSpotify(src) {
|
|
754
|
+
var holder = document.createElement('div')
|
|
755
|
+
holder.className = 'bgm-dock__player bgm-dock__player--spotify'
|
|
756
|
+
if (src.resourceType === 'song') holder.classList.add('is-song')
|
|
757
|
+
bodyEl.appendChild(holder)
|
|
758
|
+
|
|
759
|
+
var kind = src.resourceType === 'album' ? 'album' : src.resourceType === 'song' ? 'track' : 'playlist'
|
|
760
|
+
var iframe = document.createElement('iframe')
|
|
761
|
+
iframe.dataset.src = 'https://open.spotify.com/embed/' + kind + '/' + src.id
|
|
762
|
+
iframe.setAttribute('frameborder', '0')
|
|
763
|
+
iframe.setAttribute('allowtransparency', 'true')
|
|
764
|
+
iframe.setAttribute('allow', 'encrypted-media')
|
|
765
|
+
iframe.loading = 'lazy'
|
|
766
|
+
holder.appendChild(iframe)
|
|
767
|
+
return holder
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function buildSource(src) {
|
|
771
|
+
if (src.type === 'local') return buildLocal(src)
|
|
772
|
+
if (src.type === 'spotify') return buildSpotify(src)
|
|
773
|
+
return buildMeting(src)
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/* ---------- 源切换 ---------- */
|
|
777
|
+
function select(key) {
|
|
778
|
+
var src = sources.find(function (s) { return s.key === key })
|
|
779
|
+
if (!src) return
|
|
780
|
+
|
|
781
|
+
if (!built[key]) {
|
|
782
|
+
built[key] = buildSource(src)
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
Object.keys(built).forEach(function (k) {
|
|
786
|
+
built[k].classList.toggle('is-active', k === key)
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
instances.forEach(function (rec) {
|
|
790
|
+
if (!rec.holder.classList.contains('is-active') && rec.ap && typeof rec.ap.pause === 'function') {
|
|
791
|
+
try { rec.ap.pause() } catch (e) {
|
|
792
|
+
// ignore
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
bodyEl.querySelectorAll('.bgm-dock__player--spotify').forEach(function (holder) {
|
|
798
|
+
var iframe = holder.querySelector('iframe')
|
|
799
|
+
if (!iframe || !iframe.dataset.src) return
|
|
800
|
+
if (holder.classList.contains('is-active')) {
|
|
801
|
+
if (iframe.src !== iframe.dataset.src) iframe.src = iframe.dataset.src
|
|
802
|
+
} else if (iframe.src === iframe.dataset.src) {
|
|
803
|
+
iframe.src = 'about:blank'
|
|
804
|
+
}
|
|
805
|
+
})
|
|
806
|
+
|
|
807
|
+
tabsEl.querySelectorAll('.bgm-dock__tab').forEach(function (tab) {
|
|
808
|
+
tab.classList.toggle('is-active', tab.getAttribute('data-key') === key)
|
|
809
|
+
})
|
|
810
|
+
|
|
811
|
+
current = key
|
|
812
|
+
try {
|
|
813
|
+
localStorage.setItem(STORE_SRC, key)
|
|
814
|
+
} catch (e) {
|
|
815
|
+
// ignore
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/* ---------- 标签 ---------- */
|
|
820
|
+
sources.forEach(function (src) {
|
|
821
|
+
var tab = document.createElement('button')
|
|
822
|
+
tab.type = 'button'
|
|
823
|
+
tab.className = 'bgm-dock__tab'
|
|
824
|
+
tab.setAttribute('data-key', src.key)
|
|
825
|
+
tab.textContent = src.name
|
|
826
|
+
tab.addEventListener('click', function () {
|
|
827
|
+
if (suppressClick) return
|
|
828
|
+
ensurePlayerMounted()
|
|
829
|
+
select(src.key)
|
|
830
|
+
})
|
|
831
|
+
tabsEl.appendChild(tab)
|
|
832
|
+
})
|
|
833
|
+
|
|
834
|
+
function hasNonSpotify() {
|
|
835
|
+
return sources.some(function (s) { return s.type !== 'spotify' })
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/* ---------- 懒挂载:首次展开才创建播放器(双端一致) ---------- */
|
|
839
|
+
function ensurePlayerMounted() {
|
|
840
|
+
if (mounted) {
|
|
841
|
+
if (pendingKey) { var kk = pendingKey; pendingKey = null; select(kk) }
|
|
842
|
+
return
|
|
843
|
+
}
|
|
844
|
+
mounted = true
|
|
845
|
+
root.classList.add('is-mounted')
|
|
846
|
+
var k = pendingKey || initialSource.key
|
|
847
|
+
pendingKey = null
|
|
848
|
+
select(k)
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* ---------- 初始位置 / 初始源 ---------- */
|
|
852
|
+
applySavedPosition()
|
|
853
|
+
snapToEdge()
|
|
854
|
+
|
|
855
|
+
var savedKey = localStorage.getItem(STORE_SRC)
|
|
856
|
+
var initialSource =
|
|
857
|
+
sources.find(function (s) { return s.key === savedKey }) ||
|
|
858
|
+
sources.find(function (s) { return s.default }) ||
|
|
859
|
+
sources[0]
|
|
860
|
+
|
|
861
|
+
tabsEl.querySelectorAll('.bgm-dock__tab').forEach(function (tab) {
|
|
862
|
+
tab.classList.toggle('is-active', tab.getAttribute('data-key') === initialSource.key)
|
|
863
|
+
})
|
|
864
|
+
pendingKey = initialSource.key
|
|
865
|
+
current = initialSource.key
|
|
866
|
+
|
|
867
|
+
renderState()
|
|
868
|
+
|
|
869
|
+
window.addEventListener('resize', function () {
|
|
870
|
+
var rect = root.getBoundingClientRect()
|
|
871
|
+
placeFab(rect.left, rect.top)
|
|
872
|
+
if (!opened) snapToEdge()
|
|
873
|
+
})
|
|
874
|
+
})()
|
|
875
|
+
|
|
876
|
+
|