dsh-music-panel 0.3.5 → 0.3.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.7
4
+
5
+ ### 变更
6
+
7
+ - 会话输入框发送按钮左侧新增播放列表按钮(`conversation.input.right`),点击打开播放列表
8
+ - 设置 → 外观新增「输入框播放列表按钮」开关
9
+ - 播放列表改为全局浮层,主面板底栏按钮与输入框按钮两个入口共用
10
+ - 迷你浮窗并入播放列表顶部(封面 / 标题 / 播放控制),不再作为独立浮窗;移除设置里的迷你浮窗开关
11
+
12
+ ## 0.3.6
13
+
14
+ ### 变更
15
+
16
+ - 播放队列支持拖动排序(HTML5 drag & drop,交互参考 SortableJS:拖动手柄、拖动行半透明、目标行插入指示)
17
+ - 去掉队列行的上移/下移按钮,保留单曲移除
18
+ - 拖动后队列顺序即为播放顺序;随机模式下拖动后的视觉顺序同步为播放顺序
19
+
3
20
  ## 0.3.5
4
21
 
5
22
  ### 变更
package/README.md CHANGED
@@ -22,16 +22,16 @@ DeepSeek Harness 的本地音乐插件:左侧栏一个音符入口,点开是
22
22
  - **左侧栏入口**:`sidebar.panellist` 注册 `music` 图标,点开切换到独立主面板(与「插件市场」「定时任务」同一套宿主机制)。
23
23
  - **歌曲列表**:封面缩略图、标题、歌手(带 CD 徽标)、专辑、时长、采样率;当前曲高亮。
24
24
  - **播放控制**:列表循环 / 单曲循环 / 不循环,随机播放,上一首、下一首、进度拖动、音量与静音。
25
- - **播放队列**:底部「队列」按钮展开当前队列,点击任意条目跳转;「更多」里显示格式 / 码率 / 采样率,可复制文件路径。
25
+ - **播放队列**:底部「队列」按钮或输入框发送按钮左侧的播放列表按钮展开当前队列(顶部带迷你播放器),点击任意条目跳转;「更多」里显示格式 / 码率 / 采样率,可复制文件路径。
26
26
  - **记忆播放**:启动 / 刷新后恢复上次的队列、当前曲目与播放位置(默认停在暂停,点 ▶ 继续);设置里可选「恢复后自动播放」和「每首歌记住播放位置」。
27
27
  - **统计**:主面板「统计」页按最近一周 / 最近一月 / 全部时间查看每首歌的累计播放时长、播放次数与最近播放,可按时长 / 次数 / 最近排序。播放次数按 Last.fm 规则(曲目 > 30 秒且播放到一半或 4 分钟)记录,播放时长只累计真正在播放的时间。
28
28
  - **列表操作**:搜索、排序(标题 / 文件名 / 专辑 / 艺术家 / 大小 / 年份 / 文件夹 / 播放次数 / 时长 / 修改时间 / 添加时间,点击同项切换升降序)、多选后「播放选中」或加入播放列表、清空列表。
29
- - **设置面板**:主面板右上角齿轮打开,含「外观 / 歌词 / 音乐库 / 播放」四个标签页:外观调整迷你浮窗与圆形封面显示,歌词调整来源顺序、对齐、字号、翻译与状态栏歌词,音乐库管理音乐文件夹与子文件夹深度(保存后主面板自动重新扫描),播放控制启动恢复、自动播放与每首歌断点。
30
- - **外观设置**:迷你浮窗(右下角可拖动、记忆位置)、播放页圆形封面 / 黑胶底盘 / 封面旋转。
29
+ - **设置面板**:主面板右上角齿轮打开,含「外观 / 歌词 / 音乐库 / 播放」四个标签页:外观调整输入框播放列表按钮与圆形封面显示,歌词调整来源顺序、对齐、字号、翻译与状态栏歌词,音乐库管理音乐文件夹与子文件夹深度(保存后主面板自动重新扫描),播放控制启动恢复、自动播放与每首歌断点。
30
+ - **外观设置**:输入框播放列表按钮(发送按钮左侧,可关闭)、播放页圆形封面 / 黑胶底盘 / 封面旋转。
31
31
  - **歌词设置**:歌词来源顺序(内嵌 / LRC,可调)、对齐方式、字体大小与粗细、模糊非当前歌词、翻译开关与翻译字号缩放、状态栏歌词(输入框内或输入框上方显示当前唱词)。
32
32
  - **歌词校准**:播放页 → 歌词设置里按 ±0.5s 调整偏移;「写回文件」把校准后的 LRC 写回 MP3(ID3 USLT)/ FLAC(Vorbis LYRICS)内嵌歌词,写前自动备份为 `<文件>.lyrics.bak`。
33
33
  - **收藏与最近播放**:底栏 / 歌曲行爱心加入「我喜欢」tab;统计页可切换「收听排行 / 最近播放」。
34
- - **队列编辑**:播放队列支持上移、下移、移除单曲与清空。
34
+ - **队列编辑**:播放队列支持拖动排序与移除单曲。
35
35
  - **搜索**:标题 / 歌手 / 专辑 / 文件夹 / 路径 + 全拼 / 首字母(如 `zhoujielun`、`zjl`),并记住最近 8 条搜索。
36
36
  - **多标签页互斥**:用 Web Locks 选主,只有一个标签页发声与计时;其他标签页底栏提示「另一个标签页正在播放」。
37
37
  - **播放列表**:顶栏「全部歌曲」与每个歌单各占一个 tab,**页面结构完全一致**(搜索、排序、多选、播放全部、循环/随机、队列);「全部歌曲」是不可编辑集合,歌单可点标题改名、删除、逐首移除。右侧 `+` 新建歌单(自动命名,重名递增)并立即选中。歌单存于 `<DSH_HOME>/music/playlists.json`。
package/client.js CHANGED
@@ -33,6 +33,7 @@ window.__ModuleLoader__.load({
33
33
  /** Defaults for every appearance / lyric preference. */
34
34
  var PREF_DEFAULTS = {
35
35
  miniPlayer: true,
36
+ composerQueueButton: true,
36
37
  circularCover: true,
37
38
  vinylRing: false,
38
39
  coverSpin: true,
@@ -94,6 +95,7 @@ window.__ModuleLoader__.load({
94
95
  'page.clearQueue': '清空队列',
95
96
  'page.moveUp': '上移',
96
97
  'page.moveDown': '下移',
98
+ 'page.dragSort': '拖动排序',
97
99
  'page.more': '更多',
98
100
  'page.copyPath': '复制文件路径',
99
101
  'page.copied': '已复制',
@@ -209,6 +211,7 @@ window.__ModuleLoader__.load({
209
211
  'settings.pickEmpty': '没有选中文件夹。',
210
212
  'settings.pickFailed': '无法读取文件夹路径,请重试或手动填写。',
211
213
  'settings.miniPlayer': '迷你浮窗',
214
+ 'settings.composerQueueButton': '输入框播放列表按钮',
212
215
  'settings.circularCover': '播放页圆形封面',
213
216
  'settings.vinylRing': '圆形封面黑胶底盘',
214
217
  'settings.coverSpin': '圆形封面旋转',
@@ -268,6 +271,7 @@ window.__ModuleLoader__.load({
268
271
  'page.clearQueue': 'Clear queue',
269
272
  'page.moveUp': 'Move up',
270
273
  'page.moveDown': 'Move down',
274
+ 'page.dragSort': 'Drag to reorder',
271
275
  'page.more': 'More',
272
276
  'page.copyPath': 'Copy file path',
273
277
  'page.copied': 'Copied',
@@ -383,6 +387,7 @@ window.__ModuleLoader__.load({
383
387
  'settings.pickEmpty': 'No folder selected.',
384
388
  'settings.pickFailed': 'Could not read the folder path. Try again or type it manually.',
385
389
  'settings.miniPlayer': 'Mini player window',
390
+ 'settings.composerQueueButton': 'Playlist button in composer',
386
391
  'settings.circularCover': 'Circular cover on player page',
387
392
  'settings.vinylRing': 'Vinyl ring behind the cover',
388
393
  'settings.coverSpin': 'Rotate the cover',
@@ -1701,6 +1706,34 @@ window.__ModuleLoader__.load({
1701
1706
  emit()
1702
1707
  savePlaybackState()
1703
1708
  },
1709
+ /**
1710
+ * Reorder the queue to match the displayed order (drag & drop).
1711
+ * `indices` are original queue indices in their new visual order.
1712
+ * In random mode the new visual order becomes the play order too.
1713
+ */
1714
+ reorderQueue: function (indices) {
1715
+ var length = state.queue.length
1716
+ if (!Array.isArray(indices) || indices.length !== length) return
1717
+ var seen = {}
1718
+ for (var i = 0; i < length; i += 1) {
1719
+ var at = indices[i]
1720
+ if (!Number.isInteger(at) || at < 0 || at >= length || seen[at] === true) return
1721
+ seen[at] = true
1722
+ }
1723
+ var list = indices.map(function (at) {
1724
+ return state.queue[at]
1725
+ })
1726
+ var current = state.index >= 0 ? indices.indexOf(state.index) : -1
1727
+ state = Object.assign({}, state, { queue: list, index: current })
1728
+ randomOrder =
1729
+ state.order === 'random'
1730
+ ? list.map(function (_, position) {
1731
+ return position
1732
+ })
1733
+ : randomOrderOf(length)
1734
+ emit()
1735
+ savePlaybackState()
1736
+ },
1704
1737
  removeQueueAt: function (at) {
1705
1738
  var length = state.queue.length
1706
1739
  if (at < 0 || at >= length) return
@@ -2001,6 +2034,278 @@ window.__ModuleLoader__.load({
2001
2034
 
2002
2035
  // ── mini player (draggable floating window) ────────────────────────────
2003
2036
 
2037
+ // ── shared playback list (queue overlay) ──────────────────────────────
2038
+ // Opened from the composer button and the player bar. The mini player is
2039
+ // no longer a standalone floating window: it lives in this overlay header.
2040
+
2041
+ var queueOverlayOpen = false
2042
+ var queueOverlayListeners = []
2043
+
2044
+ function queueOverlaySnapshot() {
2045
+ return queueOverlayOpen
2046
+ }
2047
+
2048
+ function toggleQueueOverlay(next) {
2049
+ queueOverlayOpen = next === undefined ? !queueOverlayOpen : next === true
2050
+ queueOverlayListeners.slice().forEach(function (listener) {
2051
+ listener()
2052
+ })
2053
+ }
2054
+
2055
+ function subscribeQueueOverlay(listener) {
2056
+ queueOverlayListeners = queueOverlayListeners.concat([listener])
2057
+ return function () {
2058
+ queueOverlayListeners = queueOverlayListeners.filter(function (item) {
2059
+ return item !== listener
2060
+ })
2061
+ }
2062
+ }
2063
+
2064
+ /** [{ track, originalIndex }] in actual playback order. */
2065
+ function buildQueueView(snap, playOrder) {
2066
+ if (snap.order === 'random' && Array.isArray(playOrder) && playOrder.length === snap.queue.length) {
2067
+ return playOrder.map(function (originalIndex) {
2068
+ return { track: snap.queue[originalIndex], originalIndex: originalIndex }
2069
+ })
2070
+ }
2071
+ return snap.queue.map(function (track, originalIndex) {
2072
+ return { track: track, originalIndex: originalIndex }
2073
+ })
2074
+ }
2075
+
2076
+ function QueueNowPlaying() {
2077
+ var [snap, setSnap] = useState(player.getSnapshot())
2078
+ useEffect(function () {
2079
+ return player.subscribe(function () {
2080
+ setSnap(player.getSnapshot())
2081
+ })
2082
+ }, [])
2083
+ var track = snap.index >= 0 && snap.queue[snap.index] ? snap.queue[snap.index] : null
2084
+ if (track === null) {
2085
+ return h('div', { className: 'mu-qoverlay-now is-empty' }, t('page.noTrack'))
2086
+ }
2087
+ return h(
2088
+ 'div',
2089
+ { className: 'mu-qoverlay-now' },
2090
+ h(Cover, { track: track, size: 44, radius: 8 }),
2091
+ h(
2092
+ 'div',
2093
+ { className: 'mu-qoverlay-now-main' },
2094
+ h('div', { className: 'mu-qoverlay-now-title mu-ellipsis' }, track.title),
2095
+ h('div', { className: 'mu-qoverlay-now-artist mu-ellipsis' }, track.artist || t('page.unknownArtist')),
2096
+ ),
2097
+ h(
2098
+ 'div',
2099
+ { className: 'mu-qoverlay-now-controls' },
2100
+ h(
2101
+ 'button',
2102
+ { type: 'button', className: 'mu-icon-btn', title: t('page.prev'), onClick: function () { player.prev() } },
2103
+ icon('prev', 16),
2104
+ ),
2105
+ h(
2106
+ 'button',
2107
+ {
2108
+ type: 'button',
2109
+ className: 'mu-icon-btn',
2110
+ title: snap.playing ? t('page.pause') : t('page.play'),
2111
+ onClick: function () { player.toggle() },
2112
+ },
2113
+ icon(snap.playing ? 'pause' : 'play', 18),
2114
+ ),
2115
+ h(
2116
+ 'button',
2117
+ { type: 'button', className: 'mu-icon-btn', title: t('page.next'), onClick: function () { player.next() } },
2118
+ icon('next', 16),
2119
+ ),
2120
+ ),
2121
+ )
2122
+ }
2123
+
2124
+ function QueueOverlay() {
2125
+ var [open, setOpen] = useState(queueOverlaySnapshot())
2126
+ var [snap, setSnap] = useState(player.getSnapshot())
2127
+ var [dragFrom, setDragFrom] = useState(null)
2128
+ var [dragOver, setDragOver] = useState(null)
2129
+ var ref = useRef(null)
2130
+
2131
+ useEffect(function () {
2132
+ return subscribeQueueOverlay(function () {
2133
+ setOpen(queueOverlaySnapshot())
2134
+ })
2135
+ }, [])
2136
+ useEffect(function () {
2137
+ return player.subscribe(function () {
2138
+ setSnap(player.getSnapshot())
2139
+ })
2140
+ }, [])
2141
+ useEffect(function () {
2142
+ if (!open) return undefined
2143
+ function onKey(event) {
2144
+ if (event.key === 'Escape') toggleQueueOverlay(false)
2145
+ }
2146
+ function onPointerDown(event) {
2147
+ if (ref.current !== null && ref.current.contains(event.target)) return
2148
+ toggleQueueOverlay(false)
2149
+ }
2150
+ document.addEventListener('keydown', onKey)
2151
+ document.addEventListener('mousedown', onPointerDown)
2152
+ return function () {
2153
+ document.removeEventListener('keydown', onKey)
2154
+ document.removeEventListener('mousedown', onPointerDown)
2155
+ }
2156
+ }, [open])
2157
+
2158
+ if (!open) return null
2159
+ var queueView = buildQueueView(snap, player.getPlayOrder())
2160
+ var rows = queueView.map(function (entry, position) {
2161
+ var track = entry.track
2162
+ var originalIndex = entry.originalIndex
2163
+ var visibleCurrent = originalIndex === snap.index
2164
+ return h(
2165
+ 'div',
2166
+ {
2167
+ key: position + ':' + track.path,
2168
+ className:
2169
+ 'mu-queue-row' +
2170
+ (visibleCurrent ? ' is-current' : '') +
2171
+ (dragFrom === position ? ' is-dragging' : '') +
2172
+ (dragOver === position && dragFrom !== position ? ' is-drop-target' : ''),
2173
+ draggable: true,
2174
+ onClick: function () {
2175
+ player.select(snap.queue, originalIndex)
2176
+ },
2177
+ onDragStart: function (event) {
2178
+ setDragFrom(position)
2179
+ setDragOver(position)
2180
+ if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
2181
+ event.dataTransfer.effectAllowed = 'move'
2182
+ try {
2183
+ event.dataTransfer.setData('text/plain', String(position))
2184
+ } catch (error) {
2185
+ // ignore
2186
+ }
2187
+ }
2188
+ },
2189
+ onDragOver: function (event) {
2190
+ event.preventDefault()
2191
+ if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
2192
+ event.dataTransfer.dropEffect = 'move'
2193
+ }
2194
+ if (dragOver !== position) setDragOver(position)
2195
+ },
2196
+ onDragLeave: function () {
2197
+ if (dragOver === position) setDragOver(null)
2198
+ },
2199
+ onDrop: function (event) {
2200
+ event.preventDefault()
2201
+ var from = dragFrom
2202
+ if (from !== null && from !== position) {
2203
+ var order = queueView.map(function (item) {
2204
+ return item.originalIndex
2205
+ })
2206
+ var moved = order.splice(from, 1)[0]
2207
+ order.splice(position, 0, moved)
2208
+ player.reorderQueue(order)
2209
+ }
2210
+ setDragFrom(null)
2211
+ setDragOver(null)
2212
+ },
2213
+ onDragEnd: function () {
2214
+ setDragFrom(null)
2215
+ setDragOver(null)
2216
+ },
2217
+ },
2218
+ h('span', { className: 'mu-queue-grip', title: t('page.dragSort') }, '⋮⋮'),
2219
+ h(
2220
+ 'span',
2221
+ { className: 'mu-queue-index' },
2222
+ visibleCurrent && snap.playing ? '▶' : String(position + 1),
2223
+ ),
2224
+ h('span', { className: 'mu-ellipsis' }, track.title),
2225
+ h('span', { className: 'mu-queue-artist mu-ellipsis' }, track.artist || ''),
2226
+ h(
2227
+ 'span',
2228
+ { className: 'mu-queue-actions' },
2229
+ h(
2230
+ 'button',
2231
+ {
2232
+ type: 'button',
2233
+ className: 'mu-queue-btn',
2234
+ title: t('page.removeTrack'),
2235
+ onClick: function (event) {
2236
+ event.stopPropagation()
2237
+ player.removeQueueAt(originalIndex)
2238
+ },
2239
+ },
2240
+ '×',
2241
+ ),
2242
+ ),
2243
+ )
2244
+ })
2245
+
2246
+ return h(
2247
+ 'div',
2248
+ { className: 'mu-qoverlay', ref: ref },
2249
+ h(QueueNowPlaying, null),
2250
+ h(
2251
+ 'div',
2252
+ { className: 'mu-qoverlay-head' },
2253
+ h('span', null, t('page.queue')),
2254
+ h('span', { className: 'mu-queue-mode' }, playModeLabel(playModeOf(snap))),
2255
+ h(
2256
+ 'button',
2257
+ { type: 'button', className: 'mu-icon-btn mu-icon-btn-sm', onClick: function () { toggleQueueOverlay(false) } },
2258
+ icon('x', 14),
2259
+ ),
2260
+ ),
2261
+ h(
2262
+ 'div',
2263
+ { className: 'mu-qoverlay-list' },
2264
+ snap.queue.length === 0 ? h('div', { className: 'mu-state-sub' }, t('page.queueEmpty')) : rows,
2265
+ ),
2266
+ )
2267
+ }
2268
+
2269
+ function ComposerQueueButton() {
2270
+ var [snap, setSnap] = useState(player.getSnapshot())
2271
+ var [enabled, setEnabled] = useState(function () {
2272
+ return readPrefs().composerQueueButton !== false
2273
+ })
2274
+ var [open, setOpen] = useState(queueOverlaySnapshot())
2275
+ useEffect(function () {
2276
+ return player.subscribe(function () {
2277
+ setSnap(player.getSnapshot())
2278
+ })
2279
+ }, [])
2280
+ useEffect(function () {
2281
+ function onChange() {
2282
+ setEnabled(readPrefs().composerQueueButton !== false)
2283
+ }
2284
+ window.addEventListener(PREFS_EVENT, onChange)
2285
+ return function () {
2286
+ window.removeEventListener(PREFS_EVENT, onChange)
2287
+ }
2288
+ }, [])
2289
+ useEffect(function () {
2290
+ return subscribeQueueOverlay(function () {
2291
+ setOpen(queueOverlaySnapshot())
2292
+ })
2293
+ }, [])
2294
+ if (!enabled) return null
2295
+ return h(
2296
+ 'button',
2297
+ {
2298
+ type: 'button',
2299
+ className: 'mu-composer-queue-btn' + (open ? ' is-on' : '') + (snap.playing ? ' is-playing' : ''),
2300
+ title: t('page.queue'),
2301
+ onClick: function () {
2302
+ toggleQueueOverlay()
2303
+ },
2304
+ },
2305
+ icon('list', 16),
2306
+ )
2307
+ }
2308
+
2004
2309
  function MiniPlayer() {
2005
2310
  var [enabled, setEnabled] = useState(function () {
2006
2311
  return readPrefs().miniPlayer === true
@@ -3078,18 +3383,6 @@ window.__ModuleLoader__.load({
3078
3383
  var prefs = useRef(readPrefs()).current
3079
3384
  var [snap, setSnap] = useState(player.getSnapshot())
3080
3385
  var leaderNow = player.isLeader()
3081
- var playOrder = player.getPlayOrder()
3082
- // In random mode the queue panel lists songs in the order they will
3083
- // actually play ("from top to bottom"), while edits still target the
3084
- // original queue indices.
3085
- var queueView =
3086
- snap.order === 'random' && Array.isArray(playOrder) && playOrder.length === snap.queue.length
3087
- ? playOrder.map(function (originalIndex) {
3088
- return { track: snap.queue[originalIndex], originalIndex: originalIndex }
3089
- })
3090
- : snap.queue.map(function (track, originalIndex) {
3091
- return { track: track, originalIndex: originalIndex }
3092
- })
3093
3386
  var [config, setConfig] = useState(null)
3094
3387
  var [tracks, setTracks] = useState([])
3095
3388
  var [loading, setLoading] = useState(true)
@@ -3134,7 +3427,12 @@ window.__ModuleLoader__.load({
3134
3427
  var [sortDir, setSortDir] = useState(prefs.sortDir === 'desc' ? 'desc' : 'asc')
3135
3428
  var [sortOpen, setSortOpen] = useState(false)
3136
3429
  var [selected, setSelected] = useState([])
3137
- var [queueOpen, setQueueOpen] = useState(false)
3430
+ var [queueOpen, setQueueOpen] = useState(queueOverlaySnapshot())
3431
+ useEffect(function () {
3432
+ return subscribeQueueOverlay(function () {
3433
+ setQueueOpen(queueOverlaySnapshot())
3434
+ })
3435
+ }, [])
3138
3436
  var [moreOpen, setMoreOpen] = useState(false)
3139
3437
  var [toast, setToast] = useState('')
3140
3438
  var toastTimerRef = useRef(null)
@@ -3905,11 +4203,10 @@ window.__ModuleLoader__.load({
3905
4203
  return h(
3906
4204
  'div',
3907
4205
  { className: 'mu-root' },
3908
- queueOpen || moreOpen
4206
+ moreOpen
3909
4207
  ? h('div', {
3910
4208
  className: 'mu-scrim',
3911
4209
  onClick: function () {
3912
- setQueueOpen(false)
3913
4210
  setMoreOpen(false)
3914
4211
  },
3915
4212
  })
@@ -4225,8 +4522,8 @@ window.__ModuleLoader__.load({
4225
4522
  ? h(
4226
4523
  'div',
4227
4524
  { className: 'mu-prefs-body' },
4228
- settingToggle(t('settings.miniPlayer'), prefsUi.miniPlayer, function (value) {
4229
- updatePref('miniPlayer', value)
4525
+ settingToggle(t('settings.composerQueueButton'), prefsUi.composerQueueButton, function (value) {
4526
+ updatePref('composerQueueButton', value)
4230
4527
  }),
4231
4528
  settingToggle(t('settings.circularCover'), prefsUi.circularCover, function (value) {
4232
4529
  updatePref('circularCover', value)
@@ -4776,7 +5073,7 @@ window.__ModuleLoader__.load({
4776
5073
  className: 'mu-icon-btn' + (queueOpen ? ' is-on' : ''),
4777
5074
  title: t('page.queue'),
4778
5075
  onClick: function () {
4779
- setQueueOpen(!queueOpen)
5076
+ toggleQueueOverlay()
4780
5077
  setMoreOpen(false)
4781
5078
  },
4782
5079
  },
@@ -4810,7 +5107,7 @@ window.__ModuleLoader__.load({
4810
5107
  title: t('page.more'),
4811
5108
  onClick: function () {
4812
5109
  setMoreOpen(!moreOpen)
4813
- setQueueOpen(false)
5110
+ toggleQueueOverlay(false)
4814
5111
  },
4815
5112
  },
4816
5113
  icon('more', 18),
@@ -4819,103 +5116,6 @@ window.__ModuleLoader__.load({
4819
5116
  ),
4820
5117
  ),
4821
5118
 
4822
- queueOpen
4823
- ? h(
4824
- 'div',
4825
- { className: 'mu-pop mu-queue' },
4826
- h(
4827
- 'div',
4828
- { className: 'mu-pop-head' },
4829
- h('span', null, t('page.queue')),
4830
- h('span', { className: 'mu-queue-mode' }, loopLabel()),
4831
- h(
4832
- 'button',
4833
- {
4834
- type: 'button',
4835
- className: 'mu-icon-btn mu-icon-btn-sm',
4836
- onClick: function () {
4837
- setQueueOpen(false)
4838
- },
4839
- },
4840
- icon('x', 14),
4841
- ),
4842
- ),
4843
- h(
4844
- 'div',
4845
- { className: 'mu-pop-body' },
4846
- snap.queue.length === 0
4847
- ? h('div', { className: 'mu-state-sub' }, t('page.queueEmpty'))
4848
- : queueView.map(function (entry, position) {
4849
- var track = entry.track
4850
- var originalIndex = entry.originalIndex
4851
- var visibleCurrent = originalIndex === snap.index
4852
- return h(
4853
- 'div',
4854
- {
4855
- key: position + ':' + track.path,
4856
- className: 'mu-queue-row' + (visibleCurrent ? ' is-current' : ''),
4857
- onClick: function () {
4858
- player.select(snap.queue, originalIndex)
4859
- },
4860
- },
4861
- h(
4862
- 'span',
4863
- { className: 'mu-queue-index' },
4864
- visibleCurrent && snap.playing ? '▶' : String(position + 1),
4865
- ),
4866
- h('span', { className: 'mu-ellipsis' }, track.title),
4867
- h('span', { className: 'mu-queue-artist mu-ellipsis' }, track.artist || ''),
4868
- h(
4869
- 'span',
4870
- { className: 'mu-queue-actions' },
4871
- h(
4872
- 'button',
4873
- {
4874
- type: 'button',
4875
- className: 'mu-queue-btn',
4876
- title: t('page.moveUp'),
4877
- disabled: originalIndex === 0,
4878
- onClick: function (event) {
4879
- event.stopPropagation()
4880
- player.moveQueue(originalIndex, originalIndex - 1)
4881
- },
4882
- },
4883
- '↑',
4884
- ),
4885
- h(
4886
- 'button',
4887
- {
4888
- type: 'button',
4889
- className: 'mu-queue-btn',
4890
- title: t('page.moveDown'),
4891
- disabled: originalIndex === snap.queue.length - 1,
4892
- onClick: function (event) {
4893
- event.stopPropagation()
4894
- player.moveQueue(originalIndex, originalIndex + 1)
4895
- },
4896
- },
4897
- '↓',
4898
- ),
4899
- h(
4900
- 'button',
4901
- {
4902
- type: 'button',
4903
- className: 'mu-queue-btn',
4904
- title: t('page.removeTrack'),
4905
- onClick: function (event) {
4906
- event.stopPropagation()
4907
- player.removeQueueAt(originalIndex)
4908
- },
4909
- },
4910
- '×',
4911
- ),
4912
- ),
4913
- )
4914
- }),
4915
- ),
4916
- )
4917
- : null,
4918
-
4919
5119
  moreOpen
4920
5120
  ? h(
4921
5121
  'div',
@@ -5269,9 +5469,26 @@ window.__ModuleLoader__.load({
5269
5469
  '.mu-bar-right{display:flex;align-items:center;gap:6px;justify-self:end}',
5270
5470
  '.mu-quality{font-size:12px;color:var(--dsw-alias-label-tertiary,#a1a1aa);font-variant-numeric:tabular-nums;white-space:nowrap}',
5271
5471
  '.mu-pop{position:absolute;right:16px;bottom:84px;z-index:40;width:330px;max-height:58%;display:flex;flex-direction:column;border-radius:var(--dsw-radius-md,8px);background:var(--dsw-alias-bg-layer-1,#1a1a1e);border:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));box-shadow:var(--dsw-elevation-prominent,0 16px 48px rgba(0,0,0,.5));overflow:hidden}',
5472
+ '.mu-qoverlay{position:fixed;right:24px;bottom:96px;z-index:70;width:360px;max-width:calc(100vw - 48px);max-height:62vh;display:flex;flex-direction:column;border:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));border-radius:14px;background:var(--dsw-alias-bg-layer-1,#1a1a1e);box-shadow:var(--dsw-elevation-prominent,0 16px 48px rgba(0,0,0,.5));overflow:hidden}',
5473
+ '.mu-qoverlay-now{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08))}',
5474
+ '.mu-qoverlay-now.is-empty{padding:14px 12px;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:12px}',
5475
+ '.mu-qoverlay-now-main{flex:1;min-width:0}',
5476
+ '.mu-qoverlay-now-title{font-size:13px;font-weight:600}',
5477
+ '.mu-qoverlay-now-artist{font-size:11px;color:var(--dsw-alias-label-secondary,#a1a1aa);margin-top:1px}',
5478
+ '.mu-qoverlay-now-controls{display:flex;align-items:center;gap:2px}',
5479
+ '.mu-qoverlay-head{display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:.5px solid var(--dsw-alias-border-l1,rgba(255,255,255,.08));font-size:12px;font-weight:600}',
5480
+ '.mu-qoverlay-head .mu-icon-btn{margin-left:auto}',
5481
+ '.mu-qoverlay-list{flex:1;min-height:0;overflow-y:auto;padding:6px}',
5482
+ '.mu-composer-queue-btn{position:relative;display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-secondary,#a1a1aa);cursor:pointer;flex:none}',
5483
+ '.mu-composer-queue-btn:hover{color:var(--dsw-alias-label-primary,#f4f4f5);background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.07))}',
5484
+ '.mu-composer-queue-btn.is-on{color:var(--dsw-alias-brand-primary,#f4f4f5)}',
5485
+ '.mu-composer-queue-btn.is-playing::after{content:"";position:absolute;top:4px;right:4px;width:5px;height:5px;border-radius:50%;background:var(--dsw-alias-brand-primary,#f4f4f5)}',
5272
5486
  '.mu-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px 6px;font-size:12px;font-weight:600}',
5273
5487
  '.mu-pop-body{overflow-y:auto;padding:0 6px 8px}',
5274
- '.mu-queue-row{display:grid;grid-template-columns:26px minmax(0,1fr) minmax(0,70px) auto;align-items:center;gap:8px;padding:5px 8px;border-radius:var(--dsw-radius-sm,6px);font-size:12px;cursor:pointer}',
5488
+ '.mu-queue-row{display:grid;grid-template-columns:14px 26px minmax(0,1fr) minmax(0,70px) auto;align-items:center;gap:8px;padding:5px 8px;border-radius:var(--dsw-radius-sm,6px);font-size:12px;cursor:pointer}',
5489
+ '.mu-queue-grip{cursor:grab;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:12px;line-height:1;letter-spacing:-2px;text-align:center;user-select:none}',
5490
+ '.mu-queue-row.is-dragging{opacity:.4}',
5491
+ '.mu-queue-row.is-drop-target{box-shadow:inset 0 2px 0 var(--dsw-alias-brand-primary,#f4f4f5)}',
5275
5492
  '.mu-queue-mode{font-size:11px;color:var(--dsw-alias-label-tertiary,#8a8a93);padding:1px 6px;border-radius:999px;background:var(--dsw-alias-bg-layer-3,rgba(255,255,255,.06));white-space:nowrap}',
5276
5493
  '.mu-queue-actions{display:inline-flex;align-items:center;gap:2px;justify-self:end}',
5277
5494
  '.mu-queue-btn{width:20px;height:20px;padding:0;border:0;border-radius:4px;background:transparent;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:12px;line-height:20px;cursor:pointer}',
@@ -5664,8 +5881,14 @@ window.__ModuleLoader__.load({
5664
5881
  },
5665
5882
  )
5666
5883
 
5667
- // Global gradient background (lowest layer) and the mini player window.
5668
- mount('shell.overlay', { name: 'shell.overlay', id: 'music-mini', order: 40 }, MiniPlayer)
5884
+ // Shared playback list overlay (the mini player lives in its header)
5885
+ // and the composer shortcut button before the submit action.
5886
+ mount('shell.overlay', { name: 'shell.overlay', id: 'music-queue', order: 16 }, QueueOverlay)
5887
+ mount(
5888
+ 'conversation.input.right',
5889
+ { name: 'conversation.input.right', id: 'music-queue-button', order: 10 },
5890
+ ComposerQueueButton,
5891
+ )
5669
5892
 
5670
5893
  },
5671
5894
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-music-panel",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "type": "module",
5
5
  "description": "Local music library for DeepSeek Harness: scan a folder, browse tracks with cover art and tags, and play through a sidebar-panel player with loop, shuffle and volume. · DSH 本地音乐插件:扫描音乐目录、读取封面与标签、侧栏入口 + 全屏播放器。",
6
6
  "license": "MIT",