dsh-music-panel 0.3.11 → 0.3.13

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.13
4
+
5
+ ### 修复
6
+
7
+ - 「在文件夹中显示」在 Windows 上没有定位到文件:explorer 参数改为 `/select,"C:\path\file"`(引号紧贴逗号),并用 `windowsVerbatimArguments` 原样传参,避免 Node 重新加引号/转义导致 explorer 忽略 `/select`
8
+
9
+ ## 0.3.12
10
+
11
+ ### 变更
12
+
13
+ - 收藏爱心改为红色(`--dsw-alias-state-error-primary`)
14
+ - 多选「添加到播放列表…」改为「添加到歌单」下拉菜单:显示歌单名与曲目数、支持新建歌单并添加,外观与交互重做
15
+ - 歌曲右键菜单:添加到歌单、添加到播放列表、复制文件地址、在文件夹中显示、详细信息
16
+ - 歌单视图的「从歌单移除」移入右键菜单,行内 × 按钮移除
17
+ - 新增 Host 接口 `POST /music/reveal`(仅限已配置音乐目录内的文件):Windows `explorer /select`、macOS `open -R`、Linux `xdg-open`
18
+
3
19
  ## 0.3.11
4
20
 
5
21
  ### 变更
package/README.md CHANGED
@@ -30,7 +30,9 @@ DeepSeek Harness 的本地音乐插件:左侧栏一个音符入口,点开是
30
30
  - **外观设置**:输入框播放列表按钮(发送按钮左侧,可关闭)、播放页圆形封面 / 黑胶底盘 / 封面旋转。
31
31
  - **歌词设置**:歌词来源顺序(内嵌 / LRC,可调)、对齐方式、字体大小与粗细、模糊非当前歌词、翻译开关与翻译字号缩放、状态栏歌词(输入框内或输入框上方显示当前唱词)。
32
32
  - **歌词校准**:播放页 → 歌词设置里按 ±0.5s 调整偏移;「写回文件」把校准后的 LRC 写回 MP3(ID3 USLT)/ FLAC(Vorbis LYRICS)内嵌歌词,写前自动备份为 `<文件>.lyrics.bak`。
33
- - **收藏与最近播放**:底栏 / 歌曲行爱心加入「我喜欢」tab;统计页可切换「收听排行 / 最近播放」。
33
+ - **收藏与最近播放**:底栏 / 歌曲行爱心(红色)加入「我喜欢」tab;统计页可切换「收听排行 / 最近播放」。
34
+ - **右键菜单**:歌曲行右键可添加到歌单 / 添加到播放列表 / 复制文件地址 / 在文件夹中显示 / 查看详细信息;歌单视图里还可从歌单移除。
35
+ - **添加到歌单**:多选后从下拉菜单选择目标歌单(显示曲目数),或新建歌单并添加。
34
36
  - **队列编辑**:播放队列支持拖动排序与移除单曲。
35
37
  - **搜索**:标题 / 歌手 / 专辑 / 文件夹 / 路径 + 全拼 / 首字母(如 `zhoujielun`、`zjl`),并记住最近 8 条搜索。
36
38
  - **多标签页互斥**:用 Web Locks 选主,只有一个标签页发声与计时;其他标签页底栏提示「另一个标签页正在播放」。
@@ -82,6 +84,7 @@ plugin_manager install_bundle D:/SystemFiles/Documents/Project/DSH/plugins/dsh-m
82
84
  | GET | `/music/favorites` | 收藏列表(按收藏时间倒序) |
83
85
  | POST | `/music/favorites` | 切换收藏,body `{ path }` |
84
86
  | POST | `/music/lyrics/calibrate` | 保存歌词偏移 `{ path, offsetMs, write? }`;`write: true` 写回内嵌并清零偏移 |
87
+ | POST | `/music/reveal` | 在系统文件管理器中定位文件(仅已配置目录内),body `{ path }` |
85
88
  | GET | `/music/stats?range=week\|month\|all` | 按范围聚合的播放次数 / 时长 / 最近播放 |
86
89
  | GET | `/music/cover?path=…` | 内嵌封面;无封面返回 404 |
87
90
 
package/client.js CHANGED
@@ -122,6 +122,11 @@ window.__ModuleLoader__.load({
122
122
  'page.format': '格式',
123
123
  'page.bitrate': '码率',
124
124
  'page.sampleRate': '采样率',
125
+ 'page.artist': '歌手',
126
+ 'page.album': '专辑',
127
+ 'page.duration': '时长',
128
+ 'page.size': '文件大小',
129
+ 'page.plays': '播放次数',
125
130
  'page.none': '—',
126
131
  'page.selectedCount': '已选 {n} 首',
127
132
  'page.noResults': '没有匹配的歌曲',
@@ -172,7 +177,14 @@ window.__ModuleLoader__.load({
172
177
  'page.deleteConfirm': '删除「{name}」?此操作不可撤销。',
173
178
  'page.back': '返回',
174
179
  'page.playAll': '播放全部',
175
- 'page.addTo': '添加到播放列表…',
180
+ 'page.addTo': '添加到歌单',
181
+ 'page.addToPlaylist': '添加到播放列表',
182
+ 'page.addedToPlaylist': '已添加到播放列表',
183
+ 'page.reveal': '在文件夹中显示',
184
+ 'page.revealed': '已在文件夹中打开',
185
+ 'page.details': '详细信息',
186
+ 'page.noPlaylists': '还没有歌单',
187
+ 'page.newPlaylistAdd': '新建歌单并添加',
176
188
  'page.addedCount': '已添加 {n} 首',
177
189
  'page.trackCount': '{n} 首',
178
190
  'page.removeTrack': '从歌单移除',
@@ -298,6 +310,11 @@ window.__ModuleLoader__.load({
298
310
  'page.format': 'Format',
299
311
  'page.bitrate': 'Bitrate',
300
312
  'page.sampleRate': 'Sample rate',
313
+ 'page.artist': 'Artist',
314
+ 'page.album': 'Album',
315
+ 'page.duration': 'Duration',
316
+ 'page.size': 'File size',
317
+ 'page.plays': 'Plays',
301
318
  'page.none': '—',
302
319
  'page.selectedCount': '{n} selected',
303
320
  'page.noResults': 'No matching songs',
@@ -348,7 +365,14 @@ window.__ModuleLoader__.load({
348
365
  'page.deleteConfirm': 'Delete “{name}”? This cannot be undone.',
349
366
  'page.back': 'Back',
350
367
  'page.playAll': 'Play all',
351
- 'page.addTo': 'Add to playlist…',
368
+ 'page.addTo': 'Add to playlist',
369
+ 'page.addToPlaylist': 'Add to play queue',
370
+ 'page.addedToPlaylist': 'Added to play queue',
371
+ 'page.reveal': 'Show in folder',
372
+ 'page.revealed': 'Opened in folder',
373
+ 'page.details': 'Details',
374
+ 'page.noPlaylists': 'No playlists yet',
375
+ 'page.newPlaylistAdd': 'New playlist and add',
352
376
  'page.addedCount': '{n} added',
353
377
  'page.trackCount': '{n} tracks',
354
378
  'page.removeTrack': 'Remove from playlist',
@@ -774,6 +798,19 @@ window.__ModuleLoader__.load({
774
798
  folder: {
775
799
  parts: [['path', { d: 'M3 7a2 2 0 012-2h4l2 2h8a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z' }]],
776
800
  },
801
+ copy: {
802
+ parts: [
803
+ ['rect', { x: 9, y: 9, width: 13, height: 13, rx: 2, ry: 2 }],
804
+ ['path', { d: 'M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1' }],
805
+ ],
806
+ },
807
+ info: {
808
+ parts: [
809
+ ['circle', { cx: 12, cy: 12, r: 10 }],
810
+ ['path', { d: 'M12 16v-4' }],
811
+ ['path', { d: 'M12 8h.01' }],
812
+ ],
813
+ },
777
814
  }
778
815
 
779
816
  function merge(base, extra) {
@@ -1661,6 +1698,21 @@ window.__ModuleLoader__.load({
1661
1698
  randomOrder = randomOrderOf(list.length)
1662
1699
  load(at, true)
1663
1700
  },
1701
+ /** Append one track to the queue without interrupting playback. */
1702
+ enqueue: function (track) {
1703
+ if (track === null || track === undefined || typeof track.path !== 'string' || track.path === '') return
1704
+ if (state.queue.length === 0) {
1705
+ api.playList([track], 0)
1706
+ return
1707
+ }
1708
+ state = Object.assign({}, state, {
1709
+ queue: state.queue.concat([track]),
1710
+ index: state.index < 0 ? 0 : state.index,
1711
+ })
1712
+ randomOrder = randomOrderOf(state.queue.length)
1713
+ emit()
1714
+ savePlaybackState()
1715
+ },
1664
1716
  select: function (list, at) {
1665
1717
  if (!list || list.length === 0 || at < 0) return
1666
1718
  if (at === state.index && sameQueue(list, state.queue)) {
@@ -3574,6 +3626,22 @@ window.__ModuleLoader__.load({
3574
3626
  setQueueOpen(queueOverlaySnapshot())
3575
3627
  })
3576
3628
  }, [])
3629
+ useEffect(
3630
+ function () {
3631
+ if (ctxMenu === null) return undefined
3632
+ function onKey(event) {
3633
+ if (event.key === 'Escape') setCtxMenu(null)
3634
+ }
3635
+ window.addEventListener('keydown', onKey)
3636
+ return function () {
3637
+ window.removeEventListener('keydown', onKey)
3638
+ }
3639
+ },
3640
+ [ctxMenu],
3641
+ )
3642
+ var [ctxMenu, setCtxMenu] = useState(null)
3643
+ var [addToOpen, setAddToOpen] = useState(false)
3644
+ var [infoPath, setInfoPath] = useState(null)
3577
3645
  var [moreOpen, setMoreOpen] = useState(false)
3578
3646
  var [toast, setToast] = useState('')
3579
3647
  var toastTimerRef = useRef(null)
@@ -3868,22 +3936,51 @@ window.__ModuleLoader__.load({
3868
3936
  setSelected([])
3869
3937
  }
3870
3938
 
3871
- function copyPath() {
3872
- if (!current) return
3939
+ function copyPath(track) {
3940
+ var target = track && track.path ? track : current
3941
+ if (!target) return
3873
3942
  try {
3874
- navigator.clipboard.writeText(current.path).then(
3943
+ navigator.clipboard.writeText(target.path).then(
3875
3944
  function () {
3876
3945
  showToast(t('page.copied'))
3877
3946
  },
3878
3947
  function () {
3879
- showToast(current.path)
3948
+ showToast(target.path)
3880
3949
  },
3881
3950
  )
3882
3951
  } catch (error) {
3883
- showToast(current.path)
3952
+ showToast(target.path)
3884
3953
  }
3885
3954
  }
3886
3955
 
3956
+ function revealTrack(track) {
3957
+ if (!track) return
3958
+ requestJson('POST', API + 'reveal', { path: track.path })
3959
+ .then(function () {
3960
+ showToast(t('page.revealed'))
3961
+ })
3962
+ .catch(function (error) {
3963
+ showToast(String((error && error.message) || error))
3964
+ })
3965
+ }
3966
+
3967
+ function createPlaylistWithSelection() {
3968
+ var paths = selectedTracks.map(function (track) {
3969
+ return track.path
3970
+ })
3971
+ if (paths.length === 0) return
3972
+ requestJson('POST', API + 'playlists', { name: '' })
3973
+ .then(function (body) {
3974
+ return patchPlaylist(body.playlist.id, { add: paths })
3975
+ })
3976
+ .then(function () {
3977
+ showToast(t('page.addedCount', { n: paths.length }))
3978
+ })
3979
+ .catch(function (error) {
3980
+ showToast(String((error && error.message) || error))
3981
+ })
3982
+ }
3983
+
3887
3984
  var trackByPath = useMemo(function () {
3888
3985
  var map = {}
3889
3986
  tracks.forEach(function (track) {
@@ -4088,6 +4185,12 @@ window.__ModuleLoader__.load({
4088
4185
  onDoubleClick: function () {
4089
4186
  activate(track)
4090
4187
  },
4188
+ onContextMenu: function (event) {
4189
+ event.preventDefault()
4190
+ event.stopPropagation()
4191
+ setMoreOpen(false)
4192
+ setCtxMenu({ x: event.clientX, y: event.clientY, path: track.path, mode: 'main' })
4193
+ },
4091
4194
  },
4092
4195
  h(
4093
4196
  'button',
@@ -4148,23 +4251,6 @@ window.__ModuleLoader__.load({
4148
4251
  },
4149
4252
  icon('heart', 14),
4150
4253
  ),
4151
- view === 'playlists' && activePlaylist !== undefined
4152
- ? h(
4153
- 'button',
4154
- {
4155
- type: 'button',
4156
- className: 'mu-icon-btn mu-icon-btn-sm',
4157
- title: t('page.removeTrack'),
4158
- onClick: function (event) {
4159
- event.stopPropagation()
4160
- patchPlaylist(activePlaylist.id, { remove: [track.path] }).catch(function (error) {
4161
- showToast(String((error && error.message) || error))
4162
- })
4163
- },
4164
- },
4165
- icon('x', 14),
4166
- )
4167
- : null,
4168
4254
  h(
4169
4255
  'button',
4170
4256
  {
@@ -4352,6 +4438,14 @@ window.__ModuleLoader__.load({
4352
4438
  },
4353
4439
  })
4354
4440
  : null,
4441
+ addToOpen
4442
+ ? h('div', {
4443
+ className: 'mu-addto-scrim',
4444
+ onClick: function () {
4445
+ setAddToOpen(false)
4446
+ },
4447
+ })
4448
+ : null,
4355
4449
  h(
4356
4450
  'div',
4357
4451
  { className: 'mu-head' },
@@ -5023,21 +5117,60 @@ window.__ModuleLoader__.load({
5023
5117
  selected.length === visible.length ? t('page.selectNone') : t('page.selectAll'),
5024
5118
  ),
5025
5119
  h(
5026
- 'select',
5027
- {
5028
- className: 'mu-select',
5029
- value: '',
5030
- title: t('page.addTo'),
5031
- onChange: function (event) {
5032
- var value = event.target.value
5033
- event.target.value = ''
5034
- if (value !== '') addSelectedTo(value)
5120
+ 'div',
5121
+ { className: 'mu-addto' },
5122
+ h(
5123
+ 'button',
5124
+ {
5125
+ type: 'button',
5126
+ className: 'mu-chip' + (addToOpen ? ' is-on' : ''),
5127
+ onClick: function () {
5128
+ setAddToOpen(!addToOpen)
5129
+ },
5035
5130
  },
5036
- },
5037
- h('option', { value: '' }, t('page.addTo')),
5038
- playlists.map(function (playlist) {
5039
- return h('option', { key: playlist.id, value: playlist.id }, playlist.name)
5040
- }),
5131
+ icon('plus', 13),
5132
+ h('span', null, t('page.addTo')),
5133
+ icon('chevronDown', 12),
5134
+ ),
5135
+ addToOpen
5136
+ ? h(
5137
+ 'div',
5138
+ { className: 'mu-addto-menu' },
5139
+ playlists.length === 0
5140
+ ? h('div', { className: 'mu-addto-empty' }, t('page.noPlaylists'))
5141
+ : playlists.map(function (playlist) {
5142
+ return h(
5143
+ 'button',
5144
+ {
5145
+ key: playlist.id,
5146
+ type: 'button',
5147
+ className: 'mu-addto-item',
5148
+ onClick: function () {
5149
+ addSelectedTo(playlist.id)
5150
+ setAddToOpen(false)
5151
+ },
5152
+ },
5153
+ icon('list', 12),
5154
+ h('span', { className: 'mu-ellipsis' }, playlist.name),
5155
+ h('span', { className: 'mu-addto-count' }, String(playlist.trackPaths.length)),
5156
+ )
5157
+ }),
5158
+ h('div', { className: 'mu-addto-sep' }),
5159
+ h(
5160
+ 'button',
5161
+ {
5162
+ type: 'button',
5163
+ className: 'mu-addto-item',
5164
+ onClick: function () {
5165
+ createPlaylistWithSelection()
5166
+ setAddToOpen(false)
5167
+ },
5168
+ },
5169
+ icon('plus', 12),
5170
+ h('span', null, t('page.newPlaylistAdd')),
5171
+ ),
5172
+ )
5173
+ : null,
5041
5174
  ),
5042
5175
  )
5043
5176
  : null,
@@ -5281,7 +5414,7 @@ window.__ModuleLoader__.load({
5281
5414
  : h('div', { className: 'mu-state-sub' }, t('page.noTrack')),
5282
5415
  h(
5283
5416
  'button',
5284
- { type: 'button', className: 'mu-menu-item', disabled: !current, onClick: copyPath },
5417
+ { type: 'button', className: 'mu-menu-item', disabled: !current, onClick: function () { copyPath() } },
5285
5418
  h('span', null, t('page.copyPath')),
5286
5419
  ),
5287
5420
  )
@@ -5289,6 +5422,206 @@ window.__ModuleLoader__.load({
5289
5422
 
5290
5423
  ),
5291
5424
 
5425
+ ctxMenu !== null
5426
+ ? (function () {
5427
+ var target = trackByPath[ctxMenu.path] || null
5428
+ var winWidth = typeof window !== 'undefined' && window.innerWidth ? window.innerWidth : 1200
5429
+ var winHeight = typeof window !== 'undefined' && window.innerHeight ? window.innerHeight : 800
5430
+ var left = Math.max(8, Math.min(ctxMenu.x, winWidth - 220))
5431
+ var top = Math.max(8, Math.min(ctxMenu.y, winHeight - (ctxMenu.mode === 'playlists' ? 320 : 280)))
5432
+ function close() {
5433
+ setCtxMenu(null)
5434
+ }
5435
+ var children = []
5436
+ if (ctxMenu.mode === 'playlists') {
5437
+ children.push(
5438
+ h('div', { className: 'mu-ctx-head', key: 'head' }, t('page.addTo')),
5439
+ playlists.length === 0
5440
+ ? h('div', { className: 'mu-addto-empty', key: 'empty' }, t('page.noPlaylists'))
5441
+ : playlists.map(function (playlist) {
5442
+ return h(
5443
+ 'button',
5444
+ {
5445
+ key: playlist.id,
5446
+ type: 'button',
5447
+ className: 'mu-ctx-item',
5448
+ onClick: function () {
5449
+ if (target === null) return
5450
+ patchPlaylist(playlist.id, { add: [target.path] })
5451
+ .then(function () {
5452
+ showToast(t('page.addedCount', { n: 1 }))
5453
+ })
5454
+ .catch(function (error) {
5455
+ showToast(String((error && error.message) || error))
5456
+ })
5457
+ close()
5458
+ },
5459
+ },
5460
+ icon('list', 12),
5461
+ h('span', { className: 'mu-ellipsis' }, playlist.name),
5462
+ h('span', { className: 'mu-addto-count' }, String(playlist.trackPaths.length)),
5463
+ )
5464
+ }),
5465
+ )
5466
+ } else {
5467
+ children.push(
5468
+ h(
5469
+ 'button',
5470
+ {
5471
+ key: 'add-to-playlist',
5472
+ type: 'button',
5473
+ className: 'mu-ctx-item',
5474
+ onClick: function () {
5475
+ setCtxMenu({ x: ctxMenu.x, y: ctxMenu.y, path: ctxMenu.path, mode: 'playlists' })
5476
+ },
5477
+ },
5478
+ icon('list', 12),
5479
+ h('span', { className: 'mu-ellipsis' }, t('page.addTo')),
5480
+ h('span', { className: 'mu-ctx-arrow' }, '›'),
5481
+ ),
5482
+ h(
5483
+ 'button',
5484
+ {
5485
+ key: 'enqueue',
5486
+ type: 'button',
5487
+ className: 'mu-ctx-item',
5488
+ onClick: function () {
5489
+ if (target !== null) {
5490
+ player.enqueue(target)
5491
+ showToast(t('page.addedToPlaylist'))
5492
+ }
5493
+ close()
5494
+ },
5495
+ },
5496
+ icon('plus', 12),
5497
+ h('span', null, t('page.addToPlaylist')),
5498
+ ),
5499
+ h('div', { className: 'mu-ctx-sep', key: 'sep1' }),
5500
+ h(
5501
+ 'button',
5502
+ {
5503
+ key: 'copy',
5504
+ type: 'button',
5505
+ className: 'mu-ctx-item',
5506
+ onClick: function () {
5507
+ copyPath(target)
5508
+ close()
5509
+ },
5510
+ },
5511
+ icon('copy', 12),
5512
+ h('span', null, t('page.copyPath')),
5513
+ ),
5514
+ h(
5515
+ 'button',
5516
+ {
5517
+ key: 'reveal',
5518
+ type: 'button',
5519
+ className: 'mu-ctx-item',
5520
+ onClick: function () {
5521
+ revealTrack(target)
5522
+ close()
5523
+ },
5524
+ },
5525
+ icon('folder', 12),
5526
+ h('span', null, t('page.reveal')),
5527
+ ),
5528
+ h(
5529
+ 'button',
5530
+ {
5531
+ key: 'details',
5532
+ type: 'button',
5533
+ className: 'mu-ctx-item',
5534
+ onClick: function () {
5535
+ setInfoPath(ctxMenu.path)
5536
+ close()
5537
+ },
5538
+ },
5539
+ icon('info', 12),
5540
+ h('span', null, t('page.details')),
5541
+ ),
5542
+ view === 'playlists' && activePlaylist !== undefined
5543
+ ? h(
5544
+ 'button',
5545
+ {
5546
+ key: 'remove',
5547
+ type: 'button',
5548
+ className: 'mu-ctx-item is-danger',
5549
+ onClick: function () {
5550
+ if (target !== null) {
5551
+ patchPlaylist(activePlaylist.id, { remove: [target.path] }).catch(function (error) {
5552
+ showToast(String((error && error.message) || error))
5553
+ })
5554
+ }
5555
+ close()
5556
+ },
5557
+ },
5558
+ icon('x', 12),
5559
+ h('span', null, t('page.removeTrack')),
5560
+ )
5561
+ : null,
5562
+ )
5563
+ }
5564
+ return h(
5565
+ 'div',
5566
+ { key: 'ctx-layer' },
5567
+ h('div', { className: 'mu-ctx-scrim', onClick: close }),
5568
+ h('div', { className: 'mu-ctx', style: { left: left + 'px', top: top + 'px' } }, children),
5569
+ )
5570
+ })()
5571
+ : null,
5572
+
5573
+ infoPath !== null
5574
+ ? (function () {
5575
+ var info = trackByPath[infoPath]
5576
+ if (!info) return null
5577
+ var rows = [
5578
+ [t('page.title'), info.title],
5579
+ [t('page.artist'), info.artist || t('page.unknownArtist')],
5580
+ [t('page.album'), info.album || t('page.none')],
5581
+ [t('page.duration'), fmtTime(info.durationSec)],
5582
+ [t('page.format'), (info.codec || info.ext || '').toUpperCase()],
5583
+ [t('page.bitrate'), fmtKbps(info.bitrate) || t('page.none')],
5584
+ [t('page.sampleRate'), fmtKhz(info.sampleRate) || t('page.none')],
5585
+ [t('page.size'), info.size > 0 ? (info.size / 1024 / 1024).toFixed(2) + ' MB' : t('page.none')],
5586
+ [t('page.plays'), String(info.plays || 0)],
5587
+ [t('page.path'), info.path],
5588
+ ]
5589
+ return h(
5590
+ 'div',
5591
+ { className: 'mu-info-layer' },
5592
+ h('div', { className: 'mu-info-scrim', onClick: function () { setInfoPath(null) } }),
5593
+ h(
5594
+ 'div',
5595
+ { className: 'mu-info' },
5596
+ h(
5597
+ 'div',
5598
+ { className: 'mu-info-head' },
5599
+ h('span', { className: 'mu-info-title' }, t('page.details')),
5600
+ h(
5601
+ 'button',
5602
+ {
5603
+ type: 'button',
5604
+ className: 'mu-icon-btn mu-icon-btn-sm',
5605
+ onClick: function () {
5606
+ setInfoPath(null)
5607
+ },
5608
+ },
5609
+ icon('x', 14),
5610
+ ),
5611
+ ),
5612
+ rows.map(function (row) {
5613
+ return h(
5614
+ 'div',
5615
+ { className: 'mu-more-row', key: row[0] },
5616
+ h('span', { className: 'mu-more-key' }, row[0]),
5617
+ h('span', { className: 'mu-more-val mu-ellipsis', title: row[1] }, row[1]),
5618
+ )
5619
+ }),
5620
+ ),
5621
+ )
5622
+ })()
5623
+ : null,
5624
+
5292
5625
  toast !== '' ? h('div', { className: 'mu-toast' }, toast) : null,
5293
5626
 
5294
5627
  npOpen
@@ -5639,8 +5972,29 @@ window.__ModuleLoader__.load({
5639
5972
  '.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}',
5640
5973
  '.mu-queue-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06));color:var(--dsw-alias-label-primary,#f4f4f5)}',
5641
5974
  '.mu-queue-btn:disabled{opacity:.35;cursor:default}',
5642
- '.mu-fav.is-on{color:var(--dsw-alias-brand-primary,#f4f4f5)}',
5975
+ '.mu-fav.is-on{color:var(--dsw-alias-state-error-primary,#e5484d)}',
5643
5976
  '.mu-fav.is-on svg{fill:currentColor}',
5977
+ '.mu-addto{position:relative}',
5978
+ '.mu-addto-menu{position:absolute;right:0;top:calc(100% + 4px);z-index:53;min-width:210px;max-height:280px;overflow-y:auto;padding:4px;border:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));border-radius:10px;background:var(--dsw-alias-bg-layer-1,#1a1a1e);box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.45))}',
5979
+ '.mu-addto-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary,#f4f4f5);font-size:12px;text-align:left;cursor:pointer}',
5980
+ '.mu-addto-item:hover{background:var(--dsw-alias-bg-layer-2,#2a2a2e)}',
5981
+ '.mu-addto-count{margin-left:auto;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:11px;font-variant-numeric:tabular-nums}',
5982
+ '.mu-addto-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1,rgba(255,255,255,.08))}',
5983
+ '.mu-addto-empty{padding:10px;font-size:12px;color:var(--dsw-alias-label-tertiary,#8a8a93)}',
5984
+ '.mu-addto-scrim{position:fixed;inset:0;z-index:52}',
5985
+ '.mu-ctx-scrim{position:fixed;inset:0;z-index:89}',
5986
+ '.mu-ctx{position:fixed;z-index:90;min-width:200px;max-height:320px;overflow-y:auto;padding:4px;border:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.1));border-radius:10px;background:var(--dsw-alias-bg-layer-1,#1a1a1e);box-shadow:var(--dsw-elevation-prominent,0 16px 40px rgba(0,0,0,.5))}',
5987
+ '.mu-ctx-head{padding:6px 10px 4px;font-size:11px;color:var(--dsw-alias-label-tertiary,#8a8a93)}',
5988
+ '.mu-ctx-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 10px;border:0;border-radius:7px;background:transparent;color:var(--dsw-alias-label-primary,#f4f4f5);font-size:12px;text-align:left;cursor:pointer}',
5989
+ '.mu-ctx-item:hover{background:var(--dsw-alias-bg-layer-2,#2a2a2e)}',
5990
+ '.mu-ctx-item.is-danger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary,#e5484d) 16%,transparent);color:var(--dsw-alias-state-error-primary,#e5484d)}',
5991
+ '.mu-ctx-arrow{margin-left:auto;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:14px;line-height:1}',
5992
+ '.mu-ctx-sep{height:1px;margin:4px 6px;background:var(--dsw-alias-border-l1,rgba(255,255,255,.08))}',
5993
+ '.mu-info-layer{position:fixed;inset:0;z-index:94;pointer-events:none}',
5994
+ '.mu-info-scrim{position:absolute;inset:0;background:rgba(0,0,0,.35);pointer-events:auto}',
5995
+ '.mu-info{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:440px;max-width:calc(100vw - 48px);max-height:70vh;overflow-y:auto;padding:16px;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 20px 60px rgba(0,0,0,.55));pointer-events:auto}',
5996
+ '.mu-info-head{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:10px}',
5997
+ '.mu-info-title{font-size:14px;font-weight:600}',
5644
5998
  '.mu-chip-sm{height:24px;padding:0 8px;font-size:11px}',
5645
5999
  '.mu-search-history{position:absolute;top:34px;right:0;z-index:20;display:flex;flex-wrap:wrap;gap:4px;max-width:320px;padding:6px;border:.5px solid var(--dsw-alias-border-l2,rgba(255,255,255,.12));border-radius:8px;background:var(--dsw-alias-bg-layer-1,#1a1a1e);box-shadow:var(--dsw-elevation-prominent,0 8px 24px rgba(0,0,0,.35))}',
5646
6000
  '.mu-queue-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(255,255,255,.06))}',
package/lib/routes.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * @module dsh-music-panel/routes
22
22
  */
23
23
 
24
+ import { spawn } from 'node:child_process'
24
25
  import { createReadStream } from 'node:fs'
25
26
  import { readdir, realpath, stat } from 'node:fs/promises'
26
27
  import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
@@ -388,6 +389,43 @@ export function createRoutes({ settings, library, playlists, stats, playback, fa
388
389
  return sendJson(res, 200, { ok: true, favorite: result.favorite, favorites: result.favorites })
389
390
  }
390
391
 
392
+ // Reveal a track in the OS file manager. Only files already inside the
393
+ // configured roots reach this point; args are passed to spawn without a
394
+ // shell, so the path cannot inject commands.
395
+ if (pathname === '/reveal' && method === 'POST') {
396
+ let body
397
+ try {
398
+ body = await readJson(req)
399
+ } catch {
400
+ return fail(res, 400, 'BAD_JSON', '请求体不是合法 JSON')
401
+ }
402
+ const config = await settings.read()
403
+ const file = await resolveTrack(body?.path, config.roots)
404
+ if (file === null) return fail(res, 404, 'NOT_FOUND', '文件不在已配置的音乐目录内')
405
+ try {
406
+ // Explorer parses its command line itself: the reliable form is
407
+ // `/select,"C:\path\file.mp3"` with quotes hugging the comma. Node
408
+ // would re-quote/escape that argument, so pass it verbatim.
409
+ const opener =
410
+ process.platform === 'win32'
411
+ ? { name: 'explorer.exe', args: ['/select,"' + file + '"'], verbatim: true }
412
+ : process.platform === 'darwin'
413
+ ? { name: 'open', args: ['-R', file], verbatim: false }
414
+ : { name: 'xdg-open', args: [dirname(file)], verbatim: false }
415
+ const child = spawn(opener.name, opener.args, {
416
+ detached: true,
417
+ stdio: 'ignore',
418
+ windowsVerbatimArguments: opener.verbatim,
419
+ })
420
+ // Without this listener an ENOENT would surface as an unhandled error.
421
+ child.on('error', () => {})
422
+ child.unref()
423
+ return sendJson(res, 200, { ok: true })
424
+ } catch (error) {
425
+ return fail(res, 500, 'REVEAL_FAILED', String(error?.message ?? error))
426
+ }
427
+ }
428
+
391
429
  if (pathname === '/playlists' && method === 'GET') {
392
430
  return sendJson(res, 200, { ok: true, playlists: await playlists.list() })
393
431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-music-panel",
3
- "version": "0.3.11",
3
+ "version": "0.3.13",
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",