dsh-music-panel 0.3.6 → 0.3.8

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,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.8
4
+
5
+ ### 修复
6
+
7
+ - 修复播放队列拖拽松手后顺序不变:dragstart 中不再同步 setState(避免 Chromium 取消拖拽),拖拽源索引用 ref 记录
8
+ - 拖到行与行之间的空隙也能落位(容器兜底按指针位置计算目标行)
9
+
10
+ ## 0.3.7
11
+
12
+ ### 变更
13
+
14
+ - 会话输入框发送按钮左侧新增播放列表按钮(`conversation.input.right`),点击打开播放列表
15
+ - 设置 → 外观新增「输入框播放列表按钮」开关
16
+ - 播放列表改为全局浮层,主面板底栏按钮与输入框按钮两个入口共用
17
+ - 迷你浮窗并入播放列表顶部(封面 / 标题 / 播放控制),不再作为独立浮窗;移除设置里的迷你浮窗开关
18
+
3
19
  ## 0.3.6
4
20
 
5
21
  ### 变更
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,
@@ -210,6 +211,7 @@ window.__ModuleLoader__.load({
210
211
  'settings.pickEmpty': '没有选中文件夹。',
211
212
  'settings.pickFailed': '无法读取文件夹路径,请重试或手动填写。',
212
213
  'settings.miniPlayer': '迷你浮窗',
214
+ 'settings.composerQueueButton': '输入框播放列表按钮',
213
215
  'settings.circularCover': '播放页圆形封面',
214
216
  'settings.vinylRing': '圆形封面黑胶底盘',
215
217
  'settings.coverSpin': '圆形封面旋转',
@@ -385,6 +387,7 @@ window.__ModuleLoader__.load({
385
387
  'settings.pickEmpty': 'No folder selected.',
386
388
  'settings.pickFailed': 'Could not read the folder path. Try again or type it manually.',
387
389
  'settings.miniPlayer': 'Mini player window',
390
+ 'settings.composerQueueButton': 'Playlist button in composer',
388
391
  'settings.circularCover': 'Circular cover on player page',
389
392
  'settings.vinylRing': 'Vinyl ring behind the cover',
390
393
  'settings.coverSpin': 'Rotate the cover',
@@ -2031,6 +2034,316 @@ window.__ModuleLoader__.load({
2031
2034
 
2032
2035
  // ── mini player (draggable floating window) ────────────────────────────
2033
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 [dragOver, setDragOver] = useState(null)
2128
+ // Keep the source index in a ref: calling setState from dragstart makes
2129
+ // React re-render the dragged node synchronously, which cancels the
2130
+ // browser's drag in Chromium.
2131
+ var dragFromRef = useRef(null)
2132
+ var ref = useRef(null)
2133
+
2134
+ useEffect(function () {
2135
+ return subscribeQueueOverlay(function () {
2136
+ setOpen(queueOverlaySnapshot())
2137
+ })
2138
+ }, [])
2139
+ useEffect(function () {
2140
+ return player.subscribe(function () {
2141
+ setSnap(player.getSnapshot())
2142
+ })
2143
+ }, [])
2144
+ useEffect(function () {
2145
+ if (!open) return undefined
2146
+ function onKey(event) {
2147
+ if (event.key === 'Escape') toggleQueueOverlay(false)
2148
+ }
2149
+ function onPointerDown(event) {
2150
+ if (ref.current !== null && ref.current.contains(event.target)) return
2151
+ toggleQueueOverlay(false)
2152
+ }
2153
+ document.addEventListener('keydown', onKey)
2154
+ document.addEventListener('mousedown', onPointerDown)
2155
+ return function () {
2156
+ document.removeEventListener('keydown', onKey)
2157
+ document.removeEventListener('mousedown', onPointerDown)
2158
+ }
2159
+ }, [open])
2160
+
2161
+ if (!open) return null
2162
+ var queueView = buildQueueView(snap, player.getPlayOrder())
2163
+ var rows = queueView.map(function (entry, position) {
2164
+ var track = entry.track
2165
+ var originalIndex = entry.originalIndex
2166
+ var visibleCurrent = originalIndex === snap.index
2167
+ return h(
2168
+ 'div',
2169
+ {
2170
+ key: position + ':' + track.path,
2171
+ className:
2172
+ 'mu-queue-row' +
2173
+ (visibleCurrent ? ' is-current' : '') +
2174
+ (dragOver === position ? ' is-drop-target' : ''),
2175
+ draggable: true,
2176
+ onClick: function () {
2177
+ player.select(snap.queue, originalIndex)
2178
+ },
2179
+ onDragStart: function (event) {
2180
+ dragFromRef.current = position
2181
+ setDragOver(position)
2182
+ if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
2183
+ event.dataTransfer.effectAllowed = 'move'
2184
+ try {
2185
+ event.dataTransfer.setData('text/plain', String(position))
2186
+ } catch (error) {
2187
+ // ignore
2188
+ }
2189
+ }
2190
+ },
2191
+ onDragOver: function (event) {
2192
+ event.preventDefault()
2193
+ if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
2194
+ event.dataTransfer.dropEffect = 'move'
2195
+ }
2196
+ if (dragOver !== position) setDragOver(position)
2197
+ },
2198
+ onDragLeave: function () {
2199
+ if (dragOver === position) setDragOver(null)
2200
+ },
2201
+ onDrop: function (event) {
2202
+ event.preventDefault()
2203
+ event.stopPropagation()
2204
+ var from = dragFromRef.current
2205
+ if (from !== null && from !== position) {
2206
+ var order = queueView.map(function (item) {
2207
+ return item.originalIndex
2208
+ })
2209
+ var moved = order.splice(from, 1)[0]
2210
+ order.splice(position, 0, moved)
2211
+ player.reorderQueue(order)
2212
+ }
2213
+ dragFromRef.current = null
2214
+ setDragOver(null)
2215
+ },
2216
+ onDragEnd: function () {
2217
+ dragFromRef.current = null
2218
+ setDragOver(null)
2219
+ },
2220
+ },
2221
+ h('span', { className: 'mu-queue-grip', title: t('page.dragSort') }, '⋮⋮'),
2222
+ h(
2223
+ 'span',
2224
+ { className: 'mu-queue-index' },
2225
+ visibleCurrent && snap.playing ? '▶' : String(position + 1),
2226
+ ),
2227
+ h('span', { className: 'mu-ellipsis' }, track.title),
2228
+ h('span', { className: 'mu-queue-artist mu-ellipsis' }, track.artist || ''),
2229
+ h(
2230
+ 'span',
2231
+ { className: 'mu-queue-actions' },
2232
+ h(
2233
+ 'button',
2234
+ {
2235
+ type: 'button',
2236
+ className: 'mu-queue-btn',
2237
+ title: t('page.removeTrack'),
2238
+ onClick: function (event) {
2239
+ event.stopPropagation()
2240
+ player.removeQueueAt(originalIndex)
2241
+ },
2242
+ },
2243
+ '×',
2244
+ ),
2245
+ ),
2246
+ )
2247
+ })
2248
+
2249
+ return h(
2250
+ 'div',
2251
+ { className: 'mu-qoverlay', ref: ref },
2252
+ h(QueueNowPlaying, null),
2253
+ h(
2254
+ 'div',
2255
+ { className: 'mu-qoverlay-head' },
2256
+ h('span', null, t('page.queue')),
2257
+ h('span', { className: 'mu-queue-mode' }, playModeLabel(playModeOf(snap))),
2258
+ h(
2259
+ 'button',
2260
+ { type: 'button', className: 'mu-icon-btn mu-icon-btn-sm', onClick: function () { toggleQueueOverlay(false) } },
2261
+ icon('x', 14),
2262
+ ),
2263
+ ),
2264
+ h(
2265
+ 'div',
2266
+ {
2267
+ className: 'mu-qoverlay-list',
2268
+ onDragOver: function (event) {
2269
+ event.preventDefault()
2270
+ if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
2271
+ event.dataTransfer.dropEffect = 'move'
2272
+ }
2273
+ },
2274
+ // Dropping in the gap between rows (no row handles it) still
2275
+ // reorders: pick the row under the pointer.
2276
+ onDrop: function (event) {
2277
+ event.preventDefault()
2278
+ var from = dragFromRef.current
2279
+ if (from === null) return
2280
+ var rowEls = event.currentTarget.querySelectorAll('.mu-queue-row')
2281
+ var target = queueView.length - 1
2282
+ for (var i = 0; i < rowEls.length; i += 1) {
2283
+ var rect = rowEls[i].getBoundingClientRect()
2284
+ if (event.clientY < rect.top + rect.height / 2) {
2285
+ target = i
2286
+ break
2287
+ }
2288
+ target = i
2289
+ }
2290
+ if (target !== from) {
2291
+ var order = queueView.map(function (item) {
2292
+ return item.originalIndex
2293
+ })
2294
+ var moved = order.splice(from, 1)[0]
2295
+ order.splice(target, 0, moved)
2296
+ player.reorderQueue(order)
2297
+ }
2298
+ dragFromRef.current = null
2299
+ setDragOver(null)
2300
+ },
2301
+ },
2302
+ snap.queue.length === 0 ? h('div', { className: 'mu-state-sub' }, t('page.queueEmpty')) : rows,
2303
+ ),
2304
+ )
2305
+ }
2306
+
2307
+ function ComposerQueueButton() {
2308
+ var [snap, setSnap] = useState(player.getSnapshot())
2309
+ var [enabled, setEnabled] = useState(function () {
2310
+ return readPrefs().composerQueueButton !== false
2311
+ })
2312
+ var [open, setOpen] = useState(queueOverlaySnapshot())
2313
+ useEffect(function () {
2314
+ return player.subscribe(function () {
2315
+ setSnap(player.getSnapshot())
2316
+ })
2317
+ }, [])
2318
+ useEffect(function () {
2319
+ function onChange() {
2320
+ setEnabled(readPrefs().composerQueueButton !== false)
2321
+ }
2322
+ window.addEventListener(PREFS_EVENT, onChange)
2323
+ return function () {
2324
+ window.removeEventListener(PREFS_EVENT, onChange)
2325
+ }
2326
+ }, [])
2327
+ useEffect(function () {
2328
+ return subscribeQueueOverlay(function () {
2329
+ setOpen(queueOverlaySnapshot())
2330
+ })
2331
+ }, [])
2332
+ if (!enabled) return null
2333
+ return h(
2334
+ 'button',
2335
+ {
2336
+ type: 'button',
2337
+ className: 'mu-composer-queue-btn' + (open ? ' is-on' : '') + (snap.playing ? ' is-playing' : ''),
2338
+ title: t('page.queue'),
2339
+ onClick: function () {
2340
+ toggleQueueOverlay()
2341
+ },
2342
+ },
2343
+ icon('list', 16),
2344
+ )
2345
+ }
2346
+
2034
2347
  function MiniPlayer() {
2035
2348
  var [enabled, setEnabled] = useState(function () {
2036
2349
  return readPrefs().miniPlayer === true
@@ -3108,18 +3421,6 @@ window.__ModuleLoader__.load({
3108
3421
  var prefs = useRef(readPrefs()).current
3109
3422
  var [snap, setSnap] = useState(player.getSnapshot())
3110
3423
  var leaderNow = player.isLeader()
3111
- var playOrder = player.getPlayOrder()
3112
- // In random mode the queue panel lists songs in the order they will
3113
- // actually play ("from top to bottom"), while edits still target the
3114
- // original queue indices.
3115
- var queueView =
3116
- snap.order === 'random' && Array.isArray(playOrder) && playOrder.length === snap.queue.length
3117
- ? playOrder.map(function (originalIndex) {
3118
- return { track: snap.queue[originalIndex], originalIndex: originalIndex }
3119
- })
3120
- : snap.queue.map(function (track, originalIndex) {
3121
- return { track: track, originalIndex: originalIndex }
3122
- })
3123
3424
  var [config, setConfig] = useState(null)
3124
3425
  var [tracks, setTracks] = useState([])
3125
3426
  var [loading, setLoading] = useState(true)
@@ -3164,9 +3465,12 @@ window.__ModuleLoader__.load({
3164
3465
  var [sortDir, setSortDir] = useState(prefs.sortDir === 'desc' ? 'desc' : 'asc')
3165
3466
  var [sortOpen, setSortOpen] = useState(false)
3166
3467
  var [selected, setSelected] = useState([])
3167
- var [queueOpen, setQueueOpen] = useState(false)
3168
- var [queueDragFrom, setQueueDragFrom] = useState(null)
3169
- var [queueDragOver, setQueueDragOver] = useState(null)
3468
+ var [queueOpen, setQueueOpen] = useState(queueOverlaySnapshot())
3469
+ useEffect(function () {
3470
+ return subscribeQueueOverlay(function () {
3471
+ setQueueOpen(queueOverlaySnapshot())
3472
+ })
3473
+ }, [])
3170
3474
  var [moreOpen, setMoreOpen] = useState(false)
3171
3475
  var [toast, setToast] = useState('')
3172
3476
  var toastTimerRef = useRef(null)
@@ -3937,11 +4241,10 @@ window.__ModuleLoader__.load({
3937
4241
  return h(
3938
4242
  'div',
3939
4243
  { className: 'mu-root' },
3940
- queueOpen || moreOpen
4244
+ moreOpen
3941
4245
  ? h('div', {
3942
4246
  className: 'mu-scrim',
3943
4247
  onClick: function () {
3944
- setQueueOpen(false)
3945
4248
  setMoreOpen(false)
3946
4249
  },
3947
4250
  })
@@ -4257,8 +4560,8 @@ window.__ModuleLoader__.load({
4257
4560
  ? h(
4258
4561
  'div',
4259
4562
  { className: 'mu-prefs-body' },
4260
- settingToggle(t('settings.miniPlayer'), prefsUi.miniPlayer, function (value) {
4261
- updatePref('miniPlayer', value)
4563
+ settingToggle(t('settings.composerQueueButton'), prefsUi.composerQueueButton, function (value) {
4564
+ updatePref('composerQueueButton', value)
4262
4565
  }),
4263
4566
  settingToggle(t('settings.circularCover'), prefsUi.circularCover, function (value) {
4264
4567
  updatePref('circularCover', value)
@@ -4808,7 +5111,7 @@ window.__ModuleLoader__.load({
4808
5111
  className: 'mu-icon-btn' + (queueOpen ? ' is-on' : ''),
4809
5112
  title: t('page.queue'),
4810
5113
  onClick: function () {
4811
- setQueueOpen(!queueOpen)
5114
+ toggleQueueOverlay()
4812
5115
  setMoreOpen(false)
4813
5116
  },
4814
5117
  },
@@ -4842,7 +5145,7 @@ window.__ModuleLoader__.load({
4842
5145
  title: t('page.more'),
4843
5146
  onClick: function () {
4844
5147
  setMoreOpen(!moreOpen)
4845
- setQueueOpen(false)
5148
+ toggleQueueOverlay(false)
4846
5149
  },
4847
5150
  },
4848
5151
  icon('more', 18),
@@ -4851,122 +5154,6 @@ window.__ModuleLoader__.load({
4851
5154
  ),
4852
5155
  ),
4853
5156
 
4854
- queueOpen
4855
- ? h(
4856
- 'div',
4857
- { className: 'mu-pop mu-queue' },
4858
- h(
4859
- 'div',
4860
- { className: 'mu-pop-head' },
4861
- h('span', null, t('page.queue')),
4862
- h('span', { className: 'mu-queue-mode' }, loopLabel()),
4863
- h(
4864
- 'button',
4865
- {
4866
- type: 'button',
4867
- className: 'mu-icon-btn mu-icon-btn-sm',
4868
- onClick: function () {
4869
- setQueueOpen(false)
4870
- },
4871
- },
4872
- icon('x', 14),
4873
- ),
4874
- ),
4875
- h(
4876
- 'div',
4877
- { className: 'mu-pop-body' },
4878
- snap.queue.length === 0
4879
- ? h('div', { className: 'mu-state-sub' }, t('page.queueEmpty'))
4880
- : queueView.map(function (entry, position) {
4881
- var track = entry.track
4882
- var originalIndex = entry.originalIndex
4883
- var visibleCurrent = originalIndex === snap.index
4884
- return h(
4885
- 'div',
4886
- {
4887
- key: position + ':' + track.path,
4888
- className:
4889
- 'mu-queue-row' +
4890
- (visibleCurrent ? ' is-current' : '') +
4891
- (queueDragFrom === position ? ' is-dragging' : '') +
4892
- (queueDragOver === position && queueDragFrom !== position ? ' is-drop-target' : ''),
4893
- draggable: true,
4894
- onClick: function () {
4895
- player.select(snap.queue, originalIndex)
4896
- },
4897
- onDragStart: function (event) {
4898
- setQueueDragFrom(position)
4899
- setQueueDragOver(position)
4900
- if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
4901
- event.dataTransfer.effectAllowed = 'move'
4902
- // Firefox needs data for a drag to start.
4903
- try {
4904
- event.dataTransfer.setData('text/plain', String(position))
4905
- } catch (error) {
4906
- // ignore
4907
- }
4908
- }
4909
- },
4910
- onDragOver: function (event) {
4911
- event.preventDefault()
4912
- if (event.dataTransfer !== null && event.dataTransfer !== undefined) {
4913
- event.dataTransfer.dropEffect = 'move'
4914
- }
4915
- if (queueDragOver !== position) setQueueDragOver(position)
4916
- },
4917
- onDragLeave: function () {
4918
- if (queueDragOver === position) setQueueDragOver(null)
4919
- },
4920
- onDrop: function (event) {
4921
- event.preventDefault()
4922
- var from = queueDragFrom
4923
- if (from !== null && from !== position) {
4924
- var order = queueView.map(function (item) {
4925
- return item.originalIndex
4926
- })
4927
- var moved = order.splice(from, 1)[0]
4928
- order.splice(position, 0, moved)
4929
- player.reorderQueue(order)
4930
- }
4931
- setQueueDragFrom(null)
4932
- setQueueDragOver(null)
4933
- },
4934
- onDragEnd: function () {
4935
- setQueueDragFrom(null)
4936
- setQueueDragOver(null)
4937
- },
4938
- },
4939
- h('span', { className: 'mu-queue-grip', title: t('page.dragSort') }, '⋮⋮'),
4940
- h(
4941
- 'span',
4942
- { className: 'mu-queue-index' },
4943
- visibleCurrent && snap.playing ? '▶' : String(position + 1),
4944
- ),
4945
- h('span', { className: 'mu-ellipsis' }, track.title),
4946
- h('span', { className: 'mu-queue-artist mu-ellipsis' }, track.artist || ''),
4947
- h(
4948
- 'span',
4949
- { className: 'mu-queue-actions' },
4950
- h(
4951
- 'button',
4952
- {
4953
- type: 'button',
4954
- className: 'mu-queue-btn',
4955
- title: t('page.removeTrack'),
4956
- onClick: function (event) {
4957
- event.stopPropagation()
4958
- player.removeQueueAt(originalIndex)
4959
- },
4960
- },
4961
- '×',
4962
- ),
4963
- ),
4964
- )
4965
- }),
4966
- ),
4967
- )
4968
- : null,
4969
-
4970
5157
  moreOpen
4971
5158
  ? h(
4972
5159
  'div',
@@ -5320,6 +5507,20 @@ window.__ModuleLoader__.load({
5320
5507
  '.mu-bar-right{display:flex;align-items:center;gap:6px;justify-self:end}',
5321
5508
  '.mu-quality{font-size:12px;color:var(--dsw-alias-label-tertiary,#a1a1aa);font-variant-numeric:tabular-nums;white-space:nowrap}',
5322
5509
  '.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}',
5510
+ '.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}',
5511
+ '.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))}',
5512
+ '.mu-qoverlay-now.is-empty{padding:14px 12px;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:12px}',
5513
+ '.mu-qoverlay-now-main{flex:1;min-width:0}',
5514
+ '.mu-qoverlay-now-title{font-size:13px;font-weight:600}',
5515
+ '.mu-qoverlay-now-artist{font-size:11px;color:var(--dsw-alias-label-secondary,#a1a1aa);margin-top:1px}',
5516
+ '.mu-qoverlay-now-controls{display:flex;align-items:center;gap:2px}',
5517
+ '.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}',
5518
+ '.mu-qoverlay-head .mu-icon-btn{margin-left:auto}',
5519
+ '.mu-qoverlay-list{flex:1;min-height:0;overflow-y:auto;padding:6px}',
5520
+ '.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}',
5521
+ '.mu-composer-queue-btn:hover{color:var(--dsw-alias-label-primary,#f4f4f5);background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.07))}',
5522
+ '.mu-composer-queue-btn.is-on{color:var(--dsw-alias-brand-primary,#f4f4f5)}',
5523
+ '.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)}',
5323
5524
  '.mu-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px 6px;font-size:12px;font-weight:600}',
5324
5525
  '.mu-pop-body{overflow-y:auto;padding:0 6px 8px}',
5325
5526
  '.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}',
@@ -5718,8 +5919,14 @@ window.__ModuleLoader__.load({
5718
5919
  },
5719
5920
  )
5720
5921
 
5721
- // Global gradient background (lowest layer) and the mini player window.
5722
- mount('shell.overlay', { name: 'shell.overlay', id: 'music-mini', order: 40 }, MiniPlayer)
5922
+ // Shared playback list overlay (the mini player lives in its header)
5923
+ // and the composer shortcut button before the submit action.
5924
+ mount('shell.overlay', { name: 'shell.overlay', id: 'music-queue', order: 16 }, QueueOverlay)
5925
+ mount(
5926
+ 'conversation.input.right',
5927
+ { name: 'conversation.input.right', id: 'music-queue-button', order: 10 },
5928
+ ComposerQueueButton,
5929
+ )
5723
5930
 
5724
5931
  },
5725
5932
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-music-panel",
3
- "version": "0.3.6",
3
+ "version": "0.3.8",
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",