mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
Compare commits
348 Commits
v2.0.0-bet
...
7de1355f18
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7de1355f18 | ||
|
|
0d66ced637 | ||
|
|
ae1ee71e75 | ||
|
|
84d9c999eb | ||
|
|
1a36fbf1d5 | ||
|
|
e0e62cd906 | ||
|
|
2ed6acf934 | ||
|
|
236ee0a345 | ||
|
|
fe0f7a0f25 | ||
|
|
8529663ea5 | ||
|
|
100bae7488 | ||
|
|
91927b8d76 | ||
|
|
8be209837d | ||
|
|
9ae09b5711 | ||
|
|
3509d73ecc | ||
|
|
0bdb9c07b7 | ||
|
|
3218ab05f1 | ||
|
|
e40343f91f | ||
|
|
fcf1c235c3 | ||
|
|
ef0433c645 | ||
|
|
207f84fcd7 | ||
|
|
2985aa5977 | ||
|
|
ffd7aeff49 | ||
|
|
7f4a88daa1 | ||
|
|
91171465b2 | ||
|
|
1744a5a678 | ||
|
|
ce3d469547 | ||
|
|
7cc5b85fc5 | ||
|
|
3b3f321a3d | ||
|
|
58e3c6e21c | ||
|
|
68756f2502 | ||
|
|
cea9f7b025 | ||
|
|
6b653bc5e8 | ||
|
|
1b2985892b | ||
|
|
eb0094c189 | ||
|
|
5edbd66398 | ||
|
|
4e731e976c | ||
|
|
bd6e23435e | ||
|
|
8ec91e1392 | ||
|
|
8f69b56378 | ||
|
|
ccd0c6bdeb | ||
|
|
1c9109af73 | ||
|
|
a556a2e102 | ||
|
|
9fcd5b0e98 | ||
|
|
a1be1e16b2 | ||
|
|
fc7fc08a6e | ||
|
|
d74515142d | ||
|
|
0aae10e8a0 | ||
|
|
242c6f2ca7 | ||
|
|
413b74bf9a | ||
|
|
aad5c9461f | ||
|
|
23cccbb660 | ||
|
|
ba7666526c | ||
|
|
82ea11b6fa | ||
|
|
9771cea25f | ||
|
|
6f4afd85d7 | ||
|
|
fe82fa623b | ||
|
|
28b521a192 | ||
|
|
bdfbb4d2b1 | ||
|
|
6937a93d17 | ||
|
|
2bc4fca4dd | ||
|
|
92428593e2 | ||
|
|
95e3f74301 | ||
|
|
9145eb034b | ||
|
|
829dc3b591 | ||
|
|
c4c6178b9e | ||
|
|
9967bcb102 | ||
|
|
feb186db65 | ||
|
|
84999338e6 | ||
|
|
7943b56dbb | ||
|
|
6a1657cf20 | ||
|
|
0eaf32b8a7 | ||
|
|
424c1e76f0 | ||
|
|
e854dbb816 | ||
|
|
5e37678348 | ||
|
|
9fcb5ca633 | ||
|
|
55aedc33c3 | ||
|
|
1d4aa43185 | ||
|
|
8014eedd72 | ||
|
|
4f018775ba | ||
|
|
4e7bd95366 | ||
|
|
3035056ded | ||
|
|
df42292f0a | ||
|
|
ca2ff2a890 | ||
|
|
bd06c3f8f4 | ||
|
|
a44cd904dc | ||
|
|
09838e632d | ||
|
|
7f77ccb18a | ||
|
|
f3f05aa72b | ||
|
|
6beb9c78e1 | ||
|
|
2efc0a5228 | ||
|
|
bb0cae2be1 | ||
|
|
22a6fdd0c4 | ||
|
|
d257a441c2 | ||
|
|
49c81b6afd | ||
|
|
045497a7c8 | ||
|
|
521970b8a1 | ||
|
|
a1e9dd7746 | ||
|
|
61d2467916 | ||
|
|
f3adf773a7 | ||
|
|
ad807cf478 | ||
|
|
a1194232af | ||
|
|
9fc1746495 | ||
|
|
103bf7948d | ||
|
|
12c2dea226 | ||
|
|
7fbe8ac6a5 | ||
|
|
ef6868fd40 | ||
|
|
f238845f9b | ||
|
|
d4d16b71ae | ||
|
|
9baf571478 | ||
|
|
6370ac77e6 | ||
|
|
2bf3f2a5c6 | ||
|
|
e8bf42891e | ||
|
|
c21f970b86 | ||
|
|
56045cd338 | ||
|
|
dbf6121098 | ||
|
|
103b2fe923 | ||
|
|
c70a266b22 | ||
|
|
a7b34ca4b0 | ||
|
|
caa6fa838f | ||
|
|
e98140d0ac | ||
|
|
ac47b38773 | ||
|
|
fef6e4f6fa | ||
|
|
0215e5944d | ||
|
|
1b6ad163e3 | ||
|
|
080de35545 | ||
|
|
163423222d | ||
|
|
fc49b7ad00 | ||
|
|
b83cd99d8c | ||
|
|
74a5de96c0 | ||
|
|
fcf78cdd08 | ||
|
|
98fbc81d2f | ||
|
|
5ad562ab1c | ||
|
|
10dc011f9a | ||
|
|
3df6e91f95 | ||
|
|
31ae15d242 | ||
|
|
a2cfbb5e52 | ||
|
|
c4bd94daae | ||
|
|
e18828cd08 | ||
|
|
82f6b4607b | ||
|
|
3874ab3483 | ||
|
|
fc297cb198 | ||
|
|
aec06c5a55 | ||
|
|
87ce076f26 | ||
|
|
45b1bf130d | ||
|
|
3966578015 | ||
|
|
fed7b3678b | ||
|
|
ac6ce257b8 | ||
|
|
b8b8f747d3 | ||
|
|
53468b2e3a | ||
|
|
ecadf6ade7 | ||
|
|
8187b09fcb | ||
|
|
89117d4198 | ||
|
|
6158dd2750 | ||
|
|
86f33d054a | ||
|
|
52e8458590 | ||
|
|
554cf45500 | ||
|
|
60f751713a | ||
|
|
edd9b38cfc | ||
|
|
9a87d73289 | ||
|
|
317763e2c3 | ||
|
|
aa3f9d2ca8 | ||
|
|
e64a5ba1bc | ||
|
|
00e6f7bb60 | ||
|
|
883ef05ab4 | ||
|
|
d0b5eb3371 | ||
|
|
590ef96aa7 | ||
|
|
172ca5a2f3 | ||
|
|
6e1e56c1bd | ||
|
|
105fed4bd0 | ||
|
|
8bd3dc56d8 | ||
|
|
8e88bf64b1 | ||
|
|
244a832c52 | ||
|
|
146af3aeba | ||
|
|
143e8e29d7 | ||
|
|
c702e6e01a | ||
|
|
b2ddb9f4e2 | ||
|
|
201186bab2 | ||
|
|
bfcd59daca | ||
|
|
d5c3843c3f | ||
|
|
d3f307eac5 | ||
|
|
675a52b8d1 | ||
|
|
aee90e9c4e | ||
|
|
eb39b81d8d | ||
|
|
436df47104 | ||
|
|
e04e5e34c6 | ||
|
|
b57d685c03 | ||
|
|
6684172592 | ||
|
|
0257e74ff0 | ||
|
|
16c8865651 | ||
|
|
1a61aa2458 | ||
|
|
e543f07d8e | ||
|
|
96a0495a88 | ||
|
|
02befcd8a4 | ||
|
|
15b500806a | ||
|
|
9accf5d27d | ||
|
|
191ab29a44 | ||
|
|
a4d4cd5f70 | ||
|
|
3b07f7346f | ||
|
|
1edceeebdd | ||
|
|
fbf261f80b | ||
|
|
432fa18299 | ||
|
|
b2dcec840b | ||
|
|
6f93d65f02 | ||
|
|
852b901353 | ||
|
|
71a282157d | ||
|
|
49f462cdf5 | ||
|
|
94c0ca70e1 | ||
|
|
813637762c | ||
|
|
eb39b85a8b | ||
|
|
201fd8d687 | ||
|
|
02116d8f0f | ||
|
|
551a190edf | ||
|
|
f5015a4028 | ||
|
|
ea55616bd6 | ||
|
|
2b8eb93404 | ||
|
|
18113d94e9 | ||
|
|
a8f01d5728 | ||
|
|
3a6f8e7462 | ||
|
|
57325bfc46 | ||
|
|
a3e8931460 | ||
|
|
2e0f448f66 | ||
|
|
44cfbbf4e0 | ||
|
|
963343c39e | ||
|
|
a697799f48 | ||
|
|
7c59cc2ccb | ||
|
|
11acbcf7eb | ||
|
|
c35ede2158 | ||
|
|
dc480459eb | ||
|
|
c100bfed8d | ||
|
|
2b48713565 | ||
|
|
528f9b0aa6 | ||
|
|
0e0bde89bb | ||
|
|
acf60b8b75 | ||
|
|
3b5338d582 | ||
|
|
0c82d6a096 | ||
|
|
50374e173e | ||
|
|
62c9dc33db | ||
|
|
2b6d68ecbd | ||
|
|
8a842aa6d6 | ||
|
|
b0a08ce1b8 | ||
|
|
77ab94a59a | ||
|
|
2b65269ba9 | ||
|
|
7ed33919ab | ||
|
|
d20512e662 | ||
|
|
9423d2bf9e | ||
|
|
54fb8e74a8 | ||
|
|
7d3d7696da | ||
|
|
e66ba52889 | ||
|
|
b146dc011e | ||
|
|
ad2422d826 | ||
|
|
6b9ba74c35 | ||
|
|
22e2653e20 | ||
|
|
4cac54c84a | ||
|
|
e5f9ecd7b5 | ||
|
|
d64cfb40ec | ||
|
|
53c30caf00 | ||
|
|
6fcce91b2b | ||
|
|
5a53dfcdf7 | ||
|
|
bbe71bd62d | ||
|
|
ecedf0b7b9 | ||
|
|
f2935f3c1c | ||
|
|
d3e677d494 | ||
|
|
e49f90b0da | ||
|
|
0359b0e470 | ||
|
|
aa2373695b | ||
|
|
cf940ff405 | ||
|
|
e7c17cf531 | ||
|
|
c6a1ca4f42 | ||
|
|
8f5008df69 | ||
|
|
428ce4be86 | ||
|
|
d46c4c4285 | ||
|
|
0b871175b2 | ||
|
|
c34c4fd880 | ||
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 | ||
|
|
019b78bf38 | ||
|
|
cf88c7669f | ||
|
|
f4383ba848 | ||
|
|
adbda459ba | ||
|
|
984d747179 | ||
|
|
c012f84064 | ||
|
|
a57a18b9f5 | ||
|
|
309c323a14 | ||
|
|
6a1e606d6d | ||
|
|
af3931847e | ||
|
|
41eadb5843 | ||
|
|
8963d719d9 | ||
|
|
0a7761ffff | ||
|
|
1a63771f2d | ||
|
|
1f9141ba33 | ||
|
|
a341a69d48 | ||
|
|
0cedfe0af3 | ||
|
|
59f492ed8f | ||
|
|
8f416ff841 | ||
|
|
99ab194e4b | ||
|
|
43fb9b48dc | ||
|
|
c61e54d6a3 | ||
|
|
c8d195053f | ||
|
|
8cfe5d0481 | ||
|
|
fcc2f5015f | ||
|
|
9b98a45264 | ||
|
|
3c4e836fb8 | ||
|
|
a8111b9d3f | ||
|
|
8eaeffeda3 | ||
|
|
eed76966c4 | ||
|
|
b095e4eb36 | ||
|
|
3dbdf3e613 | ||
|
|
5ceca058a7 | ||
|
|
a8e867bbf9 | ||
|
|
4cb8eb0213 | ||
|
|
461f216cab | ||
|
|
2756313e4a | ||
|
|
e802a2f574 | ||
|
|
a45940b104 | ||
|
|
ac0ac5f4ea | ||
|
|
883b6d13a5 | ||
|
|
ee9bbf0687 | ||
|
|
693dc65b07 | ||
|
|
354d271582 | ||
|
|
eaaeb0f5d3 | ||
|
|
e4e8deec59 | ||
|
|
3f21704b82 | ||
|
|
7ad2bb8bde | ||
|
|
3123e4f5f8 | ||
|
|
60e43c9f40 | ||
|
|
298813a057 | ||
|
|
2db74f3a39 | ||
|
|
024ff1773e | ||
|
|
6046e5a153 | ||
|
|
3c39dbd87f | ||
|
|
c5747b6a3e | ||
|
|
750d570c3d | ||
|
|
b811b00b9f | ||
|
|
a372570038 | ||
|
|
6d5fa15098 | ||
|
|
b65369a8a6 | ||
|
|
0af0ac3cce | ||
|
|
f0ed78eed5 | ||
|
|
b1cda68c75 | ||
|
|
dd1081cfa2 | ||
|
|
046b8f3a92 | ||
|
|
72650a5419 | ||
|
|
d471e686b5 | ||
|
|
41c4342f76 | ||
|
|
16802aaac7 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
dist
|
||||
@@ -1,9 +1,18 @@
|
||||
# 根配置文件
|
||||
## 编辑器在查找配置时会停止查找更高层次的配置文件
|
||||
root = true
|
||||
|
||||
# 通配符,匹配所有文件
|
||||
[*]
|
||||
# 设置字符集为 UTF-8,确保文件中的文本使用 UTF-8 编码
|
||||
charset = utf-8
|
||||
# 使用空格作为缩进风格
|
||||
indent_style = space
|
||||
# 设置每个缩进级别的空格数量为 2
|
||||
indent_size = 2
|
||||
# 设置行尾换行符为LF(Line Feed)
|
||||
end_of_line = lf
|
||||
# 在文件的末尾插入一个新行
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
# 删除每一行末尾的尾随空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
29
.env
29
.env
@@ -1,29 +0,0 @@
|
||||
# 程序配置
|
||||
## 程序名称
|
||||
MAIN_VITE_TITLE = "SPlayer"
|
||||
## 程序主端口
|
||||
MAIN_VITE_MAIN_PORT = 7899
|
||||
## 程序开发环境运行端口
|
||||
MAIN_VITE_DEV_PORT = 6944
|
||||
|
||||
# 全局 API 配置
|
||||
## API 运行地址
|
||||
MAIN_VITE_SERVER_HOST = 127.0.0.1
|
||||
## API 运行端口
|
||||
MAIN_VITE_SERVER_PORT = 11451
|
||||
## API 在线地址( 网址结尾不要加 / )
|
||||
### 用于非客户端( 浏览器环境 )
|
||||
RENDERER_VITE_SERVER_URL = /api
|
||||
|
||||
# 程序信息
|
||||
RENDERER_VITE_SITE_TITLE = "SPlayer"
|
||||
RENDERER_VITE_SITE_ANTHOR = "無名"
|
||||
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
|
||||
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
RENDERER_VITE_SITE_URL = "imsyy.top"
|
||||
RENDERER_VITE_SITE_LOGO = "/images/logo/favicon.svg"
|
||||
RENDERER_VITE_SITE_APPLE_LOGO = "/images/logo/favicon-apple.png"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
MAIN_VITE_MIGU_COOKIE = ""
|
||||
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
## WEB 端口
|
||||
VITE_WEB_PORT = 14558
|
||||
## API 端口
|
||||
VITE_SERVER_PORT = 25884
|
||||
## API 地址 - 结尾不要加 /
|
||||
VITE_API_URL = /api/netease
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
304
.eslintrc-auto-import.json
Normal file
304
.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"asyncComputed": true,
|
||||
"autoResetRef": true,
|
||||
"computed": true,
|
||||
"computedAsync": true,
|
||||
"computedEager": true,
|
||||
"computedInject": true,
|
||||
"computedWithControl": true,
|
||||
"controlledComputed": true,
|
||||
"controlledRef": true,
|
||||
"createApp": true,
|
||||
"createEventHook": true,
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": true,
|
||||
"createUnrefFn": true,
|
||||
"customRef": true,
|
||||
"debouncedRef": true,
|
||||
"debouncedWatch": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"eagerComputed": true,
|
||||
"effectScope": true,
|
||||
"extendRef": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"ignorableWatch": true,
|
||||
"inject": true,
|
||||
"injectLocal": true,
|
||||
"isDefined": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onStartTyping": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"pausableWatch": true,
|
||||
"provide": true,
|
||||
"provideLocal": true,
|
||||
"reactify": true,
|
||||
"reactifyObject": true,
|
||||
"reactive": true,
|
||||
"reactiveComputed": true,
|
||||
"reactiveOmit": true,
|
||||
"reactivePick": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"refAutoReset": true,
|
||||
"refDebounced": true,
|
||||
"refDefault": true,
|
||||
"refThrottled": true,
|
||||
"refWithControl": true,
|
||||
"resolveComponent": true,
|
||||
"resolveRef": true,
|
||||
"resolveUnref": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"syncRef": true,
|
||||
"syncRefs": true,
|
||||
"templateRef": true,
|
||||
"throttledRef": true,
|
||||
"throttledWatch": true,
|
||||
"toRaw": true,
|
||||
"toReactive": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"tryOnBeforeMount": true,
|
||||
"tryOnBeforeUnmount": true,
|
||||
"tryOnMounted": true,
|
||||
"tryOnScopeDispose": true,
|
||||
"tryOnUnmounted": true,
|
||||
"unref": true,
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useAnimate": true,
|
||||
"useArrayDifference": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayFindLast": true,
|
||||
"useArrayIncludes": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useArrayUnique": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
"useBase64": true,
|
||||
"useBattery": true,
|
||||
"useBluetooth": true,
|
||||
"useBreakpoints": true,
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClipboard": true,
|
||||
"useClipboardItems": true,
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
"useCssVars": true,
|
||||
"useCurrentElement": true,
|
||||
"useCycleList": true,
|
||||
"useDark": true,
|
||||
"useDateFormat": true,
|
||||
"useDebounce": true,
|
||||
"useDebounceFn": true,
|
||||
"useDebouncedRefHistory": true,
|
||||
"useDeviceMotion": true,
|
||||
"useDeviceOrientation": true,
|
||||
"useDevicePixelRatio": true,
|
||||
"useDevicesList": true,
|
||||
"useDialog": true,
|
||||
"useDisplayMedia": true,
|
||||
"useDocumentVisibility": true,
|
||||
"useDraggable": true,
|
||||
"useDropZone": true,
|
||||
"useElementBounding": true,
|
||||
"useElementByPoint": true,
|
||||
"useElementHover": true,
|
||||
"useElementSize": true,
|
||||
"useElementVisibility": true,
|
||||
"useEventBus": true,
|
||||
"useEventListener": true,
|
||||
"useEventSource": true,
|
||||
"useEyeDropper": true,
|
||||
"useFavicon": true,
|
||||
"useFetch": true,
|
||||
"useFileDialog": true,
|
||||
"useFileSystemAccess": true,
|
||||
"useFocus": true,
|
||||
"useFocusWithin": true,
|
||||
"useFps": true,
|
||||
"useFullscreen": true,
|
||||
"useGamepad": true,
|
||||
"useGeolocation": true,
|
||||
"useId": true,
|
||||
"useIdle": true,
|
||||
"useImage": true,
|
||||
"useInfiniteScroll": true,
|
||||
"useIntersectionObserver": true,
|
||||
"useInterval": true,
|
||||
"useIntervalFn": true,
|
||||
"useKeyModifier": true,
|
||||
"useLastChanged": true,
|
||||
"useLink": true,
|
||||
"useLoadingBar": true,
|
||||
"useLocalStorage": true,
|
||||
"useMagicKeys": true,
|
||||
"useManualRefHistory": true,
|
||||
"useMediaControls": true,
|
||||
"useMediaQuery": true,
|
||||
"useMemoize": true,
|
||||
"useMemory": true,
|
||||
"useMessage": true,
|
||||
"useModel": true,
|
||||
"useMounted": true,
|
||||
"useMouse": true,
|
||||
"useMouseInElement": true,
|
||||
"useMousePressed": true,
|
||||
"useMutationObserver": true,
|
||||
"useNavigatorLanguage": true,
|
||||
"useNetwork": true,
|
||||
"useNotification": true,
|
||||
"useNow": true,
|
||||
"useObjectUrl": true,
|
||||
"useOffsetPagination": true,
|
||||
"useOnline": true,
|
||||
"usePageLeave": true,
|
||||
"useParallax": true,
|
||||
"useParentElement": true,
|
||||
"usePerformanceObserver": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerLock": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredContrast": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
"useScroll": true,
|
||||
"useScrollLock": true,
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSorted": true,
|
||||
"useSpeechRecognition": true,
|
||||
"useSpeechSynthesis": true,
|
||||
"useStepper": true,
|
||||
"useStorage": true,
|
||||
"useStorageAsync": true,
|
||||
"useStyleTag": true,
|
||||
"useSupported": true,
|
||||
"useSwipe": true,
|
||||
"useTemplateRef": true,
|
||||
"useTemplateRefsList": true,
|
||||
"useTextDirection": true,
|
||||
"useTextSelection": true,
|
||||
"useTextareaAutosize": true,
|
||||
"useThrottle": true,
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
"useTimestamp": true,
|
||||
"useTitle": true,
|
||||
"useToNumber": true,
|
||||
"useToString": true,
|
||||
"useToggle": true,
|
||||
"useTransition": true,
|
||||
"useUrlSearchParams": true,
|
||||
"useUserMedia": true,
|
||||
"useVModel": true,
|
||||
"useVModels": true,
|
||||
"useVibrate": true,
|
||||
"useVirtualList": true,
|
||||
"useWakeLock": true,
|
||||
"useWebNotification": true,
|
||||
"useWebSocket": true,
|
||||
"useWebWorker": true,
|
||||
"useWebWorkerFn": true,
|
||||
"useWindowFocus": true,
|
||||
"useWindowScroll": true,
|
||||
"useWindowSize": true,
|
||||
"watch": true,
|
||||
"watchArray": true,
|
||||
"watchAtMost": true,
|
||||
"watchDebounced": true,
|
||||
"watchDeep": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchImmediate": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"@electron-toolkit",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
rules: {
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
},
|
||||
ignorePatterns: [
|
||||
"node_modules/",
|
||||
"build/",
|
||||
"dist/",
|
||||
"out/",
|
||||
"components.d.ts",
|
||||
"auto-imports.d.ts",
|
||||
],
|
||||
globals: {
|
||||
defineProps: true,
|
||||
defineEmits: true,
|
||||
withDefaults: true,
|
||||
h: true,
|
||||
vue: true,
|
||||
ref: true,
|
||||
reactive: true,
|
||||
computed: true,
|
||||
watch: true,
|
||||
provide: true,
|
||||
inject: true,
|
||||
defineComponent: true,
|
||||
onBeforeMount: true,
|
||||
onBeforeUnmount: true,
|
||||
onUnmounted: true,
|
||||
onMounted: true,
|
||||
nextTick: true,
|
||||
watchEffect: true,
|
||||
electron: true,
|
||||
$message: true,
|
||||
$dialog: true,
|
||||
$loadingBar: true,
|
||||
$changeLogin: true,
|
||||
$notification: true,
|
||||
$changeThemeColor: true,
|
||||
$canNotConnect: true,
|
||||
},
|
||||
};
|
||||
17
.github/ISSUE_TEMPLATE/add.yml
vendored
17
.github/ISSUE_TEMPLATE/add.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 添加功能
|
||||
description: 请填写希望添加的功能的具体信息
|
||||
title: "添加功能"
|
||||
labels: [add]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "希望添加什么功能?"
|
||||
placeholder: "请填写功能名称"
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "请详细描述希望添加的功能的具体信息"
|
||||
56
.github/ISSUE_TEMPLATE/bug.yml
vendored
56
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -2,32 +2,74 @@ name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
---
|
||||
在此之前,我们默认你已经知道该如何提问。简而言之,**你要精确地描述问题并提供充足的信息**
|
||||
|
||||
此外,如果需要,你可以使用 <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> 打开开发者工具为我们提供信息
|
||||
|
||||
- type: checkboxes
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "检查清单"
|
||||
description: |
|
||||
我们需要了解一些信息,你需要检查下面的检查项 <br />
|
||||
**这里并不是每一个检查项都必要,根据你的真实情况勾选即可**
|
||||
options:
|
||||
- label: "我已检索仓库中所有的 Issues,确保我**没有重复提交问题**;或有相似 Issue,但我觉得我的情况不包含在那个相似 Issue 之内"
|
||||
- label: "我已经找到了可以复现这个问题的方法,并且写在了下面的「具体信息」中"
|
||||
- label: "此问题可以在我的设备和当前环境中**稳定复现**"
|
||||
- label: "此问题可以在最新版本 (Latest Release) 中复现"
|
||||
- label: "此问题是在我更新到当前版本后**才**出现的"
|
||||
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "是网页端还是客户端"
|
||||
placeholder: "客户端"
|
||||
options:
|
||||
- "客户端"
|
||||
- "网页端"
|
||||
default: 0
|
||||
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前系统环境"
|
||||
placeholder: "win11"
|
||||
placeholder: "如:Windows 11"
|
||||
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
attributes:
|
||||
label: "当前 Node.js 及 npm 版本"
|
||||
placeholder: "v18.16.0 / v9.6.7"
|
||||
placeholder: "如:v18.16.0 / v9.6.7 (选填)"
|
||||
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前版本"
|
||||
placeholder: "v1.0.0"
|
||||
description: |
|
||||
填写关于软件里的或 Releases 中版本号即可 <br />
|
||||
如果是自行构建或从 GitHub Actions 下载的开发版,还需要提供 Commit ID
|
||||
placeholder: "如:v1.0.0"
|
||||
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "有需要补充的信息吗?比如控制台的报错什么的"
|
||||
description: |
|
||||
请填写完整的复现步骤和遇到的问题,还请尽量提供所有可能的信息,提供包括但不限于:
|
||||
|
||||
- 分步的复现步骤,可以使用 `1. xxx` (换行) `2. xxx` 的格式
|
||||
- 截图(如果结合复现步骤还是无法详细表述,甚至可以录屏)
|
||||
- 开发者工具中的:控制台输出报错、网络请求等
|
||||
- 出现问题的在线歌曲链接、出现问题的文件(本地歌曲、歌词等)的下载链接
|
||||
|
||||
等信息,以更好地帮助我们解决你的问题
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 添加功能
|
||||
url: https://github.com/imsyy/SPlayer/discussions/new?category=%E6%83%B3%E6%B3%95-ideas
|
||||
about: 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 转到讨论区
|
||||
url: https://github.com/imsyy/SPlayer/discussions
|
||||
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
41
.github/workflows/build.yml
vendored
41
.github/workflows/build.yml
vendored
@@ -1,41 +0,0 @@
|
||||
# Dev 分支推送部署预览
|
||||
## 仅部署 Win 端
|
||||
name: Build Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and electron app
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App
|
||||
run: npm run build:win
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 清理不必要的构建产物
|
||||
- name: Cleanup Artifacts
|
||||
run: |
|
||||
npx rimraf "dist/!(*.exe)"
|
||||
# 上传构建产物
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: dist
|
||||
67
.github/workflows/dev.yml
vendored
Normal file
67
.github/workflows/dev.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# Dev 分支推送部署预览
|
||||
## 部署 Windows x64 和 ARM64 版本
|
||||
name: Build Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.x
|
||||
PNPM_VERSION: 8
|
||||
|
||||
jobs:
|
||||
# Windows x64 架构
|
||||
build-win:
|
||||
name: Build Electron App for Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 设置 pnpm 版本
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
# 清理旧的构建产物
|
||||
- name: Clean dist folder
|
||||
run: |
|
||||
if (Test-Path dist) {
|
||||
Remove-Item -Recurse -Force dist
|
||||
}
|
||||
# 构建 Electron App (x64)
|
||||
- name: Build Electron App for Windows x64
|
||||
# 仅 x64
|
||||
run: pnpm run build:win -- --arch=x64
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 清理不必要的构建产物(保留 .exe 和 .blockmap 文件)
|
||||
- name: Cleanup Artifacts
|
||||
run: npx del-cli "dist/**/*.yaml" "dist/**/*.yml"
|
||||
# 上传构建产物(仅上传 x64 架构的 .exe 文件)
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: |
|
||||
dist/**.exe
|
||||
56
.github/workflows/docker.yml
vendored
Normal file
56
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
imsyy/splayer
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
274
.github/workflows/release.yml
vendored
274
.github/workflows/release.yml
vendored
@@ -1,141 +1,169 @@
|
||||
# Release 发行版本部署
|
||||
## 多端部署
|
||||
name: Build Release
|
||||
name: Build & Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
- v* # 只在 tag v* 时触发
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.x
|
||||
PNPM_VERSION: 8
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
|
||||
jobs:
|
||||
# Windows
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
runs-on: windows-latest
|
||||
# ===================================================================
|
||||
# 并行构建所有平台和架构
|
||||
# ===================================================================
|
||||
build:
|
||||
name: Build on ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
# 矩阵策略
|
||||
# 即使一个矩阵任务失败,其他任务也会继续运行
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [macos-latest, windows-latest, ubuntu-latest]
|
||||
# 开始步骤
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/checkout@v4
|
||||
# 设置 pnpm 版本
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
# 安装 Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
# 清理旧的构建产物
|
||||
- name: Clean workspace on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
Write-Host "🧹 Cleaning workspace, node_modules, and Electron caches..."
|
||||
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
||||
if (Test-Path out) { Remove-Item -Recurse -Force out }
|
||||
if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }
|
||||
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron-builder") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
- name: Clean workspace on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux'
|
||||
run: |
|
||||
echo "🧹 Cleaning workspace, node_modules, and Electron caches..."
|
||||
rm -rf dist out node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Windows
|
||||
run: npm run build:win || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for macOS
|
||||
run: npm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env file on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
- name: Copy .env file on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux'
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
# 安装依赖
|
||||
- name: Install RPM & Pacman
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get install --no-install-recommends -y rpm &&
|
||||
sudo apt-get install --no-install-recommends -y libarchive-tools &&
|
||||
sudo apt-get install --no-install-recommends -y libopenjp2-tools
|
||||
# 安裝 Snapcraft
|
||||
- name: Install Snapcraft
|
||||
if: runner.os == 'Linux'
|
||||
uses: samuelmeuli/action-snapcraft@v2
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Linux
|
||||
run: npm run build:linux || true
|
||||
- name: Build Windows x64 & ARM64 App
|
||||
if: runner.os == 'Windows'
|
||||
run: pnpm build:win || true
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build macOS Universal App
|
||||
if: runner.os == 'macOS'
|
||||
run: pnpm build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Linux x64 & ARM64 App
|
||||
if: runner.os == 'Linux'
|
||||
run: pnpm build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 上传 Snap 包到 Snapcraft 商店
|
||||
- name: Publish Snap to Snap Store
|
||||
if: runner.os == 'Linux'
|
||||
run: snapcraft upload dist/*.snap --release stable
|
||||
continue-on-error: true
|
||||
# 合并所有构建
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-${{ runner.os }}
|
||||
path: dist/*.*
|
||||
|
||||
# ===================================================================
|
||||
# 收集并发布 Release
|
||||
# ===================================================================
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# 需要写入权限
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# 将所有产物下载到 artifacts 文件夹
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
# 创建 GitHub Release 并上传所有产物
|
||||
- name: Create GitHub Release and Upload Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 自动生成 Release 说明
|
||||
generate_release_notes: true
|
||||
# 发布为草稿
|
||||
draft: false
|
||||
# 发布为预发布
|
||||
prerelease: false
|
||||
# 全部上传
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.rpm
|
||||
artifacts/**/*.pacman
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -14,9 +14,9 @@ dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
out
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
.env
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -28,3 +28,5 @@ out
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env.development
|
||||
.env.production
|
||||
|
||||
5
.npmrc
5
.npmrc
@@ -1,2 +1,5 @@
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
shamefully-hoist=true
|
||||
|
||||
@@ -2,5 +2,7 @@ out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
# tsconfig.json
|
||||
# tsconfig.*.json
|
||||
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
singleQuote: false
|
||||
semi: true
|
||||
printWidth: 100
|
||||
trailingComma: all
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# add .env.example to .env
|
||||
RUN [ ! -e ".env" ] && cp .env.example .env || true
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# nginx
|
||||
FROM nginx:1.27-alpine-slim AS app
|
||||
|
||||
COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN apk add --no-cache npm python3 youtube-dl \
|
||||
&& npm install -g @unblockneteasemusic/server NeteaseCloudMusicApi \
|
||||
&& wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /docker-entrypoint.sh
|
||||
|
||||
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["npx", "NeteaseCloudMusicApi"]
|
||||
149
LICENSE
149
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
263
README.md
263
README.md
@@ -1,25 +1,34 @@
|
||||
> [!IMPORTANT]
|
||||
> ## 🎉 当前项目正在重构中 🎉
|
||||
>
|
||||
> - 目前版本进入维护模式,仅在遇到重大问题时会进行修复
|
||||
> - 支持客户端与网页端
|
||||
> - 支持现有版本所有功能
|
||||
> - 新增支持播放与管理本地歌曲
|
||||
# SPlayer
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
> A simple music player
|
||||
|
||||

|
||||

|
||||
[](https://github.com/imsyy/SPlayer/actions/workflows/release.yml)
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||
## 说明
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行构建
|
||||
- ~~仅对移动端做了基础适配,**不保证功能全部可用**~~
|
||||
- 欢迎各位大佬指点和 `Star` 哦 😍
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> ### 严肃警告
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
> - **禁止用于售卖或其他盈利用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 禁止在二开项目中修改程序原版权信息( 您可以添加二开作者信息 )
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) + [TypeScript](https://www.typescriptlang.org/) + [Naïve UI](https://www.naiveui.com/) + [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 支持网页端与客户端,由于设备有限,目前仅适配 `Win`,其他平台可自行解决兼容性后进行构建
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
> 请注意,本程序不打算开发移动端,也不会对移动端进行完美适配,仅保证基础可用性
|
||||
|
||||
- 欢迎各位大佬 `Star` 😍
|
||||
|
||||
## 👀 Demo
|
||||
|
||||
@@ -27,40 +36,36 @@
|
||||
|
||||
## 🎉 功能
|
||||
|
||||
- 支持扫码登录
|
||||
- 支持手机号登录
|
||||
- 自动进行每日签到及云贝签到
|
||||
- 封面主题色自适应
|
||||
- 本地歌曲管理及分类 ~~以及音乐标签编辑~~
|
||||
- **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- 下载歌曲(最高支持 Hi-Res)
|
||||
- 新建歌单及歌单编辑
|
||||
- 收藏 / 取消收藏歌单或歌手
|
||||
- 每日推荐歌曲
|
||||
- 私人 FM
|
||||
- 云盘音乐上传
|
||||
- 云盘内歌曲播放
|
||||
- 云盘内歌曲纠正
|
||||
- 云盘歌曲删除
|
||||
- 支持逐字歌词
|
||||
- 歌词滚动以及歌词翻译
|
||||
- MV 与视频播放
|
||||
- 音乐频谱显示( 暂时去除,还待完善 )
|
||||
- 音乐渐入渐出
|
||||
- 支持 PWA
|
||||
- 支持评论区及评论点赞
|
||||
- 明暗模式自动 / 手动切换
|
||||
- ~~移动端基础适配~~
|
||||
- ~~`i18n` 支持~~
|
||||
- ✨ 支持扫码登录
|
||||
- 📱 支持手机号登录
|
||||
- 📅 自动进行每日签到及云贝签到
|
||||
- 💻 支持桌面歌词
|
||||
- 💻 支持切换为本地播放器,此模式将不会连接网络
|
||||
- 🎨 封面主题色自适应,支持全站着色
|
||||
- 🌚 Light / Dark / Auto 模式自动切换
|
||||
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
|
||||
- 📁 简易的本地音乐标签编辑及封面修改
|
||||
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- ⬇️ 下载歌曲( 最高支持 Hi-Res,需具有相应会员账号 )
|
||||
- ➕ 新建歌单及歌单编辑
|
||||
- ❤️ 收藏 / 取消收藏歌单或歌手
|
||||
- 🎶 每日推荐歌曲
|
||||
- 📻 私人 FM
|
||||
- ☁️ 云盘音乐上传
|
||||
- 📂 云盘内歌曲播放
|
||||
- 🔄 云盘内歌曲纠正
|
||||
- 🗑️ 云盘歌曲删除
|
||||
- 📝 支持逐字歌词
|
||||
- 🔄 歌词滚动以及歌词翻译
|
||||
- 📹 MV 与视频播放
|
||||
- 🎶 音乐频谱显示
|
||||
- ⏭️ 音乐渐入渐出
|
||||
- 🔄 支持 PWA
|
||||
- 💬 支持评论区
|
||||
- 📱 移动端基础适配
|
||||
- ~~🌐 `i18n` 支持~~
|
||||
|
||||
#### 待办
|
||||
|
||||
- [ ] 完善音乐频谱
|
||||
- [ ] 添加桌面歌词
|
||||
- [ ] 多种布局方式
|
||||
- [ ] 发表评论
|
||||
|
||||
## 🖼️ Screenshots
|
||||
## 🖼️ screenshots
|
||||
|
||||
> 开发中,仅供参考
|
||||
|
||||
@@ -118,68 +123,111 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## ⚙️ 部署
|
||||
## Snap Store
|
||||
|
||||
> Vercel 等托管平台可在 Fork 后一键导入并自动部署
|
||||
[](https://snapcraft.io/splayer)
|
||||
|
||||
### API 服务(客户端无需理会,如果需要网页端,则必需部署)
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
|
||||
- 请在根目录下的 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 中填入 API 地址(必需)
|
||||
### 本地构建
|
||||
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "your api url";
|
||||
```
|
||||
|
||||
### 安装依赖
|
||||
> 请尽量拉取最新分支后使用本地构建方式,在线部署的仓库可能更新不及时
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
yarn install
|
||||
# 或者
|
||||
npm install
|
||||
# 构建
|
||||
docker build -t splayer .
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 25884:25884 splayer
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### 开发
|
||||
### 在线部署
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
# 或者
|
||||
yarn dev
|
||||
# 或者
|
||||
npm dev
|
||||
# 从 Docker Hub 拉取
|
||||
docker pull imsyy/splayer:latest
|
||||
# 从 GitHub ghcr 拉取
|
||||
docker pull ghcr.io/imsyy/splayer:latest
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 25884:25884 imsyy/splayer:latest
|
||||
```
|
||||
|
||||
### 构建网页端
|
||||
以上步骤成功后,将会在本地 [localhost:25884](http://localhost:25884/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
yarn build
|
||||
# 或者
|
||||
npm build
|
||||
```
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
构建完成后可将生成的 `out/renderer` 文件夹内的文件上传至服务器
|
||||
> 其他部署平台大致相同,在此不做说明
|
||||
|
||||
若使用的为第三方部署平台,比如 `Vercel`,请将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
|
||||
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
|
||||
3. 复制 `/.env.example` 文件并重命名为 `/.env`
|
||||
4. 将 `.env` 文件中的 `VITE_API_URL` 改为第一步得到的 API 地址
|
||||
|
||||

|
||||
```js
|
||||
VITE_API_URL = "https://example.com";
|
||||
```
|
||||
|
||||
### 构建客户端
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
|
||||
```bash
|
||||
# win
|
||||
pnpm build:win
|
||||
# linux
|
||||
pnpm build:linux
|
||||
# mac
|
||||
pnpm build:mac
|
||||
```
|
||||

|
||||
|
||||
构建完成后可在 `dist` 文件夹中打开可执行文件来完成安装操作
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
## ⚙️ 服务器部署
|
||||
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
```bash
|
||||
git clone https://github.com/imsyy/SPlayer.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或
|
||||
yarn install
|
||||
# 或
|
||||
npm install
|
||||
```
|
||||
|
||||
4. 编译打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或
|
||||
yarn build
|
||||
# 或
|
||||
npm build
|
||||
```
|
||||
|
||||
5. 将站点运行目录设置为 `out/renderer` 目录
|
||||
|
||||
## ⚙️ 本地部署
|
||||
|
||||
1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版
|
||||
2. 安装 pnpm
|
||||
|
||||
```bash
|
||||
npm install pnpm -g
|
||||
```
|
||||
|
||||
3. 克隆仓库并拉取至本地,此处不再赘述
|
||||
4. 使用 `pnpm install` 安装项目依赖(若安装过程中遇到网络错误,请使用国内镜像源替代,此处不再赘述)
|
||||
5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
|
||||
6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装
|
||||
|
||||
| 命令 | 系统类型 |
|
||||
| ------------------ | -------- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
|
||||
## 😘 鸣谢
|
||||
|
||||
@@ -188,18 +236,10 @@ pnpm build:mac
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
|
||||
- [applemusic-like-lyrics](https://github.com/Steve-xmh/applemusic-like-lyrics)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 GPL Version 3 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 GPL Version 3,并在适当的地方注明原始项目的许可证
|
||||
3. **免责声明:** 根据 GPL Version 3,本项目不提供任何明示或暗示的担保。请详细阅读 GPL Version 3以了解完整的免责声明内容
|
||||
4. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
5. **许可证链接:** 请阅读 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 了解更多详情
|
||||
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
|
||||
- [material-color-utilities](https://github.com/material-foundation/material-color-utilities)
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
@@ -210,3 +250,18 @@ pnpm build:mac
|
||||
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
|
||||
|
||||
感谢您的理解
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 AGPL-3.0 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 AGPL-3.0,并在适当的地方注明原始项目的许可证
|
||||
3. **注明原作者:** 在任何修改、派生作品或其他分发中,必须在适当的位置明确注明原作者及其贡献
|
||||
4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#imsyy/SPlayer&Date)
|
||||
|
||||
315
auto-eslint.mjs
Normal file
315
auto-eslint.mjs
Normal file
@@ -0,0 +1,315 @@
|
||||
export default {
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"ShallowRef": true,
|
||||
"Slot": true,
|
||||
"Slots": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"asyncComputed": true,
|
||||
"autoResetRef": true,
|
||||
"computed": true,
|
||||
"computedAsync": true,
|
||||
"computedEager": true,
|
||||
"computedInject": true,
|
||||
"computedWithControl": true,
|
||||
"controlledComputed": true,
|
||||
"controlledRef": true,
|
||||
"createApp": true,
|
||||
"createEventHook": true,
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createRef": true,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": true,
|
||||
"createUnrefFn": true,
|
||||
"customRef": true,
|
||||
"debouncedRef": true,
|
||||
"debouncedWatch": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"eagerComputed": true,
|
||||
"effectScope": true,
|
||||
"extendRef": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"getCurrentWatcher": true,
|
||||
"h": true,
|
||||
"ignorableWatch": true,
|
||||
"inject": true,
|
||||
"injectLocal": true,
|
||||
"isDefined": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"isShallow": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onElementRemoval": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onStartTyping": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"pausableWatch": true,
|
||||
"provide": true,
|
||||
"provideLocal": true,
|
||||
"reactify": true,
|
||||
"reactifyObject": true,
|
||||
"reactive": true,
|
||||
"reactiveComputed": true,
|
||||
"reactiveOmit": true,
|
||||
"reactivePick": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"refAutoReset": true,
|
||||
"refDebounced": true,
|
||||
"refDefault": true,
|
||||
"refThrottled": true,
|
||||
"refWithControl": true,
|
||||
"resolveComponent": true,
|
||||
"resolveRef": true,
|
||||
"resolveUnref": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"syncRef": true,
|
||||
"syncRefs": true,
|
||||
"templateRef": true,
|
||||
"throttledRef": true,
|
||||
"throttledWatch": true,
|
||||
"toRaw": true,
|
||||
"toReactive": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"tryOnBeforeMount": true,
|
||||
"tryOnBeforeUnmount": true,
|
||||
"tryOnMounted": true,
|
||||
"tryOnScopeDispose": true,
|
||||
"tryOnUnmounted": true,
|
||||
"unref": true,
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useAnimate": true,
|
||||
"useArrayDifference": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayFindLast": true,
|
||||
"useArrayIncludes": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useArrayUnique": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
"useBase64": true,
|
||||
"useBattery": true,
|
||||
"useBluetooth": true,
|
||||
"useBreakpoints": true,
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClipboard": true,
|
||||
"useClipboardItems": true,
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCountdown": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
"useCssVars": true,
|
||||
"useCurrentElement": true,
|
||||
"useCycleList": true,
|
||||
"useDark": true,
|
||||
"useDateFormat": true,
|
||||
"useDebounce": true,
|
||||
"useDebounceFn": true,
|
||||
"useDebouncedRefHistory": true,
|
||||
"useDeviceMotion": true,
|
||||
"useDeviceOrientation": true,
|
||||
"useDevicePixelRatio": true,
|
||||
"useDevicesList": true,
|
||||
"useDialog": true,
|
||||
"useDisplayMedia": true,
|
||||
"useDocumentVisibility": true,
|
||||
"useDraggable": true,
|
||||
"useDropZone": true,
|
||||
"useElementBounding": true,
|
||||
"useElementByPoint": true,
|
||||
"useElementHover": true,
|
||||
"useElementSize": true,
|
||||
"useElementVisibility": true,
|
||||
"useEventBus": true,
|
||||
"useEventListener": true,
|
||||
"useEventSource": true,
|
||||
"useEyeDropper": true,
|
||||
"useFavicon": true,
|
||||
"useFetch": true,
|
||||
"useFileDialog": true,
|
||||
"useFileSystemAccess": true,
|
||||
"useFocus": true,
|
||||
"useFocusWithin": true,
|
||||
"useFps": true,
|
||||
"useFullscreen": true,
|
||||
"useGamepad": true,
|
||||
"useGeolocation": true,
|
||||
"useId": true,
|
||||
"useIdle": true,
|
||||
"useImage": true,
|
||||
"useInfiniteScroll": true,
|
||||
"useIntersectionObserver": true,
|
||||
"useInterval": true,
|
||||
"useIntervalFn": true,
|
||||
"useKeyModifier": true,
|
||||
"useLastChanged": true,
|
||||
"useLink": true,
|
||||
"useLoadingBar": true,
|
||||
"useLocalStorage": true,
|
||||
"useMagicKeys": true,
|
||||
"useManualRefHistory": true,
|
||||
"useMediaControls": true,
|
||||
"useMediaQuery": true,
|
||||
"useMemoize": true,
|
||||
"useMemory": true,
|
||||
"useMessage": true,
|
||||
"useModel": true,
|
||||
"useMounted": true,
|
||||
"useMouse": true,
|
||||
"useMouseInElement": true,
|
||||
"useMousePressed": true,
|
||||
"useMutationObserver": true,
|
||||
"useNavigatorLanguage": true,
|
||||
"useNetwork": true,
|
||||
"useNotification": true,
|
||||
"useNow": true,
|
||||
"useObjectUrl": true,
|
||||
"useOffsetPagination": true,
|
||||
"useOnline": true,
|
||||
"usePageLeave": true,
|
||||
"useParallax": true,
|
||||
"useParentElement": true,
|
||||
"usePerformanceObserver": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerLock": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredContrast": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePreferredReducedTransparency": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useSSRWidth": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
"useScroll": true,
|
||||
"useScrollLock": true,
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSorted": true,
|
||||
"useSpeechRecognition": true,
|
||||
"useSpeechSynthesis": true,
|
||||
"useStepper": true,
|
||||
"useStorage": true,
|
||||
"useStorageAsync": true,
|
||||
"useStyleTag": true,
|
||||
"useSupported": true,
|
||||
"useSwipe": true,
|
||||
"useTemplateRef": true,
|
||||
"useTemplateRefsList": true,
|
||||
"useTextDirection": true,
|
||||
"useTextSelection": true,
|
||||
"useTextareaAutosize": true,
|
||||
"useThrottle": true,
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeAgoIntl": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
"useTimestamp": true,
|
||||
"useTitle": true,
|
||||
"useToNumber": true,
|
||||
"useToString": true,
|
||||
"useToggle": true,
|
||||
"useTransition": true,
|
||||
"useUrlSearchParams": true,
|
||||
"useUserMedia": true,
|
||||
"useVModel": true,
|
||||
"useVModels": true,
|
||||
"useVibrate": true,
|
||||
"useVirtualList": true,
|
||||
"useWakeLock": true,
|
||||
"useWebNotification": true,
|
||||
"useWebSocket": true,
|
||||
"useWebWorker": true,
|
||||
"useWebWorkerFn": true,
|
||||
"useWindowFocus": true,
|
||||
"useWindowScroll": true,
|
||||
"useWindowSize": true,
|
||||
"watch": true,
|
||||
"watchArray": true,
|
||||
"watchAtMost": true,
|
||||
"watchDebounced": true,
|
||||
"watchDeep": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchImmediate": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
}
|
||||
}
|
||||
242
auto-imports.d.ts
vendored
242
auto-imports.d.ts
vendored
@@ -3,67 +3,307 @@
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createRef: typeof import('@vueuse/core')['createRef']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||
const h: typeof import('vue')['h']
|
||||
const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const injectLocal: typeof import('@vueuse/core')['injectLocal']
|
||||
const isDefined: typeof import('@vueuse/core')['isDefined']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const isShallow: typeof import('vue')['isShallow']
|
||||
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onElementRemoval: typeof import('@vueuse/core')['onElementRemoval']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke']
|
||||
const onLongPress: typeof import('@vueuse/core')['onLongPress']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onStartTyping: typeof import('@vueuse/core')['onStartTyping']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const pausableWatch: typeof import('@vueuse/core')['pausableWatch']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const provideLocal: typeof import('@vueuse/core')['provideLocal']
|
||||
const reactify: typeof import('@vueuse/core')['reactify']
|
||||
const reactifyObject: typeof import('@vueuse/core')['reactifyObject']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed']
|
||||
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
|
||||
const reactivePick: typeof import('@vueuse/core')['reactivePick']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
|
||||
const refDebounced: typeof import('@vueuse/core')['refDebounced']
|
||||
const refDefault: typeof import('@vueuse/core')['refDefault']
|
||||
const refThrottled: typeof import('@vueuse/core')['refThrottled']
|
||||
const refWithControl: typeof import('@vueuse/core')['refWithControl']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const resolveRef: typeof import('@vueuse/core')['resolveRef']
|
||||
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const syncRef: typeof import('@vueuse/core')['syncRef']
|
||||
const syncRefs: typeof import('@vueuse/core')['syncRefs']
|
||||
const templateRef: typeof import('@vueuse/core')['templateRef']
|
||||
const throttledRef: typeof import('@vueuse/core')['throttledRef']
|
||||
const throttledWatch: typeof import('@vueuse/core')['throttledWatch']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toReactive: typeof import('@vueuse/core')['toReactive']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount']
|
||||
const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount']
|
||||
const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted']
|
||||
const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose']
|
||||
const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const unrefElement: typeof import('@vueuse/core')['unrefElement']
|
||||
const until: typeof import('@vueuse/core')['until']
|
||||
const useActiveElement: typeof import('@vueuse/core')['useActiveElement']
|
||||
const useAnimate: typeof import('@vueuse/core')['useAnimate']
|
||||
const useArrayDifference: typeof import('@vueuse/core')['useArrayDifference']
|
||||
const useArrayEvery: typeof import('@vueuse/core')['useArrayEvery']
|
||||
const useArrayFilter: typeof import('@vueuse/core')['useArrayFilter']
|
||||
const useArrayFind: typeof import('@vueuse/core')['useArrayFind']
|
||||
const useArrayFindIndex: typeof import('@vueuse/core')['useArrayFindIndex']
|
||||
const useArrayFindLast: typeof import('@vueuse/core')['useArrayFindLast']
|
||||
const useArrayIncludes: typeof import('@vueuse/core')['useArrayIncludes']
|
||||
const useArrayJoin: typeof import('@vueuse/core')['useArrayJoin']
|
||||
const useArrayMap: typeof import('@vueuse/core')['useArrayMap']
|
||||
const useArrayReduce: typeof import('@vueuse/core')['useArrayReduce']
|
||||
const useArraySome: typeof import('@vueuse/core')['useArraySome']
|
||||
const useArrayUnique: typeof import('@vueuse/core')['useArrayUnique']
|
||||
const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue']
|
||||
const useAsyncState: typeof import('@vueuse/core')['useAsyncState']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useBase64: typeof import('@vueuse/core')['useBase64']
|
||||
const useBattery: typeof import('@vueuse/core')['useBattery']
|
||||
const useBluetooth: typeof import('@vueuse/core')['useBluetooth']
|
||||
const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints']
|
||||
const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel']
|
||||
const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation']
|
||||
const useCached: typeof import('@vueuse/core')['useCached']
|
||||
const useClipboard: typeof import('@vueuse/core')['useClipboard']
|
||||
const useClipboardItems: typeof import('@vueuse/core')['useClipboardItems']
|
||||
const useCloned: typeof import('@vueuse/core')['useCloned']
|
||||
const useColorMode: typeof import('@vueuse/core')['useColorMode']
|
||||
const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog']
|
||||
const useCountdown: typeof import('@vueuse/core')['useCountdown']
|
||||
const useCounter: typeof import('@vueuse/core')['useCounter']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVar: typeof import('@vueuse/core')['useCssVar']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement']
|
||||
const useCycleList: typeof import('@vueuse/core')['useCycleList']
|
||||
const useDark: typeof import('@vueuse/core')['useDark']
|
||||
const useDateFormat: typeof import('@vueuse/core')['useDateFormat']
|
||||
const useDebounce: typeof import('@vueuse/core')['useDebounce']
|
||||
const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn']
|
||||
const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory']
|
||||
const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion']
|
||||
const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation']
|
||||
const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio']
|
||||
const useDevicesList: typeof import('@vueuse/core')['useDevicesList']
|
||||
const useDialog: typeof import('naive-ui')['useDialog']
|
||||
const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia']
|
||||
const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility']
|
||||
const useDraggable: typeof import('@vueuse/core')['useDraggable']
|
||||
const useDropZone: typeof import('@vueuse/core')['useDropZone']
|
||||
const useElementBounding: typeof import('@vueuse/core')['useElementBounding']
|
||||
const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint']
|
||||
const useElementHover: typeof import('@vueuse/core')['useElementHover']
|
||||
const useElementSize: typeof import('@vueuse/core')['useElementSize']
|
||||
const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility']
|
||||
const useEventBus: typeof import('@vueuse/core')['useEventBus']
|
||||
const useEventListener: typeof import('@vueuse/core')['useEventListener']
|
||||
const useEventSource: typeof import('@vueuse/core')['useEventSource']
|
||||
const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper']
|
||||
const useFavicon: typeof import('@vueuse/core')['useFavicon']
|
||||
const useFetch: typeof import('@vueuse/core')['useFetch']
|
||||
const useFileDialog: typeof import('@vueuse/core')['useFileDialog']
|
||||
const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess']
|
||||
const useFocus: typeof import('@vueuse/core')['useFocus']
|
||||
const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin']
|
||||
const useFps: typeof import('@vueuse/core')['useFps']
|
||||
const useFullscreen: typeof import('@vueuse/core')['useFullscreen']
|
||||
const useGamepad: typeof import('@vueuse/core')['useGamepad']
|
||||
const useGeolocation: typeof import('@vueuse/core')['useGeolocation']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useIdle: typeof import('@vueuse/core')['useIdle']
|
||||
const useImage: typeof import('@vueuse/core')['useImage']
|
||||
const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll']
|
||||
const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver']
|
||||
const useInterval: typeof import('@vueuse/core')['useInterval']
|
||||
const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn']
|
||||
const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier']
|
||||
const useLastChanged: typeof import('@vueuse/core')['useLastChanged']
|
||||
const useLink: typeof import('vue-router')['useLink']
|
||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||
const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage']
|
||||
const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys']
|
||||
const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory']
|
||||
const useMediaControls: typeof import('@vueuse/core')['useMediaControls']
|
||||
const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery']
|
||||
const useMemoize: typeof import('@vueuse/core')['useMemoize']
|
||||
const useMemory: typeof import('@vueuse/core')['useMemory']
|
||||
const useMessage: typeof import('naive-ui')['useMessage']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useMounted: typeof import('@vueuse/core')['useMounted']
|
||||
const useMouse: typeof import('@vueuse/core')['useMouse']
|
||||
const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement']
|
||||
const useMousePressed: typeof import('@vueuse/core')['useMousePressed']
|
||||
const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver']
|
||||
const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage']
|
||||
const useNetwork: typeof import('@vueuse/core')['useNetwork']
|
||||
const useNotification: typeof import('naive-ui')['useNotification']
|
||||
const useNow: typeof import('@vueuse/core')['useNow']
|
||||
const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl']
|
||||
const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination']
|
||||
const useOnline: typeof import('@vueuse/core')['useOnline']
|
||||
const usePageLeave: typeof import('@vueuse/core')['usePageLeave']
|
||||
const useParallax: typeof import('@vueuse/core')['useParallax']
|
||||
const useParentElement: typeof import('@vueuse/core')['useParentElement']
|
||||
const usePerformanceObserver: typeof import('@vueuse/core')['usePerformanceObserver']
|
||||
const usePermission: typeof import('@vueuse/core')['usePermission']
|
||||
const usePointer: typeof import('@vueuse/core')['usePointer']
|
||||
const usePointerLock: typeof import('@vueuse/core')['usePointerLock']
|
||||
const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe']
|
||||
const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme']
|
||||
const usePreferredContrast: typeof import('@vueuse/core')['usePreferredContrast']
|
||||
const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark']
|
||||
const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages']
|
||||
const usePreferredReducedMotion: typeof import('@vueuse/core')['usePreferredReducedMotion']
|
||||
const usePreferredReducedTransparency: typeof import('@vueuse/core')['usePreferredReducedTransparency']
|
||||
const usePrevious: typeof import('@vueuse/core')['usePrevious']
|
||||
const useRafFn: typeof import('@vueuse/core')['useRafFn']
|
||||
const useRefHistory: typeof import('@vueuse/core')['useRefHistory']
|
||||
const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver']
|
||||
const useRoute: typeof import('vue-router')['useRoute']
|
||||
const useRouter: typeof import('vue-router')['useRouter']
|
||||
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
|
||||
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
|
||||
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
|
||||
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
|
||||
const useScroll: typeof import('@vueuse/core')['useScroll']
|
||||
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']
|
||||
const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage']
|
||||
const useShare: typeof import('@vueuse/core')['useShare']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useSorted: typeof import('@vueuse/core')['useSorted']
|
||||
const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition']
|
||||
const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis']
|
||||
const useStepper: typeof import('@vueuse/core')['useStepper']
|
||||
const useStorage: typeof import('@vueuse/core')['useStorage']
|
||||
const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync']
|
||||
const useStyleTag: typeof import('@vueuse/core')['useStyleTag']
|
||||
const useSupported: typeof import('@vueuse/core')['useSupported']
|
||||
const useSwipe: typeof import('@vueuse/core')['useSwipe']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList']
|
||||
const useTextDirection: typeof import('@vueuse/core')['useTextDirection']
|
||||
const useTextSelection: typeof import('@vueuse/core')['useTextSelection']
|
||||
const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize']
|
||||
const useThrottle: typeof import('@vueuse/core')['useThrottle']
|
||||
const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn']
|
||||
const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory']
|
||||
const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo']
|
||||
const useTimeAgoIntl: typeof import('@vueuse/core')['useTimeAgoIntl']
|
||||
const useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
107
components.d.ts
vendored
107
components.d.ts
vendored
@@ -1,32 +1,58 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddPlaylist: typeof import('./src/components/Modal/AddPlaylist.vue')['default']
|
||||
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
|
||||
ArtistList: typeof import('./src/components/List/ArtistList.vue')['default']
|
||||
AutoClose: typeof import('./src/components/Modal/AutoClose.vue')['default']
|
||||
BatchList: typeof import('./src/components/Modal/BatchList.vue')['default']
|
||||
ChangeRate: typeof import('./src/components/Modal/ChangeRate.vue')['default']
|
||||
CloudMatch: typeof import('./src/components/Modal/CloudMatch.vue')['default']
|
||||
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
||||
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
||||
CoverDropdown: typeof import('./src/components/Cover/CoverDropdown.vue')['default']
|
||||
CoverList: typeof import('./src/components/List/CoverList.vue')['default']
|
||||
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
Equalizer: typeof import('./src/components/Modal/Equalizer.vue')['default']
|
||||
ExcludeLyrics: typeof import('./src/components/Modal/ExcludeLyrics.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/LoginQRCode.vue')['default']
|
||||
Lyric: typeof import('./src/components/Player/Lyric.vue')['default']
|
||||
MainControl: typeof import('./src/components/Player/MainControl.vue')['default']
|
||||
MainCover: typeof import('./src/components/Cover/MainCover.vue')['default']
|
||||
MainNav: typeof import('./src/components/Nav/MainNav.vue')['default']
|
||||
Menu: typeof import('./src/components/Global/Menu.vue')['default']
|
||||
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
|
||||
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
|
||||
KeyboardSetting: typeof import('./src/components/Setting/KeyboardSetting.vue')['default']
|
||||
LocalSetting: typeof import('./src/components/Setting/LocalSetting.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login/Login.vue')['default']
|
||||
LoginCookie: typeof import('./src/components/Modal/Login/LoginCookie.vue')['default']
|
||||
LoginPhone: typeof import('./src/components/Modal/Login/LoginPhone.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/Login/LoginQRCode.vue')['default']
|
||||
LoginUID: typeof import('./src/components/Modal/Login/LoginUID.vue')['default']
|
||||
LyricMenu: typeof import('./src/components/Player/LyricMenu.vue')['default']
|
||||
LyricsSetting: typeof import('./src/components/Setting/LyricsSetting.vue')['default']
|
||||
MainAMLyric: typeof import('./src/components/Player/MainAMLyric.vue')['default']
|
||||
MainLyric: typeof import('./src/components/Player/MainLyric.vue')['default']
|
||||
MainPlayer: typeof import('./src/components/Player/MainPlayer.vue')['default']
|
||||
MainPlayList: typeof import('./src/components/Player/MainPlayList.vue')['default']
|
||||
MainSetting: typeof import('./src/components/Setting/MainSetting.vue')['default']
|
||||
Menu: typeof import('./src/components/Layout/Menu.vue')['default']
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
Nav: typeof import('./src/components/Layout/Nav.vue')['default']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBackTop: typeof import('naive-ui')['NBackTop']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
@@ -34,67 +60,86 @@ declare module 'vue' {
|
||||
NDrawer: typeof import('naive-ui')['NDrawer']
|
||||
NDrawerContent: typeof import('naive-ui')['NDrawerContent']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NForm: typeof import('naive-ui')['NForm']
|
||||
NFormItem: typeof import('naive-ui')['NFormItem']
|
||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||
NGi: typeof import('naive-ui')['NGi']
|
||||
NGlobalStyle: typeof import('naive-ui')['NGlobalStyle']
|
||||
NGrid: typeof import('naive-ui')['NGrid']
|
||||
NH1: typeof import('naive-ui')['NH1']
|
||||
NH3: typeof import('naive-ui')['NH3']
|
||||
NH4: typeof import('naive-ui')['NH4']
|
||||
NH6: typeof import('naive-ui')['NH6']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NInputGroup: typeof import('naive-ui')['NInputGroup']
|
||||
NInputNumber: typeof import('naive-ui')['NInputNumber']
|
||||
NLayout: typeof import('naive-ui')['NLayout']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NLi: typeof import('naive-ui')['NLi']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NOl: typeof import('naive-ui')['NOl']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
OtherSetting: typeof import('./src/components/Setting/OtherSetting.vue')['default']
|
||||
PersonalFM: typeof import('./src/components/Player/PersonalFM.vue')['default']
|
||||
PlayerBackground: typeof import('./src/components/Player/PlayerBackground.vue')['default']
|
||||
PlayerComment: typeof import('./src/components/Player/PlayerComment.vue')['default']
|
||||
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
|
||||
Playlist: typeof import('./src/components/Global/Playlist.vue')['default']
|
||||
PlaylistUpdate: typeof import('./src/components/Modal/PlaylistUpdate.vue')['default']
|
||||
PrivateFm: typeof import('./src/components/Player/PrivateFm.vue')['default']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||
PlayerData: typeof import('./src/components/Player/PlayerData.vue')['default']
|
||||
PlayerMenu: typeof import('./src/components/Player/PlayerMenu.vue')['default']
|
||||
PlayerRightMenu: typeof import('./src/components/Player/PlayerRightMenu.vue')['default']
|
||||
PlayerSlider: typeof import('./src/components/Player/PlayerSlider.vue')['default']
|
||||
PlayerSpectrum: typeof import('./src/components/Player/PlayerSpectrum.vue')['default']
|
||||
PlaylistAdd: typeof import('./src/components/Modal/PlaylistAdd.vue')['default']
|
||||
PlaySetting: typeof import('./src/components/Setting/PlaySetting.vue')['default']
|
||||
Provider: typeof import('./src/components/Global/Provider.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchHot: typeof import('./src/components/Search/SearchHot.vue')['default']
|
||||
SearchDefault: typeof import('./src/components/Search/SearchDefault.vue')['default']
|
||||
SearchInp: typeof import('./src/components/Search/SearchInp.vue')['default']
|
||||
SearchSuggestions: typeof import('./src/components/Search/SearchSuggestions.vue')['default']
|
||||
SearchInpMenu: typeof import('./src/components/Menu/SearchInpMenu.vue')['default']
|
||||
SearchSuggest: typeof import('./src/components/Search/SearchSuggest.vue')['default']
|
||||
Sider: typeof import('./src/components/Layout/Sider.vue')['default']
|
||||
SImage: typeof import('./src/components/UI/s-image.vue')['default']
|
||||
SongCard: typeof import('./src/components/Card/SongCard.vue')['default']
|
||||
SongDataCard: typeof import('./src/components/Card/SongDataCard.vue')['default']
|
||||
SongInfoEditor: typeof import('./src/components/Modal/SongInfoEditor.vue')['default']
|
||||
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
||||
SongListDropdown: typeof import('./src/components/List/SongListDropdown.vue')['default']
|
||||
SpecialCover: typeof import('./src/components/Cover/SpecialCover.vue')['default']
|
||||
SpecialCoverCard: typeof import('./src/components/Cover/SpecialCoverCard.vue')['default']
|
||||
SongListCard: typeof import('./src/components/Card/SongListCard.vue')['default']
|
||||
SongListMenu: typeof import('./src/components/Menu/SongListMenu.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default']
|
||||
TitleBar: typeof import('./src/components/WinDom/TitleBar.vue')['default']
|
||||
UpCloudSong: typeof import('./src/components/Modal/UpCloudSong.vue')['default']
|
||||
UserData: typeof import('./src/components/Nav/UserData.vue')['default']
|
||||
TextContainer: typeof import('./src/components/Global/TextContainer.vue')['default']
|
||||
UpdateApp: typeof import('./src/components/Modal/UpdateApp.vue')['default']
|
||||
UpdatePlaylist: typeof import('./src/components/Modal/UpdatePlaylist.vue')['default']
|
||||
User: typeof import('./src/components/Layout/User.vue')['default']
|
||||
UserAgreement: typeof import('./src/components/Modal/UserAgreement.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
provider: github
|
||||
owner: "imsyy"
|
||||
repo: "SPlayer"
|
||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
SPlayer:
|
||||
build:
|
||||
context: .
|
||||
image: splayer
|
||||
container_name: SPlayer
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 25884:25884
|
||||
restart: always
|
||||
environment:
|
||||
# 所有变量都不是必填项
|
||||
# 网易云服务端 IP, 可在宿主机通过 ping music.163.com 获得
|
||||
- NETEASE_SERVER_IP=220.197.30.65
|
||||
# UnblockNeteaseMusic 使用的音源, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E9%9F%B3%E6%BA%90%E6%B8%85%E5%8D%95
|
||||
- UNBLOCK_SOURCES=kugou kuwo bilibili
|
||||
# 可添加 UnblockNeteaseMusic 支持的任何环境变量, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F
|
||||
- ENABLE_FLAC=false
|
||||
- ENABLE_HTTPDNS=false
|
||||
- BLOCK_ADS=true
|
||||
- DISABLE_UPGRADE_CHECK=false
|
||||
- DEVELOPMENT=false
|
||||
- FOLLOW_SOURCE_ORDER=true
|
||||
- JSON_LOG=false
|
||||
- NO_CACHE=false
|
||||
- SELECT_MAX_BR=true
|
||||
- LOG_LEVEL=info
|
||||
- SEARCH_ALBUM=true
|
||||
29
docker-entrypoint.sh
Normal file
29
docker-entrypoint.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# start unblock service in the background
|
||||
npx unblockneteasemusic -p 80:443 -s -f ${NETEASE_SERVER_IP:-220.197.30.65} -o ${UNBLOCK_SOURCES:-kugou bodian pyncmd} 2>&1 &
|
||||
|
||||
# point the neteasemusic address to the unblock service
|
||||
if ! grep -q "music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
|
||||
# start the nginx daemon
|
||||
nginx
|
||||
|
||||
# start the main process
|
||||
exec "$@"
|
||||
173
electron-builder.config.ts
Normal file
173
electron-builder.config.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { Configuration } from "electron-builder";
|
||||
|
||||
const config: Configuration = {
|
||||
// 应用程序的唯一标识符
|
||||
appId: "com.imsyy.splayer",
|
||||
// 应用程序的产品名称
|
||||
productName: "SPlayer",
|
||||
copyright: "Copyright © imsyy 2023",
|
||||
// 构建资源所在的目录
|
||||
directories: {
|
||||
buildResources: "build",
|
||||
},
|
||||
// 包含在最终应用程序构建中的文件列表
|
||||
// 使用通配符 ! 表示排除不需要的文件
|
||||
files: [
|
||||
"public/**",
|
||||
"out/**",
|
||||
"!**/.vscode/*",
|
||||
"!src/*",
|
||||
"!electron.vite.config.{js,ts,mjs,cjs}",
|
||||
"!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}",
|
||||
"!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
|
||||
],
|
||||
// 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack: ["public/**"],
|
||||
win: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/logo.ico",
|
||||
// Windows 平台全局文件名模板
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}",
|
||||
// 是否对可执行文件进行签名和编辑
|
||||
// signAndEditExecutable: false,
|
||||
// 构建类型(架构由命令行参数 --x64 或 --arm64 指定)
|
||||
target: [
|
||||
// 安装版
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// 打包版
|
||||
{
|
||||
target: "portable",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
// NSIS 安装器配置
|
||||
nsis: {
|
||||
// 是否一键式安装
|
||||
oneClick: false,
|
||||
// 安装程序的生成名称
|
||||
artifactName: "${productName}-${version}-${arch}-setup.${ext}",
|
||||
// 创建的桌面快捷方式名称
|
||||
shortcutName: "${productName}",
|
||||
// 卸载时显示的名称
|
||||
uninstallDisplayName: "${productName}",
|
||||
// 创建桌面图标
|
||||
createDesktopShortcut: "always",
|
||||
// 是否允许 UAC 提升权限
|
||||
allowElevation: true,
|
||||
// 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true,
|
||||
// 安装包图标
|
||||
installerIcon: "public/icons/favicon.ico",
|
||||
// 卸载命令图标
|
||||
uninstallerIcon: "public/icons/favicon.ico",
|
||||
},
|
||||
// Portable 便携版配置
|
||||
portable: {
|
||||
// 便携版文件名
|
||||
artifactName: "${productName}-${version}-${arch}-portable.${ext}",
|
||||
},
|
||||
// macOS 平台配置
|
||||
mac: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/favicon-512x512.png",
|
||||
// 权限继承的文件路径
|
||||
entitlementsInherit: "build/entitlements.mac.plist",
|
||||
// macOS 平台全局文件名模板
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}",
|
||||
// 扩展信息,如权限描述
|
||||
extendInfo: {
|
||||
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
||||
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
||||
NSDocumentsFolderUsageDescription:
|
||||
"Application requests access to the user's Documents folder.",
|
||||
NSDownloadsFolderUsageDescription:
|
||||
"Application requests access to the user's Downloads folder.",
|
||||
},
|
||||
// 是否启用应用程序的 Notarization(苹果的安全审核)
|
||||
notarize: false,
|
||||
darkModeSupport: true,
|
||||
category: "public.app-category.music",
|
||||
target: [
|
||||
// DMG 安装版
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// 压缩包安装版
|
||||
{
|
||||
target: "zip",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Linux 平台配置
|
||||
linux: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/favicon-512x512.png",
|
||||
// Linux 所有格式的统一文件名模板
|
||||
artifactName: "${name}-${version}-${arch}.${ext}",
|
||||
// 构建类型 - 支持 x64 和 ARM64 架构
|
||||
target: [
|
||||
// Pacman 包管理器
|
||||
{
|
||||
target: "pacman",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// AppImage 格式
|
||||
{
|
||||
target: "AppImage",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// Debian 包管理器
|
||||
{
|
||||
target: "deb",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// RPM 包管理器
|
||||
{
|
||||
target: "rpm",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// Snap 包管理器(仅支持 x64 架构)
|
||||
{
|
||||
target: "snap",
|
||||
arch: ["x64"],
|
||||
},
|
||||
// 压缩包格式
|
||||
{
|
||||
target: "tar.gz",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
// 维护者信息
|
||||
maintainer: "imsyy.top",
|
||||
// 应用程序类别
|
||||
category: "Audio;Music",
|
||||
},
|
||||
// AppImage 特定配置
|
||||
appImage: {
|
||||
// AppImage 文件的生成名称
|
||||
artifactName: "${name}-${version}-${arch}.${ext}",
|
||||
},
|
||||
// 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false,
|
||||
// Electron 下载镜像配置
|
||||
electronDownload: {
|
||||
mirror: "https://npmmirror.com/mirrors/electron/",
|
||||
},
|
||||
// 发布配置
|
||||
// 先留空,不自动上传
|
||||
publish: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,99 +0,0 @@
|
||||
# 应用程序的唯一标识符
|
||||
appId: com.imsyy.splayer
|
||||
# 应用程序的产品名称
|
||||
productName: SPlayer
|
||||
copyright: Copyright © imsyy 2023
|
||||
# 构建资源所在的目录
|
||||
directories:
|
||||
buildResources: build
|
||||
# 包含在最终应用程序构建中的文件列表,这里使用通配符 ! 表示排除不需要的文件
|
||||
files:
|
||||
- "!**/.vscode/*"
|
||||
- "!src/*"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}"
|
||||
- "!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}"
|
||||
- "!{.env,.env.*,.npmrc,pnpm-lock.yaml}"
|
||||
# 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack:
|
||||
- public/**
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
# 构建类型
|
||||
target:
|
||||
- nsis
|
||||
- portable
|
||||
# 管理员权限
|
||||
requestedExecutionLevel: highestAvailable
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
oneClick: false
|
||||
# 安装程序的生成名称
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
# 创建的桌面快捷方式名称
|
||||
shortcutName: ${productName}
|
||||
# 卸载时显示的名称
|
||||
uninstallDisplayName: ${productName}
|
||||
# 创建桌面图标
|
||||
createDesktopShortcut: always
|
||||
# 是否允许 UAC 提升权限
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
extendInfo:
|
||||
- NSCameraUsageDescription: Application requests access to the device's camera.
|
||||
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
|
||||
- NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder.
|
||||
- NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder.
|
||||
# 是否启用应用程序的 Notarization(苹果的安全审核)
|
||||
notarize: false
|
||||
darkModeSupport: true
|
||||
category: public.app-category.music
|
||||
# macOS 平台的 DMG 配置
|
||||
dmg:
|
||||
# DMG 文件的生成名称
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
# 构建类型
|
||||
target:
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
- rpm
|
||||
- tar.gz
|
||||
# 维护者信息
|
||||
maintainer: imsyy.top
|
||||
# 应用程序类别
|
||||
category: Audio;Music
|
||||
# AppImage 配置
|
||||
appImage:
|
||||
# AppImage 文件的生成名称
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
# 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false
|
||||
# 自动更新的配置
|
||||
publish:
|
||||
# 更新提供商
|
||||
provider: github
|
||||
# 自动更新检查的 URL
|
||||
# url: https://example.com/auto-updates
|
||||
owner: "imsyy"
|
||||
repo: "SPlayer"
|
||||
@@ -1,97 +1,113 @@
|
||||
import { resolve } from "path";
|
||||
import {
|
||||
defineConfig,
|
||||
externalizeDepsPlugin,
|
||||
loadEnv,
|
||||
splitVendorChunkPlugin,
|
||||
} from "electron-vite";
|
||||
import { MainEnv } from "./env";
|
||||
import { defineConfig, externalizeDepsPlugin, loadEnv } from "electron-vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
// import VueDevTools from "vite-plugin-vue-devtools";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
// 读取环境变量
|
||||
const getEnv = (name) => {
|
||||
const getEnv = (name: keyof MainEnv): string => {
|
||||
return loadEnv(mode, process.cwd())[name];
|
||||
};
|
||||
console.log(command);
|
||||
// 获取端口
|
||||
const webPort: number = Number(getEnv("VITE_WEB_PORT") || 14558);
|
||||
const servePort: number = Number(getEnv("VITE_SERVER_PORT") || 25884);
|
||||
// 返回配置
|
||||
return {
|
||||
// 主进程
|
||||
main: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@main": resolve(__dirname, "electron/main"),
|
||||
},
|
||||
},
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/main/index.js"),
|
||||
index: resolve(__dirname, "electron/main/index.ts"),
|
||||
lyric: resolve(__dirname, "web/lyric.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 预渲染
|
||||
// 预加载
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.js"),
|
||||
index: resolve(__dirname, "electron/preload/index.ts"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 渲染进程
|
||||
renderer: {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
root: ".",
|
||||
plugins: [
|
||||
vue(),
|
||||
// mode === "development" && VueDevTools(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
"vue",
|
||||
"vue-router",
|
||||
"@vueuse/core",
|
||||
{
|
||||
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
|
||||
},
|
||||
],
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filepath: "./auto-eslint.mjs",
|
||||
},
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()],
|
||||
}),
|
||||
// viteCompression
|
||||
viteCompression(),
|
||||
// splitVendorChunkPlugin
|
||||
splitVendorChunkPlugin(),
|
||||
wasm(),
|
||||
],
|
||||
// 服务器配置
|
||||
server: {
|
||||
port: getEnv("MAIN_VITE_DEV_PORT"),
|
||||
// 代理
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://${getEnv("MAIN_VITE_SERVER_HOST")}:${getEnv("MAIN_VITE_SERVER_PORT")}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src/"),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["legacy-js-api"],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 构建
|
||||
root: ".",
|
||||
server: {
|
||||
port: webPort,
|
||||
// 代理
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://127.0.0.1:${servePort}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, "/api"),
|
||||
},
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
port: webPort,
|
||||
},
|
||||
build: {
|
||||
minify: "terser",
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
loading: resolve(__dirname, "web/loading/index.html"),
|
||||
},
|
||||
output: {
|
||||
manualChunks: {
|
||||
stores: ["src/stores/data.ts", "src/stores/index.ts"],
|
||||
},
|
||||
},
|
||||
},
|
||||
terserOptions: {
|
||||
@@ -100,9 +116,6 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
win: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
1
electron/main/index.d.ts
vendored
Normal file
1
electron/main/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="electron-vite/node" />
|
||||
@@ -1,154 +0,0 @@
|
||||
import { join } from "path";
|
||||
import { app, shell, BrowserWindow, globalShortcut } from "electron";
|
||||
import { electronApp, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import createSystemInfo from "@main/utils/createSystemInfo";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import log from "electron-log";
|
||||
|
||||
// 主窗口
|
||||
let mainWindow;
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 单例锁
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/splayer-log.txt");
|
||||
// 设置日志文件的最大大小为 10 MB
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024;
|
||||
// 绑定 console.log
|
||||
console.log = log.log.bind(log);
|
||||
|
||||
// 创建主窗口
|
||||
const createWindow = () => {
|
||||
// 创建浏览器窗口
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280, // 窗口宽度
|
||||
height: 720, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: join(__dirname, "../../public/images/logo/favicon.png"),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 窗口准备就绪时显示窗口
|
||||
mainWindow.on("ready-to-show", () => {
|
||||
mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
});
|
||||
|
||||
// 设置窗口打开处理程序
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
}
|
||||
|
||||
// 监听关闭
|
||||
mainWindow.on("close", (event) => {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
mainWindow.hide();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化完成
|
||||
app.whenReady().then(async () => {
|
||||
// 尝试获取单例锁
|
||||
if (!gotTheLock) {
|
||||
// 如果获取不到单例锁,表示已经有一个实例在运行
|
||||
app.quit();
|
||||
log.error("已有一个程序正在运行");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("splayer");
|
||||
|
||||
// 初始化完成并准备创建浏览器窗口
|
||||
// 为 Windows 设置应用程序用户模型 ID
|
||||
electronApp.setAppUserModelId("com.electron");
|
||||
|
||||
// 在开发模式下默认通过 F12 打开或关闭 DevTools
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 创建主窗口
|
||||
createWindow();
|
||||
|
||||
// 创建系统信息
|
||||
createSystemInfo(mainWindow);
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 启动网易云 API
|
||||
await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(mainWindow);
|
||||
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) await startMainServer();
|
||||
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(mainWindow);
|
||||
|
||||
// 检测更新
|
||||
configureAutoUpdater(process.platform);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
86
electron/main/index.ts
Normal file
86
electron/main/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { electronApp } from "@electron-toolkit/utils";
|
||||
import { release, type } from "os";
|
||||
import { isMac } from "./utils/config";
|
||||
import { unregisterShortcuts } from "./shortcut";
|
||||
import { initTray, MainTray } from "./tray";
|
||||
import { processLog } from "./logger";
|
||||
import initAppServer from "../server";
|
||||
import { initSingleLock } from "./utils/single-lock";
|
||||
import loadWindow from "./windows/load-window";
|
||||
import mainWindow from "./windows/main-window";
|
||||
import initIpc from "./ipc";
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
// 窗口
|
||||
mainWindow: BrowserWindow | null = null;
|
||||
loadWindow: BrowserWindow | null = null;
|
||||
// 托盘
|
||||
mainTray: MainTray | null = null;
|
||||
// 是否退出
|
||||
isQuit: boolean = false;
|
||||
constructor() {
|
||||
processLog.info("🚀 Main process startup");
|
||||
// 程序单例锁
|
||||
initSingleLock();
|
||||
// 禁用 Windows 7 的 GPU 加速功能
|
||||
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
|
||||
// 监听应用事件
|
||||
this.handleAppEvents();
|
||||
// Electron 初始化完成后
|
||||
// 某些API只有在此事件发生后才能使用
|
||||
app.whenReady().then(async () => {
|
||||
processLog.info("🚀 Application Process Startup");
|
||||
// 设置应用程序名称
|
||||
electronApp.setAppUserModelId("com.imsyy.splayer");
|
||||
// 启动主服务进程
|
||||
await initAppServer();
|
||||
// 启动窗口
|
||||
this.loadWindow = loadWindow.create();
|
||||
this.mainWindow = mainWindow.create();
|
||||
// 注册其他服务
|
||||
this.mainTray = initTray(this.mainWindow!);
|
||||
// 注册 IPC 通信
|
||||
initIpc();
|
||||
});
|
||||
}
|
||||
// 应用程序事件
|
||||
handleAppEvents() {
|
||||
// 窗口被关闭时
|
||||
app.on("window-all-closed", () => {
|
||||
if (!isMac) app.quit();
|
||||
this.mainWindow = null;
|
||||
this.loadWindow = null;
|
||||
});
|
||||
|
||||
// 应用被激活
|
||||
app.on("activate", () => {
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
if (allWindows.length) {
|
||||
allWindows[0].focus();
|
||||
}
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
processLog.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
unregisterShortcuts();
|
||||
});
|
||||
|
||||
// 退出前
|
||||
app.on("before-quit", () => {
|
||||
this.isQuit = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new MainProcess();
|
||||
27
electron/main/ipc/index.ts
Normal file
27
electron/main/ipc/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import initFileIpc from "./ipc-file";
|
||||
import initLyricIpc from "./ipc-lyric";
|
||||
import initShortcutIpc from "./ipc-shortcut";
|
||||
import initStoreIpc from "./ipc-store";
|
||||
import initSystemIpc from "./ipc-system";
|
||||
import initThumbarIpc from "./ipc-thumbar";
|
||||
import initTrayIpc from "./ipc-tray";
|
||||
import initUpdateIpc from "./ipc-update";
|
||||
import initWindowsIpc from "./ipc-window";
|
||||
|
||||
/**
|
||||
* 初始化全部 IPC 通信
|
||||
* @returns void
|
||||
*/
|
||||
const initIpc = (): void => {
|
||||
initSystemIpc();
|
||||
initWindowsIpc();
|
||||
initUpdateIpc();
|
||||
initFileIpc();
|
||||
initTrayIpc();
|
||||
initLyricIpc();
|
||||
initStoreIpc();
|
||||
initThumbarIpc();
|
||||
initShortcutIpc();
|
||||
};
|
||||
|
||||
export default initIpc;
|
||||
390
electron/main/ipc/ipc-file.ts
Normal file
390
electron/main/ipc/ipc-file.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell } from "electron";
|
||||
import { basename, isAbsolute, join, relative, resolve } from "path";
|
||||
import { access, readFile, stat, unlink, writeFile } from "fs/promises";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { getFileID, getFileMD5, metaDataLyricsArrayToLrc } from "../utils/helper";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { ipcLog } from "../logger";
|
||||
import FastGlob from "fast-glob";
|
||||
import { download } from "electron-dl";
|
||||
|
||||
/**
|
||||
* 文件相关 IPC
|
||||
*/
|
||||
const initFileIpc = (): void => {
|
||||
// 默认文件夹
|
||||
ipcMain.handle(
|
||||
"get-default-dir",
|
||||
(_event, type: "documents" | "downloads" | "pictures" | "music" | "videos"): string => {
|
||||
return app.getPath(type);
|
||||
},
|
||||
);
|
||||
|
||||
// 遍历音乐文件
|
||||
ipcMain.handle("get-music-files", async (_, dirPath: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const filePath = resolve(dirPath).replace(/\\/g, "/");
|
||||
console.info(`📂 Fetching music files from: ${filePath}`);
|
||||
// 查找指定目录下的所有音乐文件
|
||||
const musicFiles = await FastGlob("**/*.{mp3,wav,flac,aac,webm}", { cwd: filePath });
|
||||
// 解析元信息
|
||||
const metadataPromises = musicFiles.map(async (file) => {
|
||||
const filePath = join(dirPath, file);
|
||||
// 处理元信息
|
||||
const { common, format } = await parseFile(filePath);
|
||||
// 获取文件大小
|
||||
const { size } = await stat(filePath);
|
||||
// 判断音质等级
|
||||
let quality: string;
|
||||
if ((format.sampleRate || 0) >= 96000 || (format.bitsPerSample || 0) > 16) {
|
||||
quality = "Hi-Res";
|
||||
} else if ((format.sampleRate || 0) >= 44100) {
|
||||
quality = "HQ";
|
||||
} else {
|
||||
quality = "SQ";
|
||||
}
|
||||
return {
|
||||
id: getFileID(filePath),
|
||||
name: common.title || basename(filePath),
|
||||
artists: common.artists?.[0] || common.artist,
|
||||
album: common.album || "",
|
||||
alia: common.comment?.[0]?.text || "",
|
||||
duration: (format?.duration ?? 0) * 1000,
|
||||
size: (size / (1024 * 1024)).toFixed(2),
|
||||
path: filePath,
|
||||
quality,
|
||||
};
|
||||
});
|
||||
const metadataArray = await Promise.all(metadataPromises);
|
||||
return metadataArray;
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error fetching music metadata:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取音乐元信息
|
||||
ipcMain.handle("get-music-metadata", async (_, path: string) => {
|
||||
try {
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common, format } = await parseFile(filePath);
|
||||
return {
|
||||
// 文件名称
|
||||
fileName: basename(filePath),
|
||||
// 文件大小
|
||||
fileSize: (await stat(filePath)).size / (1024 * 1024),
|
||||
// 元信息
|
||||
common,
|
||||
// 歌词
|
||||
lyric:
|
||||
metaDataLyricsArrayToLrc(common?.lyrics?.[0]?.syncText || []) ||
|
||||
common?.lyrics?.[0]?.text ||
|
||||
"",
|
||||
// 音质信息
|
||||
format,
|
||||
// md5
|
||||
md5: await getFileMD5(filePath),
|
||||
};
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error fetching music metadata:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 修改音乐元信息
|
||||
ipcMain.handle("set-music-metadata", async (_, path: string, metadata: any) => {
|
||||
try {
|
||||
const { name, artist, album, alia, lyric, cover } = metadata;
|
||||
// 规范化路径
|
||||
const songPath = resolve(path);
|
||||
const coverPath = cover ? resolve(cover) : null;
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songPath);
|
||||
// 读取封面文件
|
||||
const songCover = coverPath ? Picture.fromPath(coverPath) : null;
|
||||
// 保存元数据
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = name || "未知曲目";
|
||||
songFile.tag.performers = [artist || "未知艺术家"];
|
||||
songFile.tag.album = album || "未知专辑";
|
||||
songFile.tag.albumArtists = [artist || "未知艺术家"];
|
||||
songFile.tag.lyrics = lyric || "";
|
||||
songFile.tag.description = alia || "";
|
||||
songFile.tag.comment = alia || "";
|
||||
if (songCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
return true;
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error setting music metadata:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取音乐歌词
|
||||
ipcMain.handle(
|
||||
"get-music-lyric",
|
||||
async (
|
||||
_,
|
||||
path: string,
|
||||
): Promise<{
|
||||
lyric: string;
|
||||
format: "lrc" | "ttml";
|
||||
}> => {
|
||||
try {
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common } = await parseFile(filePath);
|
||||
|
||||
// 尝试获取同名的歌词文件
|
||||
const filePathWithoutExt = filePath.replace(/\.[^.]+$/, "");
|
||||
for (const ext of ["ttml", "lrc"] as const) {
|
||||
const lyricPath = `${filePathWithoutExt}.${ext}`;
|
||||
ipcLog.info("lyricPath", lyricPath);
|
||||
try {
|
||||
await access(lyricPath);
|
||||
const lyric = await readFile(lyricPath, "utf-8");
|
||||
if (lyric && lyric != "") return { lyric, format: ext };
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试获取元数据
|
||||
const lyric = common?.lyrics?.[0]?.syncText;
|
||||
if (lyric && lyric.length > 0) {
|
||||
return { lyric: metaDataLyricsArrayToLrc(lyric), format: "lrc" };
|
||||
} else if (common?.lyrics?.[0]?.text) {
|
||||
return { lyric: common?.lyrics?.[0]?.text, format: "lrc" };
|
||||
}
|
||||
|
||||
// 没有歌词
|
||||
return { lyric: "", format: "lrc" };
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error fetching music lyric:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 获取音乐封面
|
||||
ipcMain.handle(
|
||||
"get-music-cover",
|
||||
async (_, path: string): Promise<{ data: Buffer; format: string } | null> => {
|
||||
try {
|
||||
const { common } = await parseFile(path);
|
||||
// 获取封面数据
|
||||
const picture = common.picture?.[0];
|
||||
if (picture) {
|
||||
return { data: Buffer.from(picture.data), format: picture.format };
|
||||
} else {
|
||||
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
||||
try {
|
||||
await access(coverFilePath);
|
||||
const coverData = await readFile(coverFilePath);
|
||||
return { data: coverData, format: "image/jpeg" };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error fetching music cover:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 读取本地歌词
|
||||
ipcMain.handle(
|
||||
"read-local-lyric",
|
||||
async (_, lyricDir: string, id: number, ext: string): Promise<string> => {
|
||||
const pattern = `**/{,*.}${id}.${ext}`;
|
||||
try {
|
||||
const files = await FastGlob(pattern, { cwd: lyricDir });
|
||||
if (files.length > 0) {
|
||||
const firstMatch = join(lyricDir, files[0]);
|
||||
await access(firstMatch);
|
||||
const lyric = await readFile(firstMatch, "utf-8");
|
||||
if (lyric) return lyric;
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
return "";
|
||||
},
|
||||
);
|
||||
|
||||
// 删除文件
|
||||
ipcMain.handle("delete-file", async (_, path: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const resolvedPath = resolve(path);
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await access(resolvedPath);
|
||||
} catch {
|
||||
throw new Error("❌ File not found");
|
||||
}
|
||||
// 删除文件
|
||||
await unlink(resolvedPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ File delete error", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 打开文件夹
|
||||
ipcMain.on("open-folder", async (_, path: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const resolvedPath = resolve(path);
|
||||
// 检查文件夹是否存在
|
||||
try {
|
||||
await access(resolvedPath);
|
||||
} catch {
|
||||
throw new Error("❌ Folder not found");
|
||||
}
|
||||
// 打开文件夹
|
||||
shell.showItemInFolder(resolvedPath);
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Folder open error", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 图片选择窗口
|
||||
ipcMain.handle("choose-image", async () => {
|
||||
try {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }],
|
||||
});
|
||||
if (!filePaths || filePaths.length === 0) return null;
|
||||
return filePaths[0];
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Image choose error", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 路径选择窗口
|
||||
ipcMain.handle("choose-path", async () => {
|
||||
try {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
title: "选择文件夹",
|
||||
defaultPath: app.getPath("downloads"),
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
buttonLabel: "选择文件夹",
|
||||
});
|
||||
if (!filePaths || filePaths.length === 0) return null;
|
||||
return filePaths[0];
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Path choose error", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
ipcMain.handle(
|
||||
"download-file",
|
||||
async (
|
||||
event,
|
||||
url: string,
|
||||
options: {
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
path: string;
|
||||
downloadMeta?: boolean;
|
||||
downloadCover?: boolean;
|
||||
downloadLyric?: boolean;
|
||||
saveMetaFile?: boolean;
|
||||
lyric?: string;
|
||||
songData?: any;
|
||||
} = {
|
||||
fileName: "未知文件名",
|
||||
fileType: "mp3",
|
||||
path: app.getPath("downloads"),
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
// 获取窗口
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) return false;
|
||||
// 获取配置
|
||||
const {
|
||||
fileName,
|
||||
fileType,
|
||||
path,
|
||||
lyric,
|
||||
downloadMeta,
|
||||
downloadCover,
|
||||
downloadLyric,
|
||||
saveMetaFile,
|
||||
songData,
|
||||
} = options;
|
||||
// 规范化路径
|
||||
const downloadPath = resolve(path);
|
||||
// 检查文件夹是否存在
|
||||
try {
|
||||
await access(downloadPath);
|
||||
} catch {
|
||||
throw new Error("❌ Folder not found");
|
||||
}
|
||||
// 下载文件
|
||||
const songDownload = await download(win, url, {
|
||||
directory: downloadPath,
|
||||
filename: `${fileName}.${fileType}`,
|
||||
});
|
||||
if (!downloadMeta || !songData?.cover) return true;
|
||||
// 下载封面
|
||||
const coverUrl = songData?.coverSize?.l || songData.cover;
|
||||
const coverDownload = await download(win, coverUrl, {
|
||||
directory: downloadPath,
|
||||
filename: `${fileName}.jpg`,
|
||||
});
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = songData?.name || "未知曲目";
|
||||
songFile.tag.album = songData?.album?.name || "未知专辑";
|
||||
songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
||||
songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
||||
if (lyric && downloadLyric) songFile.tag.lyrics = lyric;
|
||||
if (songCover && downloadCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
// 创建同名歌词文件
|
||||
if (lyric && saveMetaFile && downloadLyric) {
|
||||
const lrcPath = join(downloadPath, `${fileName}.lrc`);
|
||||
await writeFile(lrcPath, lyric, "utf-8");
|
||||
}
|
||||
// 是否删除封面
|
||||
if (!saveMetaFile || !downloadCover) await unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error downloading file:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 检查是否是子文件夹
|
||||
ipcMain.handle("check-if-subfolder", (_, localFilesPath: string[], selectedDir: string) => {
|
||||
const resolvedSelectedDir = resolve(selectedDir);
|
||||
const allPaths = localFilesPath.map((p) => resolve(p));
|
||||
return allPaths.some((existingPath) => {
|
||||
const relativePath = relative(existingPath, resolvedSelectedDir);
|
||||
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default initFileIpc;
|
||||
201
electron/main/ipc/ipc-lyric.ts
Normal file
201
electron/main/ipc/ipc-lyric.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { BrowserWindow, ipcMain, screen } from "electron";
|
||||
import { useStore } from "../store";
|
||||
import lyricWindow from "../windows/lyric-window";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 歌词相关 IPC
|
||||
*/
|
||||
const initLyricIpc = (): void => {
|
||||
const store = useStore();
|
||||
|
||||
// 歌词窗口
|
||||
let lyricWin: BrowserWindow | null = null;
|
||||
|
||||
/**
|
||||
* 窗口是否存活
|
||||
* @param win 窗口实例
|
||||
* @returns 是否存活
|
||||
*/
|
||||
const isWinAlive = (win: BrowserWindow | null): win is BrowserWindow =>
|
||||
!!win && !win.isDestroyed();
|
||||
|
||||
// 切换桌面歌词
|
||||
ipcMain.on("toggle-desktop-lyric", (_event, val: boolean) => {
|
||||
if (val) {
|
||||
if (!isWinAlive(lyricWin)) {
|
||||
lyricWin = lyricWindow.create();
|
||||
// 监听关闭,置空引用,防止后续调用报错
|
||||
lyricWin?.on("closed", () => {
|
||||
lyricWin = null;
|
||||
});
|
||||
// 设置位置
|
||||
const { x, y } = store.get("lyric");
|
||||
const xPos = Number(x);
|
||||
const yPos = Number(y);
|
||||
if (Number.isFinite(xPos) && Number.isFinite(yPos)) {
|
||||
lyricWin?.setPosition(Math.round(xPos), Math.round(yPos));
|
||||
}
|
||||
} else {
|
||||
lyricWin.show();
|
||||
}
|
||||
if (isWinAlive(lyricWin)) {
|
||||
lyricWin.setAlwaysOnTop(true, "screen-saver");
|
||||
}
|
||||
} else {
|
||||
// 关闭:不销毁窗口,直接隐藏,保留位置与状态
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
lyricWin.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 更新歌词窗口数据
|
||||
ipcMain.on("update-desktop-lyric-data", (_, lyricData) => {
|
||||
if (!lyricData || !isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
|
||||
});
|
||||
|
||||
// 更新歌词窗口配置
|
||||
ipcMain.on("update-desktop-lyric-option", (_, option, callback: boolean = false) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!option || !isWinAlive(lyricWin)) return;
|
||||
// 增量更新
|
||||
const prevOption = store.get("lyric.config");
|
||||
if (prevOption) {
|
||||
option = { ...prevOption, ...option };
|
||||
}
|
||||
store.set("lyric.config", option);
|
||||
// 触发窗口更新
|
||||
if (callback && isWinAlive(lyricWin)) {
|
||||
lyricWin.webContents.send("update-desktop-lyric-option", option);
|
||||
}
|
||||
if (isWinAlive(mainWin)) {
|
||||
mainWin?.webContents.send("update-desktop-lyric-option", option);
|
||||
}
|
||||
});
|
||||
|
||||
// 播放状态更改
|
||||
ipcMain.on("play-status-change", (_, status) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", { playStatus: status });
|
||||
});
|
||||
|
||||
// 音乐名称更改
|
||||
ipcMain.on("play-song-change", (_, title) => {
|
||||
if (!title || !isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", { playName: title });
|
||||
});
|
||||
|
||||
// 音乐歌词更改
|
||||
ipcMain.on("play-lyric-change", (_, lyricData) => {
|
||||
if (!lyricData || !isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
|
||||
});
|
||||
|
||||
// 获取窗口位置
|
||||
ipcMain.handle("get-window-bounds", () => {
|
||||
if (!isWinAlive(lyricWin)) return {};
|
||||
return lyricWin.getBounds();
|
||||
});
|
||||
|
||||
// 获取屏幕尺寸
|
||||
ipcMain.handle("get-screen-size", () => {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||
return { width, height };
|
||||
});
|
||||
|
||||
// 获取多屏虚拟边界(支持负坐标)
|
||||
ipcMain.handle("get-virtual-screen-bounds", () => {
|
||||
const displays = screen.getAllDisplays();
|
||||
const bounds = displays.map((d) => d.workArea);
|
||||
const minX = Math.min(...bounds.map((b) => b.x));
|
||||
const minY = Math.min(...bounds.map((b) => b.y));
|
||||
const maxX = Math.max(...bounds.map((b) => b.x + b.width));
|
||||
const maxY = Math.max(...bounds.map((b) => b.y + b.height));
|
||||
return { minX, minY, maxX, maxY };
|
||||
});
|
||||
|
||||
// 移动窗口
|
||||
ipcMain.on("move-window", (_, x, y, width, height) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
lyricWin.setBounds({ x, y, width, height });
|
||||
// 保存配置
|
||||
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
|
||||
});
|
||||
|
||||
// 更新歌词窗口宽高
|
||||
ipcMain.on("update-lyric-size", (_, width, height) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
// 更新窗口宽度
|
||||
lyricWin.setBounds({ width, height });
|
||||
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||
});
|
||||
|
||||
// 更新高度
|
||||
ipcMain.on("update-window-height", (_, height) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
const store = useStore();
|
||||
const { width } = lyricWin.getBounds();
|
||||
// 更新窗口高度
|
||||
lyricWin.setBounds({ width, height });
|
||||
store.set("lyric", { ...store.get("lyric"), height });
|
||||
});
|
||||
|
||||
// 是否固定当前最大宽高
|
||||
ipcMain.on(
|
||||
"toggle-fixed-max-size",
|
||||
(_, options: { width: number; height: number; fixed: boolean }) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
const { width, height, fixed } = options;
|
||||
if (fixed) {
|
||||
lyricWin.setMaximumSize(width, height);
|
||||
} else {
|
||||
lyricWin.setMaximumSize(1400, 360);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 请求歌词数据
|
||||
ipcMain.on("request-desktop-lyric-data", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||
// 触发窗口更新
|
||||
mainWin?.webContents.send("request-desktop-lyric-data");
|
||||
});
|
||||
|
||||
// 请求歌词配置
|
||||
ipcMain.handle("request-desktop-lyric-option", () => {
|
||||
const config = store.get("lyric.config");
|
||||
if (isWinAlive(lyricWin)) {
|
||||
lyricWin.webContents.send("update-desktop-lyric-option", config);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 关闭桌面歌词
|
||||
ipcMain.on("closeDesktopLyric", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||
lyricWin.hide();
|
||||
mainWin?.webContents.send("closeDesktopLyric");
|
||||
});
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean, isTemp: boolean = false) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||
// 是否穿透
|
||||
if (isLock) {
|
||||
lyricWin.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
lyricWin.setIgnoreMouseEvents(false);
|
||||
}
|
||||
if (isTemp) return;
|
||||
store.set("lyric.config", { ...store.get("lyric.config"), isLock });
|
||||
// 触发窗口更新
|
||||
const config = store.get("lyric.config");
|
||||
mainWin?.webContents.send("update-desktop-lyric-option", config);
|
||||
});
|
||||
};
|
||||
|
||||
export default initLyricIpc;
|
||||
36
electron/main/ipc/ipc-shortcut.ts
Normal file
36
electron/main/ipc/ipc-shortcut.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "../shortcut";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 初始化快捷键 IPC 主进程
|
||||
* @returns void
|
||||
*/
|
||||
const initShortcutIpc = (): void => {
|
||||
// 快捷键是否被注册
|
||||
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
|
||||
|
||||
// 注册快捷键
|
||||
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin || !allShortcuts) return false;
|
||||
// 卸载所有快捷键
|
||||
unregisterShortcuts();
|
||||
// 注册快捷键
|
||||
const failedShortcuts: string[] = [];
|
||||
for (const key in allShortcuts) {
|
||||
const shortcut = allShortcuts[key].globalShortcut;
|
||||
if (!shortcut) continue;
|
||||
// 快捷键回调
|
||||
const callback = () => mainWin.webContents.send(key);
|
||||
const isSuccess = registerShortcut(shortcut, callback);
|
||||
if (!isSuccess) failedShortcuts.push(shortcut);
|
||||
}
|
||||
return failedShortcuts;
|
||||
});
|
||||
|
||||
// 卸载所有快捷键
|
||||
ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts());
|
||||
};
|
||||
|
||||
export default initShortcutIpc;
|
||||
45
electron/main/ipc/ipc-store.ts
Normal file
45
electron/main/ipc/ipc-store.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { useStore } from "../store";
|
||||
import type { StoreType } from "../store";
|
||||
|
||||
/**
|
||||
* 初始化 store IPC 主进程
|
||||
*/
|
||||
const initStoreIpc = (): void => {
|
||||
const store = useStore();
|
||||
if (!store) return;
|
||||
|
||||
// 获取配置项
|
||||
ipcMain.handle("store-get", (_event, key: keyof StoreType) => {
|
||||
return store.get(key as any);
|
||||
});
|
||||
|
||||
// 设置配置项
|
||||
ipcMain.handle("store-set", (_event, key: keyof StoreType, value: unknown) => {
|
||||
store.set(key as any, value as any);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 判断配置项是否存在
|
||||
ipcMain.handle("store-has", (_event, key: keyof StoreType) => {
|
||||
return store.has(key as any);
|
||||
});
|
||||
|
||||
// 删除配置项
|
||||
ipcMain.handle("store-delete", (_event, key: keyof StoreType) => {
|
||||
store.delete(key as any);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 重置配置(支持指定 keys 或全部重置)
|
||||
ipcMain.handle("store-reset", (_event, keys?: (keyof StoreType)[]) => {
|
||||
if (keys && keys.length > 0) {
|
||||
store.reset(...(keys as any));
|
||||
} else {
|
||||
store.reset();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export default initStoreIpc;
|
||||
105
electron/main/ipc/ipc-system.ts
Normal file
105
electron/main/ipc/ipc-system.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { app, ipcMain, net, powerSaveBlocker, session } from "electron";
|
||||
import { ipcLog } from "../logger";
|
||||
import { getFonts } from "font-list";
|
||||
import { useStore } from "../store";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 初始化系统 IPC 通信
|
||||
* @returns void
|
||||
*/
|
||||
const initSystemIpc = (): void => {
|
||||
const store = useStore();
|
||||
|
||||
/** 阻止系统息屏 ID */
|
||||
let preventId: number | null = null;
|
||||
|
||||
// 是否阻止系统息屏
|
||||
ipcMain.on("prevent-sleep", (_event, val: boolean) => {
|
||||
if (val) {
|
||||
preventId = powerSaveBlocker.start("prevent-display-sleep");
|
||||
ipcLog.info("⏾ System sleep prevention started");
|
||||
} else {
|
||||
if (preventId !== null) {
|
||||
powerSaveBlocker.stop(preventId);
|
||||
ipcLog.info("✅ System sleep prevention stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 退出应用
|
||||
ipcMain.on("quit-app", () => {
|
||||
app.exit(0);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 获取系统全部字体
|
||||
ipcMain.handle("get-all-fonts", async () => {
|
||||
try {
|
||||
const fonts = await getFonts();
|
||||
return fonts;
|
||||
} catch (error) {
|
||||
ipcLog.error(`❌ Failed to get all system fonts: ${error}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 取消代理
|
||||
ipcMain.on("remove-proxy", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
store.set("proxy", "");
|
||||
if (mainWin) {
|
||||
mainWin?.webContents.session.setProxy({ proxyRules: "" });
|
||||
}
|
||||
ipcLog.info("✅ Remove proxy successfully");
|
||||
});
|
||||
|
||||
// 配置网络代理
|
||||
ipcMain.on("set-proxy", (_, config) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
store.set("proxy", proxyRules);
|
||||
mainWin?.webContents.session.setProxy({ proxyRules });
|
||||
ipcLog.info("✅ Set proxy successfully:", proxyRules);
|
||||
});
|
||||
|
||||
// 代理测试
|
||||
ipcMain.handle("test-proxy", async (_, config) => {
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
try {
|
||||
// 设置代理
|
||||
const ses = session.defaultSession;
|
||||
await ses.setProxy({ proxyRules });
|
||||
// 测试请求
|
||||
const request = net.request({ url: "https://www.baidu.com" });
|
||||
return new Promise((resolve) => {
|
||||
request.on("response", (response) => {
|
||||
if (response.statusCode === 200) {
|
||||
ipcLog.info("✅ Proxy test successful");
|
||||
resolve(true);
|
||||
} else {
|
||||
ipcLog.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
request.on("error", (error) => {
|
||||
ipcLog.error("❌ Error testing proxy:", error);
|
||||
resolve(false);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error testing proxy:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 重置全部设置
|
||||
ipcMain.on("reset-setting", () => {
|
||||
store.reset();
|
||||
ipcLog.info("✅ Reset setting successfully");
|
||||
});
|
||||
};
|
||||
|
||||
export default initSystemIpc;
|
||||
15
electron/main/ipc/ipc-thumbar.ts
Normal file
15
electron/main/ipc/ipc-thumbar.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { getThumbar } from "../thumbar";
|
||||
|
||||
const initThumbarIpc = (): void => {
|
||||
// 更新工具栏
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
const thumbar = getThumbar();
|
||||
if (!thumbar) {
|
||||
return;
|
||||
}
|
||||
thumbar.updateThumbar(playStatus);
|
||||
});
|
||||
};
|
||||
|
||||
export default initThumbarIpc;
|
||||
48
electron/main/ipc/ipc-tray.ts
Normal file
48
electron/main/ipc/ipc-tray.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { getMainTray } from "../tray";
|
||||
import lyricWindow from "../windows/lyric-window";
|
||||
|
||||
/**
|
||||
* 托盘 IPC
|
||||
*/
|
||||
const initTrayIpc = (): void => {
|
||||
const tray = getMainTray();
|
||||
|
||||
// 音乐播放状态更改
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
const lyricWin = lyricWindow.getWin();
|
||||
tray?.setPlayState(playStatus ? "play" : "pause");
|
||||
if (!lyricWin) return;
|
||||
lyricWin.webContents.send("play-status-change", playStatus);
|
||||
});
|
||||
|
||||
// 音乐名称更改
|
||||
ipcMain.on("play-song-change", (_, title) => {
|
||||
if (!title) return;
|
||||
// 更改标题
|
||||
tray?.setTitle(title);
|
||||
tray?.setPlayName(title);
|
||||
});
|
||||
|
||||
// 播放模式切换
|
||||
ipcMain.on("play-mode-change", (_, mode) => {
|
||||
tray?.setPlayMode(mode);
|
||||
});
|
||||
|
||||
// 喜欢状态切换
|
||||
ipcMain.on("like-status-change", (_, likeStatus: boolean) => {
|
||||
tray?.setLikeState(likeStatus);
|
||||
});
|
||||
|
||||
// 桌面歌词开关
|
||||
ipcMain.on("toggle-desktop-lyric", (_, val: boolean) => {
|
||||
tray?.setDesktopLyricShow(val);
|
||||
});
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
||||
tray?.setDesktopLyricLock(isLock);
|
||||
});
|
||||
};
|
||||
|
||||
export default initTrayIpc;
|
||||
17
electron/main/ipc/ipc-update.ts
Normal file
17
electron/main/ipc/ipc-update.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { checkUpdate, startDownloadUpdate } from "../update";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
const initUpdateIpc = () => {
|
||||
// 检查更新
|
||||
ipcMain.on("check-update", (_event, showTip) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
checkUpdate(mainWin, showTip);
|
||||
});
|
||||
|
||||
// 开始下载更新
|
||||
ipcMain.on("start-download-update", () => startDownloadUpdate());
|
||||
};
|
||||
|
||||
export default initUpdateIpc;
|
||||
152
electron/main/ipc/ipc-window.ts
Normal file
152
electron/main/ipc/ipc-window.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import { useStore } from "../store";
|
||||
import { isDev } from "../utils/config";
|
||||
import { initThumbar } from "../thumbar";
|
||||
import mainWindow from "../windows/main-window";
|
||||
import loadWindow from "../windows/load-window";
|
||||
import loginWindow from "../windows/login-window";
|
||||
|
||||
/**
|
||||
* 窗口 IPC 通信
|
||||
* @returns void
|
||||
*/
|
||||
const initWindowsIpc = (): void => {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
// 当前窗口状态
|
||||
ipcMain.on("win-state", (event) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
event.returnValue = mainWin?.isMaximized();
|
||||
});
|
||||
|
||||
// 加载完成
|
||||
ipcMain.on("win-loaded", () => {
|
||||
const loadWin = loadWindow.getWin();
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (loadWin && !loadWin.isDestroyed()) loadWin.destroy();
|
||||
const isMaximized = store.get("window")?.maximized;
|
||||
if (isMaximized) mainWin?.maximize();
|
||||
if (!mainWin) return;
|
||||
mainWin?.show();
|
||||
mainWin?.focus();
|
||||
// 解决窗口不立即显示
|
||||
mainWin?.setAlwaysOnTop(true);
|
||||
// 100ms 后取消置顶
|
||||
const timer = setTimeout(() => {
|
||||
if (mainWin && !mainWin.isDestroyed()) {
|
||||
mainWin.setAlwaysOnTop(false);
|
||||
mainWin.focus();
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}, 100);
|
||||
// 初始化缩略图工具栏
|
||||
if (mainWin) initThumbar(mainWin);
|
||||
});
|
||||
|
||||
// 最小化
|
||||
ipcMain.on("win-min", (event) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
event.preventDefault();
|
||||
mainWin?.minimize();
|
||||
});
|
||||
|
||||
// 最大化
|
||||
ipcMain.on("win-max", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.maximize();
|
||||
});
|
||||
|
||||
// 还原
|
||||
ipcMain.on("win-restore", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.restore();
|
||||
});
|
||||
|
||||
// 隐藏
|
||||
ipcMain.on("win-hide", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.hide();
|
||||
});
|
||||
|
||||
// 显示
|
||||
ipcMain.on("win-show", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.show();
|
||||
mainWin?.focus();
|
||||
});
|
||||
|
||||
// 重启
|
||||
ipcMain.on("win-reload", () => {
|
||||
app.quit();
|
||||
app.relaunch();
|
||||
});
|
||||
|
||||
// 向主窗口发送事件
|
||||
ipcMain.on("send-to-mainWin", (_, eventName, ...args) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin || mainWin.isDestroyed() || mainWin.webContents.isDestroyed()) return;
|
||||
mainWin.webContents.send(eventName, ...args);
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
ipcMain.on("set-bar", (_event, val: number | "none" | "indeterminate" | "error" | "paused") => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
switch (val) {
|
||||
case "none":
|
||||
mainWin?.setProgressBar(-1);
|
||||
break;
|
||||
case "indeterminate":
|
||||
mainWin?.setProgressBar(2, { mode: "indeterminate" });
|
||||
break;
|
||||
case "error":
|
||||
mainWin?.setProgressBar(1, { mode: "error" });
|
||||
break;
|
||||
case "paused":
|
||||
mainWin?.setProgressBar(1, { mode: "paused" });
|
||||
break;
|
||||
default:
|
||||
if (typeof val === "number") {
|
||||
mainWin?.setProgressBar(val / 100);
|
||||
} else {
|
||||
mainWin?.setProgressBar(-1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 开启控制台
|
||||
ipcMain.on("open-dev-tools", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.webContents.openDevTools({
|
||||
title: "SPlayer DevTools",
|
||||
mode: isDev ? "right" : "detach",
|
||||
});
|
||||
});
|
||||
|
||||
// 开启登录窗口
|
||||
ipcMain.on("open-login-web", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
loginWindow.create(mainWin);
|
||||
});
|
||||
|
||||
// 开启设置
|
||||
ipcMain.on("open-setting", (_, type) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.show();
|
||||
mainWin?.focus();
|
||||
mainWin?.webContents.send("openSetting", type);
|
||||
});
|
||||
};
|
||||
|
||||
export default initWindowsIpc;
|
||||
47
electron/main/logger/index.ts
Normal file
47
electron/main/logger/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 日志输出
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { app } from "electron";
|
||||
import log from "electron-log";
|
||||
|
||||
// 日志文件路径
|
||||
const logDir = join(app.getPath("logs"));
|
||||
|
||||
// 是否存在日志目录
|
||||
if (!existsSync(logDir)) mkdirSync(logDir);
|
||||
|
||||
// 获取日期 - YYYY-MM-DD
|
||||
const dateString = new Date().toISOString().slice(0, 10);
|
||||
const logFilePath = join(logDir, `${dateString}.log`);
|
||||
|
||||
// 配置日志系统
|
||||
log.transports.console.useStyles = true; // 颜色输出
|
||||
log.transports.file.level = "info"; // 仅记录 info 及以上级别
|
||||
log.transports.file.resolvePathFn = (): string => logFilePath; // 日志文件路径
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024; // 文件最大 2MB
|
||||
|
||||
// 日志格式化
|
||||
// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}";
|
||||
|
||||
// 绑定默认事件
|
||||
const defaultLog = log.scope("default");
|
||||
console.log = defaultLog.log;
|
||||
console.info = defaultLog.info;
|
||||
console.warn = defaultLog.warn;
|
||||
console.error = defaultLog.error;
|
||||
|
||||
// 分作用域导出
|
||||
export { defaultLog };
|
||||
export const ipcLog = log.scope("ipc");
|
||||
export const trayLog = log.scope("tray");
|
||||
export const thumbarLog = log.scope("thumbar");
|
||||
export const storeLog = log.scope("store");
|
||||
export const updateLog = log.scope("update");
|
||||
export const systemLog = log.scope("system");
|
||||
export const configLog = log.scope("config");
|
||||
export const windowsLog = log.scope("windows");
|
||||
export const processLog = log.scope("process");
|
||||
export const preloadLog = log.scope("preload");
|
||||
export const rendererLog = log.scope("renderer");
|
||||
export const shortcutLog = log.scope("shortcut");
|
||||
export const serverLog = log.scope("server");
|
||||
@@ -1,278 +0,0 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { write } from "node-id3";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
/**
|
||||
* 监听主进程的 IPC 事件
|
||||
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
|
||||
*/
|
||||
|
||||
const mainIpcMain = (win) => {
|
||||
// 窗口操作部分
|
||||
ipcMain.on("window-min", (ev) => {
|
||||
// 阻止最小化
|
||||
ev.preventDefault();
|
||||
// 最小化
|
||||
win.minimize();
|
||||
});
|
||||
ipcMain.on("window-maxOrRestore", (ev) => {
|
||||
const winSizeState = win.isMaximized();
|
||||
winSizeState ? win.restore() : win.maximize();
|
||||
ev.reply("window-state", win.isMaximized());
|
||||
});
|
||||
ipcMain.on("window-restore", () => {
|
||||
win.restore();
|
||||
});
|
||||
ipcMain.on("window-hide", () => {
|
||||
win.hide();
|
||||
});
|
||||
ipcMain.on("window-close", () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
});
|
||||
ipcMain.on("window-relaunch", () => {
|
||||
app.isQuiting = true;
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
ipcMain.on("setProgressBar", (_, val) => {
|
||||
if (val === "close") {
|
||||
win.setProgressBar(-1);
|
||||
return false;
|
||||
}
|
||||
win.setProgressBar(val / 100);
|
||||
});
|
||||
|
||||
// 解灰
|
||||
ipcMain.handle("getMusicNumUrl", async (_, data) => {
|
||||
// 解析传入数据
|
||||
const songData = JSON.parse(data);
|
||||
const songName = `${songData?.name}-${songData?.artists?.[0].name}`;
|
||||
console.log("开始解灰:", songName);
|
||||
const url = await getNeteaseMusicUrl(songName);
|
||||
console.log("解灰地址:", url);
|
||||
return url;
|
||||
});
|
||||
|
||||
// bili 链接解析
|
||||
ipcMain.handle("getBiliUrlData", async (_, url) => {
|
||||
const data = await getBiliUrlBase64(url);
|
||||
return data;
|
||||
});
|
||||
|
||||
// 默认音乐文件夹
|
||||
ipcMain.handle("getdefaultMusicPath", async () => {
|
||||
const path = app.getPath("music");
|
||||
return path;
|
||||
});
|
||||
|
||||
// 选择文件夹
|
||||
ipcMain.handle("selectDir", async (_, isChooseDl = false) => {
|
||||
try {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
title: isChooseDl ? "选择下载目录" : "选择添加目录",
|
||||
defaultPath: isChooseDl ? app.getPath("downloads") : app.getPath("music"),
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
buttonLabel: "选择文件夹",
|
||||
});
|
||||
if (!canceled) {
|
||||
const selectedDirectory = filePaths[0];
|
||||
return selectedDirectory;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("选择文件夹时发生错误:", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取文件夹内容
|
||||
ipcMain.handle("getDirContents", async (_, selectedDir) => {
|
||||
try {
|
||||
// 使用 readDirAsync 函数递归地读取文件夹内容
|
||||
const directoryContents = await readDirAsync(selectedDir);
|
||||
return directoryContents;
|
||||
} catch (err) {
|
||||
console.error("读取文件夹内容时发生错误:", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取音乐歌词
|
||||
ipcMain.handle("getMusicLyric", async (_, path) => {
|
||||
try {
|
||||
const data = await parseFile(path);
|
||||
const lyric = data.common.lyrics;
|
||||
if (lyric && lyric.length > 0) {
|
||||
return lyric[0];
|
||||
}
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
// 返回读取的 lrc 数据,如果没有则返回 null
|
||||
return lrcData || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("读取音乐歌词出错:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取音乐封面
|
||||
ipcMain.handle("getMusicCover", async (_, path) => {
|
||||
try {
|
||||
const data = await parseFile(path);
|
||||
const picture = data.common.picture;
|
||||
if (picture && picture.length > 0) {
|
||||
const coverData = picture[0].data;
|
||||
const coverFormat = picture[0].format;
|
||||
return { coverData, coverFormat };
|
||||
}
|
||||
// 如果封面数据不存在,尝试读取同名的封面图片文件
|
||||
else {
|
||||
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
||||
const coverData = await fs.readFile(coverFilePath);
|
||||
// 返回读取的封面图片数据,如果没有则返回 null
|
||||
return coverData ? { coverData, coverFormat: "jpg" } : null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("读取音乐封面出错:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 执行复制操作
|
||||
ipcMain.handle("copyData", async (_, data) => {
|
||||
try {
|
||||
clipboard.writeText(data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("复制操作出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 本地磁盘文件删除
|
||||
ipcMain.handle("deleteFile", async (_, path) => {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (fs.access(path)) {
|
||||
// 尝试删除文件
|
||||
fs.unlink(path);
|
||||
console.log(`文件已删除:${path}`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`文件不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`文件删除操作出错:${path}`, err);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 打开歌曲目录
|
||||
ipcMain.on("openSongLocal", (_, path) => {
|
||||
try {
|
||||
if (fs.access(path)) {
|
||||
shell.showItemInFolder(path);
|
||||
} else {
|
||||
console.log(`文件不存在:${path}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("打开歌曲目录时出错:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 下载文件至指定目录
|
||||
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
|
||||
try {
|
||||
if (fs.access(path)) {
|
||||
const songData = JSON.parse(song);
|
||||
console.info("开始下载:", songData, data);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, data.url, {
|
||||
directory: path,
|
||||
filename: `${songName}.${songType}`,
|
||||
});
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, songData.cover, {
|
||||
directory: path,
|
||||
filename: `${songName}.jpg`,
|
||||
});
|
||||
// 生成歌曲文件的元数据
|
||||
const songTag = {
|
||||
title: songData.name,
|
||||
artist: Array.isArray(songData.artists)
|
||||
? songData.artists.map((ar) => ar.name).join(" / ")
|
||||
: songData.artists || "未知歌手",
|
||||
album: songData.album?.name || songData.album,
|
||||
image: coverDownload.getSavePath(),
|
||||
};
|
||||
// 保存修改后的元数据
|
||||
write(songTag, songDownload.getSavePath());
|
||||
// 删除封面
|
||||
await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} else {
|
||||
console.log(`目录不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("下载文件时出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 Bilibili 视频中获取文件的 Base64 数据
|
||||
*
|
||||
* @param {string} url - 要获取的文件的 URL
|
||||
* @returns {Promise<string>} - 文件的 Base64 数据
|
||||
*/
|
||||
const getBiliUrlBase64 = async (url) => {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
Referer: "https://www.bilibili.com/",
|
||||
"User-Agent": "okhttp/3.4.1",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
withCredentials: false,
|
||||
});
|
||||
// 将二进制数据转换为缓冲区
|
||||
const buffer = toBuffer(response.data);
|
||||
// 将缓冲区中的数据转换为 Base64 编码的字符串
|
||||
const encodedData = buffer.toString("base64");
|
||||
// 返回 Base64 编码的文件数据
|
||||
return encodedData;
|
||||
} catch (error) {
|
||||
console.error("获取文件数据时发生错误:" + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数据转换为缓冲区( Buffer )
|
||||
*
|
||||
* @param {ArrayBuffer|Buffer|Uint8Array} data - 要转换的数据
|
||||
* @returns {Buffer} - 转换后的缓冲区
|
||||
*/
|
||||
const toBuffer = (data) => {
|
||||
if (data instanceof Buffer) {
|
||||
return data;
|
||||
} else {
|
||||
return Buffer.from(data);
|
||||
}
|
||||
};
|
||||
|
||||
export default mainIpcMain;
|
||||
30
electron/main/shortcut/index.ts
Normal file
30
electron/main/shortcut/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { shortcutLog } from "../logger";
|
||||
|
||||
// 注册快捷键并检查
|
||||
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
|
||||
try {
|
||||
const success = globalShortcut.register(shortcut, callback);
|
||||
if (!success) {
|
||||
shortcutLog.error(`❌ Failed to register shortcut: ${shortcut}`);
|
||||
return false;
|
||||
} else {
|
||||
shortcutLog.info(`✅ Shortcut registered: ${shortcut}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
shortcutLog.error(`ℹ️ Error registering shortcut ${shortcut}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查快捷键是否被注册
|
||||
export const isShortcutRegistered = (shortcut: string): boolean => {
|
||||
return globalShortcut.isRegistered(shortcut);
|
||||
};
|
||||
|
||||
// 卸载所有快捷键
|
||||
export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
shortcutLog.info("🚫 All shortcuts unregistered.");
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { join } from "path";
|
||||
import express from "express";
|
||||
import expressProxy from "express-http-proxy";
|
||||
|
||||
/**
|
||||
* 启动主服务器
|
||||
* @returns {import('http').Server} HTTP 服务器实例
|
||||
*/
|
||||
export const startMainServer = async () => {
|
||||
const { MAIN_VITE_MAIN_PORT, MAIN_VITE_SERVER_HOST, MAIN_VITE_SERVER_PORT } = import.meta.env;
|
||||
const port = MAIN_VITE_MAIN_PORT ?? 7899;
|
||||
const apiHost = `http://${MAIN_VITE_SERVER_HOST}:${MAIN_VITE_SERVER_PORT}`;
|
||||
const expressApp = express();
|
||||
// 代理
|
||||
expressApp.use("/", express.static(join(__dirname, "../renderer/")));
|
||||
expressApp.use("/api", expressProxy(apiHost));
|
||||
// 启动 Express 应用服务器,并监听指定端口
|
||||
return expressApp.listen(port, "127.0.0.1");
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const netEaseApi = require("NeteaseCloudMusicApi");
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - 服务器配置
|
||||
* @param {number} [options.port=12141] - 服务器端口
|
||||
* @param {string} [options.host="127.0.0.1"] - 服务器主机地址
|
||||
* @returns {Promise<void>} 返回一个 Promise,在 API 服务器成功启动后 resolve
|
||||
*/
|
||||
export const startNcmServer = async (
|
||||
options = {
|
||||
port: 11451,
|
||||
host: "127.0.0.1",
|
||||
},
|
||||
) => {
|
||||
console.log(options);
|
||||
return await netEaseApi.serveNcmApi(options);
|
||||
};
|
||||
52
electron/main/store/index.ts
Normal file
52
electron/main/store/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { screen } from "electron";
|
||||
import { storeLog } from "../logger";
|
||||
import type { LyricConfig } from "../../../src/types/desktop-lyric";
|
||||
import defaultLyricConfig from "../../../src/assets/data/lyricConfig";
|
||||
import Store from "electron-store";
|
||||
|
||||
storeLog.info("🌱 Store init");
|
||||
|
||||
export interface StoreType {
|
||||
window: {
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
maximized?: boolean;
|
||||
};
|
||||
lyric: {
|
||||
// 窗口位置
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
// 配置
|
||||
config?: LyricConfig;
|
||||
};
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Store
|
||||
* @returns Store<StoreType>
|
||||
*/
|
||||
export const useStore = () => {
|
||||
// 获取主屏幕
|
||||
const screenData = screen.getPrimaryDisplay();
|
||||
return new Store<StoreType>({
|
||||
defaults: {
|
||||
window: {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
},
|
||||
lyric: {
|
||||
x: screenData.workAreaSize.width / 2 - 400,
|
||||
y: screenData.workAreaSize.height - 90,
|
||||
width: 800,
|
||||
height: 136,
|
||||
config: defaultLyricConfig,
|
||||
},
|
||||
proxy: "",
|
||||
},
|
||||
});
|
||||
};
|
||||
141
electron/main/thumbar/index.ts
Normal file
141
electron/main/thumbar/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
|
||||
import { join } from "path";
|
||||
import { isWin } from "../utils/config";
|
||||
import { thumbarLog } from "../logger";
|
||||
|
||||
enum ThumbarKeys {
|
||||
Play = "play",
|
||||
Pause = "pause",
|
||||
Prev = "prev",
|
||||
Next = "next",
|
||||
}
|
||||
|
||||
type ThumbarMap = Map<ThumbarKeys, ThumbarButton>;
|
||||
|
||||
export interface Thumbar {
|
||||
clearThumbar(): void;
|
||||
updateThumbar(playing: boolean, clean?: boolean): void;
|
||||
}
|
||||
|
||||
// 缩略图单例
|
||||
let thumbar: Thumbar | null = null;
|
||||
|
||||
// 工具栏图标
|
||||
const thumbarIcon = (filename: string) => {
|
||||
// 是否为暗色
|
||||
const isDark = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
return nativeImage.createFromPath(
|
||||
join(__dirname, `../../public/icons/thumbar/${filename}-${isDark ? "dark" : "light"}.png`),
|
||||
);
|
||||
};
|
||||
|
||||
// 缩略图工具栏
|
||||
const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
|
||||
return new Map<ThumbarKeys, ThumbarButton>()
|
||||
.set(ThumbarKeys.Prev, {
|
||||
tooltip: "上一曲",
|
||||
icon: thumbarIcon("prev"),
|
||||
click: () => win.webContents.send("playPrev"),
|
||||
})
|
||||
.set(ThumbarKeys.Next, {
|
||||
tooltip: "下一曲",
|
||||
icon: thumbarIcon("next"),
|
||||
click: () => win.webContents.send("playNext"),
|
||||
})
|
||||
.set(ThumbarKeys.Play, {
|
||||
tooltip: "播放",
|
||||
icon: thumbarIcon("play"),
|
||||
click: () => win.webContents.send("play"),
|
||||
})
|
||||
.set(ThumbarKeys.Pause, {
|
||||
tooltip: "暂停",
|
||||
icon: thumbarIcon("pause"),
|
||||
click: () => win.webContents.send("pause"),
|
||||
});
|
||||
};
|
||||
|
||||
// 创建缩略图工具栏
|
||||
class createThumbar implements Thumbar {
|
||||
// 窗口
|
||||
private _win: BrowserWindow;
|
||||
// 工具栏
|
||||
private _thumbar: ThumbarMap;
|
||||
// 工具栏按钮
|
||||
private _prev: ThumbarButton;
|
||||
private _next: ThumbarButton;
|
||||
private _play: ThumbarButton;
|
||||
private _pause: ThumbarButton;
|
||||
// 当前播放状态
|
||||
private _isPlaying: boolean = false;
|
||||
|
||||
constructor(win: BrowserWindow) {
|
||||
// 初始化数据
|
||||
this._win = win;
|
||||
this._thumbar = createThumbarButtons(win);
|
||||
// 工具栏按钮
|
||||
this._play = this._thumbar.get(ThumbarKeys.Play)!;
|
||||
this._pause = this._thumbar.get(ThumbarKeys.Pause)!;
|
||||
this._prev = this._thumbar.get(ThumbarKeys.Prev)!;
|
||||
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
||||
// 初始化工具栏
|
||||
this.updateThumbar();
|
||||
// 监听主题变化
|
||||
this.initThemeListener();
|
||||
}
|
||||
|
||||
// 初始化主题监听器
|
||||
private initThemeListener() {
|
||||
nativeTheme.on("updated", () => {
|
||||
this.refreshThumbarButtons();
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新工具栏按钮(主题变化时)
|
||||
private refreshThumbarButtons() {
|
||||
// 重新创建按钮
|
||||
this._thumbar = createThumbarButtons(this._win);
|
||||
this._play = this._thumbar.get(ThumbarKeys.Play)!;
|
||||
this._pause = this._thumbar.get(ThumbarKeys.Pause)!;
|
||||
this._prev = this._thumbar.get(ThumbarKeys.Prev)!;
|
||||
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
||||
// 更新工具栏
|
||||
this.updateThumbar(this._isPlaying);
|
||||
}
|
||||
|
||||
// 更新工具栏
|
||||
updateThumbar(playing: boolean = false, clean: boolean = false) {
|
||||
this._isPlaying = playing;
|
||||
if (clean) return this.clearThumbar();
|
||||
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
|
||||
}
|
||||
|
||||
// 清除工具栏
|
||||
clearThumbar() {
|
||||
this._win.setThumbarButtons([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化缩略图工具栏
|
||||
* @param win 窗口
|
||||
* @returns 缩略图工具栏
|
||||
*/
|
||||
export const initThumbar = (win: BrowserWindow) => {
|
||||
try {
|
||||
// 若非 Win
|
||||
if (!isWin) return null;
|
||||
thumbarLog.info("🚀 ThumbarButtons Startup");
|
||||
thumbar = new createThumbar(win);
|
||||
return thumbar;
|
||||
} catch (error) {
|
||||
thumbarLog.error("❌ ThumbarButtons Error", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取缩略图工具栏
|
||||
* @returns 缩略图工具栏
|
||||
*/
|
||||
export const getThumbar = () => thumbar;
|
||||
327
electron/main/tray/index.ts
Normal file
327
electron/main/tray/index.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
app,
|
||||
Tray,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
BrowserWindow,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
} from "electron";
|
||||
import { isWin, appName } from "../utils/config";
|
||||
import { join } from "path";
|
||||
import { trayLog } from "../logger";
|
||||
import { useStore } from "../store";
|
||||
import lyricWindow from "../windows/lyric-window";
|
||||
|
||||
// 播放模式
|
||||
type PlayMode = "repeat" | "repeat-once" | "shuffle";
|
||||
type PlayState = "play" | "pause" | "loading";
|
||||
|
||||
// 全局数据
|
||||
let playMode: PlayMode = "repeat";
|
||||
let playState: PlayState = "pause";
|
||||
let playName: string = "未播放歌曲";
|
||||
let likeSong: boolean = false;
|
||||
let desktopLyricShow: boolean = false;
|
||||
let desktopLyricLock: boolean = false;
|
||||
|
||||
export interface MainTray {
|
||||
setTitle(title: string): void;
|
||||
setPlayMode(mode: PlayMode): void;
|
||||
setLikeState(like: boolean): void;
|
||||
setPlayState(state: PlayState): void;
|
||||
setPlayName(name: string): void;
|
||||
setDesktopLyricShow(show: boolean): void;
|
||||
setDesktopLyricLock(lock: boolean): void;
|
||||
destroyTray(): void;
|
||||
}
|
||||
|
||||
// 托盘单例
|
||||
let mainTrayInstance: MainTray | null = null;
|
||||
|
||||
// 托盘图标
|
||||
const trayIcon = (filename: string) => {
|
||||
// const rootPath = isDev
|
||||
// ? join(__dirname, "../../public/icons/tray")
|
||||
// : join(app.getAppPath(), "../../public/icons/tray");
|
||||
// return nativeImage.createFromPath(join(rootPath, filename));
|
||||
return nativeImage.createFromPath(join(__dirname, `../../public/icons/tray/${filename}`));
|
||||
};
|
||||
|
||||
// 托盘菜单
|
||||
const createTrayMenu = (win: BrowserWindow): MenuItemConstructorOptions[] => {
|
||||
// 区分明暗图标
|
||||
const showIcon = (iconName: string) => {
|
||||
const isDark = nativeTheme.shouldUseDarkColors;
|
||||
return trayIcon(`${iconName}${isDark ? "-dark" : "-light"}.png`).resize({
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
};
|
||||
// 菜单
|
||||
const menu: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
id: "name",
|
||||
label: playName,
|
||||
icon: showIcon("music"),
|
||||
click: () => {
|
||||
win.show();
|
||||
win.focus();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "toogleLikeSong",
|
||||
label: likeSong ? "从我喜欢中移除" : "添加到我喜欢",
|
||||
icon: showIcon(likeSong ? "like" : "unlike"),
|
||||
click: () => win.webContents.send("toogleLikeSong"),
|
||||
},
|
||||
{
|
||||
id: "changeMode",
|
||||
label:
|
||||
playMode === "repeat" ? "列表循环" : playMode === "repeat-once" ? "单曲循环" : "随机播放",
|
||||
icon: showIcon(playMode),
|
||||
submenu: [
|
||||
{
|
||||
id: "repeat",
|
||||
label: "列表循环",
|
||||
icon: showIcon("repeat"),
|
||||
checked: playMode === "repeat",
|
||||
type: "radio",
|
||||
click: () => win.webContents.send("changeMode", "repeat"),
|
||||
},
|
||||
{
|
||||
id: "repeat-once",
|
||||
label: "单曲循环",
|
||||
icon: showIcon("repeat-once"),
|
||||
checked: playMode === "repeat-once",
|
||||
type: "radio",
|
||||
click: () => win.webContents.send("changeMode", "repeat-once"),
|
||||
},
|
||||
{
|
||||
id: "shuffle",
|
||||
label: "随机播放",
|
||||
icon: showIcon("shuffle"),
|
||||
checked: playMode === "shuffle",
|
||||
type: "radio",
|
||||
click: () => win.webContents.send("changeMode", "shuffle"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "playNext",
|
||||
label: "上一曲",
|
||||
icon: showIcon("prev"),
|
||||
click: () => win.webContents.send("playPrev"),
|
||||
},
|
||||
{
|
||||
id: "playOrPause",
|
||||
label: playState === "pause" ? "播放" : "暂停",
|
||||
icon: showIcon(playState === "pause" ? "play" : "pause"),
|
||||
click: () => win.webContents.send(playState === "pause" ? "play" : "pause"),
|
||||
},
|
||||
{
|
||||
id: "playNext",
|
||||
label: "下一曲",
|
||||
icon: showIcon("next"),
|
||||
click: () => win.webContents.send("playNext"),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "toogleDesktopLyric",
|
||||
label: `${desktopLyricShow ? "关闭" : "开启"}桌面歌词`,
|
||||
icon: showIcon("lyric"),
|
||||
click: () => win.webContents.send("toogleDesktopLyric"),
|
||||
},
|
||||
{
|
||||
id: "toogleDesktopLyricLock",
|
||||
label: `${desktopLyricLock ? "解锁" : "锁定"}桌面歌词`,
|
||||
icon: showIcon(desktopLyricLock ? "lock" : "unlock"),
|
||||
visible: desktopLyricShow,
|
||||
click: () => {
|
||||
const store = useStore();
|
||||
// 更新锁定状态
|
||||
store.set("lyric.config", { ...store.get("lyric.config"), isLock: !desktopLyricLock });
|
||||
// 触发窗口更新
|
||||
const config = store.get("lyric.config");
|
||||
const lyricWin = lyricWindow.getWin();
|
||||
if (!lyricWin) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-option", config);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "setting",
|
||||
label: "全局设置",
|
||||
icon: showIcon("setting"),
|
||||
click: () => {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("openSetting");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "exit",
|
||||
label: "退出",
|
||||
icon: showIcon("power"),
|
||||
click: () => {
|
||||
// win.close();
|
||||
app.exit(0);
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
return menu;
|
||||
};
|
||||
|
||||
// 创建托盘
|
||||
class CreateTray implements MainTray {
|
||||
// 窗口
|
||||
private _win: BrowserWindow;
|
||||
// 托盘
|
||||
private _tray: Tray;
|
||||
// 菜单
|
||||
private _menu: MenuItemConstructorOptions[];
|
||||
private _contextMenu: Menu;
|
||||
|
||||
constructor(win: BrowserWindow) {
|
||||
// 托盘图标
|
||||
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
});
|
||||
// 初始化数据
|
||||
this._win = win;
|
||||
this._tray = new Tray(icon);
|
||||
this._menu = createTrayMenu(this._win);
|
||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||
// 初始化事件
|
||||
this.initTrayMenu();
|
||||
this.initEvents();
|
||||
this.setTitle(appName);
|
||||
}
|
||||
// 托盘菜单
|
||||
private initTrayMenu() {
|
||||
this._menu = createTrayMenu(this._win);
|
||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||
this._tray.setContextMenu(this._contextMenu);
|
||||
}
|
||||
// 托盘事件
|
||||
private initEvents() {
|
||||
// 点击
|
||||
this._tray.on("click", () => this._win.show());
|
||||
// 明暗变化
|
||||
nativeTheme.on("updated", () => {
|
||||
this.initTrayMenu();
|
||||
});
|
||||
}
|
||||
// 设置标题
|
||||
/**
|
||||
* 设置标题
|
||||
* @param title 标题
|
||||
*/
|
||||
setTitle(title: string) {
|
||||
this._win.setTitle(title);
|
||||
this._tray.setTitle(title);
|
||||
this._tray.setToolTip(title);
|
||||
}
|
||||
/**
|
||||
* 设置播放名称
|
||||
* @param name 播放名称
|
||||
*/
|
||||
setPlayName(name: string) {
|
||||
// 超长处理
|
||||
if (name.length > 20) name = name.slice(0, 20) + "...";
|
||||
playName = name;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 设置播放状态
|
||||
* @param state 播放状态
|
||||
*/
|
||||
setPlayState(state: PlayState) {
|
||||
playState = state;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 设置播放模式
|
||||
* @param mode 播放模式
|
||||
*/
|
||||
setPlayMode(mode: PlayMode) {
|
||||
playMode = mode;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 设置喜欢状态
|
||||
* @param like 喜欢状态
|
||||
*/
|
||||
setLikeState(like: boolean) {
|
||||
likeSong = like;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 桌面歌词开关
|
||||
* @param show 桌面歌词开关状态
|
||||
*/
|
||||
setDesktopLyricShow(show: boolean) {
|
||||
desktopLyricShow = show;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 锁定桌面歌词
|
||||
* @param lock 锁定桌面歌词状态
|
||||
*/
|
||||
setDesktopLyricLock(lock: boolean) {
|
||||
desktopLyricLock = lock;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 销毁托盘
|
||||
*/
|
||||
destroyTray() {
|
||||
this._tray.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化托盘
|
||||
* @param win 主窗口
|
||||
* @param lyricWin 歌词窗口
|
||||
* @returns 托盘实例
|
||||
*/
|
||||
export const initTray = (win: BrowserWindow) => {
|
||||
try {
|
||||
trayLog.info("🚀 Tray Process Startup");
|
||||
const tray = new CreateTray(win);
|
||||
// 保存单例实例
|
||||
mainTrayInstance = tray;
|
||||
return tray;
|
||||
} catch (error) {
|
||||
trayLog.error("❌ Tray Process Error", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取托盘实例
|
||||
* @returns 托盘实例
|
||||
*/
|
||||
export const getMainTray = (): MainTray | null => mainTrayInstance;
|
||||
84
electron/main/update/index.ts
Normal file
84
electron/main/update/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { app, type BrowserWindow } from "electron";
|
||||
import { updateLog } from "../logger";
|
||||
import electronUpdater from "electron-updater";
|
||||
import { isDev } from "../utils/config";
|
||||
|
||||
// import
|
||||
const { autoUpdater } = electronUpdater;
|
||||
|
||||
// 开发环境启用
|
||||
if (isDev) {
|
||||
Object.defineProperty(app, "isPackaged", {
|
||||
get: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新源
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "github",
|
||||
owner: "imsyy",
|
||||
repo: "SPlayer",
|
||||
});
|
||||
|
||||
// 禁用自动下载
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
// 是否初始化
|
||||
let isInit: boolean = false;
|
||||
|
||||
// 是否提示
|
||||
let isShowTip: boolean = false;
|
||||
|
||||
// 事件监听
|
||||
const initUpdaterListeners = (win: BrowserWindow) => {
|
||||
if (isInit) return;
|
||||
|
||||
// 当有新版本可用时
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
win.webContents.send("update-available", info);
|
||||
updateLog.info(`🚀 New version available: ${info.version}`);
|
||||
});
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
win.webContents.send("download-progress", progress);
|
||||
updateLog.info(`🚀 Downloading: ${progress.percent}%`);
|
||||
});
|
||||
|
||||
// 当下载完成时
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
win.webContents.send("update-downloaded", info);
|
||||
updateLog.info(`🚀 Update downloaded: ${info.version}`);
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
// 当没有新版本时
|
||||
autoUpdater.on("update-not-available", (info) => {
|
||||
if (isShowTip) win.webContents.send("update-not-available", info);
|
||||
updateLog.info(`✅ No new version available: ${info.version}`);
|
||||
});
|
||||
|
||||
// 更新错误
|
||||
autoUpdater.on("error", (err) => {
|
||||
win.webContents.send("update-error", err);
|
||||
updateLog.error(`❌ Update error: ${err.message}`);
|
||||
});
|
||||
|
||||
isInit = true;
|
||||
};
|
||||
|
||||
// 检查更新
|
||||
export const checkUpdate = (win: BrowserWindow, showTip: boolean = false) => {
|
||||
// 初始化事件监听器
|
||||
initUpdaterListeners(win);
|
||||
// 更改提示
|
||||
isShowTip = showTip;
|
||||
// 检查更新
|
||||
autoUpdater.checkForUpdates();
|
||||
};
|
||||
|
||||
// 开始下载
|
||||
export const startDownloadUpdate = () => {
|
||||
autoUpdater.downloadUpdate();
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
const { autoUpdater } = require("electron-updater");
|
||||
|
||||
const checkForUpdates = () => {
|
||||
autoUpdater.checkForUpdates();
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
checkForUpdates();
|
||||
|
||||
// 监听检查更新的事件
|
||||
autoUpdater.on("checking-for-update", () => {
|
||||
console.log("Checking for update...");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
console.log("Update available:", info);
|
||||
});
|
||||
|
||||
autoUpdater.on("update-not-available", () => {
|
||||
console.log("Update not available.");
|
||||
});
|
||||
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
console.log("Update downloaded. Ready to install.");
|
||||
|
||||
// 在需要的时候,触发安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
};
|
||||
60
electron/main/utils/config.ts
Normal file
60
electron/main/utils/config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import { app } from "electron";
|
||||
|
||||
/**
|
||||
* 是否为开发环境
|
||||
* @returns boolean
|
||||
*/
|
||||
export const isDev = is.dev;
|
||||
|
||||
/** 是否为 Windows 系统 */
|
||||
export const isWin = process.platform === "win32";
|
||||
/** 是否为 macOS 系统 */
|
||||
export const isMac = process.platform === "darwin";
|
||||
/** 是否为 Linux 系统 */
|
||||
export const isLinux = process.platform === "linux";
|
||||
|
||||
/**
|
||||
* 软件版本
|
||||
* @returns string
|
||||
*/
|
||||
export const appVersion = app.getVersion();
|
||||
|
||||
/**
|
||||
* 程序名称
|
||||
* @returns string
|
||||
*/
|
||||
export const appName = app.getName() || "SPlayer";
|
||||
|
||||
/**
|
||||
* 服务器端口
|
||||
* @returns number
|
||||
*/
|
||||
export const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
|
||||
|
||||
/**
|
||||
* 主窗口加载地址
|
||||
* @returns string
|
||||
*/
|
||||
export const mainWinUrl =
|
||||
isDev && process.env["ELECTRON_RENDERER_URL"]
|
||||
? process.env["ELECTRON_RENDERER_URL"]
|
||||
: `http://localhost:${port}`;
|
||||
|
||||
/**
|
||||
* 歌词窗口加载地址
|
||||
* @returns string
|
||||
*/
|
||||
export const lyricWinUrl =
|
||||
isDev && process.env["ELECTRON_RENDERER_URL"]
|
||||
? `${process.env["ELECTRON_RENDERER_URL"]}/#/desktop-lyric`
|
||||
: `http://localhost:${port}/#/desktop-lyric`;
|
||||
|
||||
/**
|
||||
* 加载窗口地址
|
||||
* @returns string
|
||||
*/
|
||||
export const loadWinUrl =
|
||||
isDev && process.env["ELECTRON_RENDERER_URL"]
|
||||
? `${process.env["ELECTRON_RENDERER_URL"]}/web/loading/index.html`
|
||||
: `http://localhost:${port}/web/loading/index.html`;
|
||||
@@ -1,14 +0,0 @@
|
||||
import { globalShortcut } from "electron";
|
||||
|
||||
/**
|
||||
* 注册全局快捷键
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createGlobalShortcut = (win) => {
|
||||
// 刷新程序
|
||||
globalShortcut.register("CommandOrControl+R", () => {
|
||||
win?.reload();
|
||||
});
|
||||
};
|
||||
|
||||
export default createGlobalShortcut;
|
||||
@@ -1,115 +0,0 @@
|
||||
import { join } from "path";
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
|
||||
// 当前播放歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统自定义信息
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemInfo = (win) => {
|
||||
// 弹出列表
|
||||
app.setUserTasks([]);
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(join(__dirname, "../../public/images/logo/favicon.png"));
|
||||
// 给托盘图标设置气球提示
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 歌曲数据改变时
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
// 托盘图标标题
|
||||
mainTray.setToolTip(val);
|
||||
// 更改应用标题
|
||||
win.setTitle(val);
|
||||
});
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
});
|
||||
// 左键事件
|
||||
mainTray.on("click", () => {
|
||||
// 显示窗口
|
||||
win.show();
|
||||
});
|
||||
// 右键事件
|
||||
mainTray.on("right-click", () => {
|
||||
mainTray.popUpContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
});
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icon/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icon/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
// 返回菜单
|
||||
return [
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
click() {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("showPlayer");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.webContents.send("setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "退出",
|
||||
icon: createIcon("power"),
|
||||
click: () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default createSystemInfo;
|
||||
@@ -1,66 +0,0 @@
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 网易云音乐解灰
|
||||
* 目前音源采用 咪咕音乐
|
||||
*/
|
||||
|
||||
// 请求头
|
||||
const requestHeader = {
|
||||
Origin: "http://music.migu.cn/",
|
||||
Referer: "http://m.music.migu.cn/v3/",
|
||||
aversionid: import.meta.env.MAIN_VITE_MIGU_COOKIE || null,
|
||||
channel: "0146921",
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取咪咕音乐歌曲 ID
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongId = async (keyword) => {
|
||||
const url =
|
||||
"https://m.music.migu.cn/migu/remoting/scr_search_tag?keyword=" +
|
||||
keyword.toString() +
|
||||
"&type=2&rows=20&pgc=1";
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.musics?.length) {
|
||||
console.log(result.data.musics[0]);
|
||||
const oldName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(oldName[0])) {
|
||||
console.log("匹配失败");
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取给定关键字的音乐 URL
|
||||
* @param {string} keyword - 关键字
|
||||
* @returns {Promise<?string>} 如果找到,则解析为音乐 URL 的 Promise;如果未找到,则为 null
|
||||
* @throws {Error} 抛出错误
|
||||
*/
|
||||
const getNeteaseMusicUrl = async (keyword) => {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
const soundQuality = "PQ";
|
||||
const url =
|
||||
"https://app.c.nf.migu.cn/MIGUM2.0/strategy/listen-url/v2.4?netType=01&resourceType=2&songId=" +
|
||||
songId.toString() +
|
||||
"&toneFlag=" +
|
||||
soundQuality;
|
||||
const result = await axios.get(url, {
|
||||
headers: requestHeader,
|
||||
});
|
||||
if (result.data?.data?.url) {
|
||||
return result.data.data.url;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default getNeteaseMusicUrl;
|
||||
58
electron/main/utils/helper.ts
Normal file
58
electron/main/utils/helper.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createHash } from "crypto";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
/**
|
||||
* 生成文件唯一ID
|
||||
* @param filePath 文件路径
|
||||
* @returns 唯一ID
|
||||
*/
|
||||
export const getFileID = (filePath: string): number => {
|
||||
// SHA-256
|
||||
const hash = createHash("sha256");
|
||||
hash.update(filePath);
|
||||
const digest = hash.digest("hex");
|
||||
// 将哈希值的前 16 位转换为十进制数字
|
||||
const uniqueId = parseInt(digest.substring(0, 16), 16);
|
||||
return Number(uniqueId.toString().padStart(16, "0"));
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成文件 MD5
|
||||
* @param path 文件路径
|
||||
* @returns MD5值
|
||||
*/
|
||||
export const getFileMD5 = async (path: string): Promise<string> => {
|
||||
const data = await readFile(path);
|
||||
const hash = createHash("md5");
|
||||
hash.update(data);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 music-metadata 库中的歌词数组转换为LRC格式字符串
|
||||
* @param lyrics 歌词数组,每个元素包含时间戳(毫秒)和歌词文本
|
||||
* @returns LRC格式的字符串
|
||||
*/
|
||||
export const metaDataLyricsArrayToLrc = (
|
||||
lyrics: {
|
||||
text: string;
|
||||
timestamp?: number;
|
||||
}[],
|
||||
): string => {
|
||||
return lyrics
|
||||
.map(({ timestamp, text }) => {
|
||||
if (!timestamp) return "";
|
||||
const totalSeconds = Math.floor(timestamp / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
const centiseconds = Math.floor((timestamp % 1000) / 10);
|
||||
|
||||
// 格式化为两位数字
|
||||
const mm = String(minutes).padStart(2, "0");
|
||||
const ss = String(seconds).padStart(2, "0");
|
||||
const cs = String(centiseconds).padStart(2, "0");
|
||||
|
||||
return `[${mm}:${ss}.${cs}]${text}`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
import { parseFile } from "music-metadata";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 从指定文件夹中递归读取音乐文件,并将它们以数组形式返回
|
||||
* @param {string} directoryPath - 要读取的文件夹路径
|
||||
* @param {number} fileLimit - 返回的音乐文件数量限制
|
||||
* @returns {Array} - 包含音乐文件信息的数组
|
||||
*/
|
||||
export const readDirAsync = async (directoryPath, fileLimit = 5000) => {
|
||||
const result = [];
|
||||
// 递归读取文件夹中的项目
|
||||
const readItem = async (item) => {
|
||||
const itemPath = path.join(directoryPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
// 若为音频文件
|
||||
if (stats.isFile() && isAudioFile(itemPath)) {
|
||||
try {
|
||||
const { common, format } = await parseFile(itemPath);
|
||||
// 音乐文件信息
|
||||
const fileInfo = {
|
||||
id: generateId(itemPath),
|
||||
name: common.title,
|
||||
path: itemPath,
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2),
|
||||
time: stats.mtime?.getTime(),
|
||||
artists: common.artists?.[0],
|
||||
album: common.album,
|
||||
alia: common.comment?.[0],
|
||||
duration: formatDuration(format.duration),
|
||||
};
|
||||
result.push(fileInfo);
|
||||
} catch (error) {
|
||||
console.error("解析音乐文件元数据时出错:", error);
|
||||
}
|
||||
}
|
||||
// 若为文件夹
|
||||
if (stats.isDirectory()) {
|
||||
// 读取子文件夹中的项目
|
||||
const subItems = await fs.readdir(itemPath);
|
||||
for (const subItem of subItems) {
|
||||
await readItem(path.join(item, subItem));
|
||||
}
|
||||
}
|
||||
};
|
||||
// 从根目录开始读取
|
||||
await readItem("");
|
||||
// 返回不超过上限的音乐文件列表
|
||||
return result.slice(0, fileLimit);
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归地读取文件夹内容,包括文件和子文件夹的信息
|
||||
* @param {string} directoryPath - 要读取的文件夹路径
|
||||
* @param {number} depth - 递归深度(默认为 -1,无限递归)
|
||||
* @param {number} fileLimit - 文件总数
|
||||
* @returns {Promise<Array>} 包含文件和子文件夹信息的树形数组
|
||||
*/
|
||||
export const readDirTreeAsync = async (directoryPath, depth = -1, fileLimit = 5000) => {
|
||||
const result = [];
|
||||
|
||||
const readItem = async (item) => {
|
||||
const itemPath = path.join(directoryPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
|
||||
const fileInfo = {
|
||||
id: generateId(item),
|
||||
name: item,
|
||||
path: itemPath,
|
||||
type: stats.isFile() ? "song" : "dir",
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2), // 文件大小
|
||||
modified: stats.mtime, // 修改日期
|
||||
};
|
||||
|
||||
if (stats.isFile() && isAudioFile(itemPath)) {
|
||||
try {
|
||||
const { common, format } = await parseFile(itemPath);
|
||||
fileInfo.metadata = {
|
||||
name: common.title,
|
||||
artists: common.artists,
|
||||
album: common.album,
|
||||
date: common.date,
|
||||
alia: common.comment?.[0],
|
||||
year: common.year,
|
||||
duration: formatDuration(format.duration),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("解析音乐文件元数据时出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.isDirectory() && (depth === -1 || depth > 0)) {
|
||||
// 如果是文件夹且未达到递归深度限制,且文件数量未达到上限,则递归读取文件夹内容
|
||||
if (fileInfo.type === "dir" && result.length < fileLimit) {
|
||||
fileInfo.children = await readDirAsync(
|
||||
itemPath,
|
||||
depth === -1 ? -1 : depth - 1,
|
||||
fileLimit - result.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result.push(fileInfo);
|
||||
};
|
||||
|
||||
const items = await fs.readdir(directoryPath);
|
||||
await Promise.all(items.map(readItem));
|
||||
|
||||
return result.slice(0, fileLimit); // 返回不超过上限的文件列表
|
||||
};
|
||||
|
||||
/**
|
||||
* 歌曲时长时间戳转换
|
||||
* @param {number} mss 毫秒数
|
||||
* @returns {string} 格式为 "mm:ss" 的字符串
|
||||
*/
|
||||
const formatDuration = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
|
||||
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`;
|
||||
return `${formattedMinutes}:${formattedSeconds}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断文件是否为音频文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {boolean} - 是否为音频文件
|
||||
*/
|
||||
const isAudioFile = (filePath) => {
|
||||
const audioExtensions = [".flac", ".mp3"];
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
return audioExtensions.includes(extension);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从文件名生成数字 ID
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {number} - 生成的数字ID
|
||||
*/
|
||||
const generateId = (fileName) => {
|
||||
// 将文件名转换为哈希值
|
||||
let hash = 0;
|
||||
for (let i = 0; i < fileName.length; i++) {
|
||||
hash = (hash << 5) - hash + fileName.charCodeAt(i);
|
||||
}
|
||||
// 将哈希值转换为正整数
|
||||
const numericId = Math.abs(hash % 10000000000);
|
||||
return numericId;
|
||||
};
|
||||
25
electron/main/utils/single-lock.ts
Normal file
25
electron/main/utils/single-lock.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { app } from "electron";
|
||||
import { systemLog } from "../logger";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 初始化单实例锁
|
||||
* @returns 如果当前实例获得了锁,返回 true;否则返回 false
|
||||
*/
|
||||
export const initSingleLock = (): boolean => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
// 如果未获得锁,退出当前实例
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
systemLog.warn("❌ 已有一个实例正在运行");
|
||||
return false;
|
||||
}
|
||||
// 当第二个实例启动时触发
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
systemLog.warn("❌ 第二个实例将要启动");
|
||||
mainWindow.getWin()?.show();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
43
electron/main/windows/index.ts
Normal file
43
electron/main/windows/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import { windowsLog } from "../logger";
|
||||
import { appName } from "../utils/config";
|
||||
import { join } from "path";
|
||||
import icon from "../../../public/icons/favicon.png?asset";
|
||||
|
||||
export const createWindow = (
|
||||
options: BrowserWindowConstructorOptions = {},
|
||||
): BrowserWindow | null => {
|
||||
try {
|
||||
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||
title: appName,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frame: false, // 是否显示窗口边框
|
||||
center: true, // 窗口居中
|
||||
icon, // 窗口图标
|
||||
autoHideMenuBar: true, // 隐藏菜单栏
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
// 禁用渲染器沙盒
|
||||
sandbox: false,
|
||||
// 禁用同源策略
|
||||
webSecurity: false,
|
||||
// 允许 HTTP
|
||||
allowRunningInsecureContent: true,
|
||||
// 禁用拼写检查
|
||||
spellcheck: false,
|
||||
// 启用 Node.js
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
},
|
||||
};
|
||||
// 合并参数
|
||||
options = Object.assign(defaultOptions, options);
|
||||
// 创建窗口
|
||||
const win = new BrowserWindow(options);
|
||||
return win;
|
||||
} catch (error) {
|
||||
windowsLog.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
60
electron/main/windows/load-window.ts
Normal file
60
electron/main/windows/load-window.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { loadWinUrl } from "../utils/config";
|
||||
|
||||
class LoadWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
private winURL: string;
|
||||
constructor() {
|
||||
this.winURL = loadWinUrl;
|
||||
}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return;
|
||||
// 准备好显示
|
||||
this.win.on("ready-to-show", () => {
|
||||
this.win?.show();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
this.win = createWindow({
|
||||
width: 800,
|
||||
height: 560,
|
||||
maxWidth: 800,
|
||||
maxHeight: 560,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
// 不在任务栏显示
|
||||
skipTaskbar: true,
|
||||
// 窗口不能最小化
|
||||
minimizable: false,
|
||||
// 窗口不能最大化
|
||||
maximizable: false,
|
||||
// 窗口不能进入全屏状态
|
||||
fullscreenable: false,
|
||||
show: false,
|
||||
});
|
||||
if (!this.win) return null;
|
||||
// 加载地址
|
||||
this.win.loadURL(this.winURL);
|
||||
// 窗口事件
|
||||
this.event();
|
||||
return this.win;
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
return this.win;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LoadWindow();
|
||||
99
electron/main/windows/login-window.ts
Normal file
99
electron/main/windows/login-window.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { app, BrowserWindow, session } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { join } from "path";
|
||||
|
||||
class LoginWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
private loginTimer: NodeJS.Timeout | null = null;
|
||||
private loginSession: Electron.Session | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
private getLoginSession(): Electron.Session {
|
||||
if (!this.loginSession) {
|
||||
this.loginSession = session.fromPartition("persist:login");
|
||||
}
|
||||
return this.loginSession;
|
||||
}
|
||||
// 事件绑定
|
||||
private event(mainWin: BrowserWindow): void {
|
||||
if (!this.win) return;
|
||||
// 阻止新窗口创建
|
||||
this.win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
||||
// 加载完成后显示并开始轮询登录状态
|
||||
this.win.webContents.once("did-finish-load", () => {
|
||||
this.win?.show();
|
||||
this.loginTimer = setInterval(() => this.checkLogin(mainWin), 1000);
|
||||
this.win?.on("closed", () => {
|
||||
if (this.loginTimer) clearInterval(this.loginTimer);
|
||||
this.loginTimer = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
private async checkLogin(mainWin: BrowserWindow) {
|
||||
if (!this.win) return;
|
||||
try {
|
||||
this.win.webContents.executeJavaScript(
|
||||
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'",
|
||||
);
|
||||
// 判断 MUSIC_U
|
||||
const MUSIC_U = await this.getLoginSession().cookies.get({ name: "MUSIC_U" });
|
||||
if (MUSIC_U && MUSIC_U.length > 0) {
|
||||
if (this.loginTimer) clearInterval(this.loginTimer);
|
||||
this.loginTimer = null;
|
||||
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||
// 发送回主进程
|
||||
mainWin?.webContents.send("send-cookies", value);
|
||||
this.win.destroy();
|
||||
this.win = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建登录窗口
|
||||
async create(mainWin: BrowserWindow): Promise<BrowserWindow | null> {
|
||||
await app.whenReady();
|
||||
const loginSession = this.getLoginSession();
|
||||
// 清理登录会话存储
|
||||
await loginSession.clearStorageData({
|
||||
storages: ["cookies", "localstorage"],
|
||||
});
|
||||
|
||||
this.win = createWindow({
|
||||
parent: mainWin,
|
||||
title: "登录网易云音乐( 若遇到无响应请关闭后重试 )",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
allowRunningInsecureContent: true,
|
||||
spellcheck: false,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
session: loginSession,
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.win) return null;
|
||||
// 加载登录地址
|
||||
this.win.loadURL("https://music.163.com/#/login/");
|
||||
// 绑定事件
|
||||
this.event(mainWin);
|
||||
return this.win;
|
||||
}
|
||||
|
||||
// 获取窗口
|
||||
getWin(): BrowserWindow | null {
|
||||
return this.win;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LoginWindow();
|
||||
89
electron/main/windows/lyric-window.ts
Normal file
89
electron/main/windows/lyric-window.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { useStore } from "../store";
|
||||
import { lyricWinUrl } from "../utils/config";
|
||||
import mainWindow from "./main-window";
|
||||
|
||||
class LyricWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
constructor() {}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return;
|
||||
// 准备好显示
|
||||
this.win.on("ready-to-show", () => {
|
||||
this.win?.show();
|
||||
});
|
||||
// 歌词窗口缩放
|
||||
this.win?.on("resized", () => {
|
||||
const store = useStore();
|
||||
const bounds = this.win?.getBounds();
|
||||
if (bounds) {
|
||||
const { width, height } = bounds;
|
||||
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||
}
|
||||
});
|
||||
// 歌词窗口关闭
|
||||
this.win?.on("close", () => {
|
||||
this.win = null;
|
||||
const mainWin = mainWindow?.getWin();
|
||||
if (mainWin) {
|
||||
mainWin?.webContents.send("closeDesktopLyric");
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建主窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
const store = useStore();
|
||||
const { width, height, x, y } = store.get("lyric");
|
||||
this.win = createWindow({
|
||||
width: width || 800,
|
||||
height: height || 180,
|
||||
minWidth: 640,
|
||||
minHeight: 140,
|
||||
maxWidth: 1400,
|
||||
maxHeight: 360,
|
||||
// 没有指定位置时居中显示
|
||||
center: !(x && y),
|
||||
// 窗口位置
|
||||
x,
|
||||
y,
|
||||
transparent: true,
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
alwaysOnTop: true,
|
||||
resizable: true,
|
||||
movable: true,
|
||||
show: false,
|
||||
// 不在任务栏显示
|
||||
skipTaskbar: true,
|
||||
// 窗口不能最小化
|
||||
minimizable: false,
|
||||
// 窗口不能最大化
|
||||
maximizable: false,
|
||||
// 窗口不能进入全屏状态
|
||||
fullscreenable: false,
|
||||
});
|
||||
if (!this.win) return null;
|
||||
// 加载地址
|
||||
this.win.loadURL(lyricWinUrl);
|
||||
// 窗口事件
|
||||
this.event();
|
||||
return this.win;
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
if (this.win && !this.win?.isDestroyed()) return this.win;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LyricWindow();
|
||||
134
electron/main/windows/main-window.ts
Normal file
134
electron/main/windows/main-window.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { BrowserWindow, shell } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { mainWinUrl } from "../utils/config";
|
||||
import { useStore } from "../store";
|
||||
import { isLinux } from "../utils/config";
|
||||
|
||||
class MainWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
private winURL: string;
|
||||
constructor() {
|
||||
this.winURL = mainWinUrl;
|
||||
}
|
||||
/**
|
||||
* 保存窗口大小和状态
|
||||
*/
|
||||
private saveBounds() {
|
||||
if (this.win?.isFullScreen()) return;
|
||||
const store = useStore();
|
||||
const bounds = this.win?.getBounds();
|
||||
if (bounds) {
|
||||
const maximized = this.win?.isMaximized();
|
||||
store.set("window", { ...bounds, maximized });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return;
|
||||
const store = useStore();
|
||||
// 配置网络代理
|
||||
if (store.get("proxy")) {
|
||||
this.win.webContents.session.setProxy({ proxyRules: store.get("proxy") });
|
||||
}
|
||||
|
||||
// 窗口打开处理程序
|
||||
this.win.webContents.setWindowOpenHandler((details) => {
|
||||
const { url } = details;
|
||||
if (url.startsWith("https://") || url.startsWith("http://")) {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
return { action: "deny" };
|
||||
});
|
||||
// 窗口显示时
|
||||
this.win?.on("show", () => {
|
||||
this.win?.webContents.send("lyricsScroll");
|
||||
});
|
||||
// 窗口获得焦点时
|
||||
this.win?.on("focus", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
// 窗口大小改变时
|
||||
this.win?.on("resized", () => {
|
||||
// 若处于全屏则不保存
|
||||
if (this.win?.isFullScreen()) return;
|
||||
this.saveBounds();
|
||||
});
|
||||
// 窗口位置改变时
|
||||
this.win?.on("moved", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
// 窗口最大化时
|
||||
this.win?.on("maximize", () => {
|
||||
this.saveBounds();
|
||||
this.win?.webContents.send("win-state-change", true);
|
||||
});
|
||||
// 窗口取消最大化时
|
||||
this.win?.on("unmaximize", () => {
|
||||
this.saveBounds();
|
||||
this.win?.webContents.send("win-state-change", false);
|
||||
});
|
||||
// Linux 无法使用 resized 和 moved
|
||||
if (isLinux) {
|
||||
this.win?.on("resize", () => {
|
||||
// 若处于全屏则不保存
|
||||
if (this.win?.isFullScreen()) return;
|
||||
this.saveBounds();
|
||||
});
|
||||
this.win?.on("move", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
}
|
||||
// 窗口关闭
|
||||
this.win?.on("close", (event) => {
|
||||
event.preventDefault();
|
||||
this.win?.hide();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
const store = useStore();
|
||||
const { width, height } = store.get("window");
|
||||
this.win = createWindow({
|
||||
// 菜单栏
|
||||
titleBarStyle: "customButtonsOnHover",
|
||||
width,
|
||||
height,
|
||||
minHeight: 600,
|
||||
minWidth: 800,
|
||||
show: false,
|
||||
});
|
||||
if (!this.win) return null;
|
||||
// 加载地址
|
||||
this.win.loadURL(this.winURL);
|
||||
// 窗口事件
|
||||
this.event();
|
||||
return this.win;
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
if (this.win && !this.win.isDestroyed()) {
|
||||
return this.win;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 显示主窗口
|
||||
*/
|
||||
showWindow() {
|
||||
if (this.win) {
|
||||
this.win.show();
|
||||
if (this.win.isMinimized()) this.win.restore();
|
||||
this.win.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new MainWindow();
|
||||
17
electron/preload/index.d.ts
vendored
Normal file
17
electron/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ElectronAPI } from "@electron-toolkit/preload";
|
||||
import type { StoreType } from "../main/store";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI;
|
||||
api: {
|
||||
store: {
|
||||
get<K extends keyof StoreType>(key: K): Promise<StoreType[K]>;
|
||||
set<K extends keyof StoreType>(key: K, value: StoreType[K]): Promise<boolean>;
|
||||
has(key: keyof StoreType): Promise<boolean>;
|
||||
delete(key: keyof StoreType): Promise<boolean>;
|
||||
reset(keys?: (keyof StoreType)[]): Promise<boolean>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { contextBridge } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
// 如果启用了上下文隔离,使用 `contextBridge` 将 Electron API 暴露给渲染进程
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
// 使用 contextBridge 暴露 electronAPI 到渲染进程的全局对象中
|
||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// 如果上下文隔离未启用,将 electronAPI 添加到 DOM 全局对象
|
||||
window.electron = electronAPI;
|
||||
}
|
||||
23
electron/preload/index.ts
Normal file
23
electron/preload/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||
// Expose store API via preload
|
||||
contextBridge.exposeInMainWorld("api", {
|
||||
store: {
|
||||
get: (key: string) => ipcRenderer.invoke("store-get", key),
|
||||
set: (key: string, value: unknown) => ipcRenderer.invoke("store-set", key, value),
|
||||
has: (key: string) => ipcRenderer.invoke("store-has", key),
|
||||
delete: (key: string) => ipcRenderer.invoke("store-delete", key),
|
||||
reset: (keys?: string[]) => ipcRenderer.invoke("store-reset", keys),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
187
electron/server/control/index.ts
Normal file
187
electron/server/control/index.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import mainWindow from "../../main/windows/main-window";
|
||||
|
||||
/**
|
||||
* 播放控制接口
|
||||
* @param fastify Fastify 实例
|
||||
*/
|
||||
export const initControlAPI = async (fastify: FastifyInstance) => {
|
||||
// 播放控制路由前缀
|
||||
await fastify.register(
|
||||
async (fastify) => {
|
||||
// 播放
|
||||
fastify.get("/play", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("play");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "播放命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "播放失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 暂停
|
||||
fastify.get("/pause", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("pause");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "暂停命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "暂停失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 播放/暂停切换
|
||||
fastify.get("/toggle", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 这里可以根据当前播放状态来决定发送 play 还是 pause
|
||||
// 暂时先发送 toggle 事件,如果渲染进程支持的话
|
||||
mainWin.webContents.send("toggle");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "播放/暂停切换命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "播放/暂停切换失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 下一曲
|
||||
fastify.get("/next", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("playNext");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "下一曲命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "下一曲失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上一曲
|
||||
fastify.get("/prev", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("playPrev");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "上一曲命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "上一曲失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取播放状态(可选功能)
|
||||
fastify.get("/status", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 这里可以通过 IPC 获取当前播放状态
|
||||
// 暂时返回基本信息
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "获取状态成功",
|
||||
data: {
|
||||
connected: true,
|
||||
window: "available",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "获取状态失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ prefix: "/control" },
|
||||
);
|
||||
};
|
||||
63
electron/server/index.ts
Normal file
63
electron/server/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { join } from "path";
|
||||
import { isDev } from "../main/utils/config";
|
||||
import { serverLog } from "../main/logger";
|
||||
import { initNcmAPI } from "./netease";
|
||||
import { initUnblockAPI } from "./unblock";
|
||||
import { initControlAPI } from "./control";
|
||||
import fastifyCookie from "@fastify/cookie";
|
||||
import fastifyMultipart from "@fastify/multipart";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import fastify from "fastify";
|
||||
|
||||
const initAppServer = async () => {
|
||||
try {
|
||||
const server = fastify({
|
||||
routerOptions: {
|
||||
// 忽略尾随斜杠
|
||||
ignoreTrailingSlash: true,
|
||||
},
|
||||
});
|
||||
// 注册插件
|
||||
server.register(fastifyCookie);
|
||||
server.register(fastifyMultipart);
|
||||
// 生产环境启用静态文件
|
||||
if (!isDev) {
|
||||
serverLog.info("📂 Serving static files from /renderer");
|
||||
server.register(fastifyStatic, {
|
||||
root: join(__dirname, "../renderer"),
|
||||
});
|
||||
}
|
||||
// 声明
|
||||
server.get("/api", (_, reply) => {
|
||||
reply.send({
|
||||
name: "SPlayer API",
|
||||
description: "SPlayer API service",
|
||||
author: "@imsyy",
|
||||
list: [
|
||||
{
|
||||
name: "NeteaseCloudMusicApi",
|
||||
url: "/api/netease",
|
||||
},
|
||||
{
|
||||
name: "UnblockAPI",
|
||||
url: "/api/unblock",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
// 注册接口
|
||||
server.register(initNcmAPI, { prefix: "/api" });
|
||||
server.register(initUnblockAPI, { prefix: "/api" });
|
||||
server.register(initControlAPI, { prefix: "/api" });
|
||||
// 启动端口
|
||||
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
|
||||
await server.listen({ port });
|
||||
serverLog.info(`🌐 Starting AppServer on port ${port}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
serverLog.error("🚫 AppServer failed to start");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default initAppServer;
|
||||
87
electron/server/netease/index.ts
Normal file
87
electron/server/netease/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { pathCase } from "change-case";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api";
|
||||
|
||||
// 获取数据
|
||||
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
||||
return async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
serverLog.log("🌐 Request NcmAPI:", name);
|
||||
// 获取 NcmAPI 数据
|
||||
try {
|
||||
const result = await neteaseApi({
|
||||
...req.query,
|
||||
...(req.body as Record<string, any>),
|
||||
cookie: req.cookies,
|
||||
});
|
||||
return reply.send(result.body);
|
||||
} catch (error: any) {
|
||||
serverLog.error("❌ NcmAPI Error:", error);
|
||||
if ([400, 301].includes(error.status)) {
|
||||
return reply.status(error.status).send(error.body);
|
||||
}
|
||||
return reply.status(500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化 NcmAPI
|
||||
export const initNcmAPI = async (fastify: FastifyInstance) => {
|
||||
// 主信息
|
||||
fastify.get("/netease", (_, reply) => {
|
||||
reply.send({
|
||||
name: "@neteaseapireborn/api",
|
||||
version: "4.29.2",
|
||||
description: "网易云音乐 API Enhanced",
|
||||
author: "@MoeFurina",
|
||||
license: "MIT",
|
||||
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
|
||||
});
|
||||
});
|
||||
|
||||
// 注册 NeteaseCloudMusicApi 所有接口
|
||||
Object.entries(NeteaseCloudMusicApi).forEach(([routerName, neteaseApi]: [string, any]) => {
|
||||
// 例外
|
||||
if (["serveNcmApi", "getModulesDefinitions"].includes(routerName)) return;
|
||||
// 路由名称
|
||||
const pathName = pathCase(routerName);
|
||||
// 获取数据
|
||||
const handler = getHandler(pathName, neteaseApi);
|
||||
// 注册路由
|
||||
fastify.get(`/netease/${pathName}`, handler);
|
||||
fastify.post(`/netease/${pathName}`, handler);
|
||||
// 兼容路由 - 中间具有 _ 的路由
|
||||
if (routerName.includes("_")) {
|
||||
fastify.get(`/netease/${routerName}`, handler);
|
||||
fastify.post(`/netease/${routerName}`, handler);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取 TTML 歌词
|
||||
fastify.get(
|
||||
"/netease/lyric/ttml",
|
||||
async (req: FastifyRequest<{ Querystring: { id: string } }>, reply: FastifyReply) => {
|
||||
const { id } = req.query;
|
||||
if (!id) {
|
||||
return reply.status(400).send({ error: "id is required" });
|
||||
}
|
||||
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.status !== 200) {
|
||||
return reply.send(null);
|
||||
}
|
||||
const data = await response.text();
|
||||
return reply.send(data);
|
||||
} catch (error) {
|
||||
serverLog.error("❌ TTML Lyric Fetch Error:", error);
|
||||
return reply.send(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
serverLog.info("🌐 Register NcmAPI successfully");
|
||||
};
|
||||
14
electron/server/port.ts
Normal file
14
electron/server/port.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import getPort from "get-port";
|
||||
|
||||
// 默认端口
|
||||
let webPort: number;
|
||||
let servePort: number;
|
||||
|
||||
const getSafePort = async () => {
|
||||
if (webPort && servePort) return { webPort, servePort };
|
||||
webPort = await getPort({ port: 14558 });
|
||||
servePort = await getPort({ port: 25884 });
|
||||
return { webPort, servePort };
|
||||
};
|
||||
|
||||
export default getSafePort;
|
||||
161
electron/server/unblock/bodian.ts
Normal file
161
electron/server/unblock/bodian.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import { createHash } from "crypto";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 生成随机设备 ID
|
||||
* @returns 随机设备 ID
|
||||
*/
|
||||
const getRandomDeviceId = () => {
|
||||
const min = 0;
|
||||
const max = 100000000000;
|
||||
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return randomNum.toString();
|
||||
};
|
||||
|
||||
/** 随机设备 ID */
|
||||
const deviceId = getRandomDeviceId();
|
||||
|
||||
/**
|
||||
* 格式化歌曲信息
|
||||
* @param song 歌曲信息
|
||||
* @returns 格式化后的歌曲信息
|
||||
*/
|
||||
const format = (song: any) => ({
|
||||
id: song.MUSICRID.split("_").pop(),
|
||||
name: song.SONGNAME,
|
||||
duration: song.DURATION * 1000,
|
||||
album: { id: song.ALBUMID, name: song.ALBUM },
|
||||
artists: song.ARTIST.split("&").map((name: any, index: any) => ({
|
||||
id: index ? null : song.ARTISTID,
|
||||
name,
|
||||
})),
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* @param str 请求字符串
|
||||
* @returns 包含签名的请求字符串
|
||||
*/
|
||||
const generateSign = (str: string) => {
|
||||
const url = new URL(str);
|
||||
|
||||
const currentTime = Date.now();
|
||||
str += `×tamp=${currentTime}`;
|
||||
|
||||
const filteredChars = str
|
||||
.substring(str.indexOf("?") + 1)
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.split("")
|
||||
.sort();
|
||||
|
||||
const dataToEncrypt = `kuwotest${filteredChars.join("")}${url.pathname}`;
|
||||
const md5 = createHash("md5").update(dataToEncrypt).digest("hex");
|
||||
return `${str}&sign=${md5}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
* @param keyword 搜索关键词
|
||||
* @returns 歌曲 ID 或 null
|
||||
*/
|
||||
const search = async (info: string): Promise<string | null> => {
|
||||
try {
|
||||
const keyword = encodeURIComponent(info.replace(" - ", " "));
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&vipver=1&stype=comprehensive&encoding=utf8" +
|
||||
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword;
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// 获取歌曲信息
|
||||
const list = result.data.content[1].musicpage.abslist.map(format);
|
||||
if (list[0] && !list[0]?.id) return null;
|
||||
return list[0].id;
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get BodianSongId Error:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送广告免费请求
|
||||
* @returns 包含广告免费响应的 Promise
|
||||
*/
|
||||
const sendAdFreeRequest = () => {
|
||||
try {
|
||||
const adurl =
|
||||
"http://bd-api.kuwo.cn/api/service/advert/watch?uid=-1&token=×tamp=1724306124436&sign=15a676d66285117ad714e8c8371691da";
|
||||
|
||||
const headers = {
|
||||
"user-agent": "Dart/2.19 (dart:io)",
|
||||
plat: "ar",
|
||||
channel: "aliopen",
|
||||
devid: deviceId,
|
||||
ver: "3.9.0",
|
||||
host: "bd-api.kuwo.cn",
|
||||
qimei36: "1e9970cbcdc20a031dee9f37100017e1840e",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
};
|
||||
|
||||
const data = JSON.stringify({
|
||||
type: 5,
|
||||
subType: 5,
|
||||
musicId: 0,
|
||||
adToken: "",
|
||||
});
|
||||
return axios.post(adurl, data, { headers });
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get Bodian Ad Free Error:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取波点音乐歌曲 URL
|
||||
* @param keyword 搜索关键词
|
||||
* @returns 包含歌曲 URL 的结果对象
|
||||
*/
|
||||
const getBodianSongUrl = async (keyword: string): Promise<SongUrlResult> => {
|
||||
try {
|
||||
if (!keyword) return { code: 404, url: null };
|
||||
const songId = await search(keyword);
|
||||
if (!songId) return { code: 404, url: null };
|
||||
// 请求地址
|
||||
const headers = {
|
||||
"user-agent": "Dart/2.19 (dart:io)",
|
||||
plat: "ar",
|
||||
channel: "aliopen",
|
||||
devid: deviceId,
|
||||
ver: "3.9.0",
|
||||
host: "bd-api.kuwo.cn",
|
||||
"X-Forwarded-For": "1.0.1.114",
|
||||
};
|
||||
let audioUrl = `http://bd-api.kuwo.cn/api/play/music/v2/audioUrl?&br=${"320kmp3"}&musicId=${songId}`;
|
||||
// 生成签名
|
||||
audioUrl = generateSign(audioUrl);
|
||||
// 获取广告
|
||||
await sendAdFreeRequest();
|
||||
// 获取歌曲地址
|
||||
const result = await axios.get(audioUrl, { headers });
|
||||
if (typeof result.data === "object") {
|
||||
const urlMatch = result.data.data.audioUrl;
|
||||
serverLog.log("🔗 BodianSong URL:", urlMatch);
|
||||
return { code: 200, url: urlMatch };
|
||||
}
|
||||
return { code: 404, url: null };
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get BodianSong URL Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
export default getBodianSongUrl;
|
||||
79
electron/server/unblock/index.ts
Normal file
79
electron/server/unblock/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import getKuwoSongUrl from "./kuwo";
|
||||
import axios from "axios";
|
||||
import getBodianSongUrl from "./bodian";
|
||||
|
||||
/**
|
||||
* 直接获取 网易云云盘 链接
|
||||
* Thank @939163156
|
||||
* Power by GD音乐台(music.gdstudio.xyz)
|
||||
*/
|
||||
const getNeteaseSongUrl = async (id: number | string): Promise<SongUrlResult> => {
|
||||
try {
|
||||
if (!id) return { code: 404, url: null };
|
||||
const baseUrl = "https://music-api.gdstudio.xyz/api.php";
|
||||
const result = await axios.get(baseUrl, {
|
||||
params: { types: "url", id },
|
||||
});
|
||||
const songUrl = result.data.url;
|
||||
serverLog.log("🔗 NeteaseSongUrl URL:", songUrl);
|
||||
return { code: 200, url: songUrl };
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get NeteaseSongUrl Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化 UnblockAPI
|
||||
export const initUnblockAPI = async (fastify: FastifyInstance) => {
|
||||
// 主信息
|
||||
fastify.get("/unblock", (_, reply) => {
|
||||
reply.send({
|
||||
name: "UnblockAPI",
|
||||
description: "SPlayer UnblockAPI service",
|
||||
author: "@imsyy",
|
||||
content:
|
||||
"部分接口采用 @939163156 by GD音乐台(music.gdstudio.xyz),仅供本人学习使用,不可传播下载内容,不可用于商业用途。",
|
||||
});
|
||||
});
|
||||
// netease
|
||||
fastify.get(
|
||||
"/unblock/netease",
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const { id } = req.query;
|
||||
const result = await getNeteaseSongUrl(id);
|
||||
return reply.send(result);
|
||||
},
|
||||
);
|
||||
// kuwo
|
||||
fastify.get(
|
||||
"/unblock/kuwo",
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const { keyword } = req.query;
|
||||
const result = await getKuwoSongUrl(keyword);
|
||||
return reply.send(result);
|
||||
},
|
||||
);
|
||||
// bodian
|
||||
fastify.get(
|
||||
"/unblock/bodian",
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const { keyword } = req.query;
|
||||
const result = await getBodianSongUrl(keyword);
|
||||
return reply.send(result);
|
||||
},
|
||||
);
|
||||
|
||||
serverLog.info("🌐 Register UnblockAPI successfully");
|
||||
};
|
||||
66
electron/server/unblock/kuwo.ts
Normal file
66
electron/server/unblock/kuwo.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { encryptQuery } from "./kwDES";
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import axios from "axios";
|
||||
|
||||
// 获取酷我音乐歌曲 ID
|
||||
const getKuwoSongId = async (keyword: string): Promise<string | null> => {
|
||||
try {
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword;
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// 获取歌曲信息
|
||||
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
|
||||
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword?.split("-") ?? keyword;
|
||||
if (songName && !songName?.includes(originalName[0])) return null;
|
||||
return songId.slice("MUSIC_".length);
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get KuwoSongId Error:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取酷我音乐歌曲 URL
|
||||
const getKuwoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
|
||||
try {
|
||||
if (!keyword) return { code: 404, url: null };
|
||||
const songId = await getKuwoSongId(keyword);
|
||||
if (!songId) return { code: 404, url: null };
|
||||
// 请求地址
|
||||
const PackageName = "kwplayer_ar_5.1.0.0_B_jiakong_vh.apk";
|
||||
const url =
|
||||
"http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
|
||||
encryptQuery(
|
||||
`corp=kuwo&source=${PackageName}&p2p=1&type=convert_url2&sig=0&format=mp3` +
|
||||
"&rid=" +
|
||||
songId,
|
||||
);
|
||||
const result = await axios.get(url, {
|
||||
headers: {
|
||||
"User-Agent": "okhttp/3.10.0",
|
||||
},
|
||||
});
|
||||
if (result.data) {
|
||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||
serverLog.log("🔗 KuwoSong URL:", urlMatch);
|
||||
return { code: 200, url: urlMatch };
|
||||
}
|
||||
return { code: 404, url: null };
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get KuwoSong URL Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
export default getKuwoSongUrl;
|
||||
573
electron/server/unblock/kwDES.js
Normal file
573
electron/server/unblock/kwDES.js
Normal file
@@ -0,0 +1,573 @@
|
||||
/*
|
||||
Thanks to
|
||||
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
|
||||
https://github.com/Levi233/MusicPlayer/blob/master/app/src/main/java/com/chenhao/musicplayer/utils/crypt/KuwoDES.java
|
||||
*/
|
||||
|
||||
const Long = (n) => {
|
||||
const bN = BigInt(n);
|
||||
|
||||
return {
|
||||
low: Number(bN),
|
||||
valueOf: () => bN.valueOf(),
|
||||
toString: () => bN.toString(),
|
||||
not: () => Long(~bN),
|
||||
isNegative: () => bN < 0,
|
||||
or: (x) => Long(bN | BigInt(x)),
|
||||
and: (x) => Long(bN & BigInt(x)),
|
||||
xor: (x) => Long(bN ^ BigInt(x)),
|
||||
equals: (x) => bN === BigInt(x),
|
||||
multiply: (x) => Long(bN * BigInt(x)),
|
||||
shiftLeft: (x) => Long(bN << BigInt(x)),
|
||||
shiftRight: (x) => Long(bN >> BigInt(x)),
|
||||
};
|
||||
};
|
||||
|
||||
const range = (n) => Array.from(new Array(n).keys());
|
||||
const power = (base, index) =>
|
||||
Array(index)
|
||||
.fill(null)
|
||||
.reduce((result) => result.multiply(base), Long(1));
|
||||
const LongArray = (...array) => array.map((n) => (n === -1 ? Long(-1, -1) : Long(n)));
|
||||
|
||||
// EXPANSION
|
||||
const arrayE = LongArray(
|
||||
31,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
-1,
|
||||
-1,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
-1,
|
||||
-1,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
-1,
|
||||
-1,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
-1,
|
||||
-1,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
-1,
|
||||
-1,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
// INITIAL_PERMUTATION
|
||||
const arrayIP = LongArray(
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
63,
|
||||
55,
|
||||
47,
|
||||
39,
|
||||
31,
|
||||
23,
|
||||
15,
|
||||
7,
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
);
|
||||
|
||||
// INVERSE_PERMUTATION
|
||||
const arrayIP_1 = LongArray(
|
||||
39,
|
||||
7,
|
||||
47,
|
||||
15,
|
||||
55,
|
||||
23,
|
||||
63,
|
||||
31,
|
||||
38,
|
||||
6,
|
||||
46,
|
||||
14,
|
||||
54,
|
||||
22,
|
||||
62,
|
||||
30,
|
||||
37,
|
||||
5,
|
||||
45,
|
||||
13,
|
||||
53,
|
||||
21,
|
||||
61,
|
||||
29,
|
||||
36,
|
||||
4,
|
||||
44,
|
||||
12,
|
||||
52,
|
||||
20,
|
||||
60,
|
||||
28,
|
||||
35,
|
||||
3,
|
||||
43,
|
||||
11,
|
||||
51,
|
||||
19,
|
||||
59,
|
||||
27,
|
||||
34,
|
||||
2,
|
||||
42,
|
||||
10,
|
||||
50,
|
||||
18,
|
||||
58,
|
||||
26,
|
||||
33,
|
||||
1,
|
||||
41,
|
||||
9,
|
||||
49,
|
||||
17,
|
||||
57,
|
||||
25,
|
||||
32,
|
||||
0,
|
||||
40,
|
||||
8,
|
||||
48,
|
||||
16,
|
||||
56,
|
||||
24,
|
||||
);
|
||||
|
||||
// ROTATES
|
||||
const arrayLs = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1];
|
||||
const arrayLsMask = LongArray(0, 0x100001, 0x300003);
|
||||
const arrayMask = range(64).map((n) => power(2, n));
|
||||
arrayMask[arrayMask.length - 1] = arrayMask[arrayMask.length - 1].multiply(-1);
|
||||
|
||||
// PERMUTATION
|
||||
const arrayP = LongArray(
|
||||
15,
|
||||
6,
|
||||
19,
|
||||
20,
|
||||
28,
|
||||
11,
|
||||
27,
|
||||
16,
|
||||
0,
|
||||
14,
|
||||
22,
|
||||
25,
|
||||
4,
|
||||
17,
|
||||
30,
|
||||
9,
|
||||
1,
|
||||
7,
|
||||
23,
|
||||
13,
|
||||
31,
|
||||
26,
|
||||
2,
|
||||
8,
|
||||
18,
|
||||
12,
|
||||
29,
|
||||
5,
|
||||
21,
|
||||
10,
|
||||
3,
|
||||
24,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE1
|
||||
const arrayPC_1 = LongArray(
|
||||
56,
|
||||
48,
|
||||
40,
|
||||
32,
|
||||
24,
|
||||
16,
|
||||
8,
|
||||
0,
|
||||
57,
|
||||
49,
|
||||
41,
|
||||
33,
|
||||
25,
|
||||
17,
|
||||
9,
|
||||
1,
|
||||
58,
|
||||
50,
|
||||
42,
|
||||
34,
|
||||
26,
|
||||
18,
|
||||
10,
|
||||
2,
|
||||
59,
|
||||
51,
|
||||
43,
|
||||
35,
|
||||
62,
|
||||
54,
|
||||
46,
|
||||
38,
|
||||
30,
|
||||
22,
|
||||
14,
|
||||
6,
|
||||
61,
|
||||
53,
|
||||
45,
|
||||
37,
|
||||
29,
|
||||
21,
|
||||
13,
|
||||
5,
|
||||
60,
|
||||
52,
|
||||
44,
|
||||
36,
|
||||
28,
|
||||
20,
|
||||
12,
|
||||
4,
|
||||
27,
|
||||
19,
|
||||
11,
|
||||
3,
|
||||
);
|
||||
|
||||
// PERMUTED_CHOICE2
|
||||
const arrayPC_2 = LongArray(
|
||||
13,
|
||||
16,
|
||||
10,
|
||||
23,
|
||||
0,
|
||||
4,
|
||||
-1,
|
||||
-1,
|
||||
2,
|
||||
27,
|
||||
14,
|
||||
5,
|
||||
20,
|
||||
9,
|
||||
-1,
|
||||
-1,
|
||||
22,
|
||||
18,
|
||||
11,
|
||||
3,
|
||||
25,
|
||||
7,
|
||||
-1,
|
||||
-1,
|
||||
15,
|
||||
6,
|
||||
26,
|
||||
19,
|
||||
12,
|
||||
1,
|
||||
-1,
|
||||
-1,
|
||||
40,
|
||||
51,
|
||||
30,
|
||||
36,
|
||||
46,
|
||||
54,
|
||||
-1,
|
||||
-1,
|
||||
29,
|
||||
39,
|
||||
50,
|
||||
44,
|
||||
32,
|
||||
47,
|
||||
-1,
|
||||
-1,
|
||||
43,
|
||||
48,
|
||||
38,
|
||||
55,
|
||||
33,
|
||||
52,
|
||||
-1,
|
||||
-1,
|
||||
45,
|
||||
41,
|
||||
49,
|
||||
35,
|
||||
28,
|
||||
31,
|
||||
-1,
|
||||
-1,
|
||||
);
|
||||
|
||||
const matrixNSBox = [
|
||||
[
|
||||
14, 4, 3, 15, 2, 13, 5, 3, 13, 14, 6, 9, 11, 2, 0, 5, 4, 1, 10, 12, 15, 6, 9, 10, 1, 8, 12, 7,
|
||||
8, 11, 7, 0, 0, 15, 10, 5, 14, 4, 9, 10, 7, 8, 12, 3, 13, 1, 3, 6, 15, 12, 6, 11, 2, 9, 5, 0, 4,
|
||||
2, 11, 14, 1, 7, 8, 13,
|
||||
],
|
||||
[
|
||||
15, 0, 9, 5, 6, 10, 12, 9, 8, 7, 2, 12, 3, 13, 5, 2, 1, 14, 7, 8, 11, 4, 0, 3, 14, 11, 13, 6, 4,
|
||||
1, 10, 15, 3, 13, 12, 11, 15, 3, 6, 0, 4, 10, 1, 7, 8, 4, 11, 14, 13, 8, 0, 6, 2, 15, 9, 5, 7,
|
||||
1, 10, 12, 14, 2, 5, 9,
|
||||
],
|
||||
[
|
||||
10, 13, 1, 11, 6, 8, 11, 5, 9, 4, 12, 2, 15, 3, 2, 14, 0, 6, 13, 1, 3, 15, 4, 10, 14, 9, 7, 12,
|
||||
5, 0, 8, 7, 13, 1, 2, 4, 3, 6, 12, 11, 0, 13, 5, 14, 6, 8, 15, 2, 7, 10, 8, 15, 4, 9, 11, 5, 9,
|
||||
0, 14, 3, 10, 7, 1, 12,
|
||||
],
|
||||
[
|
||||
7, 10, 1, 15, 0, 12, 11, 5, 14, 9, 8, 3, 9, 7, 4, 8, 13, 6, 2, 1, 6, 11, 12, 2, 3, 0, 5, 14, 10,
|
||||
13, 15, 4, 13, 3, 4, 9, 6, 10, 1, 12, 11, 0, 2, 5, 0, 13, 14, 2, 8, 15, 7, 4, 15, 1, 10, 7, 5,
|
||||
6, 12, 11, 3, 8, 9, 14,
|
||||
],
|
||||
[
|
||||
2, 4, 8, 15, 7, 10, 13, 6, 4, 1, 3, 12, 11, 7, 14, 0, 12, 2, 5, 9, 10, 13, 0, 3, 1, 11, 15, 5,
|
||||
6, 8, 9, 14, 14, 11, 5, 6, 4, 1, 3, 10, 2, 12, 15, 0, 13, 2, 8, 5, 11, 8, 0, 15, 7, 14, 9, 4,
|
||||
12, 7, 10, 9, 1, 13, 6, 3,
|
||||
],
|
||||
[
|
||||
12, 9, 0, 7, 9, 2, 14, 1, 10, 15, 3, 4, 6, 12, 5, 11, 1, 14, 13, 0, 2, 8, 7, 13, 15, 5, 4, 10,
|
||||
8, 3, 11, 6, 10, 4, 6, 11, 7, 9, 0, 6, 4, 2, 13, 1, 9, 15, 3, 8, 15, 3, 1, 14, 12, 5, 11, 0, 2,
|
||||
12, 14, 7, 5, 10, 8, 13,
|
||||
],
|
||||
[
|
||||
4, 1, 3, 10, 15, 12, 5, 0, 2, 11, 9, 6, 8, 7, 6, 9, 11, 4, 12, 15, 0, 3, 10, 5, 14, 13, 7, 8,
|
||||
13, 14, 1, 2, 13, 6, 14, 9, 4, 1, 2, 14, 11, 13, 5, 0, 1, 10, 8, 3, 0, 11, 3, 5, 9, 4, 15, 2, 7,
|
||||
8, 12, 15, 10, 7, 6, 12,
|
||||
],
|
||||
[
|
||||
13, 7, 10, 0, 6, 9, 5, 15, 8, 4, 3, 10, 11, 14, 12, 5, 2, 11, 9, 6, 15, 12, 0, 3, 4, 1, 14, 13,
|
||||
1, 2, 7, 8, 1, 2, 12, 15, 10, 4, 0, 3, 13, 14, 6, 9, 7, 8, 9, 6, 15, 1, 5, 12, 3, 10, 14, 5, 8,
|
||||
7, 11, 0, 4, 13, 2, 11,
|
||||
],
|
||||
];
|
||||
|
||||
const bitTransform = (arrInt, n, l) => {
|
||||
// int[], int, long : long
|
||||
let l2 = Long(0);
|
||||
range(n).forEach((i) => {
|
||||
if (arrInt[i].isNegative() || l.and(arrayMask[arrInt[i].low]).equals(0)) return;
|
||||
l2 = l2.or(arrayMask[i]);
|
||||
});
|
||||
return l2;
|
||||
};
|
||||
|
||||
const DES64 = (longs, l) => {
|
||||
const pR = range(8).map(() => Long(0));
|
||||
const pSource = [Long(0), Long(0)];
|
||||
let L = Long(0);
|
||||
let R = Long(0);
|
||||
let out = bitTransform(arrayIP, 64, l);
|
||||
pSource[0] = out.and(0xffffffff);
|
||||
pSource[1] = out.and(-4294967296).shiftRight(32);
|
||||
|
||||
range(16).forEach((i) => {
|
||||
let SOut = Long(0);
|
||||
|
||||
R = Long(pSource[1]);
|
||||
R = bitTransform(arrayE, 64, R);
|
||||
R = R.xor(longs[i]);
|
||||
range(8).forEach((j) => {
|
||||
pR[j] = R.shiftRight(j * 8).and(255);
|
||||
});
|
||||
range(8)
|
||||
.reverse()
|
||||
.forEach((sbi) => {
|
||||
SOut = SOut.shiftLeft(4).or(matrixNSBox[sbi][pR[sbi]]);
|
||||
});
|
||||
R = bitTransform(arrayP, 32, SOut);
|
||||
L = Long(pSource[0]);
|
||||
pSource[0] = Long(pSource[1]);
|
||||
pSource[1] = L.xor(R);
|
||||
});
|
||||
pSource.reverse();
|
||||
out = pSource[1].shiftLeft(32).and(-4294967296).or(pSource[0].and(0xffffffff));
|
||||
out = bitTransform(arrayIP_1, 64, out);
|
||||
return out;
|
||||
};
|
||||
|
||||
const subKeys = (l, longs, n) => {
|
||||
// long, long[], int
|
||||
let l2 = bitTransform(arrayPC_1, 56, l);
|
||||
range(16).forEach((i) => {
|
||||
l2 = l2
|
||||
.and(arrayLsMask[arrayLs[i]])
|
||||
.shiftLeft(28 - arrayLs[i])
|
||||
.or(l2.and(arrayLsMask[arrayLs[i]].not()).shiftRight(arrayLs[i]));
|
||||
longs[i] = bitTransform(arrayPC_2, 64, l2);
|
||||
});
|
||||
if (n === 1) {
|
||||
range(8).forEach((j) => {
|
||||
[longs[j], longs[15 - j]] = [longs[15 - j], longs[j]];
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const crypt = (msg, key, mode) => {
|
||||
// 处理密钥块
|
||||
let l = Long(0);
|
||||
range(8).forEach((i) => {
|
||||
l = Long(key[i])
|
||||
.shiftLeft(i * 8)
|
||||
.or(l);
|
||||
});
|
||||
|
||||
const j = Math.floor(msg.length / 8);
|
||||
// arrLong1 存放的是转换后的密钥块, 在解密时只需要把这个密钥块反转就行了
|
||||
|
||||
const arrLong1 = range(16).map(() => Long(0));
|
||||
subKeys(l, arrLong1, mode);
|
||||
|
||||
// arrLong2 存放的是前部分的明文
|
||||
const arrLong2 = range(j).map(() => Long(0));
|
||||
|
||||
range(j).forEach((m) => {
|
||||
range(8).forEach((n) => {
|
||||
arrLong2[m] = Long(msg[n + m * 8])
|
||||
.shiftLeft(n * 8)
|
||||
.or(arrLong2[m]);
|
||||
});
|
||||
});
|
||||
|
||||
// 用于存放密文
|
||||
const arrLong3 = range(Math.floor((1 + 8 * (j + 1)) / 8)).map(() => Long(0));
|
||||
|
||||
// 计算前部的数据块(除了最后一部分)
|
||||
range(j).forEach((i1) => {
|
||||
arrLong3[i1] = DES64(arrLong1, arrLong2[i1]);
|
||||
});
|
||||
|
||||
// 保存多出来的字节
|
||||
const arrByte1 = msg.slice(j * 8);
|
||||
let l2 = Long(0);
|
||||
|
||||
range(msg.length % 8).forEach((i1) => {
|
||||
l2 = Long(arrByte1[i1])
|
||||
.shiftLeft(i1 * 8)
|
||||
.or(l2);
|
||||
});
|
||||
|
||||
// 计算多出的那一位(最后一位)
|
||||
if (arrByte1.length || mode === 0) arrLong3[j] = DES64(arrLong1, l2); // 解密不需要
|
||||
|
||||
// 将密文转为字节型
|
||||
const arrByte2 = range(8 * arrLong3.length).map(() => 0);
|
||||
let i4 = 0;
|
||||
arrLong3.forEach((l3) => {
|
||||
range(8).forEach((i6) => {
|
||||
arrByte2[i4] = l3.shiftRight(i6 * 8).and(255).low;
|
||||
i4 += 1;
|
||||
});
|
||||
});
|
||||
return Buffer.from(arrByte2);
|
||||
};
|
||||
|
||||
const SECRET_KEY = Buffer.from("ylzsxkwm");
|
||||
export const encrypt = (msg) => crypt(msg, SECRET_KEY, 0);
|
||||
export const decrypt = (msg) => crypt(msg, SECRET_KEY, 1);
|
||||
export const encryptQuery = (query) => encrypt(Buffer.from(query)).toString("base64");
|
||||
4
electron/server/unblock/unblock.d.ts
vendored
Normal file
4
electron/server/unblock/unblock.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export type SongUrlResult = {
|
||||
code: number;
|
||||
url: string | null;
|
||||
};
|
||||
5
env.d.ts
vendored
Normal file
5
env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface MainEnv {
|
||||
readonly VITE_WEB_PORT: string;
|
||||
readonly VITE_SERVER_PORT: string;
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
65
eslint.config.mjs
Normal file
65
eslint.config.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import vue from "eslint-plugin-vue";
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import autoEslint from "./auto-eslint.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"**/node_modules",
|
||||
"**/dist",
|
||||
"**/out",
|
||||
"**/.gitignore",
|
||||
"**/auto-imports.d.ts",
|
||||
"**/components.d.ts",
|
||||
],
|
||||
},
|
||||
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
|
||||
{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
vue,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...autoEslint.globals,
|
||||
},
|
||||
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/.eslintrc.{js,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: { ...globals.node },
|
||||
ecmaVersion: 5,
|
||||
sourceType: "commonjs",
|
||||
},
|
||||
},
|
||||
];
|
||||
19
index.html
19
index.html
@@ -3,22 +3,19 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="%RENDERER_VITE_SITE_LOGO%" />
|
||||
<link rel="apple-touch-icon" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="bookmark" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="200x200" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="icon" type="image/icon" href="/icons/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<title>SPlayer</title>
|
||||
<!-- font -->
|
||||
<link rel="stylesheet" href="/fonts/font.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
52
nginx.conf
Normal file
52
nginx.conf
Normal file
@@ -0,0 +1,52 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 25884;
|
||||
listen [::]:25884;
|
||||
server_name localhost;
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location @rewrites {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/netease/song/url/v1 {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/song/url/v1;
|
||||
|
||||
sub_filter '"url":"http://music.163.com' '"url":"/music/unblock';
|
||||
sub_filter '"url":"https://music.163.com' '"url":"/music/unblock';
|
||||
sub_filter_types application/json;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
location /api/netease/ {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
|
||||
location /music/unblock/ {
|
||||
proxy_pass https://music.163.com/;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
159
package.json
159
package.json
@@ -1,68 +1,135 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.2",
|
||||
"productName": "SPlayer",
|
||||
"version": "3.0.0-beta.5",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"blog": "https://blog.imsyy.top",
|
||||
"repository": "github:imsyy/SPlayer",
|
||||
"license": "AGPL-3.0",
|
||||
"license-file": "LICENSE",
|
||||
"engines": {
|
||||
"node": ">=16.16.0"
|
||||
"node": ">=20",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"lint": "npx eslint . --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "chcp 65001 && electron-vite dev --watch",
|
||||
"build": "electron-vite build",
|
||||
"dev": "node scripts/dev.mjs",
|
||||
"build": "npx rimraf dist && npm run typecheck && electron-vite build",
|
||||
"postinstall": "electron-builder install-app-deps",
|
||||
"build:win": "npm run build && electron-builder --win --config",
|
||||
"build:mac": "npm run build && electron-builder --mac --config",
|
||||
"build:linux": "npm run build && electron-builder --linux --config"
|
||||
"build:web": "npm run build",
|
||||
"build:unpack": "npm run build && electron-builder --dir",
|
||||
"build:win": "npm run build && electron-builder --win --config electron-builder.config.ts",
|
||||
"build:mac": "npm run build && electron-builder --mac --config electron-builder.config.ts",
|
||||
"build:linux": "npm run build && electron-builder --linux --config electron-builder.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.13.5",
|
||||
"axios": "^1.4.0",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^1.6.3",
|
||||
"howler": "^2.2.3",
|
||||
"@applemusic-like-lyrics/core": "^0.1.3",
|
||||
"@applemusic-like-lyrics/lyric": "^0.3.0",
|
||||
"@applemusic-like-lyrics/vue": "^0.1.5",
|
||||
"@electron-toolkit/preload": "^3.0.2",
|
||||
"@electron-toolkit/utils": "^4.0.0",
|
||||
"@imsyy/color-utils": "^1.0.2",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@neteasecloudmusicapienhanced/api": "^4.29.16",
|
||||
"@pixi/app": "^7.4.3",
|
||||
"@pixi/core": "^7.4.3",
|
||||
"@pixi/display": "^7.4.3",
|
||||
"@pixi/filter-blur": "^7.4.3",
|
||||
"@pixi/filter-bulge-pinch": "^5.1.1",
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@vueuse/core": "^13.9.0",
|
||||
"axios": "^1.13.2",
|
||||
"axios-retry": "^4.5.0",
|
||||
"change-case": "^5.4.4",
|
||||
"dayjs": "^1.11.18",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-store": "^11.0.2",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"font-list": "^2.0.1",
|
||||
"get-port": "^7.1.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"localforage": "^1.10.0",
|
||||
"music-metadata": "7.13.4",
|
||||
"node-id3": "^0.2.6",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"plyr": "^3.7.8",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^16.4.0",
|
||||
"md5": "^2.3.0",
|
||||
"music-metadata": "^11.9.0",
|
||||
"pinia": "^3.0.3",
|
||||
"pinia-plugin-persistedstate": "^4.5.0",
|
||||
"plyr": "^3.8.3",
|
||||
"vue-virt-list": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.1",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"electron": "^25.6.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"naive-ui": "^2.34.4",
|
||||
"prettier": "^3.0.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/tsconfig": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^4",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/http-proxy": "^11.3.0",
|
||||
"@fastify/multipart": "^9.2.1",
|
||||
"@fastify/static": "^8.2.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^24.7.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"ajv": "^8.17.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron": "38.2.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-vite": "^4.0.1",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint-plugin-vue": "^10.5.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.6.1",
|
||||
"naive-ui": "^2.43.1",
|
||||
"node-taglib-sharp": "^6.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.93.2",
|
||||
"terser": "^5.44.0",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^29.1.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "^3.3.4"
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue-tsc": "^3.1.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"dmg-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "26.0.12"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@applemusic-like-lyrics/lyric",
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"electron",
|
||||
"electron-winstaller",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
11761
pnpm-lock.yaml
generated
11761
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
1
public/font/font.min.css
vendored
1
public/font/font.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user