dsh-music-player 0.3.3 → 0.3.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/lib/client.js +113 -95
- package/lib/index.js +12 -9
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -19,9 +19,19 @@ window.__ModuleLoader__.load({
|
|
|
19
19
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
20
20
|
|
|
21
21
|
const React = require('react');
|
|
22
|
+
const ReactDOM = require('react-dom');
|
|
22
23
|
const useState = React.useState;
|
|
23
24
|
const useEffect = React.useEffect;
|
|
24
25
|
const useRef = React.useRef;
|
|
26
|
+
// Directory/file pickers are rendered into the panel DOM, but the panel's
|
|
27
|
+
// initial height is small (empty track list => only ~200px + 60px), which
|
|
28
|
+
// would clamp the picker and show just a few directory rows. Portal the
|
|
29
|
+
// overlay to <body> (position: fixed; inset: 0) so it spans the whole DSH
|
|
30
|
+
// window instead of the panel, regardless of the panel size.
|
|
31
|
+
const createPortal = (ReactDOM && typeof ReactDOM.createPortal === 'function')
|
|
32
|
+
? (node, container) => ReactDOM.createPortal(node, container)
|
|
33
|
+
: (node) => node; // defensive fallback (react-dom is always provided by DSH)
|
|
34
|
+
const portalToBody = (node) => createPortal(node, document.body);
|
|
25
35
|
|
|
26
36
|
// This host/environment throws a harmless, unhandled rejection from
|
|
27
37
|
// Chromium's media pipeline — "Cannot read properties of undefined (reading
|
|
@@ -619,7 +629,7 @@ window.__ModuleLoader__.load({
|
|
|
619
629
|
savePlayback();
|
|
620
630
|
const promise = audio.play();
|
|
621
631
|
if (promise !== undefined && typeof promise.catch === 'function') {
|
|
622
|
-
promise.catch(() => { set({ error: '
|
|
632
|
+
promise.catch(() => { set({ error: '浏览器拦截了自动播放,请点击一次播放按钮', pendingId: id, pendingName: track.name }); });
|
|
623
633
|
}
|
|
624
634
|
}
|
|
625
635
|
const bookById = (id) => (store.books || []).find((b) => b.id === id) || null;
|
|
@@ -706,9 +716,9 @@ window.__ModuleLoader__.load({
|
|
|
706
716
|
}
|
|
707
717
|
// Label a section type for display in the toc (chapter/分部/前言/后记/分节).
|
|
708
718
|
const sectionTypeLabel = (t) => ({
|
|
709
|
-
chapter: '
|
|
710
|
-
epilogue: '
|
|
711
|
-
})[t] || '
|
|
719
|
+
chapter: '章节', part: '分部', preface: '前言',
|
|
720
|
+
epilogue: '后记', named: '分节', toc: '目录',
|
|
721
|
+
})[t] || '正文';
|
|
712
722
|
// Heading of the section that contains the given chunk index.
|
|
713
723
|
function sectionForChunk(sections, chunk) {
|
|
714
724
|
if (!Array.isArray(sections) || sections.length === 0) return '';
|
|
@@ -893,7 +903,7 @@ window.__ModuleLoader__.load({
|
|
|
893
903
|
if (bookTotal >= 0 && bookFromRef + 1 < bookTotal) preloadBook(id, bookFromRef + 1);
|
|
894
904
|
}
|
|
895
905
|
const promise = audio.play();
|
|
896
|
-
if (promise !== undefined && typeof promise.catch === 'function') promise.catch(() => set({ error: '
|
|
906
|
+
if (promise !== undefined && typeof promise.catch === 'function') promise.catch(() => set({ error: '浏览器拦截了自动播放,请点击播放按钮' }));
|
|
897
907
|
// Envelope decoding is deferred to play (see loadTracks) — decode the
|
|
898
908
|
// restored track lazily so its spectrum is ready once it resumes.
|
|
899
909
|
if (!String(store.currentId).startsWith('book:')) {
|
|
@@ -997,7 +1007,7 @@ window.__ModuleLoader__.load({
|
|
|
997
1007
|
})
|
|
998
1008
|
.catch((err) => {
|
|
999
1009
|
if (mySeq !== voiceSwitchSeq) return;
|
|
1000
|
-
set({ voiceSwitching: false, voice: prevVoice, bookError: '
|
|
1010
|
+
set({ voiceSwitching: false, voice: prevVoice, bookError: '声音切换失败:' + String((err && err.message) || err) });
|
|
1001
1011
|
});
|
|
1002
1012
|
}
|
|
1003
1013
|
function stop() {
|
|
@@ -1121,7 +1131,7 @@ window.__ModuleLoader__.load({
|
|
|
1121
1131
|
if (store.mode === 'single' && store.currentId !== null) {
|
|
1122
1132
|
audio.currentTime = 0;
|
|
1123
1133
|
const promise = audio.play();
|
|
1124
|
-
if (promise !== undefined && typeof promise.catch === 'function') promise.catch(() => set({ error: '
|
|
1134
|
+
if (promise !== undefined && typeof promise.catch === 'function') promise.catch(() => set({ error: '播放失败', playing: false }));
|
|
1125
1135
|
return;
|
|
1126
1136
|
}
|
|
1127
1137
|
step(1);
|
|
@@ -1140,7 +1150,7 @@ window.__ModuleLoader__.load({
|
|
|
1140
1150
|
playBookFrom(retryId, bookFromRef, true);
|
|
1141
1151
|
return;
|
|
1142
1152
|
}
|
|
1143
|
-
set({ bookBuffering: false, bookBufferingSince: 0, playing: false, bookError: '
|
|
1153
|
+
set({ bookBuffering: false, bookBufferingSince: 0, playing: false, bookError: '讲书音频获取失败,请重试' });
|
|
1144
1154
|
// Best-effort: fetch the URL to show the server's actual diagnostic
|
|
1145
1155
|
// (e.g. "TTS 请求失败 401 ..."), which the <audio> error object lacks.
|
|
1146
1156
|
const id = String(store.currentId).slice('book:'.length);
|
|
@@ -1154,7 +1164,7 @@ window.__ModuleLoader__.load({
|
|
|
1154
1164
|
}).catch(() => {});
|
|
1155
1165
|
}
|
|
1156
1166
|
} else {
|
|
1157
|
-
set({ error: '
|
|
1167
|
+
set({ error: '音频加载或解码失败', playing: false });
|
|
1158
1168
|
}
|
|
1159
1169
|
};
|
|
1160
1170
|
audio.addEventListener('timeupdate', onTime);
|
|
@@ -1321,7 +1331,7 @@ window.__ModuleLoader__.load({
|
|
|
1321
1331
|
restoreScope(result.playlists || []);
|
|
1322
1332
|
restoreLatest(list);
|
|
1323
1333
|
} catch (err) {
|
|
1324
|
-
set({ loading: false, error: '
|
|
1334
|
+
set({ loading: false, error: '无法读取音乐库:' + String((err && err.message) || err) });
|
|
1325
1335
|
}
|
|
1326
1336
|
}
|
|
1327
1337
|
function saveRoot(path, kind) {
|
|
@@ -1341,10 +1351,10 @@ window.__ModuleLoader__.load({
|
|
|
1341
1351
|
});
|
|
1342
1352
|
restoreLatest(result.tracks || []);
|
|
1343
1353
|
} else {
|
|
1344
|
-
set({ loading: false, error: (result && result.error) || '
|
|
1354
|
+
set({ loading: false, error: (result && result.error) || '设置目录失败' });
|
|
1345
1355
|
}
|
|
1346
1356
|
}).catch((err) => {
|
|
1347
|
-
set({ loading: false, error: '
|
|
1357
|
+
set({ loading: false, error: '设置目录失败:' + String((err && err.message) || err) });
|
|
1348
1358
|
});
|
|
1349
1359
|
}
|
|
1350
1360
|
|
|
@@ -1419,7 +1429,7 @@ window.__ModuleLoader__.load({
|
|
|
1419
1429
|
return React.createElement('div',
|
|
1420
1430
|
{ className: 'dsh-music-vol-slider', ref: trackRef,
|
|
1421
1431
|
onPointerDown, onPointerMove, onPointerUp,
|
|
1422
|
-
title: '
|
|
1432
|
+
title: '音量 ' + pct + '%' },
|
|
1423
1433
|
React.createElement('div', { className: 'dsh-music-vol-track' }),
|
|
1424
1434
|
React.createElement('div', { className: 'dsh-music-vol-fill', style: { height: pct + '%' } }),
|
|
1425
1435
|
React.createElement('div', { className: 'dsh-music-vol-thumb', style: { bottom: 'calc(' + pct + '% - 7px)' } }),
|
|
@@ -1427,30 +1437,30 @@ window.__ModuleLoader__.load({
|
|
|
1427
1437
|
}
|
|
1428
1438
|
// Fallback voice list if /manifest hasn't delivered one (older host / offline).
|
|
1429
1439
|
const FALLBACK_VOICES = [
|
|
1430
|
-
{ id: '
|
|
1431
|
-
{ id: '
|
|
1432
|
-
{ id: '
|
|
1433
|
-
{ id: '
|
|
1440
|
+
{ id: '冰糖', label: '冰糖', gender: '女', lang: '中文' },
|
|
1441
|
+
{ id: '茉莉', label: '茉莉', gender: '女', lang: '中文' },
|
|
1442
|
+
{ id: '苏打', label: '苏打', gender: '男', lang: '中文' },
|
|
1443
|
+
{ id: '白桦', label: '白桦', gender: '男', lang: '中文' },
|
|
1434
1444
|
];
|
|
1435
1445
|
// AI 讲书 voice picker, shown in the volume popup only while reading a book.
|
|
1436
1446
|
function VoicePicker() {
|
|
1437
1447
|
const s = useStore();
|
|
1438
1448
|
const voices = (s.voices && s.voices.length > 0) ? s.voices : FALLBACK_VOICES;
|
|
1439
1449
|
const cur = voices.find((v) => v.id === s.voice);
|
|
1440
|
-
const currentLabel = cur ? (cur.label + (cur.gender && cur.gender !== '
|
|
1450
|
+
const currentLabel = cur ? (cur.label + (cur.gender && cur.gender !== '自动' ? '(' + cur.gender + ')' : '')) : s.voice;
|
|
1441
1451
|
return React.createElement('div', { className: 'dsh-music-voice' },
|
|
1442
|
-
React.createElement('span', { className: 'dsh-music-voice-label' }, 'AI
|
|
1452
|
+
React.createElement('span', { className: 'dsh-music-voice-label' }, 'AI 声音'),
|
|
1443
1453
|
React.createElement('select', {
|
|
1444
1454
|
className: 'dsh-music-voice-select',
|
|
1445
|
-
value: voices.some((v) => v.id === s.voice) ? s.voice : '
|
|
1446
|
-
title: '
|
|
1455
|
+
value: voices.some((v) => v.id === s.voice) ? s.voice : '白桦',
|
|
1456
|
+
title: '当前:' + currentLabel,
|
|
1447
1457
|
onChange: (e) => setVoice(e.target.value),
|
|
1448
1458
|
},
|
|
1449
1459
|
voices.map((v) => React.createElement('option', {
|
|
1450
1460
|
key: v.id, value: v.id,
|
|
1451
|
-
}, (v.label || v.id) + (v.lang ? '
|
|
1461
|
+
}, (v.label || v.id) + (v.lang ? '·' + v.lang : '') + (v.gender && v.gender !== '自动' ? '(' + v.gender + ')' : ''))),
|
|
1452
1462
|
),
|
|
1453
|
-
s.voiceSwitching ? React.createElement('span', { className: 'dsh-music-voice-switching' }, '
|
|
1463
|
+
s.voiceSwitching ? React.createElement('span', { className: 'dsh-music-voice-switching' }, '切换中…') : null,
|
|
1454
1464
|
);
|
|
1455
1465
|
}
|
|
1456
1466
|
// Novel status shown after the title on the now-playing bar: a live
|
|
@@ -1470,16 +1480,16 @@ window.__ModuleLoader__.load({
|
|
|
1470
1480
|
const secs = s.bookBufferingSince > 0 ? Math.floor((now - s.bookBufferingSince) / 1000) : 0;
|
|
1471
1481
|
return React.createElement('span', { className: 'dsh-music-bar-buffering' },
|
|
1472
1482
|
React.createElement('span', { className: 'dsh-music-spinner' }),
|
|
1473
|
-
' AI
|
|
1483
|
+
' AI 合成中… ' + secs + 's');
|
|
1474
1484
|
}
|
|
1475
1485
|
if (s.bookError) {
|
|
1476
1486
|
return React.createElement('span', { className: 'dsh-music-bar-berr', title: s.bookError },
|
|
1477
1487
|
React.createElement('span', { className: 'dsh-music-bar-berr-text' }, s.bookError),
|
|
1478
1488
|
React.createElement('button', {
|
|
1479
1489
|
className: 'dsh-music-bar-btn retry',
|
|
1480
|
-
title: '
|
|
1490
|
+
title: '重新合成当前段落',
|
|
1481
1491
|
onClick: retryBook,
|
|
1482
|
-
}, '
|
|
1492
|
+
}, '重试'));
|
|
1483
1493
|
}
|
|
1484
1494
|
return null;
|
|
1485
1495
|
}
|
|
@@ -1501,9 +1511,9 @@ window.__ModuleLoader__.load({
|
|
|
1501
1511
|
if (hasTrack && s.vizState === 'unavailable') {
|
|
1502
1512
|
vizBadge = React.createElement('button', {
|
|
1503
1513
|
className: 'dsh-music-bar-warn',
|
|
1504
|
-
title: '
|
|
1514
|
+
title: '频谱不可用,点击重试',
|
|
1505
1515
|
onClick: () => { const t = resolvePlayable(s.currentId); if (t !== null) loadEnvelope(t.id, t.url); },
|
|
1506
|
-
}, '
|
|
1516
|
+
}, '频谱不可用,点击重试');
|
|
1507
1517
|
}
|
|
1508
1518
|
const isBook = s.currentId !== null && String(s.currentId).startsWith('book:');
|
|
1509
1519
|
// 名称前的图标:讲书用话筒图标,音乐用音符图标(空闲态无曲目 = 音乐)。
|
|
@@ -1517,7 +1527,7 @@ window.__ModuleLoader__.load({
|
|
|
1517
1527
|
let sectionBadge = null;
|
|
1518
1528
|
if (isBook && s.currentSection) {
|
|
1519
1529
|
sectionBadge = React.createElement('span', { className: 'dsh-music-bar-section', title: s.currentSection },
|
|
1520
|
-
'
|
|
1530
|
+
'▸ ' + s.currentSection);
|
|
1521
1531
|
}
|
|
1522
1532
|
// 自建歌单:收藏爱心按钮(收藏时用主题色)。
|
|
1523
1533
|
const faved = hasTrack && !isBook && isCurrentFaved();
|
|
@@ -1533,28 +1543,28 @@ window.__ModuleLoader__.load({
|
|
|
1533
1543
|
React.createElement('div', { className: 'dsh-music-bar' + (isBook ? ' book' : '') },
|
|
1534
1544
|
hasTrack
|
|
1535
1545
|
? React.createElement('span', { className: 'dsh-music-bar-name', title: name }, note, ' ', name, afterName)
|
|
1536
|
-
: React.createElement('span', { className: 'dsh-music-bar-idle' }, note, '
|
|
1546
|
+
: React.createElement('span', { className: 'dsh-music-bar-idle' }, note, ' 本地音乐播放器'),
|
|
1537
1547
|
// 章节名独立占一整行、完整显示(不再被省略号截断)。
|
|
1538
1548
|
sectionBadge,
|
|
1539
1549
|
!isBook && hasTrack && s.playing ? React.createElement('canvas', { className: 'dsh-music-viz', width: 64, height: 14, ref: (el) => { barCanvasNode = el; } }) : null,
|
|
1540
1550
|
vizBadge,
|
|
1541
1551
|
hasTrack
|
|
1542
1552
|
? (showHint
|
|
1543
|
-
? React.createElement('span', { className: 'dsh-music-bar-hint' }, '
|
|
1553
|
+
? React.createElement('span', { className: 'dsh-music-bar-hint' }, '⚠ 自动播放被拦截,点击▶解锁')
|
|
1544
1554
|
: React.createElement('span', { className: 'dsh-music-bar-time' }, fmtTime(s.position) + ' / ' + fmtTime(s.duration)))
|
|
1545
1555
|
: null,
|
|
1546
1556
|
heartBtn,
|
|
1547
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '
|
|
1548
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '
|
|
1549
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '
|
|
1550
|
-
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '
|
|
1557
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '上一章' : '上一首', onClick: () => (isBook ? stepBook(-1) : step(-1)) }, '⏮') : null,
|
|
1558
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '播放/暂停', onClick: togglePlay }, s.playing ? '⏸' : '▶') : null,
|
|
1559
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '下一章' : '下一首', onClick: () => (isBook ? stepBook(1) : step(1)) }, '⏭') : null,
|
|
1560
|
+
hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '停止', onClick: stop }, '⏹') : null,
|
|
1551
1561
|
// 章节目录按钮:仅讲书(book)时出现,点击弹出章节列表并可跳章。
|
|
1552
1562
|
// 与音量/播放模式按钮同款圆形样式(dsh-music-mode-trigger);弹层用
|
|
1553
1563
|
// 与音量/播放模式同款的「相对容器 + 绝对定位」锚在按钮正上方。
|
|
1554
1564
|
isBook ? React.createElement('div', { className: 'dsh-music-toc-trigger' },
|
|
1555
1565
|
React.createElement('button', {
|
|
1556
1566
|
className: 'dsh-music-mode-trigger' + (s.tocOpen ? ' active' : ''),
|
|
1557
|
-
title: '
|
|
1567
|
+
title: '章节目录',
|
|
1558
1568
|
onClick: openToc,
|
|
1559
1569
|
}, React.createElement('svg', {
|
|
1560
1570
|
viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true,
|
|
@@ -1566,7 +1576,7 @@ window.__ModuleLoader__.load({
|
|
|
1566
1576
|
React.createElement('div', { className: 'dsh-music-bar-vol', ref: volRef },
|
|
1567
1577
|
React.createElement('button', {
|
|
1568
1578
|
className: 'dsh-music-mode-trigger' + (volOpen ? ' active' : ''),
|
|
1569
|
-
title: '
|
|
1579
|
+
title: '音量',
|
|
1570
1580
|
onClick: () => setVolOpen((o) => !o),
|
|
1571
1581
|
}, React.createElement('svg', {
|
|
1572
1582
|
viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true,
|
|
@@ -1578,7 +1588,7 @@ window.__ModuleLoader__.load({
|
|
|
1578
1588
|
),
|
|
1579
1589
|
React.createElement('button', {
|
|
1580
1590
|
className: panelCls,
|
|
1581
|
-
title: s.panelOpen ? '
|
|
1591
|
+
title: s.panelOpen ? '关闭播放列表' : '打开播放列表',
|
|
1582
1592
|
onClick: togglePanel,
|
|
1583
1593
|
}, React.createElement('svg', {
|
|
1584
1594
|
viewBox: '0 0 24 24', width: 16, height: 16, fill: 'currentColor', 'aria-hidden': true,
|
|
@@ -1592,9 +1602,9 @@ window.__ModuleLoader__.load({
|
|
|
1592
1602
|
// with currentColor so they match the accent of the other round transport
|
|
1593
1603
|
// buttons (green), which a native <select> cannot color.
|
|
1594
1604
|
const MODES = [
|
|
1595
|
-
{ id: 'single', label: '
|
|
1596
|
-
{ id: 'order', label: '
|
|
1597
|
-
{ id: 'shuffle', label: '
|
|
1605
|
+
{ id: 'single', label: '单曲循环', title: '单曲循环:播放结束重复当前曲目', d: 'M7 7h10v3l4-4-4-4v3H5v6h2V7zm10 10H7v-3l-4 4 4 4v-3h12v-6h-2v4z' },
|
|
1606
|
+
{ id: 'order', label: '顺序播放', title: '顺序播放:自动播放列表中的下一首', 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' },
|
|
1607
|
+
{ id: 'shuffle', label: '乱序播放', title: '乱序播放:随机挑选下一首', 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' },
|
|
1598
1608
|
];
|
|
1599
1609
|
function ModeIcon(props) {
|
|
1600
1610
|
return React.createElement('svg', {
|
|
@@ -1676,14 +1686,14 @@ window.__ModuleLoader__.load({
|
|
|
1676
1686
|
});
|
|
1677
1687
|
const body = (s.bookToc || []).length > 0
|
|
1678
1688
|
? rows
|
|
1679
|
-
: React.createElement('div', { className: 'dsh-music-empty' }, '
|
|
1689
|
+
: React.createElement('div', { className: 'dsh-music-empty' }, '暂无章节结构(该书无法识别分节。)');
|
|
1680
1690
|
// 弹层用 CSS 定位在「章节目录」按钮正上方(与音量/播放模式弹窗同款:
|
|
1681
1691
|
// 相对容器 + position:absolute + bottom:calc(100%+6px) 居中),见
|
|
1682
1692
|
// .dsh-music-toc-trigger / .dsh-music-toc 样式。
|
|
1683
1693
|
return React.createElement('div', { className: 'dsh-music-toc', ref },
|
|
1684
1694
|
React.createElement('div', { className: 'dsh-music-toc-head' },
|
|
1685
|
-
React.createElement('span', { className: 'dsh-music-toc-title' }, '
|
|
1686
|
-
React.createElement('button', { className: 'dsh-music-icon-btn', title: '
|
|
1695
|
+
React.createElement('span', { className: 'dsh-music-toc-title' }, '章节目录'),
|
|
1696
|
+
React.createElement('button', { className: 'dsh-music-icon-btn', title: '关闭', onClick: closeToc }, '✕')),
|
|
1687
1697
|
React.createElement('div', { className: 'dsh-music-toc-list', ref: listRef }, body),
|
|
1688
1698
|
);
|
|
1689
1699
|
}
|
|
@@ -1693,7 +1703,7 @@ window.__ModuleLoader__.load({
|
|
|
1693
1703
|
const listRef = useRef(null);
|
|
1694
1704
|
const panelRef = useRef(null);
|
|
1695
1705
|
// Draggable panel position + size ({x, y, w, h} left/top/width/height once
|
|
1696
|
-
// dragged or resized; null = default
|
|
1706
|
+
// dragged or resized; null = CSS default: centered, 380px, auto height).
|
|
1697
1707
|
const [pos, setPos] = useState(loadPanelPos);
|
|
1698
1708
|
const dragRef = useRef(null); // head-drag state
|
|
1699
1709
|
const resizeRef = useRef(null); // corner-resize state
|
|
@@ -1704,11 +1714,13 @@ window.__ModuleLoader__.load({
|
|
|
1704
1714
|
setAddMenu({ track, x: r.right, y: r.top });
|
|
1705
1715
|
};
|
|
1706
1716
|
|
|
1707
|
-
// Once the panel is dragged/resized we switch from CSS
|
|
1708
|
-
// to explicit left/top/width/height.
|
|
1709
|
-
//
|
|
1710
|
-
//
|
|
1711
|
-
|
|
1717
|
+
// Once the panel is dragged/resized we switch from CSS centering
|
|
1718
|
+
// (left:50%; top:50%; translate(-50%,-50%)) to explicit left/top/width/height.
|
|
1719
|
+
// Locking height, clearing max-height and nulling the CSS translate matters:
|
|
1720
|
+
// with only top+left and the CSS max-height:72vh still applying, a fixed
|
|
1721
|
+
// element whose CSS also sets the translate would collapse/clamp and shift
|
|
1722
|
+
// by half its own size while dragging.
|
|
1723
|
+
const style = pos === null ? null : { left: pos.x, top: pos.y, width: pos.w, height: pos.h, maxHeight: 'none', transform: 'none' };
|
|
1712
1724
|
|
|
1713
1725
|
const onHeadDown = (e) => {
|
|
1714
1726
|
if (e.button !== undefined && e.button !== 0) return;
|
|
@@ -1794,8 +1806,14 @@ window.__ModuleLoader__.load({
|
|
|
1794
1806
|
if (!s.panelOpen) return;
|
|
1795
1807
|
// Close the playlist panel when the user clicks outside it
|
|
1796
1808
|
// (mousedown precedes the toggle's click, so both stay consistent).
|
|
1809
|
+
// The directory/file pickers are portaled to <body>, so a click inside
|
|
1810
|
+
// them is technically outside the panel's DOM — treat those as "inside"
|
|
1811
|
+
// so interacting with the picker never closes the panel underneath.
|
|
1797
1812
|
const onDown = (e) => {
|
|
1798
|
-
if (panelRef.current !== null && !panelRef.current.contains(e.target)
|
|
1813
|
+
if (panelRef.current !== null && !panelRef.current.contains(e.target)
|
|
1814
|
+
&& !(e.target.closest && e.target.closest('.dsh-music-picker-overlay'))) {
|
|
1815
|
+
set({ panelOpen: false });
|
|
1816
|
+
}
|
|
1799
1817
|
};
|
|
1800
1818
|
document.addEventListener('mousedown', onDown);
|
|
1801
1819
|
return () => document.removeEventListener('mousedown', onDown);
|
|
@@ -1819,14 +1837,14 @@ window.__ModuleLoader__.load({
|
|
|
1819
1837
|
title: t.url,
|
|
1820
1838
|
onClick: () => { if (active) togglePlay(); else startPlayFrom(t.id, 'library'); },
|
|
1821
1839
|
},
|
|
1822
|
-
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '
|
|
1840
|
+
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '▶ ' : '') + t.name),
|
|
1823
1841
|
React.createElement('span', { className: 'dsh-music-track-size' }, formatSize(t.size)),
|
|
1824
1842
|
),
|
|
1825
1843
|
React.createElement('button', {
|
|
1826
1844
|
className: 'dsh-music-playlist-mini add',
|
|
1827
1845
|
title: '加入歌单',
|
|
1828
1846
|
onClick: (e) => { e.stopPropagation(); openAddMenu(t, e); },
|
|
1829
|
-
}, '
|
|
1847
|
+
}, '+'),
|
|
1830
1848
|
);
|
|
1831
1849
|
});
|
|
1832
1850
|
const bookRows = s.books.map((b) => {
|
|
@@ -1838,7 +1856,7 @@ window.__ModuleLoader__.load({
|
|
|
1838
1856
|
title: b.url,
|
|
1839
1857
|
onClick: () => { if (active) togglePlay(); else resumeOrPlayBook(b.id); },
|
|
1840
1858
|
},
|
|
1841
|
-
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '
|
|
1859
|
+
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '▶ ' : '') + b.name),
|
|
1842
1860
|
React.createElement('span', { className: 'dsh-music-track-size' }, formatSize(b.size)),
|
|
1843
1861
|
);
|
|
1844
1862
|
});
|
|
@@ -1854,11 +1872,11 @@ window.__ModuleLoader__.load({
|
|
|
1854
1872
|
onClick: () => set({ subTab: key }),
|
|
1855
1873
|
}, label);
|
|
1856
1874
|
const musicSubTabs = React.createElement('div', { className: 'dsh-music-subtabs' },
|
|
1857
|
-
subTabBtn('library', '
|
|
1858
|
-
subTabBtn(FAV_PLAYLIST_ID, '
|
|
1875
|
+
subTabBtn('library', '曲库'),
|
|
1876
|
+
subTabBtn(FAV_PLAYLIST_ID, '♥ 我最喜欢'),
|
|
1859
1877
|
// 自建歌单排在 + 号之前;+ 固定在末尾用于新建。
|
|
1860
1878
|
(s.playlists || []).filter((p) => p.id !== FAV_PLAYLIST_ID).map((p) => subTabBtn(p.id, p.name, null, p.id)),
|
|
1861
|
-
React.createElement('button', { className: 'dsh-music-subtab add', title: '新建歌单', onClick: onCreatePlaylist }, '
|
|
1879
|
+
React.createElement('button', { className: 'dsh-music-subtab add', title: '新建歌单', onClick: onCreatePlaylist }, '+'),
|
|
1862
1880
|
);
|
|
1863
1881
|
const isPlaylistView = s.subTab !== 'library';
|
|
1864
1882
|
const plView = isPlaylistView ? playlistById(s.subTab) : null;
|
|
@@ -1866,29 +1884,29 @@ window.__ModuleLoader__.load({
|
|
|
1866
1884
|
? React.createElement(PlaylistDetail, { pl: plView })
|
|
1867
1885
|
: (rows.length > 0
|
|
1868
1886
|
? rows
|
|
1869
|
-
: React.createElement('div', { className: 'dsh-music-empty' }, '
|
|
1887
|
+
: React.createElement('div', { className: 'dsh-music-empty' }, '暂无音乐。点击上方“选择音乐目录”并选择目录后自动扫描。'));
|
|
1870
1888
|
const listBody = s.tab === 'music'
|
|
1871
1889
|
? musicBody
|
|
1872
1890
|
: (s.books.length > 0
|
|
1873
1891
|
? bookRows
|
|
1874
1892
|
: (s.ttsConfigured
|
|
1875
|
-
? React.createElement('div', { className: 'dsh-music-empty' }, '
|
|
1876
|
-
: React.createElement('div', { className: 'dsh-music-error' }, s.ttsReason || '
|
|
1893
|
+
? React.createElement('div', { className: 'dsh-music-empty' }, '未发现 .txt 小说文件。')
|
|
1894
|
+
: React.createElement('div', { className: 'dsh-music-error' }, s.ttsReason || '未配置xiaomi提供方。')));
|
|
1877
1895
|
return React.createElement('div', { className: 'dsh-music-panel', ref: panelRef, style },
|
|
1878
1896
|
React.createElement('div', {
|
|
1879
1897
|
className: 'dsh-music-panel-head dsh-music-panel-drag',
|
|
1880
1898
|
onPointerDown: onHeadDown, onPointerMove: onHeadMove, onPointerUp: onHeadUp,
|
|
1881
1899
|
},
|
|
1882
|
-
React.createElement('span', { className: 'dsh-music-panel-grip', 'aria-hidden': true }, '
|
|
1883
|
-
React.createElement('span', { className: 'dsh-music-panel-title' }, '
|
|
1884
|
-
React.createElement('button', { className: 'dsh-music-icon-btn', title: '
|
|
1885
|
-
React.createElement('div', { className: 'dsh-music-tabs' }, tabBtn('music', '
|
|
1900
|
+
React.createElement('span', { className: 'dsh-music-panel-grip', 'aria-hidden': true }, '⠿'),
|
|
1901
|
+
React.createElement('span', { className: 'dsh-music-panel-title' }, '播放列表'),
|
|
1902
|
+
React.createElement('button', { className: 'dsh-music-icon-btn', title: '关闭', onClick: () => set({ panelOpen: false }) }, '✕')),
|
|
1903
|
+
React.createElement('div', { className: 'dsh-music-tabs' }, tabBtn('music', '音乐'), tabBtn('book', '小说')),
|
|
1886
1904
|
React.createElement(DirectorySetting, null),
|
|
1887
1905
|
s.tab === 'music' ? musicSubTabs : null,
|
|
1888
1906
|
// While a novel is playing, keep music-only errors/scanning out of the
|
|
1889
1907
|
// panel (novel status shows on the playback bar instead).
|
|
1890
1908
|
!isBook && s.error ? React.createElement('div', { className: 'dsh-music-error' }, s.error) : null,
|
|
1891
|
-
!isBook && s.loading ? React.createElement('div', { className: 'dsh-music-loading' }, '
|
|
1909
|
+
!isBook && s.loading ? React.createElement('div', { className: 'dsh-music-loading' }, '扫描中…') : null,
|
|
1892
1910
|
React.createElement('div', { className: 'dsh-music-list', style: pos === null ? null : { maxHeight: 'none' }, ref: (el) => { listRef.current = el; } }, listBody),
|
|
1893
1911
|
React.createElement('div', { className: 'dsh-music-resize', title: '拖动调整面板大小', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp }),
|
|
1894
1912
|
addMenu ? React.createElement(AddToPlaylistMenu, {
|
|
@@ -1909,23 +1927,23 @@ window.__ModuleLoader__.load({
|
|
|
1909
1927
|
const [dirError, setDirError] = useState(null);
|
|
1910
1928
|
const isBook = s.tab === 'book';
|
|
1911
1929
|
const activeRoot = isBook ? s.bookRoot : s.root;
|
|
1912
|
-
const pickerTitle = isBook ? '
|
|
1930
|
+
const pickerTitle = isBook ? '选择小说目录' : '选择音乐目录';
|
|
1913
1931
|
const hint = isBook
|
|
1914
|
-
? '
|
|
1915
|
-
: '
|
|
1932
|
+
? '支持 .txt 文件,AI语音目前仅支持xiaomi提供方(限时免费),请在设置中配置好再使用此功能。'
|
|
1933
|
+
: '支持 mp3 / m4a / flac / wav / ogg / opus / aac / webm 等格式,自动递归扫描子目录。';
|
|
1916
1934
|
return React.createElement('div', { className: 'dsh-music-settings' },
|
|
1917
1935
|
React.createElement('div', { className: 'dsh-music-settings-row' },
|
|
1918
1936
|
React.createElement('span', { className: 'dsh-music-settings-cur', title: activeRoot || '' },
|
|
1919
|
-
'
|
|
1937
|
+
'📁 ' + (activeRoot || '未配置')),
|
|
1920
1938
|
React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => openPicker() }, pickerTitle)),
|
|
1921
1939
|
s.error ? React.createElement('p', { className: 'dsh-music-error' }, s.error) : null,
|
|
1922
1940
|
React.createElement('p', { className: 'dsh-music-hint' }, hint),
|
|
1923
|
-
pickerOpen ? React.createElement('div', { className: 'dsh-music-picker-overlay' },
|
|
1941
|
+
pickerOpen ? portalToBody(React.createElement('div', { className: 'dsh-music-picker-overlay' },
|
|
1924
1942
|
React.createElement('div', { className: 'dsh-music-picker' },
|
|
1925
1943
|
React.createElement('div', { className: 'dsh-music-picker-head' },
|
|
1926
1944
|
React.createElement('span', { className: 'dsh-music-picker-title' }, pickerTitle)),
|
|
1927
1945
|
React.createElement('div', { className: 'dsh-music-picker-cur', title: curPath },
|
|
1928
|
-
curName || curPath || '
|
|
1946
|
+
curName || curPath || '家目录'),
|
|
1929
1947
|
React.createElement('div', { className: 'dsh-music-picker-list' },
|
|
1930
1948
|
dirs.length > 0
|
|
1931
1949
|
? dirs.map((d) => React.createElement('button', {
|
|
@@ -1933,17 +1951,17 @@ window.__ModuleLoader__.load({
|
|
|
1933
1951
|
className: 'dsh-music-picker-item',
|
|
1934
1952
|
title: d.path,
|
|
1935
1953
|
onClick: () => browse(d.path),
|
|
1936
|
-
}, '
|
|
1937
|
-
: React.createElement('div', { className: 'dsh-music-picker-empty' }, '
|
|
1954
|
+
}, '📁 ' + d.name))
|
|
1955
|
+
: React.createElement('div', { className: 'dsh-music-picker-empty' }, '本目录下无子目录,可直接选择。'),
|
|
1938
1956
|
dirError ? React.createElement('div', { className: 'dsh-music-error' }, dirError) : null,
|
|
1939
1957
|
),
|
|
1940
1958
|
React.createElement('div', { className: 'dsh-music-picker-foot' },
|
|
1941
|
-
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => goUp() }, '
|
|
1942
|
-
React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => pickCurrent() }, '
|
|
1943
|
-
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => setPickerOpen(false) }, '
|
|
1959
|
+
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => goUp() }, '返回上级'),
|
|
1960
|
+
React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => pickCurrent() }, '选择此目录'),
|
|
1961
|
+
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => setPickerOpen(false) }, '取消'),
|
|
1944
1962
|
),
|
|
1945
1963
|
),
|
|
1946
|
-
) : null,
|
|
1964
|
+
)) : null,
|
|
1947
1965
|
);
|
|
1948
1966
|
function openPicker() {
|
|
1949
1967
|
setPickerOpen(true);
|
|
@@ -2026,8 +2044,8 @@ window.__ModuleLoader__.load({
|
|
|
2026
2044
|
className: 'dsh-music-add-pop-item',
|
|
2027
2045
|
title: '加入「' + p.name + '」',
|
|
2028
2046
|
onClick: () => addTo(p.id),
|
|
2029
|
-
}, (p.id === FAV_PLAYLIST_ID ? '
|
|
2030
|
-
React.createElement('button', { className: 'dsh-music-add-pop-item new', onClick: addNew }, '
|
|
2047
|
+
}, (p.id === FAV_PLAYLIST_ID ? '♥ ' : '') + p.name + '(' + p.count + ')')) : null,
|
|
2048
|
+
React.createElement('button', { className: 'dsh-music-add-pop-item new', onClick: addNew }, '+ 新建歌单'),
|
|
2031
2049
|
);
|
|
2032
2050
|
}
|
|
2033
2051
|
// 歌单详情:添加歌曲 + 重命名/删除 + 歌曲列表(移除/上移/下移)。
|
|
@@ -2042,17 +2060,17 @@ window.__ModuleLoader__.load({
|
|
|
2042
2060
|
title: t.url,
|
|
2043
2061
|
onClick: () => { if (active) togglePlay(); else startPlayFrom(t.id, 'playlist', pl.id); },
|
|
2044
2062
|
},
|
|
2045
|
-
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '
|
|
2063
|
+
React.createElement('span', { className: 'dsh-music-track-name' }, (playing ? '▶ ' : '') + (idx + 1) + '. ' + t.name),
|
|
2046
2064
|
React.createElement('span', { className: 'dsh-music-track-size' }, formatSize(t.size)),
|
|
2047
2065
|
),
|
|
2048
|
-
React.createElement('button', { className: 'dsh-music-playlist-mini', title: '上移', onClick: (e) => { e.stopPropagation(); movePlaylistTrack(pl, t.path, -1); } }, '
|
|
2049
|
-
React.createElement('button', { className: 'dsh-music-playlist-mini', title: '下移', onClick: (e) => { e.stopPropagation(); movePlaylistTrack(pl, t.path, 1); } }, '
|
|
2050
|
-
React.createElement('button', { className: 'dsh-music-playlist-mini del', title: '从歌单移除', onClick: (e) => { e.stopPropagation(); apiPlaylistRemove(pl.id, [t.path]); } }, '
|
|
2066
|
+
React.createElement('button', { className: 'dsh-music-playlist-mini', title: '上移', onClick: (e) => { e.stopPropagation(); movePlaylistTrack(pl, t.path, -1); } }, '↑'),
|
|
2067
|
+
React.createElement('button', { className: 'dsh-music-playlist-mini', title: '下移', onClick: (e) => { e.stopPropagation(); movePlaylistTrack(pl, t.path, 1); } }, '↓'),
|
|
2068
|
+
React.createElement('button', { className: 'dsh-music-playlist-mini del', title: '从歌单移除', onClick: (e) => { e.stopPropagation(); apiPlaylistRemove(pl.id, [t.path]); } }, '×'),
|
|
2051
2069
|
);
|
|
2052
2070
|
});
|
|
2053
2071
|
return React.createElement('div', { className: 'dsh-music-playlist' },
|
|
2054
2072
|
React.createElement('div', { className: 'dsh-music-playlist-head' },
|
|
2055
|
-
React.createElement('button', { className: 'dsh-music-playlist-btn', onClick: () => setPickerOpen(true) }, '
|
|
2073
|
+
React.createElement('button', { className: 'dsh-music-playlist-btn', onClick: () => setPickerOpen(true) }, '+ 添加歌曲'),
|
|
2056
2074
|
React.createElement('button', { className: 'dsh-music-playlist-btn', onClick: () => onClearPlaylist(pl) }, '清空'),
|
|
2057
2075
|
!pl.fixed ? React.createElement('button', { className: 'dsh-music-playlist-btn', onClick: () => onRenamePlaylist(pl) }, '重命名') : null,
|
|
2058
2076
|
!pl.fixed ? React.createElement('button', { className: 'dsh-music-playlist-btn', onClick: () => onDeletePlaylist(pl) }, '删除') : null,
|
|
@@ -2089,17 +2107,17 @@ window.__ModuleLoader__.load({
|
|
|
2089
2107
|
setBusy(true);
|
|
2090
2108
|
apiPlaylistAdd(pl.id, paths, () => onClose());
|
|
2091
2109
|
};
|
|
2092
|
-
return React.createElement('div', { className: 'dsh-music-picker-overlay' },
|
|
2110
|
+
return portalToBody(React.createElement('div', { className: 'dsh-music-picker-overlay' },
|
|
2093
2111
|
React.createElement('div', { className: 'dsh-music-picker' },
|
|
2094
2112
|
React.createElement('div', { className: 'dsh-music-picker-head' },
|
|
2095
|
-
React.createElement('span', { className: 'dsh-music-picker-title' }, '
|
|
2113
|
+
React.createElement('span', { className: 'dsh-music-picker-title' }, '添加歌曲到「' + pl.name + '」'),
|
|
2096
2114
|
),
|
|
2097
|
-
React.createElement('div', { className: 'dsh-music-picker-cur', title: cur.path }, cur.name || cur.path || '
|
|
2115
|
+
React.createElement('div', { className: 'dsh-music-picker-cur', title: cur.path }, cur.name || cur.path || '家目录'),
|
|
2098
2116
|
React.createElement('div', { className: 'dsh-music-picker-list' },
|
|
2099
2117
|
(cur.dirs || []).map((d) => React.createElement('button', {
|
|
2100
2118
|
key: d.path, className: 'dsh-music-picker-item', title: d.path,
|
|
2101
2119
|
onClick: () => browse(d.path),
|
|
2102
|
-
}, '
|
|
2120
|
+
}, '📁 ' + d.name)),
|
|
2103
2121
|
(cur.files || []).map((f) => {
|
|
2104
2122
|
const checked = sel.has(f.path);
|
|
2105
2123
|
return React.createElement('button', {
|
|
@@ -2108,7 +2126,7 @@ window.__ModuleLoader__.load({
|
|
|
2108
2126
|
title: f.path,
|
|
2109
2127
|
onClick: () => toggle(f.path),
|
|
2110
2128
|
},
|
|
2111
|
-
React.createElement('span', { className: 'dsh-music-file-check' }, checked ? '
|
|
2129
|
+
React.createElement('span', { className: 'dsh-music-file-check' }, checked ? '✓' : ''),
|
|
2112
2130
|
React.createElement('span', { className: 'dsh-music-file-name' }, f.name),
|
|
2113
2131
|
React.createElement('span', { className: 'dsh-music-track-size' }, formatSize(f.size)),
|
|
2114
2132
|
);
|
|
@@ -2121,7 +2139,7 @@ window.__ModuleLoader__.load({
|
|
|
2121
2139
|
React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: onClose }, '取消'),
|
|
2122
2140
|
),
|
|
2123
2141
|
),
|
|
2124
|
-
);
|
|
2142
|
+
));
|
|
2125
2143
|
function goUp() {
|
|
2126
2144
|
const u = cur.up;
|
|
2127
2145
|
if (u === '__drives__') { browse('__drives__'); return; }
|
|
@@ -2185,7 +2203,7 @@ window.__ModuleLoader__.load({
|
|
|
2185
2203
|
if (action === 'pause') { audio.pause(); set({ playing: false }); return; }
|
|
2186
2204
|
if (action === 'resume') {
|
|
2187
2205
|
const p = audio.play();
|
|
2188
|
-
if (p !== undefined && typeof p.catch === 'function') p.catch(() => set({ error: '
|
|
2206
|
+
if (p !== undefined && typeof p.catch === 'function') p.catch(() => set({ error: '播放失败' }));
|
|
2189
2207
|
return;
|
|
2190
2208
|
}
|
|
2191
2209
|
if (action === 'stop') { stop(); return; }
|
|
@@ -2224,7 +2242,7 @@ window.__ModuleLoader__.load({
|
|
|
2224
2242
|
savePlayback();
|
|
2225
2243
|
const promise = audio.play();
|
|
2226
2244
|
if (promise !== undefined && typeof promise.catch === 'function') {
|
|
2227
|
-
promise.catch(() => set({ error: '
|
|
2245
|
+
promise.catch(() => set({ error: '浏览器拦截了自动播放,请在播放条点击▶解锁', pendingId: intent.id, pendingName: track.name }));
|
|
2228
2246
|
}
|
|
2229
2247
|
}
|
|
2230
2248
|
}).catch(() => {});
|
|
@@ -2291,7 +2309,7 @@ window.__ModuleLoader__.load({
|
|
|
2291
2309
|
'.dsh-music-bar .dsh-music-mode-trigger { width: 24px; height: 24px; }\n' +
|
|
2292
2310
|
'.dsh-music-bar .dsh-music-mode-trigger svg { flex: none; }\n' +
|
|
2293
2311
|
'.dsh-music-bar .dsh-music-mode-menu { align-self: center; }\n' +
|
|
2294
|
-
'.dsh-music-panel { position: fixed;
|
|
2312
|
+
'.dsh-music-panel { position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); 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' +
|
|
2295
2313
|
'.dsh-music-resize { position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; touch-action: none; z-index: 5; }\n' +
|
|
2296
2314
|
'.dsh-music-resize::after { content: ""; position: absolute; right: 4px; bottom: 4px; width: 5px; height: 5px; border-right: 2px solid var(--dsw-alias-label-secondary, #8a8f98); border-bottom: 2px solid var(--dsw-alias-label-secondary, #8a8f98); opacity: 0.7; }\n' +
|
|
2297
2315
|
'.dsh-music-resize:hover::after { opacity: 1; }\n' +
|
|
@@ -2328,8 +2346,8 @@ window.__ModuleLoader__.load({
|
|
|
2328
2346
|
'.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' +
|
|
2329
2347
|
'.dsh-music-settings-btn { padding: 6px 12px; border-radius: 8px; border: none; background: var(--dsh-music-accent, #2f9e6e); color: var(--dsh-music-accent-fg, #fff); cursor: pointer; font-size: 13px; white-space: nowrap; }\n' +
|
|
2330
2348
|
'.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' +
|
|
2331
|
-
'.dsh-music-picker-overlay { position:
|
|
2332
|
-
'.dsh-music-picker { box-sizing: border-box; width: 88%; max-height: 100%; margin: auto; 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' +
|
|
2349
|
+
'.dsh-music-picker-overlay { position: fixed; inset: 0; z-index: 2000; display: flex; overflow: auto; padding: 16px; background: rgba(0,0,0,0.45); }\n' +
|
|
2350
|
+
'.dsh-music-picker { box-sizing: border-box; width: 88%; max-width: 640px; max-height: 100%; margin: auto; 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' +
|
|
2333
2351
|
'.dsh-music-picker-head { display: flex; align-items: center; flex: none; }\n' +
|
|
2334
2352
|
'.dsh-music-picker-title { font-weight: 600; }\n' +
|
|
2335
2353
|
'.dsh-music-picker-cur { flex: none; font-size: 12px; color: var(--dsw-alias-label-secondary, #8a8f98); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n' +
|
package/lib/index.js
CHANGED
|
@@ -580,6 +580,9 @@ export function apply(ctx) {
|
|
|
580
580
|
// Reuse the api key the user already configured in DSH. DSH stores configured
|
|
581
581
|
// provider keys in ~/.dsh/.credentials.yaml keyed by the same name as
|
|
582
582
|
// settings.yaml's apiKeyEnv, so read it there (falling back to a real env var).
|
|
583
|
+
// Since DSH v1 the file nests keys under a refs: block (two-space indent,
|
|
584
|
+
// root "version: 1"); older builds used a flat layout with keys at column 0.
|
|
585
|
+
// Allow leading whitespace so both layouts resolve.
|
|
583
586
|
const readCredential = async (envName) => {
|
|
584
587
|
if (!envName) return null
|
|
585
588
|
if (process.env[envName]) return process.env[envName]
|
|
@@ -588,7 +591,7 @@ export function apply(ctx) {
|
|
|
588
591
|
if (file === null || !existsSync(file)) return null
|
|
589
592
|
const lines = readFileSync(file, 'utf8').split(/\r?\n/)
|
|
590
593
|
for (const ln of lines) {
|
|
591
|
-
const m = new RegExp('
|
|
594
|
+
const m = new RegExp('^\\s*' + envName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*[:=]\\s*(.+?)\\s*$').exec(ln)
|
|
592
595
|
if (m) return m[1]
|
|
593
596
|
}
|
|
594
597
|
return null
|
|
@@ -654,8 +657,8 @@ export function apply(ctx) {
|
|
|
654
657
|
if (!baseURL) baseURL = MIMO_DEFAULT_BASE_URL
|
|
655
658
|
let reason = ''
|
|
656
659
|
if (key) reason = 'ok'
|
|
657
|
-
else if (providerFound) reason = 'provider
|
|
658
|
-
else reason = '
|
|
660
|
+
else if (providerFound) reason = 'provider已配置,但未读到 ' + (apiKeyEnv || 'MIMO_API_KEY') + '的值。请在 DSH 模型设置中确认已填入xiaomi密钥。'
|
|
661
|
+
else reason = '未找到xiaomi提供方。请在DSH模型设置中配置。'
|
|
659
662
|
return { providerId, baseURL, apiKeyEnv, key, configured: !!key, reason }
|
|
660
663
|
}
|
|
661
664
|
const ttsCache = {}
|
|
@@ -672,7 +675,7 @@ export function apply(ctx) {
|
|
|
672
675
|
// Synthesize a chunk of prose into a Buffer of wav audio (non-streaming, format=wav).
|
|
673
676
|
const synthesize = async (text, voice) => {
|
|
674
677
|
const { baseURL, key } = await resolveTts()
|
|
675
|
-
if (!key) throw new Error('未配置
|
|
678
|
+
if (!key) throw new Error('未配置xiaomi提供方(缺少api key)')
|
|
676
679
|
const body = {
|
|
677
680
|
model: MIMO_DEFAULT_MODEL,
|
|
678
681
|
messages: [
|
|
@@ -1085,7 +1088,7 @@ export function apply(ctx) {
|
|
|
1085
1088
|
const root = letter + ':\\'
|
|
1086
1089
|
try { if (existsSync(root)) roots.push({ name: root, path: root }) } catch {}
|
|
1087
1090
|
}
|
|
1088
|
-
writeJson(res, { path: '__drives__', name: '
|
|
1091
|
+
writeJson(res, { path: '__drives__', name: '本机磁盘', up: null, dirs: roots })
|
|
1089
1092
|
} else {
|
|
1090
1093
|
writeJson(res, { path: '/', name: '/', up: null, dirs: [] })
|
|
1091
1094
|
}
|
|
@@ -1306,7 +1309,7 @@ export function apply(ctx) {
|
|
|
1306
1309
|
const root = letter + ':\\'
|
|
1307
1310
|
try { if (existsSync(root)) roots.push({ name: root, path: root }) } catch {}
|
|
1308
1311
|
}
|
|
1309
|
-
writeJson(res, { path: '__drives__', name: '
|
|
1312
|
+
writeJson(res, { path: '__drives__', name: '本机磁盘', up: null, dirs: roots, files: [] })
|
|
1310
1313
|
} else {
|
|
1311
1314
|
writeJson(res, { path: '/', name: '/', up: null, dirs: [], files: [] })
|
|
1312
1315
|
}
|
|
@@ -1534,7 +1537,7 @@ export function apply(ctx) {
|
|
|
1534
1537
|
pendingIntent = { action: 'play', kind: 'book', id: pick.id, name: pick.name }
|
|
1535
1538
|
return {
|
|
1536
1539
|
action, played: true, kind: 'book', track: pick.name, matches: bookCount, count: bookCount,
|
|
1537
|
-
notice: '已请求播放小说「' + pick.name + '」。讲书需已配置
|
|
1540
|
+
notice: '已请求播放小说「' + pick.name + '」。讲书需已配置xiaomi TTS;若被拦截请点 ▶ 解锁。',
|
|
1538
1541
|
}
|
|
1539
1542
|
}
|
|
1540
1543
|
|
|
@@ -1544,7 +1547,7 @@ export function apply(ctx) {
|
|
|
1544
1547
|
pendingIntent = { action: 'play', kind: 'book', id: pick.id, name: pick.name }
|
|
1545
1548
|
return {
|
|
1546
1549
|
action, played: true, kind: 'book', track: pick.name, matches: bookPool.length, count: bookCount,
|
|
1547
|
-
notice: '已请求播放小说「' + pick.name + '」(匹配 ' + bookPool.length + ' / 共 ' + bookCount + ' 本)。讲书需已配置
|
|
1550
|
+
notice: '已请求播放小说「' + pick.name + '」(匹配 ' + bookPool.length + ' / 共 ' + bookCount + ' 本)。讲书需已配置xiaomi TTS;若被拦截请点 ▶ 解锁。',
|
|
1548
1551
|
}
|
|
1549
1552
|
}
|
|
1550
1553
|
|
|
@@ -1566,7 +1569,7 @@ export function apply(ctx) {
|
|
|
1566
1569
|
// ---- light prompt hint so the agent knows it can play local music/novels ----
|
|
1567
1570
|
ctx.systemPrompt.section({
|
|
1568
1571
|
name: 'tool:music-player', order: 116,
|
|
1569
|
-
text: '本机已挂载本地音乐播放器与 AI 讲书:可用 music_play 工具按关键词播放 ~/Music(或设置的目录)里的音乐,或按歌单名播放自建歌单(playlist 参数),或按小说名播放本地 .txt 小说(AI 讲书,需配置
|
|
1572
|
+
text: '本机已挂载本地音乐播放器与 AI 讲书:可用 music_play 工具按关键词播放 ~/Music(或设置的目录)里的音乐,或按歌单名播放自建歌单(playlist 参数),或按小说名播放本地 .txt 小说(AI 讲书,需配置xiaomi提供方);并支持 action 暂停/继续/停止/下一首·下一章/上一首·上一章。',
|
|
1570
1573
|
})
|
|
1571
1574
|
|
|
1572
1575
|
void ensureStarted()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.5",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|