dsh-music-panel 0.3.10 → 0.3.12
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 +17 -0
- package/README.md +4 -1
- package/client.js +483 -42
- package/lib/routes.js +31 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.12
|
|
4
|
+
|
|
5
|
+
### 变更
|
|
6
|
+
|
|
7
|
+
- 收藏爱心改为红色(`--dsw-alias-state-error-primary`)
|
|
8
|
+
- 多选「添加到播放列表…」改为「添加到歌单」下拉菜单:显示歌单名与曲目数、支持新建歌单并添加,外观与交互重做
|
|
9
|
+
- 歌曲右键菜单:添加到歌单、添加到播放列表、复制文件地址、在文件夹中显示、详细信息
|
|
10
|
+
- 歌单视图的「从歌单移除」移入右键菜单,行内 × 按钮移除
|
|
11
|
+
- 新增 Host 接口 `POST /music/reveal`(仅限已配置音乐目录内的文件):Windows `explorer /select`、macOS `open -R`、Linux `xdg-open`
|
|
12
|
+
|
|
13
|
+
## 0.3.11
|
|
14
|
+
|
|
15
|
+
### 变更
|
|
16
|
+
|
|
17
|
+
- 播放列表面板支持拖动移动位置:拖动顶部正在播放条/标题区即可,位置记在偏好里,下次打开恢复
|
|
18
|
+
- 播放列表面板内禁用文本框选(`user-select: none`)
|
|
19
|
+
|
|
3
20
|
## 0.3.10
|
|
4
21
|
|
|
5
22
|
### 变更
|
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
|
@@ -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
|
+
queuePos: null,
|
|
36
37
|
composerQueueButton: true,
|
|
37
38
|
circularCover: true,
|
|
38
39
|
vinylRing: false,
|
|
@@ -121,6 +122,11 @@ window.__ModuleLoader__.load({
|
|
|
121
122
|
'page.format': '格式',
|
|
122
123
|
'page.bitrate': '码率',
|
|
123
124
|
'page.sampleRate': '采样率',
|
|
125
|
+
'page.artist': '歌手',
|
|
126
|
+
'page.album': '专辑',
|
|
127
|
+
'page.duration': '时长',
|
|
128
|
+
'page.size': '文件大小',
|
|
129
|
+
'page.plays': '播放次数',
|
|
124
130
|
'page.none': '—',
|
|
125
131
|
'page.selectedCount': '已选 {n} 首',
|
|
126
132
|
'page.noResults': '没有匹配的歌曲',
|
|
@@ -171,7 +177,14 @@ window.__ModuleLoader__.load({
|
|
|
171
177
|
'page.deleteConfirm': '删除「{name}」?此操作不可撤销。',
|
|
172
178
|
'page.back': '返回',
|
|
173
179
|
'page.playAll': '播放全部',
|
|
174
|
-
'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': '新建歌单并添加',
|
|
175
188
|
'page.addedCount': '已添加 {n} 首',
|
|
176
189
|
'page.trackCount': '{n} 首',
|
|
177
190
|
'page.removeTrack': '从歌单移除',
|
|
@@ -297,6 +310,11 @@ window.__ModuleLoader__.load({
|
|
|
297
310
|
'page.format': 'Format',
|
|
298
311
|
'page.bitrate': 'Bitrate',
|
|
299
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',
|
|
300
318
|
'page.none': '—',
|
|
301
319
|
'page.selectedCount': '{n} selected',
|
|
302
320
|
'page.noResults': 'No matching songs',
|
|
@@ -347,7 +365,14 @@ window.__ModuleLoader__.load({
|
|
|
347
365
|
'page.deleteConfirm': 'Delete “{name}”? This cannot be undone.',
|
|
348
366
|
'page.back': 'Back',
|
|
349
367
|
'page.playAll': 'Play all',
|
|
350
|
-
'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',
|
|
351
376
|
'page.addedCount': '{n} added',
|
|
352
377
|
'page.trackCount': '{n} tracks',
|
|
353
378
|
'page.removeTrack': 'Remove from playlist',
|
|
@@ -773,6 +798,19 @@ window.__ModuleLoader__.load({
|
|
|
773
798
|
folder: {
|
|
774
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' }]],
|
|
775
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
|
+
},
|
|
776
814
|
}
|
|
777
815
|
|
|
778
816
|
function merge(base, extra) {
|
|
@@ -1660,6 +1698,21 @@ window.__ModuleLoader__.load({
|
|
|
1660
1698
|
randomOrder = randomOrderOf(list.length)
|
|
1661
1699
|
load(at, true)
|
|
1662
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
|
+
},
|
|
1663
1716
|
select: function (list, at) {
|
|
1664
1717
|
if (!list || list.length === 0 || at < 0) return
|
|
1665
1718
|
if (at === state.index && sameQueue(list, state.queue)) {
|
|
@@ -2124,6 +2177,12 @@ window.__ModuleLoader__.load({
|
|
|
2124
2177
|
function QueueOverlay() {
|
|
2125
2178
|
var [open, setOpen] = useState(queueOverlaySnapshot())
|
|
2126
2179
|
var [snap, setSnap] = useState(player.getSnapshot())
|
|
2180
|
+
var [pos, setPos] = useState(function () {
|
|
2181
|
+
var saved = readPrefs().queuePos
|
|
2182
|
+
if (saved === null || typeof saved !== 'object') return null
|
|
2183
|
+
if (!isFinite(saved.x) || !isFinite(saved.y)) return null
|
|
2184
|
+
return { x: Number(saved.x), y: Number(saved.y) }
|
|
2185
|
+
})
|
|
2127
2186
|
// Drag state lives entirely in refs/DOM classes so React never
|
|
2128
2187
|
// re-renders the dragged row mid-drag (Chromium cancels that).
|
|
2129
2188
|
var dragFromRef = useRef(null)
|
|
@@ -2245,6 +2304,73 @@ window.__ModuleLoader__.load({
|
|
|
2245
2304
|
document.addEventListener('pointercancel', onUp)
|
|
2246
2305
|
}
|
|
2247
2306
|
|
|
2307
|
+
// Drag the whole panel by its header strip (buttons and queue rows keep
|
|
2308
|
+
// their own gestures). Position is remembered in prefs.
|
|
2309
|
+
function startPanelDrag(event) {
|
|
2310
|
+
if (event.button !== undefined && event.button !== 0) return
|
|
2311
|
+
var target = event.target
|
|
2312
|
+
if (target === null || target === undefined || typeof target.closest !== 'function') return
|
|
2313
|
+
if (target.closest('button, .mu-queue-row, .mu-queue-grip') !== null) return
|
|
2314
|
+
if (target.closest('.mu-qoverlay-now, .mu-qoverlay-head') === null) return
|
|
2315
|
+
var el = ref.current
|
|
2316
|
+
if (el === null) return
|
|
2317
|
+
var rect = el.getBoundingClientRect()
|
|
2318
|
+
var start = {
|
|
2319
|
+
x: event.clientX,
|
|
2320
|
+
y: event.clientY,
|
|
2321
|
+
left: rect.left,
|
|
2322
|
+
top: rect.top,
|
|
2323
|
+
width: rect.width,
|
|
2324
|
+
height: rect.height,
|
|
2325
|
+
moved: false,
|
|
2326
|
+
}
|
|
2327
|
+
event.preventDefault()
|
|
2328
|
+
document.body.style.userSelect = 'none'
|
|
2329
|
+
document.body.style.cursor = 'grabbing'
|
|
2330
|
+
el.classList.add('is-dragging-panel')
|
|
2331
|
+
|
|
2332
|
+
function clamp(value, min, max) {
|
|
2333
|
+
return Math.max(min, Math.min(max, value))
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function onMove(moveEvent) {
|
|
2337
|
+
start.moved = true
|
|
2338
|
+
var x = clamp(
|
|
2339
|
+
start.left + (moveEvent.clientX - start.x),
|
|
2340
|
+
8,
|
|
2341
|
+
Math.max(8, window.innerWidth - start.width - 8),
|
|
2342
|
+
)
|
|
2343
|
+
var y = clamp(
|
|
2344
|
+
start.top + (moveEvent.clientY - start.y),
|
|
2345
|
+
8,
|
|
2346
|
+
Math.max(8, window.innerHeight - start.height - 8),
|
|
2347
|
+
)
|
|
2348
|
+
el.style.left = x + 'px'
|
|
2349
|
+
el.style.top = y + 'px'
|
|
2350
|
+
el.style.right = 'auto'
|
|
2351
|
+
el.style.bottom = 'auto'
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
function onUp() {
|
|
2355
|
+
document.removeEventListener('pointermove', onMove)
|
|
2356
|
+
document.removeEventListener('pointerup', onUp)
|
|
2357
|
+
document.removeEventListener('pointercancel', onUp)
|
|
2358
|
+
document.body.style.userSelect = ''
|
|
2359
|
+
document.body.style.cursor = ''
|
|
2360
|
+
el.classList.remove('is-dragging-panel')
|
|
2361
|
+
if (start.moved) {
|
|
2362
|
+
var next = el.getBoundingClientRect()
|
|
2363
|
+
var position = { x: Math.round(next.left), y: Math.round(next.top) }
|
|
2364
|
+
setPos(position)
|
|
2365
|
+
savePrefs({ queuePos: position })
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
document.addEventListener('pointermove', onMove)
|
|
2370
|
+
document.addEventListener('pointerup', onUp)
|
|
2371
|
+
document.addEventListener('pointercancel', onUp)
|
|
2372
|
+
}
|
|
2373
|
+
|
|
2248
2374
|
var rows = queueView.map(function (entry, position) {
|
|
2249
2375
|
var track = entry.track
|
|
2250
2376
|
var originalIndex = entry.originalIndex
|
|
@@ -2299,7 +2425,17 @@ window.__ModuleLoader__.load({
|
|
|
2299
2425
|
|
|
2300
2426
|
return h(
|
|
2301
2427
|
'div',
|
|
2302
|
-
{
|
|
2428
|
+
{
|
|
2429
|
+
className: 'mu-qoverlay',
|
|
2430
|
+
ref: ref,
|
|
2431
|
+
style:
|
|
2432
|
+
pos !== null
|
|
2433
|
+
? { left: pos.x + 'px', top: pos.y + 'px', right: 'auto', bottom: 'auto' }
|
|
2434
|
+
: undefined,
|
|
2435
|
+
onPointerDown: function (event) {
|
|
2436
|
+
startPanelDrag(event)
|
|
2437
|
+
},
|
|
2438
|
+
},
|
|
2303
2439
|
h(QueueNowPlaying, null),
|
|
2304
2440
|
h(
|
|
2305
2441
|
'div',
|
|
@@ -3490,6 +3626,22 @@ window.__ModuleLoader__.load({
|
|
|
3490
3626
|
setQueueOpen(queueOverlaySnapshot())
|
|
3491
3627
|
})
|
|
3492
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)
|
|
3493
3645
|
var [moreOpen, setMoreOpen] = useState(false)
|
|
3494
3646
|
var [toast, setToast] = useState('')
|
|
3495
3647
|
var toastTimerRef = useRef(null)
|
|
@@ -3784,22 +3936,51 @@ window.__ModuleLoader__.load({
|
|
|
3784
3936
|
setSelected([])
|
|
3785
3937
|
}
|
|
3786
3938
|
|
|
3787
|
-
function copyPath() {
|
|
3788
|
-
|
|
3939
|
+
function copyPath(track) {
|
|
3940
|
+
var target = track && track.path ? track : current
|
|
3941
|
+
if (!target) return
|
|
3789
3942
|
try {
|
|
3790
|
-
navigator.clipboard.writeText(
|
|
3943
|
+
navigator.clipboard.writeText(target.path).then(
|
|
3791
3944
|
function () {
|
|
3792
3945
|
showToast(t('page.copied'))
|
|
3793
3946
|
},
|
|
3794
3947
|
function () {
|
|
3795
|
-
showToast(
|
|
3948
|
+
showToast(target.path)
|
|
3796
3949
|
},
|
|
3797
3950
|
)
|
|
3798
3951
|
} catch (error) {
|
|
3799
|
-
showToast(
|
|
3952
|
+
showToast(target.path)
|
|
3800
3953
|
}
|
|
3801
3954
|
}
|
|
3802
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
|
+
|
|
3803
3984
|
var trackByPath = useMemo(function () {
|
|
3804
3985
|
var map = {}
|
|
3805
3986
|
tracks.forEach(function (track) {
|
|
@@ -4004,6 +4185,12 @@ window.__ModuleLoader__.load({
|
|
|
4004
4185
|
onDoubleClick: function () {
|
|
4005
4186
|
activate(track)
|
|
4006
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
|
+
},
|
|
4007
4194
|
},
|
|
4008
4195
|
h(
|
|
4009
4196
|
'button',
|
|
@@ -4064,23 +4251,6 @@ window.__ModuleLoader__.load({
|
|
|
4064
4251
|
},
|
|
4065
4252
|
icon('heart', 14),
|
|
4066
4253
|
),
|
|
4067
|
-
view === 'playlists' && activePlaylist !== undefined
|
|
4068
|
-
? h(
|
|
4069
|
-
'button',
|
|
4070
|
-
{
|
|
4071
|
-
type: 'button',
|
|
4072
|
-
className: 'mu-icon-btn mu-icon-btn-sm',
|
|
4073
|
-
title: t('page.removeTrack'),
|
|
4074
|
-
onClick: function (event) {
|
|
4075
|
-
event.stopPropagation()
|
|
4076
|
-
patchPlaylist(activePlaylist.id, { remove: [track.path] }).catch(function (error) {
|
|
4077
|
-
showToast(String((error && error.message) || error))
|
|
4078
|
-
})
|
|
4079
|
-
},
|
|
4080
|
-
},
|
|
4081
|
-
icon('x', 14),
|
|
4082
|
-
)
|
|
4083
|
-
: null,
|
|
4084
4254
|
h(
|
|
4085
4255
|
'button',
|
|
4086
4256
|
{
|
|
@@ -4268,6 +4438,14 @@ window.__ModuleLoader__.load({
|
|
|
4268
4438
|
},
|
|
4269
4439
|
})
|
|
4270
4440
|
: null,
|
|
4441
|
+
addToOpen
|
|
4442
|
+
? h('div', {
|
|
4443
|
+
className: 'mu-addto-scrim',
|
|
4444
|
+
onClick: function () {
|
|
4445
|
+
setAddToOpen(false)
|
|
4446
|
+
},
|
|
4447
|
+
})
|
|
4448
|
+
: null,
|
|
4271
4449
|
h(
|
|
4272
4450
|
'div',
|
|
4273
4451
|
{ className: 'mu-head' },
|
|
@@ -4939,21 +5117,60 @@ window.__ModuleLoader__.load({
|
|
|
4939
5117
|
selected.length === visible.length ? t('page.selectNone') : t('page.selectAll'),
|
|
4940
5118
|
),
|
|
4941
5119
|
h(
|
|
4942
|
-
'
|
|
4943
|
-
{
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
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
|
+
},
|
|
4951
5130
|
},
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
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,
|
|
4957
5174
|
),
|
|
4958
5175
|
)
|
|
4959
5176
|
: null,
|
|
@@ -5197,7 +5414,7 @@ window.__ModuleLoader__.load({
|
|
|
5197
5414
|
: h('div', { className: 'mu-state-sub' }, t('page.noTrack')),
|
|
5198
5415
|
h(
|
|
5199
5416
|
'button',
|
|
5200
|
-
{ type: 'button', className: 'mu-menu-item', disabled: !current, onClick: copyPath },
|
|
5417
|
+
{ type: 'button', className: 'mu-menu-item', disabled: !current, onClick: function () { copyPath() } },
|
|
5201
5418
|
h('span', null, t('page.copyPath')),
|
|
5202
5419
|
),
|
|
5203
5420
|
)
|
|
@@ -5205,6 +5422,206 @@ window.__ModuleLoader__.load({
|
|
|
5205
5422
|
|
|
5206
5423
|
),
|
|
5207
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
|
+
|
|
5208
5625
|
toast !== '' ? h('div', { className: 'mu-toast' }, toast) : null,
|
|
5209
5626
|
|
|
5210
5627
|
npOpen
|
|
@@ -5527,7 +5944,10 @@ window.__ModuleLoader__.load({
|
|
|
5527
5944
|
'.mu-bar-right{display:flex;align-items:center;gap:6px;justify-self:end}',
|
|
5528
5945
|
'.mu-quality{font-size:12px;color:var(--dsw-alias-label-tertiary,#a1a1aa);font-variant-numeric:tabular-nums;white-space:nowrap}',
|
|
5529
5946
|
'.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}',
|
|
5530
|
-
'.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}',
|
|
5947
|
+
'.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;user-select:none}',
|
|
5948
|
+
'.mu-qoverlay.is-dragging-panel{cursor:grabbing}',
|
|
5949
|
+
'.mu-qoverlay-now,.mu-qoverlay-head{cursor:grab}',
|
|
5950
|
+
'.mu-qoverlay-now:active,.mu-qoverlay-head:active{cursor:grabbing}',
|
|
5531
5951
|
'.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))}',
|
|
5532
5952
|
'.mu-qoverlay-now.is-empty{padding:14px 12px;color:var(--dsw-alias-label-tertiary,#8a8a93);font-size:12px}',
|
|
5533
5953
|
'.mu-qoverlay-now-main{flex:1;min-width:0}',
|
|
@@ -5552,8 +5972,29 @@ window.__ModuleLoader__.load({
|
|
|
5552
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}',
|
|
5553
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)}',
|
|
5554
5974
|
'.mu-queue-btn:disabled{opacity:.35;cursor:default}',
|
|
5555
|
-
'.mu-fav.is-on{color:var(--dsw-alias-
|
|
5975
|
+
'.mu-fav.is-on{color:var(--dsw-alias-state-error-primary,#e5484d)}',
|
|
5556
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}',
|
|
5557
5998
|
'.mu-chip-sm{height:24px;padding:0 8px;font-size:11px}',
|
|
5558
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))}',
|
|
5559
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,36 @@ 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
|
+
const opener =
|
|
407
|
+
process.platform === 'win32'
|
|
408
|
+
? { name: 'explorer.exe', args: ['/select,' + file] }
|
|
409
|
+
: process.platform === 'darwin'
|
|
410
|
+
? { name: 'open', args: ['-R', file] }
|
|
411
|
+
: { name: 'xdg-open', args: [dirname(file)] }
|
|
412
|
+
const child = spawn(opener.name, opener.args, { detached: true, stdio: 'ignore' })
|
|
413
|
+
// Without this listener an ENOENT would surface as an unhandled error.
|
|
414
|
+
child.on('error', () => {})
|
|
415
|
+
child.unref()
|
|
416
|
+
return sendJson(res, 200, { ok: true })
|
|
417
|
+
} catch (error) {
|
|
418
|
+
return fail(res, 500, 'REVEAL_FAILED', String(error?.message ?? error))
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
391
422
|
if (pathname === '/playlists' && method === 'GET') {
|
|
392
423
|
return sendJson(res, 200, { ok: true, playlists: await playlists.list() })
|
|
393
424
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-music-panel",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.12",
|
|
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",
|