dsh-music-panel 0.3.6 → 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,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.7
4
+
5
+ ### 变更
6
+
7
+ - 会话输入框发送按钮左侧新增播放列表按钮(`conversation.input.right`),点击打开播放列表
8
+ - 设置 → 外观新增「输入框播放列表按钮」开关
9
+ - 播放列表改为全局浮层,主面板底栏按钮与输入框按钮两个入口共用
10
+ - 迷你浮窗并入播放列表顶部(封面 / 标题 / 播放控制),不再作为独立浮窗;移除设置里的迷你浮窗开关
11
+
3
12
  ## 0.3.6
4
13
 
5
14
  ### 变更
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,278 @@ 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 [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
+
2034
2309
  function MiniPlayer() {
2035
2310
  var [enabled, setEnabled] = useState(function () {
2036
2311
  return readPrefs().miniPlayer === true
@@ -3108,18 +3383,6 @@ window.__ModuleLoader__.load({
3108
3383
  var prefs = useRef(readPrefs()).current
3109
3384
  var [snap, setSnap] = useState(player.getSnapshot())
3110
3385
  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
3386
  var [config, setConfig] = useState(null)
3124
3387
  var [tracks, setTracks] = useState([])
3125
3388
  var [loading, setLoading] = useState(true)
@@ -3164,9 +3427,12 @@ window.__ModuleLoader__.load({
3164
3427
  var [sortDir, setSortDir] = useState(prefs.sortDir === 'desc' ? 'desc' : 'asc')
3165
3428
  var [sortOpen, setSortOpen] = useState(false)
3166
3429
  var [selected, setSelected] = useState([])
3167
- var [queueOpen, setQueueOpen] = useState(false)
3168
- var [queueDragFrom, setQueueDragFrom] = useState(null)
3169
- var [queueDragOver, setQueueDragOver] = useState(null)
3430
+ var [queueOpen, setQueueOpen] = useState(queueOverlaySnapshot())
3431
+ useEffect(function () {
3432
+ return subscribeQueueOverlay(function () {
3433
+ setQueueOpen(queueOverlaySnapshot())
3434
+ })
3435
+ }, [])
3170
3436
  var [moreOpen, setMoreOpen] = useState(false)
3171
3437
  var [toast, setToast] = useState('')
3172
3438
  var toastTimerRef = useRef(null)
@@ -3937,11 +4203,10 @@ window.__ModuleLoader__.load({
3937
4203
  return h(
3938
4204
  'div',
3939
4205
  { className: 'mu-root' },
3940
- queueOpen || moreOpen
4206
+ moreOpen
3941
4207
  ? h('div', {
3942
4208
  className: 'mu-scrim',
3943
4209
  onClick: function () {
3944
- setQueueOpen(false)
3945
4210
  setMoreOpen(false)
3946
4211
  },
3947
4212
  })
@@ -4257,8 +4522,8 @@ window.__ModuleLoader__.load({
4257
4522
  ? h(
4258
4523
  'div',
4259
4524
  { className: 'mu-prefs-body' },
4260
- settingToggle(t('settings.miniPlayer'), prefsUi.miniPlayer, function (value) {
4261
- updatePref('miniPlayer', value)
4525
+ settingToggle(t('settings.composerQueueButton'), prefsUi.composerQueueButton, function (value) {
4526
+ updatePref('composerQueueButton', value)
4262
4527
  }),
4263
4528
  settingToggle(t('settings.circularCover'), prefsUi.circularCover, function (value) {
4264
4529
  updatePref('circularCover', value)
@@ -4808,7 +5073,7 @@ window.__ModuleLoader__.load({
4808
5073
  className: 'mu-icon-btn' + (queueOpen ? ' is-on' : ''),
4809
5074
  title: t('page.queue'),
4810
5075
  onClick: function () {
4811
- setQueueOpen(!queueOpen)
5076
+ toggleQueueOverlay()
4812
5077
  setMoreOpen(false)
4813
5078
  },
4814
5079
  },
@@ -4842,7 +5107,7 @@ window.__ModuleLoader__.load({
4842
5107
  title: t('page.more'),
4843
5108
  onClick: function () {
4844
5109
  setMoreOpen(!moreOpen)
4845
- setQueueOpen(false)
5110
+ toggleQueueOverlay(false)
4846
5111
  },
4847
5112
  },
4848
5113
  icon('more', 18),
@@ -4851,122 +5116,6 @@ window.__ModuleLoader__.load({
4851
5116
  ),
4852
5117
  ),
4853
5118
 
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
5119
  moreOpen
4971
5120
  ? h(
4972
5121
  'div',
@@ -5320,6 +5469,20 @@ window.__ModuleLoader__.load({
5320
5469
  '.mu-bar-right{display:flex;align-items:center;gap:6px;justify-self:end}',
5321
5470
  '.mu-quality{font-size:12px;color:var(--dsw-alias-label-tertiary,#a1a1aa);font-variant-numeric:tabular-nums;white-space:nowrap}',
5322
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)}',
5323
5486
  '.mu-pop-head{display:flex;align-items:center;justify-content:space-between;padding:10px 12px 6px;font-size:12px;font-weight:600}',
5324
5487
  '.mu-pop-body{overflow-y:auto;padding:0 6px 8px}',
5325
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}',
@@ -5718,8 +5881,14 @@ window.__ModuleLoader__.load({
5718
5881
  },
5719
5882
  )
5720
5883
 
5721
- // Global gradient background (lowest layer) and the mini player window.
5722
- 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
+ )
5723
5892
 
5724
5893
  },
5725
5894
  }
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.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",