dsh-music-player 0.1.0

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/lib/client.js ADDED
@@ -0,0 +1,867 @@
1
+ /**
2
+ * dsh-music-player client half: the browser player, loaded by the web
3
+ * ModuleLoader as a plain React plugin. It injects a now-playing bar into the
4
+ * composer dock and a floating player panel (track list / modes / volume /
5
+ * spectrum) that also holds the music-directory setting in-panel.
6
+ *
7
+ * Audio is a native <audio> element. A per-track peak envelope is decoded via
8
+ * XMLHttpRequest(arraybuffer) + decodeAudioData and drives a smoothed 7-bar
9
+ * equalizer drawn on a canvas rAF loop. Play mode and volume persist across
10
+ * reloads via localStorage; the current track + position are restored without
11
+ * autoplay (a tap on ▶ resumes). Host communication is plain HTTP to the
12
+ * /dsh-music/(manifest|intent|set-root|id) routes.
13
+ */
14
+ window.__ModuleLoader__.load({
15
+ id: 'dsh-music-player',
16
+ factory: (require) => {
17
+ var module = { exports: {} };
18
+ var exports = module.exports;
19
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
20
+
21
+ const React = require('react');
22
+ const useState = React.useState;
23
+ const useEffect = React.useEffect;
24
+ const useRef = React.useRef;
25
+
26
+ // ---- persisted prefs / playback ----
27
+ const PREF_MODE = 'dsh-music-mode';
28
+ const PREF_VOL = 'dsh-music-volume';
29
+ const PREF_PLAYBACK = 'dsh-music-playback';
30
+ const PREF_ROOT = 'dsh-music-root';
31
+ const loadPref = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
32
+ const savePref = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
33
+ const clearPref = (k) => { try { localStorage.removeItem(k); } catch (e) {} };
34
+ const jsonGet = (url) => fetch(url, { cache: 'no-store' }).then((r) => r.json());
35
+
36
+ // ---- engine + shared store (React re-renders on set) ----
37
+ const SMOOTH_BARS = 7;
38
+ const PEAK_DECAY = 0.016;
39
+ const audio = new Audio();
40
+ audio.preload = 'auto';
41
+
42
+ const store = {
43
+ root: null, tracks: [], count: 0, currentId: null, currentName: null,
44
+ playing: false, position: 0, duration: 0, volume: 0.8,
45
+ panelOpen: false, loading: false, error: null, pendingId: null, pendingName: null,
46
+ mode: 'order', vizState: 'ok',
47
+ };
48
+ const listeners = new Set();
49
+ function set(patch) {
50
+ Object.assign(store, patch);
51
+ if ('mode' in patch) savePref(PREF_MODE, patch.mode);
52
+ if ('volume' in patch) savePref(PREF_VOL, String(patch.volume));
53
+ for (const fn of [...listeners]) fn();
54
+ }
55
+ function useStore() {
56
+ const [snap, setSnap] = useState(store);
57
+ useEffect(() => {
58
+ const update = () => setSnap({ ...store });
59
+ listeners.add(update);
60
+ update();
61
+ return () => { listeners.delete(update); };
62
+ }, []);
63
+ return snap;
64
+ }
65
+ const trackById = (id) => (store.tracks || []).find((t) => t.id === id) || null;
66
+
67
+ // restore persisted prefs
68
+ try {
69
+ const m = loadPref(PREF_MODE);
70
+ if (m === 'single' || m === 'order' || m === 'shuffle') store.mode = m;
71
+ const v = parseFloat(loadPref(PREF_VOL));
72
+ if (Number.isFinite(v)) { store.volume = Math.min(1, Math.max(0, v)); audio.volume = store.volume; }
73
+ } catch (e) {}
74
+
75
+ function savePlayback() {
76
+ if (store.currentId === null) { clearPref(PREF_PLAYBACK); return; }
77
+ savePref(PREF_PLAYBACK, JSON.stringify({ id: store.currentId, name: store.currentName, position: audio.currentTime || 0 }));
78
+ }
79
+ function loadPlayback() {
80
+ const raw = loadPref(PREF_PLAYBACK);
81
+ if (raw === null) return null;
82
+ try { const p = JSON.parse(raw); if (p && typeof p.id === 'string') return p; } catch (e) {}
83
+ return null;
84
+ }
85
+
86
+ // ---- envelope decode (current-track guarded by generation token; prefetch caches only) ----
87
+ let decodeCtx = null;
88
+ let trackEnv = null;
89
+ let envReqId = 0;
90
+ const envCache = new Map();
91
+ function closeDecodeCtx() {
92
+ if (decodeCtx !== null && decodeCtx.state !== 'closed') decodeCtx.close();
93
+ decodeCtx = null;
94
+ }
95
+ function ensureDecodeCtx() {
96
+ if (decodeCtx !== null) return decodeCtx;
97
+ try {
98
+ const Ctor = (window.AudioContext || window.webkitAudioContext);
99
+ if (Ctor === undefined) return null;
100
+ decodeCtx = new Ctor();
101
+ } catch { decodeCtx = null; }
102
+ return decodeCtx;
103
+ }
104
+ function trimCache() { while (envCache.size > 24) envCache.delete(envCache.keys().next().value); }
105
+ function loadEnvelope(id, url, isPrefetch) {
106
+ const cached = envCache.get(id);
107
+ const isCurrent = () => store.currentId === id;
108
+ if (cached !== undefined) { if (isCurrent()) { trackEnv = cached; set({ vizState: 'ok' }); } return; }
109
+ const reqId = isPrefetch ? -1 : (++envReqId);
110
+ if (isCurrent()) { trackEnv = null; set({ vizState: 'loading' }); }
111
+ try {
112
+ const xhr = new XMLHttpRequest();
113
+ xhr.open('GET', url, true);
114
+ xhr.responseType = 'arraybuffer';
115
+ xhr.onload = () => {
116
+ const ac = ensureDecodeCtx();
117
+ if (ac === null || xhr.response === null) return;
118
+ ac.decodeAudioData(xhr.response).then((buf) => {
119
+ const ch = buf.getChannelData(0);
120
+ const dt = 0.05;
121
+ const n = Math.max(1, Math.ceil(buf.duration / dt));
122
+ const peaks = new Float32Array(n);
123
+ const sRate = buf.sampleRate;
124
+ for (let i = 0; i < n; i++) {
125
+ const s = Math.floor(i * dt * sRate);
126
+ const e = Math.min(ch.length, Math.floor((i + 1) * dt * sRate));
127
+ let p = 0;
128
+ for (let j = s; j < e; j++) { const v = Math.abs(ch[j]); if (v > p) p = v; }
129
+ peaks[i] = p;
130
+ }
131
+ const env = { peaks, dt, duration: buf.duration };
132
+ envCache.set(id, env);
133
+ trimCache();
134
+ if (!isPrefetch && reqId === envReqId && isCurrent()) { trackEnv = env; set({ vizState: 'ok' }); }
135
+ }).catch(() => { if (!isPrefetch && isCurrent()) set({ vizState: 'unavailable' }); });
136
+ };
137
+ xhr.onerror = () => { if (!isPrefetch && isCurrent()) set({ vizState: 'unavailable' }); };
138
+ xhr.send();
139
+ } catch { if (!isPrefetch && isCurrent()) set({ vizState: 'unavailable' }); }
140
+ }
141
+ function prefetchNext() {
142
+ if (store.tracks.length === 0 || store.currentId === null) return;
143
+ let nextId = null;
144
+ if (store.mode === 'shuffle') {
145
+ // Prefetch the queued next track so a shuffle "next" starts instantly.
146
+ if (shuffleQueue.length === store.tracks.length) {
147
+ const pos = shuffleQueue.indexOf(store.currentId);
148
+ if (pos >= 0 && pos + 1 < shuffleQueue.length) nextId = shuffleQueue[pos + 1];
149
+ }
150
+ } else {
151
+ const idx = store.tracks.findIndex((t) => t.id === store.currentId);
152
+ const next = store.tracks[(idx + 1) % store.tracks.length];
153
+ if (next !== undefined) nextId = next.id;
154
+ }
155
+ if (nextId !== null) {
156
+ const next = trackById(nextId);
157
+ if (next !== undefined && !envCache.has(next.id)) loadEnvelope(next.id, next.url, true);
158
+ }
159
+ }
160
+
161
+ // ---- bar color sampling + canvas drawing ----
162
+ let barCanvasNode = null;
163
+ let rafId = null;
164
+ let scanCounter = 0;
165
+ let barColor = null;
166
+ const smoothCur = new Float32Array(SMOOTH_BARS);
167
+ const smoothPeak = new Float32Array(SMOOTH_BARS);
168
+ const targetBuf = new Float32Array(SMOOTH_BARS);
169
+ function extractColorFromCss(value) {
170
+ if (typeof value !== 'string') return null;
171
+ const rgba = value.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\)/);
172
+ if (rgba !== null) {
173
+ if (rgba[4] !== undefined && parseFloat(rgba[4]) < 0.15) return null;
174
+ return 'rgb(' + rgba[1] + ', ' + rgba[2] + ', ' + rgba[3] + ')';
175
+ }
176
+ const hex = value.match(/#([0-9a-fA-F]{6})/);
177
+ if (hex !== null) return '#' + hex[1];
178
+ return null;
179
+ }
180
+ function refreshBarColor() {
181
+ let best = null; let bestSat = -1;
182
+ try {
183
+ const buttons = document.querySelectorAll('button');
184
+ for (const btn of buttons) {
185
+ const cs = getComputedStyle(btn);
186
+ let col = extractColorFromCss(cs.backgroundColor);
187
+ if (col === null) {
188
+ const grads = cs.backgroundImage.match(/#[0-9a-fA-F]{6}|rgba?\([^)]*\)/g);
189
+ if (grads !== null && grads.length > 0) { for (const g of grads) { const c = extractColorFromCss(g); if (c !== null) { col = c; break; } } }
190
+ }
191
+ if (col === null) continue;
192
+ const rgb = col.match(/(\d+)/g);
193
+ if (rgb === null || rgb.length < 3) continue;
194
+ const r = parseInt(rgb[0], 10); const g = parseInt(rgb[1], 10); const b = parseInt(rgb[2], 10);
195
+ const mx = Math.max(r, g, b); const mn = Math.min(r, g, b); const sat = mx - mn;
196
+ if (sat < 40 || mx < 80) continue;
197
+ if (sat > bestSat) { bestSat = sat; best = 'rgb(' + r + ', ' + g + ', ' + b + ')'; }
198
+ }
199
+ } catch {}
200
+ if (best !== null && best !== barColor) {
201
+ barColor = best;
202
+ document.documentElement.style.setProperty('--dsh-music-accent', best);
203
+ }
204
+ }
205
+ function drawBars(canvas, useCaps) {
206
+ const c = canvas.getContext('2d');
207
+ const w = canvas.width; const h = canvas.height;
208
+ c.clearRect(0, 0, w, h);
209
+ const gap = 2;
210
+ const bw = (w - gap * (SMOOTH_BARS - 1)) / SMOOTH_BARS;
211
+ const color = barColor || '#2f9e6e';
212
+ for (let i = 0; i < SMOOTH_BARS; i++) {
213
+ const bh = Math.max(2, Math.round(smoothCur[i] * (h - 2)));
214
+ const x = Math.round(i * (bw + gap));
215
+ c.fillStyle = color;
216
+ c.fillRect(x, h - 1 - bh, Math.floor(bw), bh);
217
+ if (useCaps && smoothPeak[i] > smoothCur[i] + 0.03) {
218
+ const py = h - 1 - Math.round(smoothPeak[i] * (h - 2));
219
+ c.fillStyle = color;
220
+ c.fillRect(x, Math.max(0, py), Math.floor(bw), 2);
221
+ }
222
+ }
223
+ }
224
+ function drawViz() {
225
+ scanCounter++;
226
+ if (scanCounter % 120 === 0) refreshBarColor();
227
+ if (store.playing && trackEnv !== null) {
228
+ const envIdx = (audio.currentTime || 0) / trackEnv.dt;
229
+ const base = Math.floor(envIdx);
230
+ const n = trackEnv.peaks.length;
231
+ for (let i = 0; i < SMOOTH_BARS; i++) {
232
+ const len = 2 + (SMOOTH_BARS - 1 - i);
233
+ let mx = 0;
234
+ for (let k = base; k > base - len && k >= 0; k--) { if (k >= n) continue; const v = trackEnv.peaks[k]; if (v > mx) mx = v; }
235
+ targetBuf[i] = mx;
236
+ }
237
+ } else if (store.playing) {
238
+ const now = Date.now();
239
+ for (let i = 0; i < SMOOTH_BARS; i++) targetBuf[i] = 0.12 + 0.05 * Math.sin(now / 240 + i * 0.9);
240
+ } else {
241
+ for (let i = 0; i < SMOOTH_BARS; i++) targetBuf[i] = 0;
242
+ }
243
+ for (let i = 0; i < SMOOTH_BARS; i++) {
244
+ const t = targetBuf[i];
245
+ if (t > smoothCur[i]) smoothCur[i] += (t - smoothCur[i]) * 0.6;
246
+ else smoothCur[i] += (t - smoothCur[i]) * 0.1;
247
+ if (t > smoothPeak[i]) smoothPeak[i] = t;
248
+ else smoothPeak[i] -= PEAK_DECAY;
249
+ if (smoothPeak[i] < 0) smoothPeak[i] = 0;
250
+ }
251
+ if (barCanvasNode !== null) drawBars(barCanvasNode, true);
252
+ }
253
+ let rafRunning = false;
254
+ function startRaf() {
255
+ if (rafRunning) return;
256
+ rafRunning = true;
257
+ const tick = () => { if (!rafRunning) return; rafId = requestAnimationFrame(tick); drawViz(); };
258
+ tick();
259
+ }
260
+ function stopRaf() {
261
+ rafRunning = false;
262
+ if (rafId !== null) cancelAnimationFrame(rafId);
263
+ }
264
+
265
+ // ---- player actions ----
266
+ // Shuffle playback uses a pre-shuffled queue with a position pointer so
267
+ // "next" plays an unplayed track and "prev" returns to the previously
268
+ // played one, instead of random-without-repeat or list-order neighbors.
269
+ let shuffleQueue = [];
270
+ let shufflePos = -1;
271
+ function buildShuffleQueue(anchorId) {
272
+ const ids = store.tracks.map((t) => t.id);
273
+ // Fisher-Yates
274
+ for (let i = ids.length - 1; i > 0; i--) {
275
+ const j = Math.floor(Math.random() * (i + 1));
276
+ const tmp = ids[i]; ids[i] = ids[j]; ids[j] = tmp;
277
+ }
278
+ const a = anchorId !== undefined ? anchorId : store.currentId;
279
+ if (a !== null && ids.includes(a)) {
280
+ const ai = ids.indexOf(a);
281
+ if (ai !== 0) { ids.splice(ai, 1); ids.unshift(a); }
282
+ }
283
+ shuffleQueue = ids;
284
+ shufflePos = a !== null && ids[0] === a ? 0 : -1;
285
+ }
286
+ function syncShufflePos() {
287
+ if (store.mode !== 'shuffle') return;
288
+ if (store.currentId === null) return;
289
+ if (shuffleQueue.length !== store.tracks.length || !shuffleQueue.includes(store.currentId)) {
290
+ buildShuffleQueue(store.currentId);
291
+ return;
292
+ }
293
+ shufflePos = shuffleQueue.indexOf(store.currentId);
294
+ }
295
+ function startPlay(id) {
296
+ const track = trackById(id);
297
+ if (track === null) return;
298
+ audio.src = track.url;
299
+ audio.load();
300
+ set({ currentId: id, currentName: track.name, pendingId: null, pendingName: null, error: null });
301
+ syncShufflePos();
302
+ loadEnvelope(id, track.url);
303
+ prefetchNext();
304
+ savePlayback();
305
+ const promise = audio.play();
306
+ if (promise !== undefined && typeof promise.catch === 'function') {
307
+ promise.catch(() => { set({ error: '\u6d4f\u89c8\u5668\u62e6\u622a\u4e86\u81ea\u52a8\u64ad\u653e\uff0c\u8bf7\u70b9\u51fb\u4e00\u6b21\u64ad\u653e\u6309\u94ae', pendingId: id, pendingName: track.name }); });
308
+ }
309
+ }
310
+ function togglePlay() {
311
+ if (store.pendingId !== null && store.currentId === null) { startPlay(store.pendingId); return; }
312
+ if (store.currentId === null) { if (store.tracks.length > 0) startPlay(store.tracks[0].id); return; }
313
+ if (audio.paused) {
314
+ const promise = audio.play();
315
+ if (promise !== undefined && typeof promise.catch === 'function') promise.catch(() => set({ error: '\u6d4f\u89c8\u5668\u62e6\u622a\u4e86\u81ea\u52a8\u64ad\u653e\uff0c\u8bf7\u70b9\u51fb\u64ad\u653e\u6309\u94ae' }));
316
+ } else audio.pause();
317
+ }
318
+ function step(delta) {
319
+ if (store.tracks.length === 0) return;
320
+ if (store.mode === 'shuffle' && store.tracks.length > 1) {
321
+ // Walk the shuffled queue: next plays the next unplayed track, prev
322
+ // returns to the previously played one (not a list-order neighbor).
323
+ if (shuffleQueue.length !== store.tracks.length
324
+ || store.currentId === null || !shuffleQueue.includes(store.currentId)) {
325
+ buildShuffleQueue(store.currentId);
326
+ }
327
+ const pos = shuffleQueue.indexOf(store.currentId);
328
+ if (store.currentId === null) {
329
+ // Nothing playing yet: start from the head of the shuffled queue.
330
+ if (delta > 0) startPlay(shuffleQueue[0]);
331
+ return;
332
+ }
333
+ if (delta > 0) {
334
+ if (pos >= 0 && pos + 1 < shuffleQueue.length) {
335
+ startPlay(shuffleQueue[pos + 1]);
336
+ } else {
337
+ // Round finished: reshuffle anchored on the current track so the
338
+ // next play is a fresh unplayed one, not the track that just ended.
339
+ buildShuffleQueue(store.currentId);
340
+ startPlay(shuffleQueue.length > 1 ? shuffleQueue[1] : shuffleQueue[0]);
341
+ }
342
+ } else if (pos > 0) {
343
+ startPlay(shuffleQueue[pos - 1]);
344
+ } else {
345
+ // Already at the head of the shuffled queue: replay the current track.
346
+ startPlay(store.currentId);
347
+ }
348
+ return;
349
+ }
350
+ const idx = store.tracks.findIndex((t) => t.id === store.currentId);
351
+ const nextIdx = idx < 0 ? 0 : (idx + delta + store.tracks.length) % store.tracks.length;
352
+ startPlay(store.tracks[nextIdx].id);
353
+ }
354
+ function seekTo(seconds) {
355
+ if (Number.isFinite(seconds)) { audio.currentTime = seconds; set({ position: seconds }); savePlayback(); }
356
+ }
357
+ function changeVolume(value) {
358
+ const v = Math.min(1, Math.max(0, value));
359
+ audio.volume = v;
360
+ set({ volume: v });
361
+ }
362
+ function stop() {
363
+ envReqId++;
364
+ trackEnv = null;
365
+ audio.pause();
366
+ audio.removeAttribute('src');
367
+ audio.load();
368
+ set({ currentId: null, currentName: null, playing: false, position: 0, duration: 0, pendingId: null, pendingName: null, vizState: 'ok' });
369
+ clearPref(PREF_PLAYBACK);
370
+ }
371
+
372
+ function bindAudio() {
373
+ const onTime = () => set({ position: audio.currentTime || 0 });
374
+ const onDur = () => set({ duration: audio.duration || 0 });
375
+ const onPlay = () => set({ playing: true, error: null });
376
+ const onPause = () => { set({ playing: false }); savePlayback(); };
377
+ const onEnded = () => {
378
+ if (store.mode === 'single' && store.currentId !== null) {
379
+ audio.currentTime = 0;
380
+ const promise = audio.play();
381
+ if (promise !== undefined && typeof promise.catch === 'function') promise.catch(() => set({ error: '\u64ad\u653e\u5931\u8d25', playing: false }));
382
+ return;
383
+ }
384
+ step(1);
385
+ };
386
+ const onError = () => set({ error: '\u97f3\u9891\u52a0\u8f7d\u6216\u89e3\u7801\u5931\u8d25', playing: false });
387
+ audio.addEventListener('timeupdate', onTime);
388
+ audio.addEventListener('durationchange', onDur);
389
+ audio.addEventListener('play', onPlay);
390
+ audio.addEventListener('pause', onPause);
391
+ audio.addEventListener('ended', onEnded);
392
+ audio.addEventListener('error', onError);
393
+ return () => {
394
+ audio.pause();
395
+ audio.removeEventListener('timeupdate', onTime);
396
+ audio.removeEventListener('durationchange', onDur);
397
+ audio.removeEventListener('play', onPlay);
398
+ audio.removeEventListener('pause', onPause);
399
+ audio.removeEventListener('ended', onEnded);
400
+ audio.removeEventListener('error', onError);
401
+ };
402
+ }
403
+
404
+ function restorePlayback(list) {
405
+ const saved = loadPlayback();
406
+ if (saved === null) return;
407
+ const track = list.find((t) => t.id === saved.id);
408
+ if (track === undefined) return;
409
+ audio.src = track.url;
410
+ audio.load();
411
+ const pos = Number.isFinite(saved.position) ? saved.position : 0;
412
+ audio.currentTime = pos;
413
+ set({ currentId: track.id, currentName: track.name, position: pos, pendingId: null, pendingName: null, error: null });
414
+ savePlayback();
415
+ loadEnvelope(track.id, track.url);
416
+ prefetchNext();
417
+ }
418
+
419
+ // ---- host data ----
420
+ async function loadTracks() {
421
+ set({ loading: true });
422
+ try {
423
+ const result = await jsonGet('/dsh-music/manifest');
424
+ const rememberedRoot = loadPref(PREF_ROOT);
425
+ // If the host came up with the default root but this browser remembers
426
+ // a different one (e.g. the host state file was not yet written on an
427
+ // older restart), re-apply it so the chosen directory is restored.
428
+ if (rememberedRoot !== null && rememberedRoot !== '' && result.root !== rememberedRoot) {
429
+ saveRoot(rememberedRoot);
430
+ return;
431
+ }
432
+ set({ root: result.root || null, tracks: result.tracks || [], count: result.count || 0, loading: false, error: result.error || null });
433
+ const list = result.tracks || [];
434
+ if (list.length > 0) {
435
+ loadEnvelope(list[0].id, list[0].url, true);
436
+ if (list.length > 1) loadEnvelope(list[1].id, list[1].url, true);
437
+ }
438
+ restorePlayback(list);
439
+ } catch (err) {
440
+ set({ loading: false, error: '\u65e0\u6cd5\u8bfb\u53d6\u97f3\u4e50\u5e93\uff1a' + String((err && err.message) || err) });
441
+ }
442
+ }
443
+ function saveRoot(path) {
444
+ set({ loading: true });
445
+ fetch('/dsh-music/set-root', {
446
+ method: 'POST', cache: 'no-store',
447
+ headers: { 'content-type': 'application/json' },
448
+ body: JSON.stringify({ path }),
449
+ }).then((r) => r.json()).then((result) => {
450
+ if (result && result.ok) {
451
+ if (result.root) savePref(PREF_ROOT, result.root);
452
+ set({ root: result.root || null, tracks: result.tracks || [], count: result.count || 0, loading: false, error: null });
453
+ restorePlayback(result.tracks || []);
454
+ } else {
455
+ set({ loading: false, error: (result && result.error) || '\u8bbe\u7f6e\u76ee\u5f55\u5931\u8d25' });
456
+ }
457
+ }).catch((err) => {
458
+ set({ loading: false, error: '\u8bbe\u7f6e\u76ee\u5f55\u5931\u8d25\uff1a' + String((err && err.message) || err) });
459
+ });
460
+ }
461
+
462
+ function fmtTime(seconds) {
463
+ if (!Number.isFinite(seconds) || seconds <= 0) return '0:00';
464
+ const m = Math.floor(seconds / 60);
465
+ const s = Math.floor(seconds % 60);
466
+ return m + ':' + (s < 10 ? '0' : '') + s;
467
+ }
468
+ function MusicNote(props) {
469
+ const cls = props.className || '';
470
+ return React.createElement('svg', { className: cls, width: 12, height: 12, viewBox: '0 0 24 24', fill: 'currentColor', 'aria-hidden': true },
471
+ React.createElement('path', { d: 'M12 3v10.55A4 4 0 1 0 14 17V7h4V3h-6z' }));
472
+ }
473
+
474
+ // ---- components ----
475
+ // Custom vertical volume slider. The native <input type=range> cannot be
476
+ // fully restyled in current Chrome (track keeps gray border lines and the
477
+ // thumb ignores width/height once appearance:none is set), so the slider is
478
+ // drawn with plain divs and driven by pointer events: click to jump, drag
479
+ // the thumb to scrub. Value runs bottom (0) to top (1).
480
+ function VolumeSlider() {
481
+ const s = useStore();
482
+ const trackRef = useRef(null);
483
+ const draggingRef = useRef(false);
484
+ const valueFor = (clientY) => {
485
+ const el = trackRef.current;
486
+ if (el === null) return s.volume;
487
+ const r = el.getBoundingClientRect();
488
+ if (r.height <= 0) return s.volume;
489
+ const ratio = 1 - (clientY - r.top) / r.height;
490
+ return Math.min(1, Math.max(0, ratio));
491
+ };
492
+ const onPointerDown = (e) => {
493
+ if (e.button !== undefined && e.button !== 0) return;
494
+ draggingRef.current = true;
495
+ e.currentTarget.setPointerCapture(e.pointerId);
496
+ changeVolume(valueFor(e.clientY));
497
+ };
498
+ const onPointerMove = (e) => {
499
+ if (!draggingRef.current) return;
500
+ changeVolume(valueFor(e.clientY));
501
+ };
502
+ const onPointerUp = (e) => {
503
+ if (!draggingRef.current) return;
504
+ draggingRef.current = false;
505
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
506
+ };
507
+ const pct = Math.round(s.volume * 100);
508
+ return React.createElement('div',
509
+ { className: 'dsh-music-vol-slider', ref: trackRef,
510
+ onPointerDown, onPointerMove, onPointerUp,
511
+ title: '\u97f3\u91cf ' + pct + '%' },
512
+ React.createElement('div', { className: 'dsh-music-vol-track' }),
513
+ React.createElement('div', { className: 'dsh-music-vol-fill', style: { height: pct + '%' } }),
514
+ React.createElement('div', { className: 'dsh-music-vol-thumb', style: { bottom: 'calc(' + pct + '% - 7px)' } }),
515
+ );
516
+ }
517
+ function NowPlayingBar() {
518
+ const s = useStore();
519
+ const [volOpen, setVolOpen] = useState(false);
520
+ const volRef = useRef(null);
521
+ useEffect(() => {
522
+ if (!volOpen) return;
523
+ const onClick = (e) => { if (volRef.current !== null && !volRef.current.contains(e.target)) setVolOpen(false); };
524
+ document.addEventListener('mousedown', onClick);
525
+ return () => document.removeEventListener('mousedown', onClick);
526
+ }, [volOpen]);
527
+ const hasTrack = s.currentName !== null || s.pendingName !== null;
528
+ const name = s.currentName || s.pendingName;
529
+ const showHint = s.pendingName !== null && s.currentId === null;
530
+ const panelCls = 'dsh-music-mode-trigger' + (s.panelOpen ? ' active' : '');
531
+ let vizBadge = null;
532
+ if (hasTrack && s.vizState === 'unavailable') {
533
+ vizBadge = React.createElement('button', {
534
+ className: 'dsh-music-bar-warn',
535
+ title: '\u9891\u8c31\u4e0d\u53ef\u7528\uff0c\u70b9\u51fb\u91cd\u8bd5',
536
+ onClick: () => { const t = trackById(s.currentId); if (t !== null) loadEnvelope(t.id, t.url); },
537
+ }, '\u9891\u8c31\u4e0d\u53ef\u7528\uff0c\u70b9\u51fb\u91cd\u8bd5');
538
+ }
539
+ const note = React.createElement(MusicNote, { className: 'dsh-music-note' });
540
+ return React.createElement('div', { className: 'dsh-music-bar-wrap' },
541
+ React.createElement('div', { className: 'dsh-music-bar' },
542
+ hasTrack
543
+ ? React.createElement('span', { className: 'dsh-music-bar-name', title: name }, note, ' ', name)
544
+ : React.createElement('span', { className: 'dsh-music-bar-idle' }, note, ' \u672c\u5730\u97f3\u4e50\u64ad\u653e\u5668'),
545
+ hasTrack && s.playing ? React.createElement('canvas', { className: 'dsh-music-viz', width: 64, height: 14, ref: (el) => { barCanvasNode = el; } }) : null,
546
+ vizBadge,
547
+ hasTrack
548
+ ? (showHint
549
+ ? React.createElement('span', { className: 'dsh-music-bar-hint' }, '\u26a0 \u81ea\u52a8\u64ad\u653e\u88ab\u62e6\u622a\uff0c\u70b9\u51fb\u25b6\u89e3\u9501')
550
+ : React.createElement('span', { className: 'dsh-music-bar-time' }, fmtTime(s.position) + ' / ' + fmtTime(s.duration)))
551
+ : null,
552
+ hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '\u4e0a\u4e00\u9996', onClick: () => step(-1) }, '\u23ee') : null,
553
+ hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '\u64ad\u653e/\u6682\u505c', onClick: togglePlay }, s.playing ? '\u23f8' : '\u25b6') : null,
554
+ hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '\u4e0b\u4e00\u9996', onClick: () => step(1) }, '\u23ed') : null,
555
+ hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '\u505c\u6b62', onClick: stop }, '\u23f9') : null,
556
+ React.createElement(ModeDropdown, null),
557
+ React.createElement('div', { className: 'dsh-music-bar-vol', ref: volRef },
558
+ React.createElement('button', {
559
+ className: 'dsh-music-mode-trigger' + (volOpen ? ' active' : ''),
560
+ title: '\u97f3\u91cf',
561
+ onClick: () => setVolOpen((o) => !o),
562
+ }, React.createElement('svg', {
563
+ viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true,
564
+ }, React.createElement('path', { d: 'M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z' }))),
565
+ volOpen ? React.createElement('div', { className: 'dsh-music-bar-vol-pop' },
566
+ React.createElement(VolumeSlider, null),
567
+ ) : null,
568
+ ),
569
+ React.createElement('button', {
570
+ className: panelCls,
571
+ title: s.panelOpen ? '\u5173\u95ed\u64ad\u653e\u5217\u8868' : '\u6253\u5f00\u64ad\u653e\u5217\u8868',
572
+ onClick: () => set({ panelOpen: !s.panelOpen }),
573
+ }, React.createElement('svg', {
574
+ viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true,
575
+ }, React.createElement('path', {
576
+ d: 'M3 13h2v-2H3v2zm0 4h2v-2H3v2zm0-8h2V7H3v2zm4 4h14v-2H7v2zm0 4h14v-2H7v2zM7 7v2h14V7H7z',
577
+ }))),
578
+ ),
579
+ );
580
+ }
581
+ // Playback-mode metadata + an icon-only dropdown. Icons are inline SVGs filled
582
+ // with currentColor so they match the accent of the other round transport
583
+ // buttons (green), which a native <select> cannot color.
584
+ const MODES = [
585
+ { id: 'single', label: '\u5355\u66f2\u5faa\u73af', title: '\u5355\u66f2\u5faa\u73af\uff1a\u64ad\u653e\u7ed3\u675f\u91cd\u590d\u5f53\u524d\u66f2\u76ee', d: 'M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z' },
586
+ { id: 'order', label: '\u987a\u5e8f\u64ad\u653e', title: '\u987a\u5e8f\u64ad\u653e\uff1a\u81ea\u52a8\u64ad\u653e\u5217\u8868\u4e2d\u7684\u4e0b\u4e00\u9996', d: 'M15 6H3v2h12V6zm0 4H3v2h12v-2zM3 16h8v-2H3v2zm14-10v8.18c-.31-.11-.65-.18-1-.18-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3V8h3V6h-5z' },
587
+ { id: 'shuffle', label: '\u4e71\u5e8f\u64ad\u653e', title: '\u4e71\u5e8f\u64ad\u653e\uff1a\u968f\u673a\u6311\u9009\u4e0b\u4e00\u9996', d: 'M10.59 9.17L5.41 4 4 5.41l5.17 5.17 1.42-1.41zM14.5 4l2.04 2.04L4 18.59 5.41 20 17.96 7.46 20 9.5V4h-5.5zm.33 9.41l-1.41 1.41 3.13 3.13L14.5 20H20v-5.5l-2.04 2.04-3.13-3.13z' },
588
+ ];
589
+ function ModeIcon(props) {
590
+ return React.createElement('svg', {
591
+ viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true,
592
+ }, React.createElement('path', { d: props.d }));
593
+ }
594
+ function ModeDropdown() {
595
+ const s = useStore();
596
+ const [open, setOpen] = useState(false);
597
+ const ref = useRef(null);
598
+ useEffect(() => {
599
+ if (!open) return;
600
+ const onClick = (e) => { if (ref.current !== null && !ref.current.contains(e.target)) setOpen(false); };
601
+ document.addEventListener('mousedown', onClick);
602
+ return () => document.removeEventListener('mousedown', onClick);
603
+ }, [open]);
604
+ const cur = MODES.find((m) => m.id === s.mode) || MODES[1];
605
+ // Right-align the mode+volume+panel cluster when there is no track: during
606
+ // playback the time span already carries margin-left:auto to push these
607
+ // right, so only apply the auto margin when a name/pending name is absent.
608
+ const barRight = s.currentName === null && s.pendingName === null;
609
+ return React.createElement('div',
610
+ { className: 'dsh-music-mode-menu' + (barRight ? ' right' : ''), ref },
611
+ React.createElement('button', {
612
+ className: 'dsh-music-mode-trigger' + (open ? ' active' : ''),
613
+ title: cur.label,
614
+ onClick: () => setOpen((o) => !o),
615
+ }, React.createElement(ModeIcon, { d: cur.d })),
616
+ open ? React.createElement('div', { className: 'dsh-music-mode-pop' },
617
+ MODES.map((m) => React.createElement('button', {
618
+ key: m.id,
619
+ className: 'dsh-music-mode-item' + (s.mode === m.id ? ' active' : ''),
620
+ title: m.title,
621
+ onClick: () => { set({ mode: m.id }); setOpen(false); },
622
+ }, React.createElement(ModeIcon, { d: m.d }))),
623
+ ) : null,
624
+ );
625
+ }
626
+ function PlayerPanel() {
627
+ const s = useStore();
628
+ const listRef = useRef(null);
629
+ const panelRef = useRef(null);
630
+ useEffect(() => {
631
+ if (!s.panelOpen) return;
632
+ // Close the playlist panel when the user clicks outside it
633
+ // (mousedown precedes the toggle's click, so both stay consistent).
634
+ const onDown = (e) => {
635
+ if (panelRef.current !== null && !panelRef.current.contains(e.target)) set({ panelOpen: false });
636
+ };
637
+ document.addEventListener('mousedown', onDown);
638
+ return () => document.removeEventListener('mousedown', onDown);
639
+ }, [s.panelOpen]);
640
+ useEffect(() => {
641
+ if (!s.panelOpen) return;
642
+ const list = listRef.current;
643
+ if (list === null) return;
644
+ const active = list.querySelector('.dsh-music-track.active');
645
+ if (active !== null && typeof active.scrollIntoView === 'function') active.scrollIntoView({ block: 'nearest' });
646
+ }, [s.panelOpen, s.currentId]);
647
+ if (!s.panelOpen) return null;
648
+ const rows = s.tracks.map((t) => {
649
+ const active = t.id === s.currentId;
650
+ const playing = active && s.playing;
651
+ return React.createElement('button', {
652
+ key: t.id,
653
+ className: 'dsh-music-track' + (active ? ' active' : ''),
654
+ title: t.url,
655
+ onClick: () => { if (active) togglePlay(); else startPlay(t.id); },
656
+ },
657
+ React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '\u25b6 ' : '') + t.name),
658
+ React.createElement('span', { className: 'dsh-music-track-size' }, t.size ? Math.round(t.size / 1024 / 1024 * 10) / 10 + ' MB' : ''),
659
+ );
660
+ });
661
+ return React.createElement('div', { className: 'dsh-music-panel', ref: panelRef },
662
+ React.createElement('div', { className: 'dsh-music-panel-head' },
663
+ React.createElement('span', { className: 'dsh-music-panel-title' }, '\u64ad\u653e\u5217\u8868'),
664
+ React.createElement('button', { className: 'dsh-music-icon-btn', title: '\u5173\u95ed', onClick: () => set({ panelOpen: false }) }, '\u2715')),
665
+ React.createElement(DirectorySetting, null),
666
+ s.error ? React.createElement('div', { className: 'dsh-music-error' }, s.error) : null,
667
+ s.loading ? React.createElement('div', { className: 'dsh-music-loading' }, '\u626b\u63cf\u4e2d\u2026') : null,
668
+ React.createElement('div', { className: 'dsh-music-list', ref: (el) => { listRef.current = el; } },
669
+ rows.length > 0 ? rows : React.createElement('div', { className: 'dsh-music-empty' }, '\u6682\u65e0\u97f3\u4e50\u3002\u70b9\u51fb\u4e0a\u65b9\u201c\u9009\u62e9\u97f3\u4e50\u76ee\u5f55\u201d\u5e76\u9009\u62e9\u76ee\u5f55\u540e\u81ea\u52a8\u626b\u63cf\u3002')),
670
+ );
671
+ }
672
+ // Directory setting block, embedded in the player panel (the former
673
+ // 设置 → 音乐播放器 page moved in-panel so all library config lives in one place).
674
+ function DirectorySetting() {
675
+ const s = useStore();
676
+ const [pickerOpen, setPickerOpen] = useState(false);
677
+ const [dirs, setDirs] = useState([]);
678
+ const [curPath, setCurPath] = useState('');
679
+ const [curName, setCurName] = useState('');
680
+ const [dirError, setDirError] = useState(null);
681
+ return React.createElement('div', { className: 'dsh-music-settings' },
682
+ React.createElement('div', { className: 'dsh-music-settings-row' },
683
+ React.createElement('span', { className: 'dsh-music-settings-cur', title: s.root || '' },
684
+ '\ud83d\udcc1 ' + (s.root || '\u672a\u914d\u7f6e')),
685
+ React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => openPicker() }, '\u9009\u62e9\u97f3\u4e50\u76ee\u5f55')),
686
+ s.error ? React.createElement('p', { className: 'dsh-music-error' }, s.error) : null,
687
+ React.createElement('p', { className: 'dsh-music-hint' }, '\u652f\u6301 mp3 / m4a / flac / wav / ogg / opus / aac / webm \u7b49\u683c\u5f0f\uff0c\u81ea\u52a8\u9012\u5f52\u626b\u63cf\u5b50\u76ee\u5f55\u3002'),
688
+ pickerOpen ? React.createElement('div', { className: 'dsh-music-picker-overlay' },
689
+ React.createElement('div', { className: 'dsh-music-picker' },
690
+ React.createElement('div', { className: 'dsh-music-picker-head' },
691
+ React.createElement('span', { className: 'dsh-music-picker-title' }, '\u9009\u62e9\u97f3\u4e50\u76ee\u5f55')),
692
+ React.createElement('div', { className: 'dsh-music-picker-cur', title: curPath },
693
+ curName || curPath || '\u5bb6\u76ee\u5f55'),
694
+ React.createElement('div', { className: 'dsh-music-picker-list' },
695
+ dirs.length > 0
696
+ ? dirs.map((d) => React.createElement('button', {
697
+ key: d.path,
698
+ className: 'dsh-music-picker-item',
699
+ title: d.path,
700
+ onClick: () => browse(d.path),
701
+ }, '\ud83d\udcc1 ' + d.name))
702
+ : React.createElement('div', { className: 'dsh-music-picker-empty' }, '\u672c\u76ee\u5f55\u4e0b\u65e0\u5b50\u76ee\u5f55\uff0c\u53ef\u76f4\u63a5\u9009\u62e9\u3002'),
703
+ dirError ? React.createElement('div', { className: 'dsh-music-error' }, dirError) : null,
704
+ ),
705
+ React.createElement('div', { className: 'dsh-music-picker-foot' },
706
+ React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => goUp() }, '\u8fd4\u56de\u4e0a\u7ea7'),
707
+ React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => pickCurrent() }, '\u9009\u62e9\u6b64\u76ee\u5f55'),
708
+ React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => setPickerOpen(false) }, '\u53d6\u6d88'),
709
+ ),
710
+ ),
711
+ ) : null,
712
+ );
713
+ function openPicker() {
714
+ setPickerOpen(true);
715
+ setDirError(null);
716
+ // Open directly at the currently configured root so the user sees the
717
+ // existing choice first; fall back to the home directory when unset.
718
+ browse(s.root || '');
719
+ }
720
+ async function browse(path) {
721
+ setDirError(null);
722
+ try {
723
+ const data = await jsonGet('/dsh-music/dir?path=' + encodeURIComponent(path || ''));
724
+ if (data && data.error) { setDirError(data.error); return; }
725
+ setCurPath(data.path || '');
726
+ setCurName(data.name || '');
727
+ setDirs(data.dirs || []);
728
+ } catch (err) {
729
+ setDirError('读取目录失败:' + String((err && err.message) || err));
730
+ }
731
+ }
732
+ function goUp() {
733
+ const p = curPath;
734
+ if (p === '' || p === '/') return;
735
+ const idx = p.lastIndexOf('/');
736
+ browse(idx <= 0 ? '/' : p.slice(0, idx));
737
+ }
738
+ function pickCurrent() {
739
+ const p = curPath;
740
+ if (p === '') return;
741
+ setPickerOpen(false);
742
+ saveRoot(p);
743
+ }
744
+ }
745
+
746
+ const inject = ['slots'];
747
+ function apply(ctx) {
748
+ const slots = ctx.get('slots');
749
+ if (slots === undefined) return;
750
+
751
+ ctx.effect(() => {
752
+ const styleEl = document.createElement('style');
753
+ styleEl.setAttribute('data-plugin', 'dsh-music-player');
754
+ styleEl.textContent = PLAYER_CSS;
755
+ document.head.appendChild(styleEl);
756
+ return () => { if (styleEl.parentNode) styleEl.parentNode.removeChild(styleEl); };
757
+ });
758
+
759
+ ctx.effect(() => {
760
+ const unbind = bindAudio();
761
+ startRaf();
762
+ return () => { stopRaf(); unbind(); closeDecodeCtx(); };
763
+ }, 'music-player: audio + viz engine');
764
+
765
+ loadTracks();
766
+
767
+ const intentTimer = setInterval(() => {
768
+ jsonGet('/dsh-music/intent').then((intent) => {
769
+ if (intent === null || typeof intent !== 'object' || intent.id === undefined) return;
770
+ set({ pendingId: intent.id, pendingName: intent.name || '' });
771
+ const track = trackById(intent.id);
772
+ if (track !== null) {
773
+ audio.src = track.url;
774
+ audio.load();
775
+ set({ currentId: intent.id, currentName: track.name, error: null });
776
+ loadEnvelope(intent.id, track.url);
777
+ prefetchNext();
778
+ savePlayback();
779
+ const promise = audio.play();
780
+ if (promise !== undefined && typeof promise.catch === 'function') {
781
+ promise.catch(() => set({ error: '\u6d4f\u89c8\u5668\u62e6\u622a\u4e86\u81ea\u52a8\u64ad\u653e\uff0c\u8bf7\u5728\u64ad\u653e\u6761\u70b9\u51fb\u25b6\u89e3\u9501', pendingId: intent.id, pendingName: track.name }));
782
+ }
783
+ }
784
+ }).catch(() => {});
785
+ }, 2000);
786
+
787
+ ctx.effect(() => slots.inject('conversation.input.dock', () => slots.register(
788
+ { name: 'conversation.input.dock', id: 'music-player-bar', order: 40 },
789
+ () => React.createElement(NowPlayingBar),
790
+ )), 'music-player: now playing bar');
791
+ ctx.effect(() => slots.inject('shell.overlay', () => slots.register(
792
+ { name: 'shell.overlay', id: 'music-player-panel', order: 20 },
793
+ () => React.createElement(PlayerPanel),
794
+ )), 'music-player: overlay panel');
795
+
796
+ ctx.effect(() => () => clearInterval(intentTimer), 'music-player: intent poll stop');
797
+ }
798
+
799
+ exports.apply = apply;
800
+ exports.inject = inject;
801
+
802
+ // ---- CSS ----
803
+ const PLAYER_CSS = '\n' +
804
+ '.dsh-music-bar-wrap { box-sizing: border-box; width: 100%; padding: 0 var(--dsh-composer-side-clearance, 16px); }\n' +
805
+ '.dsh-music-bar { box-sizing: border-box; display: flex; align-items: center; gap: 8px; width: 100%; max-width: var(--dsh-composer-card-max-width, 780px); margin: 0 auto; padding: 4px 10px; font-size: 12px; color: var(--dsw-alias-label-secondary, #8a8f98); background: var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.04)); border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.2)); border-radius: 8px; }\n' +
806
+ '.dsh-music-bar-idle { color: var(--dsw-alias-label-primary, #e6e6e6); font-weight: 500; display: inline-flex; align-items: center; }\n' +
807
+ '.dsh-music-bar-name { max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: inline-flex; align-items: center; min-width: 0; }\n' +
808
+ '.dsh-music-note { color: var(--dsh-music-accent, #2f9e6e); flex: none; margin-right: 4px; }\n' +
809
+ '.dsh-music-viz { flex: none; width: 64px; height: 14px; }\n' +
810
+ '.dsh-music-bar-warn { background: transparent; border: none; color: var(--dsw-alias-state-warn-primary, #d9a441); font-size: 12px; cursor: pointer; padding: 0; white-space: nowrap; }\n' +
811
+ '.dsh-music-bar-btn { display: inline-flex; align-items: center; justify-content: center; flex: none; height: 20px; background: transparent; border: none; color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; font-size: 13px; line-height: 1; padding: 0 4px; border-radius: 4px; }\n' +
812
+ '.dsh-music-bar-btn:hover { color: var(--dsw-alias-brand-primary, #4f8cff); }\n' +
813
+ '.dsh-music-bar-btn.active { color: var(--dsw-alias-brand-primary, #4f8cff); }\n' +
814
+ '.dsh-music-bar-vol { position: relative; flex: none; display: inline-flex; align-self: center; }\n' +
815
+ '.dsh-music-bar-vol-pop { position: absolute; bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); display: flex; align-items: center; justify-content: center; width: 36px; height: 108px; background: var(--dsw-alias-bg-overlay, #1e1f22); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35)); border-radius: 8px; box-shadow: 0 8px 20px rgba(0,0,0,0.3); z-index: 60; }\n' +
816
+ '.dsh-music-vol-slider { position: relative; width: 24px; height: 84px; cursor: pointer; touch-action: none; }\n' +
817
+ '.dsh-music-vol-track { position: absolute; left: 50%; top: 0; bottom: 0; width: 4px; transform: translateX(-50%); border-radius: 2px; background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.14)); }\n' +
818
+ '.dsh-music-vol-fill { position: absolute; left: 50%; bottom: 0; width: 4px; transform: translateX(-50%); border-radius: 2px; background: var(--dsh-music-accent, #2f9e6e); }\n' +
819
+ '.dsh-music-vol-thumb { position: absolute; left: 50%; transform: translateX(-50%); width: 14px; height: 14px; border-radius: 50%; background: var(--dsh-music-accent, #2f9e6e); box-shadow: 0 1px 3px rgba(0,0,0,0.4); }\n' +
820
+ '.dsh-music-bar-time { margin-left: auto; line-height: 1; font-variant-numeric: tabular-nums; }\n' +
821
+ '.dsh-music-bar-hint { margin-left: auto; color: var(--dsw-alias-state-warn-primary, #d9a441); }\n' +
822
+ '.dsh-music-bar .dsh-music-mode-trigger { width: 24px; height: 24px; }\n' +
823
+ '.dsh-music-bar .dsh-music-mode-trigger svg { flex: none; }\n' +
824
+ '.dsh-music-bar .dsh-music-mode-menu { align-self: center; }\n' +
825
+ '.dsh-music-panel { position: fixed; right: 24px; bottom: 84px; width: 380px; max-height: 72vh; display: flex; flex-direction: column; gap: 8px; padding: 12px; background: var(--dsw-alias-bg-overlay, #1e1f22); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35)); border-radius: 12px; box-shadow: 0 12px 32px rgba(0,0,0,0.35); color: var(--dsw-alias-label-primary, #e6e6e6); font-size: 13px; z-index: 1000; pointer-events: auto; overflow: hidden; }\n' +
826
+ '.dsh-music-panel-head { display: flex; align-items: center; gap: 6px; }\n' +
827
+ '.dsh-music-panel-title { font-weight: 600; margin-right: auto; }\n' +
828
+ '.dsh-music-icon-btn { background: transparent; border: none; color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; font-size: 14px; padding: 2px 6px; border-radius: 6px; }\n' +
829
+ '.dsh-music-icon-btn:hover { color: var(--dsw-alias-label-primary, #e6e6e6); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
830
+ '.dsh-music-panel-root { font-size: 12px; color: var(--dsw-alias-label-secondary, #8a8f98); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
831
+ '.dsh-music-mode-menu { position: relative; flex: none; }\n' +
832
+ '.dsh-music-mode-menu.right { margin-left: auto; }\n' +
833
+ '.dsh-music-mode-trigger { display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.25)); background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.05)); color: var(--dsh-music-accent, #2f9e6e); cursor: pointer; }\n' +
834
+ '.dsh-music-mode-trigger:hover, .dsh-music-mode-trigger.active { background: var(--dsh-music-accent, #2f9e6e); color: #fff; }\n' +
835
+ '.dsh-music-mode-pop { position: absolute; left: 50%; transform: translateX(-50%); bottom: calc(100% + 6px); z-index: 60; display: flex; flex-direction: column; gap: 4px; padding: 6px; background: var(--dsw-alias-bg-overlay, #1e1f22); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35)); border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); }\n' +
836
+ '.dsh-music-mode-item { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; border: none; border-radius: 8px; background: transparent; color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; }\n' +
837
+ '.dsh-music-mode-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); color: var(--dsh-music-accent, #2f9e6e); }\n' +
838
+ '.dsh-music-mode-item.active { background: var(--dsh-music-accent, #2f9e6e); color: #fff; }\n' +
839
+ '.dsh-music-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; min-height: 60px; max-height: 40vh; }\n' +
840
+ '.dsh-music-track { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; padding: 6px 8px; border: none; background: transparent; border-radius: 6px; color: var(--dsw-alias-label-primary, #e6e6e6); cursor: pointer; font-size: 12px; }\n' +
841
+ '.dsh-music-track:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
842
+ '.dsh-music-track.active { color: var(--dsh-music-accent, #2f9e6e); }\n' +
843
+ '.dsh-music-track-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
844
+ '.dsh-music-track-size { font-size: 11px; color: var(--dsw-alias-label-secondary, #8a8f98); }\n' +
845
+ '.dsh-music-empty { padding: 12px; text-align: center; color: var(--dsw-alias-label-secondary, #8a8f98); font-size: 12px; }\n' +
846
+ '.dsh-music-error { color: var(--dsw-alias-state-error-primary, #e5534b); font-size: 12px; }\n' +
847
+ '.dsh-music-loading { color: var(--dsw-alias-label-secondary, #8a8f98); font-size: 12px; }\n' +
848
+ '.dsh-music-settings { display: flex; flex-direction: column; gap: 10px; }\n' +
849
+ '.dsh-music-settings-row { display: flex; gap: 8px; align-items: center; }\n' +
850
+ '.dsh-music-settings-cur { flex: 1; min-width: 0; padding: 6px 10px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.3)); background: var(--dsw-alias-bg-layer-1, rgba(0,0,0,0.04)); color: var(--dsw-alias-label-primary, #e6e6e6); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
851
+ '.dsh-music-settings-btn { padding: 6px 12px; border-radius: 8px; border: none; background: var(--dsh-music-accent, #2f9e6e); color: #fff; cursor: pointer; font-size: 13px; white-space: nowrap; }\n' +
852
+ '.dsh-music-settings-btn.ghost { background: transparent; border: 1px solid var(--dsw-alias-border-l1, rgba(128,128,128,0.3)); color: var(--dsw-alias-label-secondary, #8a8f98); }\n' +
853
+ '.dsh-music-picker-overlay { position: absolute; inset: 0; z-index: 70; display: flex; align-items: center; justify-content: center; background: rgba(0,0,0,0.45); }\n' +
854
+ '.dsh-music-picker { box-sizing: border-box; width: 88%; max-height: 80%; display: flex; flex-direction: column; gap: 8px; padding: 12px; background: var(--dsw-alias-bg-overlay, #1e1f22); border: 1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.35)); border-radius: 12px; color: var(--dsw-alias-label-primary, #e6e6e6); }\n' +
855
+ '.dsh-music-picker-head { display: flex; align-items: center; }\n' +
856
+ '.dsh-music-picker-title { font-weight: 600; }\n' +
857
+ '.dsh-music-picker-cur { font-size: 12px; color: var(--dsw-alias-label-secondary, #8a8f98); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
858
+ '.dsh-music-picker-list { flex: 1; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }\n' +
859
+ '.dsh-music-picker-item { text-align: left; padding: 6px 8px; border: none; background: transparent; border-radius: 6px; color: var(--dsw-alias-label-primary, #e6e6e6); cursor: pointer; font-size: 13px; }\n' +
860
+ '.dsh-music-picker-item:hover { background: var(--dsw-alias-bg-layer-2, rgba(255,255,255,0.06)); }\n' +
861
+ '.dsh-music-picker-empty { padding: 8px; font-size: 12px; color: var(--dsw-alias-label-secondary, #8a8f98); }\n' +
862
+ '.dsh-music-picker-foot { display: flex; gap: 8px; justify-content: flex-end; }\n' +
863
+ '.dsh-music-hint { font-size: 12px; color: var(--dsw-alias-label-secondary, #8a8f98); }\n';
864
+
865
+ return module.exports;
866
+ },
867
+ });