dsh-music-player 0.2.0 → 0.2.2
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 +167 -55
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -52,13 +52,54 @@ window.__ModuleLoader__.load({
|
|
|
52
52
|
const PREF_MODE = 'dsh-music-mode';
|
|
53
53
|
const PREF_VOL = 'dsh-music-volume';
|
|
54
54
|
const PREF_PLAYBACK = 'dsh-music-playback';
|
|
55
|
-
const
|
|
55
|
+
const PREF_BOOKS_PLAYBACK = 'dsh-music-books-playback';
|
|
56
56
|
const PREF_ROOT = 'dsh-music-root';
|
|
57
57
|
const PREF_PANEL_POS = 'dsh-music-panel-pos';
|
|
58
58
|
const PREF_VOICE = 'dsh-music-voice';
|
|
59
59
|
const loadPref = (k) => { try { return localStorage.getItem(k); } catch (e) { return null; } };
|
|
60
60
|
const savePref = (k, v) => { try { localStorage.setItem(k, v); } catch (e) {} };
|
|
61
61
|
const clearPref = (k) => { try { localStorage.removeItem(k); } catch (e) {} };
|
|
62
|
+
// ---- per-book novel progress (independent from music) ----
|
|
63
|
+
// Every novel remembers its own position, keyed by its filename, so switching
|
|
64
|
+
// between books — or to music — never loses another book's place. Music
|
|
65
|
+
// progress lives in PREF_PLAYBACK and this subsystem never touches it.
|
|
66
|
+
const PREF_BOOK_PLAYBACK = 'dsh-music-book-playback'; // legacy single-book key
|
|
67
|
+
function readBooksPlayback() {
|
|
68
|
+
try {
|
|
69
|
+
const raw = loadPref(PREF_BOOKS_PLAYBACK);
|
|
70
|
+
if (raw) { const o = JSON.parse(raw); if (o && typeof o === 'object') return o; }
|
|
71
|
+
} catch (e) {}
|
|
72
|
+
return {};
|
|
73
|
+
}
|
|
74
|
+
function writeBooksPlayback(map) { savePref(PREF_BOOKS_PLAYBACK, JSON.stringify(map)); }
|
|
75
|
+
function getBookPlayback(name) { return readBooksPlayback()[name] || null; }
|
|
76
|
+
function clearBookPlayback(name) {
|
|
77
|
+
const map = readBooksPlayback();
|
|
78
|
+
if (Object.prototype.hasOwnProperty.call(map, name)) { delete map[name]; writeBooksPlayback(map); }
|
|
79
|
+
}
|
|
80
|
+
// Persist the currently playing novel's position into the per-book map.
|
|
81
|
+
function saveCurrentBookPlayback() {
|
|
82
|
+
const id = currentBookId();
|
|
83
|
+
if (id === null) return;
|
|
84
|
+
const book = bookById(id);
|
|
85
|
+
if (book === null) return;
|
|
86
|
+
const map = readBooksPlayback();
|
|
87
|
+
map[book.name] = {
|
|
88
|
+
from: bookFromRef, base: bookBaseTime,
|
|
89
|
+
pos: audio.currentTime || 0, total: bookTotal,
|
|
90
|
+
ts: Date.now(),
|
|
91
|
+
};
|
|
92
|
+
writeBooksPlayback(map);
|
|
93
|
+
}
|
|
94
|
+
// The most recently played novel (largest ts), used by refresh restore.
|
|
95
|
+
function latestBookPlayback() {
|
|
96
|
+
const map = readBooksPlayback();
|
|
97
|
+
let best = null, bestTs = -1;
|
|
98
|
+
for (const [name, e] of Object.entries(map)) {
|
|
99
|
+
if (e && typeof e.from === 'number' && e.ts > bestTs) { best = { name, ...e }; bestTs = e.ts; }
|
|
100
|
+
}
|
|
101
|
+
return best;
|
|
102
|
+
}
|
|
62
103
|
// Restore the playback-panel position ({x,y,h}) previously saved by dragging, if any.
|
|
63
104
|
function loadPanelPos() {
|
|
64
105
|
const raw = loadPref(PREF_PANEL_POS);
|
|
@@ -159,23 +200,28 @@ window.__ModuleLoader__.load({
|
|
|
159
200
|
if (typeof voice === 'string' && voice !== '') store.voice = voice;
|
|
160
201
|
} catch (e) {}
|
|
161
202
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}));
|
|
176
|
-
return;
|
|
203
|
+
// Migrate legacy single-book progress (pre-0.2.1) into the per-book map once.
|
|
204
|
+
try {
|
|
205
|
+
const legacy = loadPref(PREF_BOOK_PLAYBACK);
|
|
206
|
+
if (legacy) {
|
|
207
|
+
const p = JSON.parse(legacy);
|
|
208
|
+
if (p && typeof p.id === 'string' && typeof p.name === 'string' && p.name !== '') {
|
|
209
|
+
const map = readBooksPlayback();
|
|
210
|
+
if (!Object.prototype.hasOwnProperty.call(map, p.name)) {
|
|
211
|
+
map[p.name] = { from: p.from, base: p.base, pos: p.pos, total: p.total, ts: p.ts || Date.now() };
|
|
212
|
+
writeBooksPlayback(map);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
clearPref(PREF_BOOK_PLAYBACK);
|
|
177
216
|
}
|
|
178
|
-
|
|
217
|
+
} catch (e) {}
|
|
218
|
+
|
|
219
|
+
// Persist the current playback position. Music and novels are fully separate:
|
|
220
|
+
// music writes PREF_PLAYBACK, novels write into the per-book map — neither
|
|
221
|
+
// clears the other, so switching modes never loses progress.
|
|
222
|
+
function savePlayback() {
|
|
223
|
+
if (store.currentId === null) { clearPref(PREF_PLAYBACK); return; }
|
|
224
|
+
if (String(store.currentId).startsWith('book:')) { saveCurrentBookPlayback(); return; }
|
|
179
225
|
savePref(PREF_PLAYBACK, JSON.stringify({
|
|
180
226
|
id: store.currentId, name: store.currentName,
|
|
181
227
|
// While a restored track is still paused, the <audio> currentTime can
|
|
@@ -184,6 +230,7 @@ window.__ModuleLoader__.load({
|
|
|
184
230
|
// Persist the known duration too: on restore the browser may not have
|
|
185
231
|
// loaded the track's metadata yet, and we don't want a "0:00" readout.
|
|
186
232
|
duration: Number.isFinite(audio.duration) ? audio.duration : 0,
|
|
233
|
+
ts: Date.now(),
|
|
187
234
|
}));
|
|
188
235
|
}
|
|
189
236
|
function loadPlayback() {
|
|
@@ -274,15 +321,30 @@ window.__ModuleLoader__.load({
|
|
|
274
321
|
const smoothCur = new Float32Array(SMOOTH_BARS);
|
|
275
322
|
const smoothPeak = new Float32Array(SMOOTH_BARS);
|
|
276
323
|
const targetBuf = new Float32Array(SMOOTH_BARS);
|
|
277
|
-
// Accent color for the spectrum bars
|
|
278
|
-
// --dsh-music-accent
|
|
324
|
+
// Accent color for the spectrum bars. DSH defines its --dsw-alias-* theme
|
|
325
|
+
// tokens on <body> — never on :root — so --dsh-music-accent must be read
|
|
326
|
+
// from body (reading documentElement would always return the fallback and
|
|
327
|
+
// the bars would never follow the theme). The value is cached but the cache
|
|
328
|
+
// is invalidated whenever the theme changes at runtime: the ThemePresenter
|
|
329
|
+
// projects tokens + the dark attribute onto body, so a MutationObserver on
|
|
330
|
+
// body's style/dark-attribute keeps the bars tracking live brand changes.
|
|
279
331
|
let accentColor = null;
|
|
332
|
+
let accentObserver = null;
|
|
333
|
+
function readAccent() {
|
|
334
|
+
const el = document.body || document.documentElement;
|
|
335
|
+
return getComputedStyle(el).getPropertyValue('--dsh-music-accent').trim() || '#2f9e6e';
|
|
336
|
+
}
|
|
280
337
|
function currentAccent() {
|
|
281
|
-
if (accentColor === null)
|
|
282
|
-
accentColor = getComputedStyle(document.documentElement).getPropertyValue('--dsh-music-accent').trim() || '#2f9e6e';
|
|
283
|
-
}
|
|
338
|
+
if (accentColor === null) accentColor = readAccent();
|
|
284
339
|
return accentColor;
|
|
285
340
|
}
|
|
341
|
+
function watchAccent() {
|
|
342
|
+
if (accentObserver !== null) return accentObserver;
|
|
343
|
+
if (typeof MutationObserver === 'undefined') return null;
|
|
344
|
+
accentObserver = new MutationObserver(() => { accentColor = readAccent(); });
|
|
345
|
+
accentObserver.observe(document.body, { attributes: true, attributeFilter: ['style', 'data-ds-dark-theme'] });
|
|
346
|
+
return accentObserver;
|
|
347
|
+
}
|
|
286
348
|
function drawBars(canvas, useCaps) {
|
|
287
349
|
const c = canvas.getContext('2d');
|
|
288
350
|
const w = canvas.width; const h = canvas.height;
|
|
@@ -581,7 +643,25 @@ window.__ModuleLoader__.load({
|
|
|
581
643
|
}
|
|
582
644
|
}
|
|
583
645
|
// `from` lets the toc jump straight to a chapter's chunk index.
|
|
584
|
-
function playBook(id, from = 0) { unlockAutoplay(); bookTotal = -1; bookBufferedFrom = -1; bookBaseTime = 0; bookRestorePos = -1;
|
|
646
|
+
function playBook(id, from = 0) { unlockAutoplay(); bookTotal = -1; bookBufferedFrom = -1; bookBaseTime = 0; bookRestorePos = -1; playBookFrom(id, from, false); saveCurrentBookPlayback(); }
|
|
647
|
+
// Play a novel from its saved progress when available (e.g. switching to
|
|
648
|
+
// music and back), otherwise start fresh from the beginning. Explicit
|
|
649
|
+
// chapter jumps keep using playBook(id, fromChunk) and are unaffected.
|
|
650
|
+
function resumeOrPlayBook(id) {
|
|
651
|
+
const book = bookById(id);
|
|
652
|
+
if (book === null) return;
|
|
653
|
+
const entry = getBookPlayback(book.name);
|
|
654
|
+
if (entry === null) { playBook(id); return; }
|
|
655
|
+
// Seed chunk / cumulative clock / in-chunk position from this book's entry.
|
|
656
|
+
restoreBookPlayback(book.name);
|
|
657
|
+
if (String(store.currentId) === 'book:' + id) {
|
|
658
|
+
// Restore applied: play the saved chunk; onTime seeks to the in-chunk pos.
|
|
659
|
+
unlockAutoplay();
|
|
660
|
+
playBookFrom(id, bookFromRef, false);
|
|
661
|
+
} else {
|
|
662
|
+
playBook(id); // restore bailed (book gone) → start fresh
|
|
663
|
+
}
|
|
664
|
+
}
|
|
585
665
|
// When a chunk ends, switch to the next HTTP chunk (warmed by preAudio).
|
|
586
666
|
// The switch is silent: no buffering flash, and the book-wide clock keeps
|
|
587
667
|
// the completed chunk's duration so the readout never resets.
|
|
@@ -745,6 +825,10 @@ window.__ModuleLoader__.load({
|
|
|
745
825
|
});
|
|
746
826
|
}
|
|
747
827
|
function stop() {
|
|
828
|
+
// Capture the current novel before resetting, so stopping forgets only
|
|
829
|
+
// that one book's position (other novels keep their own progress).
|
|
830
|
+
const stoppedBook = (store.currentId !== null && String(store.currentId).startsWith('book:'))
|
|
831
|
+
? bookById(currentBookId()) : null;
|
|
748
832
|
envReqId++;
|
|
749
833
|
trackEnv = null;
|
|
750
834
|
restoredMusicPos = null;
|
|
@@ -754,7 +838,7 @@ window.__ModuleLoader__.load({
|
|
|
754
838
|
stopBookHelper();
|
|
755
839
|
set({ currentId: null, currentName: null, playing: false, position: 0, duration: 0, pendingId: null, pendingName: null, vizState: 'ok', bookBuffering: false, bookError: '', bookBufferingSince: 0, bookBufferingSilent: false, tocOpen: false, currentSection: '', voiceSwitching: false });
|
|
756
840
|
clearPref(PREF_PLAYBACK);
|
|
757
|
-
|
|
841
|
+
if (stoppedBook !== null) clearBookPlayback(stoppedBook.name);
|
|
758
842
|
releaseWakeLock();
|
|
759
843
|
}
|
|
760
844
|
|
|
@@ -923,37 +1007,41 @@ window.__ModuleLoader__.load({
|
|
|
923
1007
|
restoredMusicPos = null;
|
|
924
1008
|
}
|
|
925
1009
|
// Save the restored spot explicitly so it survives another refresh.
|
|
926
|
-
savePref(PREF_PLAYBACK, JSON.stringify({ id: track.id, name: track.name, position: pos, duration: savedDur }));
|
|
1010
|
+
savePref(PREF_PLAYBACK, JSON.stringify({ id: track.id, name: track.name, position: pos, duration: savedDur, ts: Date.now() }));
|
|
927
1011
|
}
|
|
928
1012
|
|
|
929
|
-
// Restore a novel's playback after a refresh
|
|
930
|
-
// cumulative clock, and same in-chunk position —
|
|
931
|
-
// matching how music is restored.
|
|
932
|
-
//
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
let
|
|
937
|
-
|
|
938
|
-
if (
|
|
939
|
-
|
|
1013
|
+
// Restore a novel's playback after a refresh (or when resuming a book):
|
|
1014
|
+
// same book, same chunk, same cumulative clock, and same in-chunk position —
|
|
1015
|
+
// paused (tap ▶ to resume), matching how music is restored. When targetName
|
|
1016
|
+
// is given, restore that book; otherwise restore the most recently played
|
|
1017
|
+
// novel from the per-book map.
|
|
1018
|
+
function restoreBookPlayback(targetName) {
|
|
1019
|
+
const map = readBooksPlayback();
|
|
1020
|
+
let name = targetName;
|
|
1021
|
+
let entry = null;
|
|
1022
|
+
if (typeof name === 'string' && name !== '') {
|
|
1023
|
+
entry = map[name] || null;
|
|
1024
|
+
} else {
|
|
1025
|
+
for (const [n, e] of Object.entries(map)) {
|
|
1026
|
+
if (e && typeof e.from === 'number' && (entry === null || e.ts > entry.ts)) { entry = e; name = n; }
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
if (entry === null || !name) return;
|
|
1030
|
+
const book = store.books.find((b) => b.name === name);
|
|
940
1031
|
if (book === undefined) return; // the book is no longer in the library
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const base = Number.isFinite(saved.base) ? saved.base : 0;
|
|
946
|
-
const pos = Number.isFinite(saved.pos) ? saved.pos : 0;
|
|
947
|
-
bookTotal = Number.isFinite(saved.total) ? saved.total : -1;
|
|
1032
|
+
const from = Number.isFinite(entry.from) && entry.from >= 0 ? entry.from : 0;
|
|
1033
|
+
const base = Number.isFinite(entry.base) ? entry.base : 0;
|
|
1034
|
+
const pos = Number.isFinite(entry.pos) ? entry.pos : 0;
|
|
1035
|
+
bookTotal = Number.isFinite(entry.total) ? entry.total : -1;
|
|
948
1036
|
bookFromRef = from;
|
|
949
1037
|
bookBaseTime = base;
|
|
950
|
-
const url = bookUrl(
|
|
1038
|
+
const url = bookUrl(book.id, from);
|
|
951
1039
|
if (url === null) return;
|
|
952
1040
|
// Mark the book as current. Like music restore, we do NOT touch the
|
|
953
1041
|
// <audio> element here (avoiding the Chromium 'getTopURL' quirk on
|
|
954
1042
|
// refresh); togglePlay loads + seeks the chunk when the user resumes.
|
|
955
1043
|
set({
|
|
956
|
-
currentId: 'book:' +
|
|
1044
|
+
currentId: 'book:' + book.id, currentName: book.name,
|
|
957
1045
|
position: base + pos, duration: base + (Number.isFinite(audio.duration) ? audio.duration : 0),
|
|
958
1046
|
pendingId: null, pendingName: null, error: null, playing: false,
|
|
959
1047
|
bookBuffering: false, bookBufferingSilent: false, bookError: '', bookBufferingSince: 0,
|
|
@@ -963,13 +1051,31 @@ window.__ModuleLoader__.load({
|
|
|
963
1051
|
// changed since the save); fall back to the saved total on failure.
|
|
964
1052
|
const savedTotal = bookTotal;
|
|
965
1053
|
bookTotal = -1;
|
|
966
|
-
void ensureBookTotal(
|
|
1054
|
+
void ensureBookTotal(book.id).then((total) => {
|
|
967
1055
|
if (!Number.isFinite(total) || total < 0) { bookTotal = savedTotal; return; }
|
|
968
1056
|
bookTotal = total;
|
|
969
|
-
if (from + 1 < total) preloadBook(
|
|
1057
|
+
if (from + 1 < total) preloadBook(book.id, from + 1);
|
|
970
1058
|
});
|
|
971
1059
|
}
|
|
972
1060
|
|
|
1061
|
+
// Restore whichever (music vs novel) was playing most recently — both now
|
|
1062
|
+
// persist independently, so a music interlude never wipes a novel's progress.
|
|
1063
|
+
function restoreLatest(list) {
|
|
1064
|
+
let musicTs = -1;
|
|
1065
|
+
try {
|
|
1066
|
+
const p = JSON.parse(loadPref(PREF_PLAYBACK) || 'null');
|
|
1067
|
+
if (p && typeof p.ts === 'number') musicTs = p.ts;
|
|
1068
|
+
} catch (e) {}
|
|
1069
|
+
const book = latestBookPlayback();
|
|
1070
|
+
const bookTs = book ? book.ts : -1;
|
|
1071
|
+
if (bookTs > musicTs) { restoreBookPlayback(); return; }
|
|
1072
|
+
if (musicTs > bookTs) { restorePlayback(list); return; }
|
|
1073
|
+
// tie / legacy data without timestamps: restore whatever exists (music
|
|
1074
|
+
// first, book last — legacy data only ever had one of the two populated).
|
|
1075
|
+
restorePlayback(list);
|
|
1076
|
+
restoreBookPlayback();
|
|
1077
|
+
}
|
|
1078
|
+
|
|
973
1079
|
// ---- host data ----
|
|
974
1080
|
async function loadTracks() {
|
|
975
1081
|
set({ loading: true });
|
|
@@ -994,8 +1100,7 @@ window.__ModuleLoader__.load({
|
|
|
994
1100
|
// Envelope (spectrum) decoding is deferred to actual playback — no need
|
|
995
1101
|
// to decode several full files eagerly at page load; the current track's
|
|
996
1102
|
// envelope decodes on play (startPlay / resume).
|
|
997
|
-
|
|
998
|
-
restoreBookPlayback();
|
|
1103
|
+
restoreLatest(list);
|
|
999
1104
|
} catch (err) {
|
|
1000
1105
|
set({ loading: false, error: '\u65e0\u6cd5\u8bfb\u53d6\u97f3\u4e50\u5e93\uff1a' + String((err && err.message) || err) });
|
|
1001
1106
|
}
|
|
@@ -1015,8 +1120,7 @@ window.__ModuleLoader__.load({
|
|
|
1015
1120
|
tracks: result.tracks || [], books: result.books || [],
|
|
1016
1121
|
count: result.count || 0, loading: false, error: null,
|
|
1017
1122
|
});
|
|
1018
|
-
|
|
1019
|
-
restoreBookPlayback();
|
|
1123
|
+
restoreLatest(result.tracks || []);
|
|
1020
1124
|
} else {
|
|
1021
1125
|
set({ loading: false, error: (result && result.error) || '\u8bbe\u7f6e\u76ee\u5f55\u5931\u8d25' });
|
|
1022
1126
|
}
|
|
@@ -1417,7 +1521,7 @@ window.__ModuleLoader__.load({
|
|
|
1417
1521
|
key: b.id,
|
|
1418
1522
|
className: 'dsh-music-track' + (active ? ' active' : ''),
|
|
1419
1523
|
title: b.url,
|
|
1420
|
-
onClick: () => { if (active) togglePlay(); else
|
|
1524
|
+
onClick: () => { if (active) togglePlay(); else resumeOrPlayBook(b.id); },
|
|
1421
1525
|
},
|
|
1422
1526
|
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '\u25b6 ' : '') + b.name),
|
|
1423
1527
|
React.createElement('span', { className: 'dsh-music-track-size' }, formatSize(b.size)),
|
|
@@ -1559,6 +1663,7 @@ window.__ModuleLoader__.load({
|
|
|
1559
1663
|
attachAudioElements();
|
|
1560
1664
|
const unbind = bindAudio();
|
|
1561
1665
|
startRaf();
|
|
1666
|
+
const accentWatch = watchAccent();
|
|
1562
1667
|
// Browsers auto-release a wake lock when the page is hidden; re-acquire
|
|
1563
1668
|
// on return if playback is still running, and drop it on hide so we
|
|
1564
1669
|
// don't hold it while the tab is backgrounded.
|
|
@@ -1578,6 +1683,8 @@ window.__ModuleLoader__.load({
|
|
|
1578
1683
|
return () => {
|
|
1579
1684
|
window.removeEventListener('pagehide', onPageHide);
|
|
1580
1685
|
document.removeEventListener('visibilitychange', onVis); stopRaf(); unbind(); closeDecodeCtx(); releaseWakeLock();
|
|
1686
|
+
if (accentWatch !== null) accentWatch.disconnect();
|
|
1687
|
+
accentObserver = null;
|
|
1581
1688
|
};
|
|
1582
1689
|
}, 'music-player: audio + viz engine');
|
|
1583
1690
|
|
|
@@ -1609,7 +1716,7 @@ window.__ModuleLoader__.load({
|
|
|
1609
1716
|
const book = bookById(intent.id);
|
|
1610
1717
|
if (book !== null) {
|
|
1611
1718
|
set({ pendingId: 'book:' + book.id, pendingName: intent.name || book.name, error: null });
|
|
1612
|
-
|
|
1719
|
+
resumeOrPlayBook(book.id);
|
|
1613
1720
|
return;
|
|
1614
1721
|
}
|
|
1615
1722
|
const track = trackById(intent.id);
|
|
@@ -1651,8 +1758,13 @@ window.__ModuleLoader__.load({
|
|
|
1651
1758
|
const PLAYER_CSS = '\n' +
|
|
1652
1759
|
// Accent follows the host app's theme brand color (stable from the start —
|
|
1653
1760
|
// no green-default-to-sampled-blue flash); green is only the fallback when
|
|
1654
|
-
// the app exposes no brand color.
|
|
1655
|
-
|
|
1761
|
+
// the app exposes no brand color. The alias must be declared on BODY, not
|
|
1762
|
+
// :root: DSH defines its --dsw-alias-* theme tokens on <body> only, and a
|
|
1763
|
+
// var() reference resolves against the element that declares it — on
|
|
1764
|
+
// :root (html) it cannot see body's tokens and would always fall back to
|
|
1765
|
+
// green. Declared on body, the reference resolves and children inherit
|
|
1766
|
+
// the theme's actual brand color.
|
|
1767
|
+
'body { --dsh-music-accent: var(--dsw-alias-brand-primary, #2f9e6e); }\n' +
|
|
1656
1768
|
'.dsh-music-bar-wrap { box-sizing: border-box; width: 100%; padding: 0 var(--dsh-composer-side-clearance, 16px); }\n' +
|
|
1657
1769
|
'.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' +
|
|
1658
1770
|
'.dsh-music-bar-idle { color: var(--dsw-alias-label-primary, #e6e6e6); font-weight: 500; display: inline-flex; align-items: center; }\n' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|