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 +16 -0
- package/README.md +4 -1
- package/client.js +394 -40
- package/lib/routes.js +38 -0
- package/package.json +1 -1
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
|
-
- **收藏与最近播放**:底栏 /
|
|
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
|
-
|
|
3939
|
+
function copyPath(track) {
|
|
3940
|
+
var target = track && track.path ? track : current
|
|
3941
|
+
if (!target) return
|
|
3873
3942
|
try {
|
|
3874
|
-
navigator.clipboard.writeText(
|
|
3943
|
+
navigator.clipboard.writeText(target.path).then(
|
|
3875
3944
|
function () {
|
|
3876
3945
|
showToast(t('page.copied'))
|
|
3877
3946
|
},
|
|
3878
3947
|
function () {
|
|
3879
|
-
showToast(
|
|
3948
|
+
showToast(target.path)
|
|
3880
3949
|
},
|
|
3881
3950
|
)
|
|
3882
3951
|
} catch (error) {
|
|
3883
|
-
showToast(
|
|
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
|
-
'
|
|
5027
|
-
{
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
5032
|
-
|
|
5033
|
-
|
|
5034
|
-
|
|
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
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
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-
|
|
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.
|
|
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",
|