dsh-music-player 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/client.js +75 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ DeepSeek Harness 本地音乐/小说播放插件。
|
|
|
14
14
|
- 顺序播放、单曲循环、乱序播放三种模式
|
|
15
15
|
- 实时 7 段频谱可视化(解码音频包络驱动)
|
|
16
16
|
- 播放时申请屏幕唤醒锁,防止听歌时熄屏/休眠(支持 Wake Lock 的浏览器,如 Chrome/Edge)
|
|
17
|
-
-
|
|
17
|
+
- 播放列表面板可自由拖动,右下角可拖拽调整大小,位置与尺寸跨刷新记忆
|
|
18
18
|
- AI 讲书:本地 `.txt` 小说经 MiMo TTS 合成朗读,自动识别**书名/前言/章节/尾声**结构,播放条带**章节目录**跳转、章节切歌,可选 4 种中文 AI 声音(默认白桦)
|
|
19
19
|
- `music_play` 模型工具:agent 可按关键词播放本地音乐,也可按小说名启动 AI 讲书
|
|
20
20
|
- 支持的格式:`mp3 / m4a / m4b / aac / flac / wav / ogg / opus / webm / aiff`(自动递归扫描子目录,上限 500 首)
|
package/lib/client.js
CHANGED
|
@@ -101,14 +101,24 @@ window.__ModuleLoader__.load({
|
|
|
101
101
|
}
|
|
102
102
|
return best;
|
|
103
103
|
}
|
|
104
|
-
//
|
|
104
|
+
// Playback-panel geometry: default CSS width (must match .dsh-music-panel),
|
|
105
|
+
// resize bounds, and the viewport-height fraction cap when user-resized.
|
|
106
|
+
const PANEL_W = 380;
|
|
107
|
+
const PANEL_MIN_W = 280;
|
|
108
|
+
const PANEL_MAX_W = 640;
|
|
109
|
+
const PANEL_MIN_H = 200;
|
|
110
|
+
const PANEL_MAX_H_VH = 0.8;
|
|
111
|
+
// Restore the playback-panel position ({x,y,w,h}) previously saved by
|
|
112
|
+
// dragging/resizing, if any. Old saves ({x,y,h}) migrate with a default width.
|
|
105
113
|
function loadPanelPos() {
|
|
106
114
|
const raw = loadPref(PREF_PANEL_POS);
|
|
107
115
|
if (raw === null) return null;
|
|
108
116
|
try {
|
|
109
117
|
const p = JSON.parse(raw);
|
|
110
118
|
if (p && typeof p.x === 'number' && typeof p.y === 'number'
|
|
111
|
-
&& typeof p.h === 'number' && p.h > 0)
|
|
119
|
+
&& typeof p.h === 'number' && p.h > 0) {
|
|
120
|
+
return { x: p.x, y: p.y, w: (typeof p.w === 'number' && p.w > 0) ? p.w : PANEL_W, h: p.h };
|
|
121
|
+
}
|
|
112
122
|
} catch (e) {}
|
|
113
123
|
return null;
|
|
114
124
|
}
|
|
@@ -1631,9 +1641,11 @@ window.__ModuleLoader__.load({
|
|
|
1631
1641
|
const isBook = s.currentId !== null && String(s.currentId).startsWith('book:');
|
|
1632
1642
|
const listRef = useRef(null);
|
|
1633
1643
|
const panelRef = useRef(null);
|
|
1634
|
-
// Draggable panel position ({x, y, h} left/top/height once
|
|
1644
|
+
// Draggable panel position + size ({x, y, w, h} left/top/width/height once
|
|
1645
|
+
// dragged or resized; null = default right/bottom, 380px, auto height).
|
|
1635
1646
|
const [pos, setPos] = useState(loadPanelPos);
|
|
1636
|
-
const dragRef = useRef(null);
|
|
1647
|
+
const dragRef = useRef(null); // head-drag state
|
|
1648
|
+
const resizeRef = useRef(null); // corner-resize state
|
|
1637
1649
|
// 曲库每行「+」打开的「加入歌单」菜单:{track, x, y}(锚点=按钮右上角视口坐标)。
|
|
1638
1650
|
const [addMenu, setAddMenu] = useState(null);
|
|
1639
1651
|
const openAddMenu = (track, e) => {
|
|
@@ -1641,11 +1653,11 @@ window.__ModuleLoader__.load({
|
|
|
1641
1653
|
setAddMenu({ track, x: r.right, y: r.top });
|
|
1642
1654
|
};
|
|
1643
1655
|
|
|
1644
|
-
// Once the panel is dragged we switch from CSS right/bottom anchoring
|
|
1645
|
-
// explicit left/top/height. Locking
|
|
1646
|
-
//
|
|
1647
|
-
//
|
|
1648
|
-
const style = pos === null ? null : { left: pos.x, top: pos.y, height: pos.h };
|
|
1656
|
+
// Once the panel is dragged/resized we switch from CSS right/bottom anchoring
|
|
1657
|
+
// to explicit left/top/width/height. Locking height and clearing max-height
|
|
1658
|
+
// matters: with only top+left and the CSS max-height:72vh still applying, a
|
|
1659
|
+
// fixed element whose CSS also sets bottom would collapse/clamp while dragging.
|
|
1660
|
+
const style = pos === null ? null : { left: pos.x, top: pos.y, width: pos.w, height: pos.h, maxHeight: 'none' };
|
|
1649
1661
|
|
|
1650
1662
|
const onHeadDown = (e) => {
|
|
1651
1663
|
if (e.button !== undefined && e.button !== 0) return;
|
|
@@ -1654,15 +1666,16 @@ window.__ModuleLoader__.load({
|
|
|
1654
1666
|
const el = panelRef.current;
|
|
1655
1667
|
if (el === null) return;
|
|
1656
1668
|
const rect = el.getBoundingClientRect();
|
|
1669
|
+
const w = pos !== null ? pos.w : PANEL_W;
|
|
1657
1670
|
const h = pos !== null ? pos.h : rect.height;
|
|
1658
|
-
const next = { x: pos !== null ? pos.x : rect.left, y: pos !== null ? pos.y : rect.top, h };
|
|
1671
|
+
const next = { x: pos !== null ? pos.x : rect.left, y: pos !== null ? pos.y : rect.top, w, h };
|
|
1659
1672
|
dragRef.current = {
|
|
1660
1673
|
startX: e.clientX, startY: e.clientY,
|
|
1661
|
-
originX: next.x, originY: next.y, h,
|
|
1674
|
+
originX: next.x, originY: next.y, w, h,
|
|
1662
1675
|
};
|
|
1663
1676
|
setPos(next);
|
|
1664
1677
|
savePref(PREF_PANEL_POS, JSON.stringify(next));
|
|
1665
|
-
e.currentTarget.setPointerCapture(e.pointerId);
|
|
1678
|
+
if (typeof e.currentTarget.setPointerCapture === 'function') e.currentTarget.setPointerCapture(e.pointerId);
|
|
1666
1679
|
};
|
|
1667
1680
|
const onHeadMove = (e) => {
|
|
1668
1681
|
const d = dragRef.current;
|
|
@@ -1674,13 +1687,56 @@ window.__ModuleLoader__.load({
|
|
|
1674
1687
|
x = Math.max(0, Math.min(x, window.innerWidth - el.offsetWidth));
|
|
1675
1688
|
y = Math.max(0, Math.min(y, window.innerHeight - el.offsetHeight));
|
|
1676
1689
|
}
|
|
1677
|
-
const next = { x, y, h: d.h };
|
|
1690
|
+
const next = { x, y, w: d.w, h: d.h };
|
|
1678
1691
|
setPos(next);
|
|
1679
1692
|
savePref(PREF_PANEL_POS, JSON.stringify(next));
|
|
1680
1693
|
};
|
|
1681
1694
|
const onHeadUp = (e) => {
|
|
1682
1695
|
dragRef.current = null;
|
|
1683
|
-
if (
|
|
1696
|
+
if (typeof e.currentTarget.releasePointerCapture === 'function'
|
|
1697
|
+
&& typeof e.currentTarget.hasPointerCapture === 'function'
|
|
1698
|
+
&& e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
|
1699
|
+
};
|
|
1700
|
+
|
|
1701
|
+
// Corner drag-to-resize: grow/shrink width & height from the bottom-right
|
|
1702
|
+
// handle, clamped to [min, max] and kept inside the viewport. The panel's
|
|
1703
|
+
// top-left (x/y) is untouched by a resize.
|
|
1704
|
+
const onResizeDown = (e) => {
|
|
1705
|
+
if (e.button !== undefined && e.button !== 0) return;
|
|
1706
|
+
const el = panelRef.current;
|
|
1707
|
+
if (el === null) return;
|
|
1708
|
+
const rect = el.getBoundingClientRect();
|
|
1709
|
+
const cur = {
|
|
1710
|
+
x: pos !== null ? pos.x : rect.left,
|
|
1711
|
+
y: pos !== null ? pos.y : rect.top,
|
|
1712
|
+
w: pos !== null ? pos.w : PANEL_W,
|
|
1713
|
+
h: pos !== null ? pos.h : rect.height,
|
|
1714
|
+
};
|
|
1715
|
+
resizeRef.current = { startX: e.clientX, startY: e.clientY, originW: cur.w, originH: cur.h };
|
|
1716
|
+
setPos(cur);
|
|
1717
|
+
savePref(PREF_PANEL_POS, JSON.stringify(cur));
|
|
1718
|
+
if (typeof e.currentTarget.setPointerCapture === 'function') e.currentTarget.setPointerCapture(e.pointerId);
|
|
1719
|
+
};
|
|
1720
|
+
const onResizeMove = (e) => {
|
|
1721
|
+
const d = resizeRef.current;
|
|
1722
|
+
if (d === null) return;
|
|
1723
|
+
const el = panelRef.current;
|
|
1724
|
+
const x = pos !== null ? pos.x : (el !== null ? el.getBoundingClientRect().left : 0);
|
|
1725
|
+
const y = pos !== null ? pos.y : (el !== null ? el.getBoundingClientRect().top : 0);
|
|
1726
|
+
const vw = window.innerWidth, vh = window.innerHeight;
|
|
1727
|
+
const maxW = Math.min(PANEL_MAX_W, Math.max(PANEL_MIN_W, vw - x));
|
|
1728
|
+
const maxH = Math.min(Math.floor(vh * PANEL_MAX_H_VH), Math.max(PANEL_MIN_H, vh - y));
|
|
1729
|
+
const w = Math.max(PANEL_MIN_W, Math.min(d.originW + (e.clientX - d.startX), maxW));
|
|
1730
|
+
const h = Math.max(PANEL_MIN_H, Math.min(d.originH + (e.clientY - d.startY), maxH));
|
|
1731
|
+
const next = { x, y, w, h };
|
|
1732
|
+
setPos(next);
|
|
1733
|
+
savePref(PREF_PANEL_POS, JSON.stringify(next));
|
|
1734
|
+
};
|
|
1735
|
+
const onResizeUp = (e) => {
|
|
1736
|
+
resizeRef.current = null;
|
|
1737
|
+
if (typeof e.currentTarget.releasePointerCapture === 'function'
|
|
1738
|
+
&& typeof e.currentTarget.hasPointerCapture === 'function'
|
|
1739
|
+
&& e.currentTarget.hasPointerCapture(e.pointerId)) e.currentTarget.releasePointerCapture(e.pointerId);
|
|
1684
1740
|
};
|
|
1685
1741
|
|
|
1686
1742
|
useEffect(() => {
|
|
@@ -1782,7 +1838,8 @@ window.__ModuleLoader__.load({
|
|
|
1782
1838
|
// panel (novel status shows on the playback bar instead).
|
|
1783
1839
|
!isBook && s.error ? React.createElement('div', { className: 'dsh-music-error' }, s.error) : null,
|
|
1784
1840
|
!isBook && s.loading ? React.createElement('div', { className: 'dsh-music-loading' }, '\u626b\u63cf\u4e2d\u2026') : null,
|
|
1785
|
-
React.createElement('div', { className: 'dsh-music-list', ref: (el) => { listRef.current = el; } }, listBody),
|
|
1841
|
+
React.createElement('div', { className: 'dsh-music-list', style: pos === null ? null : { maxHeight: 'none' }, ref: (el) => { listRef.current = el; } }, listBody),
|
|
1842
|
+
React.createElement('div', { className: 'dsh-music-resize', title: '拖动调整面板大小', onPointerDown: onResizeDown, onPointerMove: onResizeMove, onPointerUp: onResizeUp }),
|
|
1786
1843
|
addMenu ? React.createElement(AddToPlaylistMenu, {
|
|
1787
1844
|
track: addMenu.track, anchor: { x: addMenu.x, y: addMenu.y },
|
|
1788
1845
|
onClose: () => setAddMenu(null),
|
|
@@ -2188,6 +2245,9 @@ window.__ModuleLoader__.load({
|
|
|
2188
2245
|
'.dsh-music-bar .dsh-music-mode-trigger svg { flex: none; }\n' +
|
|
2189
2246
|
'.dsh-music-bar .dsh-music-mode-menu { align-self: center; }\n' +
|
|
2190
2247
|
'.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' +
|
|
2248
|
+
'.dsh-music-resize { position: absolute; right: 0; bottom: 0; width: 16px; height: 16px; cursor: nwse-resize; touch-action: none; z-index: 5; }\n' +
|
|
2249
|
+
'.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' +
|
|
2250
|
+
'.dsh-music-resize:hover::after { opacity: 1; }\n' +
|
|
2191
2251
|
'.dsh-music-panel-head { display: flex; align-items: center; gap: 6px; }\n' +
|
|
2192
2252
|
'.dsh-music-tabs { display: flex; gap: 4px; }\n' +
|
|
2193
2253
|
'.dsh-music-tab { flex: 1; padding: 5px 0; border: none; border-radius: 6px; background: transparent; color: var(--dsw-alias-label-secondary, #8a8f98); cursor: pointer; font-size: 12px; }\n' +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-player",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "DeepSeek Harness 本地音乐 + AI 讲书插件:Host 扫描音乐目录并以 HTTP 流式提供音频、解析 .txt 小说结构并经 MiMo TTS 合成朗读,浏览器侧提供播放条/播放面板/章节目录跳转/多声音选择/实时频谱,并注册 music_play 模型工具",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|