dsh-music-player 0.3.4 → 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 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: '\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 }); });
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: '\u7ae0\u8282', part: '\u5206\u90e8', preface: '\u524d\u8a00',
710
- epilogue: '\u540e\u8bb0', named: '\u5206\u8282', toc: '\u76ee\u5f55',
711
- })[t] || '\u6b63\u6587';
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: '\u6d4f\u89c8\u5668\u62e6\u622a\u4e86\u81ea\u52a8\u64ad\u653e\uff0c\u8bf7\u70b9\u51fb\u64ad\u653e\u6309\u94ae' }));
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: '\u58f0\u97f3\u5207\u6362\u5931\u8d25\uff1a' + String((err && err.message) || err) });
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: '\u64ad\u653e\u5931\u8d25', playing: false }));
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: '\u8bb2\u4e66\u97f3\u9891\u83b7\u53d6\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5' });
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: '\u97f3\u9891\u52a0\u8f7d\u6216\u89e3\u7801\u5931\u8d25', playing: false });
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: '\u65e0\u6cd5\u8bfb\u53d6\u97f3\u4e50\u5e93\uff1a' + String((err && err.message) || err) });
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) || '\u8bbe\u7f6e\u76ee\u5f55\u5931\u8d25' });
1354
+ set({ loading: false, error: (result && result.error) || '设置目录失败' });
1345
1355
  }
1346
1356
  }).catch((err) => {
1347
- set({ loading: false, error: '\u8bbe\u7f6e\u76ee\u5f55\u5931\u8d25\uff1a' + String((err && err.message) || err) });
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: '\u97f3\u91cf ' + pct + '%' },
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: '\u51b0\u7cd6', label: '\u51b0\u7cd6', gender: '\u5973', lang: '\u4e2d\u6587' },
1431
- { id: '\u8309\u8389', label: '\u8309\u8389', gender: '\u5973', lang: '\u4e2d\u6587' },
1432
- { id: '\u82cf\u6253', label: '\u82cf\u6253', gender: '\u7537', lang: '\u4e2d\u6587' },
1433
- { id: '\u767d\u6866', label: '\u767d\u6866', gender: '\u7537', lang: '\u4e2d\u6587' },
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 !== '\u81ea\u52a8' ? '\uff08' + cur.gender + '\uff09' : '')) : s.voice;
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 \u58f0\u97f3'),
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 : '\u767d\u6866',
1446
- title: '\u5f53\u524d\uff1a' + currentLabel,
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 ? '\u00b7' + v.lang : '') + (v.gender && v.gender !== '\u81ea\u52a8' ? '\uff08' + v.gender + '\uff09' : ''))),
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' }, '\u5207\u6362\u4e2d\u2026') : null,
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 \u5408\u6210\u4e2d\u2026 ' + secs + 's');
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: '\u91cd\u65b0\u5408\u6210\u5f53\u524d\u6bb5\u843d',
1490
+ title: '重新合成当前段落',
1481
1491
  onClick: retryBook,
1482
- }, '\u91cd\u8bd5'));
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: '\u9891\u8c31\u4e0d\u53ef\u7528\uff0c\u70b9\u51fb\u91cd\u8bd5',
1514
+ title: '频谱不可用,点击重试',
1505
1515
  onClick: () => { const t = resolvePlayable(s.currentId); if (t !== null) loadEnvelope(t.id, t.url); },
1506
- }, '\u9891\u8c31\u4e0d\u53ef\u7528\uff0c\u70b9\u51fb\u91cd\u8bd5');
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
- '\u25b8 ' + s.currentSection);
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, ' \u672c\u5730\u97f3\u4e50\u64ad\u653e\u5668'),
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' }, '\u26a0 \u81ea\u52a8\u64ad\u653e\u88ab\u62e6\u622a\uff0c\u70b9\u51fb\u25b6\u89e3\u9501')
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 ? '\u4e0a\u4e00\u7ae0' : '\u4e0a\u4e00\u9996', onClick: () => (isBook ? stepBook(-1) : step(-1)) }, '\u23ee') : null,
1548
- hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '\u64ad\u653e/\u6682\u505c', onClick: togglePlay }, s.playing ? '\u23f8' : '\u25b6') : null,
1549
- hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: isBook ? '\u4e0b\u4e00\u7ae0' : '\u4e0b\u4e00\u9996', onClick: () => (isBook ? stepBook(1) : step(1)) }, '\u23ed') : null,
1550
- hasTrack ? React.createElement('button', { className: 'dsh-music-bar-btn', title: '\u505c\u6b62', onClick: stop }, '\u23f9') : null,
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: '\u7ae0\u8282\u76ee\u5f55',
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: '\u97f3\u91cf',
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 ? '\u5173\u95ed\u64ad\u653e\u5217\u8868' : '\u6253\u5f00\u64ad\u653e\u5217\u8868',
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: '\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' },
1596
- { 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' },
1597
- { 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' },
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' }, '\u6682\u65e0\u7ae0\u8282\u7ed3\u6784\uff08\u8be5\u4e66\u65e0\u6cd5\u8bc6\u522b\u5206\u8282\u3002\uff09');
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' }, '\u7ae0\u8282\u76ee\u5f55'),
1686
- React.createElement('button', { className: 'dsh-music-icon-btn', title: '\u5173\u95ed', onClick: closeToc }, '\u2715')),
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 right/bottom, 380px, auto height).
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 right/bottom anchoring
1708
- // to explicit left/top/width/height. Locking height and clearing max-height
1709
- // matters: with only top+left and the CSS max-height:72vh still applying, a
1710
- // fixed element whose CSS also sets bottom would collapse/clamp while dragging.
1711
- const style = pos === null ? null : { left: pos.x, top: pos.y, width: pos.w, height: pos.h, maxHeight: 'none' };
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)) set({ panelOpen: false });
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 ? '\u25b6 ' : '') + t.name),
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
- }, '\uff0b'),
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 ? '\u25b6 ' : '') + b.name),
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', '\u66f2\u5e93'),
1858
- subTabBtn(FAV_PLAYLIST_ID, '\u2665 \u6211\u6700\u559c\u6b22'),
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 }, '\uff0b'),
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' }, '\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'));
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' }, '\u672a\u53d1\u73b0 .txt \u5c0f\u8bf4\u6587\u4ef6\u3002')
1876
- : React.createElement('div', { className: 'dsh-music-error' }, s.ttsReason || '\u672a\u914d\u7f6e xiaomi/MiMo TTS \u6a21\u578b\u3002')));
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 }, '\u283f'),
1883
- React.createElement('span', { className: 'dsh-music-panel-title' }, '\u64ad\u653e\u5217\u8868'),
1884
- React.createElement('button', { className: 'dsh-music-icon-btn', title: '\u5173\u95ed', onClick: () => set({ panelOpen: false }) }, '\u2715')),
1885
- React.createElement('div', { className: 'dsh-music-tabs' }, tabBtn('music', '\u97f3\u4e50'), tabBtn('book', '\u5c0f\u8bf4')),
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' }, '\u626b\u63cf\u4e2d\u2026') : null,
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 ? '\u9009\u62e9\u5c0f\u8bf4\u76ee\u5f55' : '\u9009\u62e9\u97f3\u4e50\u76ee\u5f55';
1930
+ const pickerTitle = isBook ? '选择小说目录' : '选择音乐目录';
1913
1931
  const hint = isBook
1914
- ? '\u652f\u6301 .txt \u6587\u4ef6\uff0cAI\u8bed\u97f3\u76ee\u524d\u4ec5\u652f\u6301xiaomi\u63d0\u4f9b\u65b9\uff08\u9650\u65f6\u514d\u8d39\uff09\uff0c\u8bf7\u5728\u8bbe\u7f6e\u4e2d\u914d\u7f6e\u597d\u518d\u4f7f\u7528\u6b64\u529f\u80fd\u3002'
1915
- : '\u652f\u6301 mp3 / m4a / flac / wav / ogg / opus / aac / webm \u7b49\u683c\u5f0f\uff0c\u81ea\u52a8\u9012\u5f52\u626b\u63cf\u5b50\u76ee\u5f55\u3002';
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
- '\ud83d\udcc1 ' + (activeRoot || '\u672a\u914d\u7f6e')),
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 || '\u5bb6\u76ee\u5f55'),
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
- }, '\ud83d\udcc1 ' + d.name))
1937
- : React.createElement('div', { className: 'dsh-music-picker-empty' }, '\u672c\u76ee\u5f55\u4e0b\u65e0\u5b50\u76ee\u5f55\uff0c\u53ef\u76f4\u63a5\u9009\u62e9\u3002'),
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() }, '\u8fd4\u56de\u4e0a\u7ea7'),
1942
- React.createElement('button', { className: 'dsh-music-settings-btn', onClick: () => pickCurrent() }, '\u9009\u62e9\u6b64\u76ee\u5f55'),
1943
- React.createElement('button', { className: 'dsh-music-settings-btn ghost', onClick: () => setPickerOpen(false) }, '\u53d6\u6d88'),
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 ? '\u2665 ' : '') + p.name + '\uff08' + p.count + '\uff09')) : null,
2030
- React.createElement('button', { className: 'dsh-music-add-pop-item new', onClick: addNew }, '\uff0b \u65b0\u5efa\u6b4c\u5355'),
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 ? '\u25b6 ' : '') + (idx + 1) + '. ' + t.name),
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); } }, '\u2191'),
2049
- React.createElement('button', { className: 'dsh-music-playlist-mini', title: '下移', onClick: (e) => { e.stopPropagation(); movePlaylistTrack(pl, t.path, 1); } }, '\u2193'),
2050
- React.createElement('button', { className: 'dsh-music-playlist-mini del', title: '从歌单移除', onClick: (e) => { e.stopPropagation(); apiPlaylistRemove(pl.id, [t.path]); } }, '\u00d7'),
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) }, '\uff0b \u6dfb\u52a0\u6b4c\u66f2'),
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' }, '\u6dfb\u52a0\u6b4c\u66f2\u5230\u300c' + pl.name + '\u300d'),
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 || '\u5bb6\u76ee\u5f55'),
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
- }, '\ud83d\udcc1 ' + d.name)),
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 ? '\u2713' : ''),
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: '\u64ad\u653e\u5931\u8d25' }));
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: '\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 }));
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; 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' +
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: absolute; inset: 0; z-index: 70; display: flex; overflow: auto; padding: 16px; background: rgba(0,0,0,0.45); }\n' +
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
@@ -657,8 +657,8 @@ export function apply(ctx) {
657
657
  if (!baseURL) baseURL = MIMO_DEFAULT_BASE_URL
658
658
  let reason = ''
659
659
  if (key) reason = 'ok'
660
- else if (providerFound) reason = 'provider\u5df2\u914d\u7f6e\uff0c\u4f46\u672a\u8bfb\u5230 ' + (apiKeyEnv || 'MIMO_API_KEY') + '\u7684\u503c\u3002\u8bf7\u5728 DSH \u6a21\u578b\u8bbe\u7f6e\u4e2d\u786e\u8ba4\u5df2\u586b\u5165 xiaomi \u5bc6\u94a5\u3002'
661
- else reason = '\u672a\u627e\u5230 xiaomi/MiMo TTS provider\u3002\u8bf7\u5728 DSH \u6a21\u578b\u8bbe\u7f6e\u4e2d\u914d\u7f6e\u3002'
660
+ else if (providerFound) reason = 'provider已配置,但未读到 ' + (apiKeyEnv || 'MIMO_API_KEY') + '的值。请在 DSH 模型设置中确认已填入xiaomi密钥。'
661
+ else reason = '未找到xiaomi提供方。请在DSH模型设置中配置。'
662
662
  return { providerId, baseURL, apiKeyEnv, key, configured: !!key, reason }
663
663
  }
664
664
  const ttsCache = {}
@@ -675,7 +675,7 @@ export function apply(ctx) {
675
675
  // Synthesize a chunk of prose into a Buffer of wav audio (non-streaming, format=wav).
676
676
  const synthesize = async (text, voice) => {
677
677
  const { baseURL, key } = await resolveTts()
678
- if (!key) throw new Error('未配置 xiaomi/MiMo 提供方(缺少 api key)')
678
+ if (!key) throw new Error('未配置xiaomi提供方(缺少api key)')
679
679
  const body = {
680
680
  model: MIMO_DEFAULT_MODEL,
681
681
  messages: [
@@ -1088,7 +1088,7 @@ export function apply(ctx) {
1088
1088
  const root = letter + ':\\'
1089
1089
  try { if (existsSync(root)) roots.push({ name: root, path: root }) } catch {}
1090
1090
  }
1091
- writeJson(res, { path: '__drives__', name: '\u672c\u673a\u78c1\u76d8', up: null, dirs: roots })
1091
+ writeJson(res, { path: '__drives__', name: '本机磁盘', up: null, dirs: roots })
1092
1092
  } else {
1093
1093
  writeJson(res, { path: '/', name: '/', up: null, dirs: [] })
1094
1094
  }
@@ -1309,7 +1309,7 @@ export function apply(ctx) {
1309
1309
  const root = letter + ':\\'
1310
1310
  try { if (existsSync(root)) roots.push({ name: root, path: root }) } catch {}
1311
1311
  }
1312
- writeJson(res, { path: '__drives__', name: '\u672c\u673a\u78c1\u76d8', up: null, dirs: roots, files: [] })
1312
+ writeJson(res, { path: '__drives__', name: '本机磁盘', up: null, dirs: roots, files: [] })
1313
1313
  } else {
1314
1314
  writeJson(res, { path: '/', name: '/', up: null, dirs: [], files: [] })
1315
1315
  }
@@ -1537,7 +1537,7 @@ export function apply(ctx) {
1537
1537
  pendingIntent = { action: 'play', kind: 'book', id: pick.id, name: pick.name }
1538
1538
  return {
1539
1539
  action, played: true, kind: 'book', track: pick.name, matches: bookCount, count: bookCount,
1540
- notice: '已请求播放小说「' + pick.name + '」。讲书需已配置 xiaomi/MiMo TTS;若被拦截请点 ▶ 解锁。',
1540
+ notice: '已请求播放小说「' + pick.name + '」。讲书需已配置xiaomi TTS;若被拦截请点 ▶ 解锁。',
1541
1541
  }
1542
1542
  }
1543
1543
 
@@ -1547,7 +1547,7 @@ export function apply(ctx) {
1547
1547
  pendingIntent = { action: 'play', kind: 'book', id: pick.id, name: pick.name }
1548
1548
  return {
1549
1549
  action, played: true, kind: 'book', track: pick.name, matches: bookPool.length, count: bookCount,
1550
- notice: '已请求播放小说「' + pick.name + '」(匹配 ' + bookPool.length + ' / 共 ' + bookCount + ' 本)。讲书需已配置 xiaomi/MiMo TTS;若被拦截请点 ▶ 解锁。',
1550
+ notice: '已请求播放小说「' + pick.name + '」(匹配 ' + bookPool.length + ' / 共 ' + bookCount + ' 本)。讲书需已配置xiaomi TTS;若被拦截请点 ▶ 解锁。',
1551
1551
  }
1552
1552
  }
1553
1553
 
@@ -1569,7 +1569,7 @@ export function apply(ctx) {
1569
1569
  // ---- light prompt hint so the agent knows it can play local music/novels ----
1570
1570
  ctx.systemPrompt.section({
1571
1571
  name: 'tool:music-player', order: 116,
1572
- text: '本机已挂载本地音乐播放器与 AI 讲书:可用 music_play 工具按关键词播放 ~/Music(或设置的目录)里的音乐,或按歌单名播放自建歌单(playlist 参数),或按小说名播放本地 .txt 小说(AI 讲书,需配置 xiaomi/MiMo TTS);并支持 action 暂停/继续/停止/下一首·下一章/上一首·上一章。',
1572
+ text: '本机已挂载本地音乐播放器与 AI 讲书:可用 music_play 工具按关键词播放 ~/Music(或设置的目录)里的音乐,或按歌单名播放自建歌单(playlist 参数),或按小说名播放本地 .txt 小说(AI 讲书,需配置xiaomi提供方);并支持 action 暂停/继续/停止/下一首·下一章/上一首·上一章。',
1573
1573
  })
1574
1574
 
1575
1575
  void ensureStarted()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-music-player",
3
- "version": "0.3.4",
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",