Compare commits
241 Commits
v2.0.8
...
8529663ea5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
44
.env.example
@@ -1,38 +1,6 @@
|
||||
# 程序配置
|
||||
## 程序名称
|
||||
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"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
MAIN_VITE_MIGU_COOKIE = ""
|
||||
|
||||
# 公告配置
|
||||
## 若无需公告,请将标题或内容任意一项设为空即可
|
||||
## 公告类型
|
||||
RENDERER_VITE_ANN_TYPE = "info"
|
||||
## 公告标题
|
||||
RENDERER_VITE_ANN_TITLE = ""
|
||||
## 公告内容
|
||||
RENDERER_VITE_ANN_CONTENT = ""
|
||||
## 公告时长(毫秒)不可超过 999999
|
||||
RENDERER_VITE_ANN_DURATION = 8000
|
||||
## WEB 端口
|
||||
VITE_WEB_PORT = 14558
|
||||
## API 端口
|
||||
VITE_SERVER_PORT = 25884
|
||||
## API 地址 - 结尾不要加 /
|
||||
VITE_API_URL = /api/netease
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
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,56 +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,
|
||||
$refreshCloudCatch: true,
|
||||
$cleanAll: true,
|
||||
$player: true,
|
||||
},
|
||||
};
|
||||
62
.github/workflows/build.yml
vendored
@@ -1,62 +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
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 复制环境变量文件
|
||||
- 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: 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@v4
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: dist
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
name: ${{ github.ref }}-rc
|
||||
body: This version is still under development, currently only provides windows version, non-developers please do not use!
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: dist/*.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
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
|
||||
10
.github/workflows/docker.yml
vendored
@@ -3,6 +3,7 @@ name: Publish Docker image
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
@@ -15,6 +16,12 @@ jobs:
|
||||
- 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:
|
||||
@@ -41,6 +48,9 @@ jobs:
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
253
.github/workflows/release.yml
vendored
@@ -1,154 +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
|
||||
timeout-minutes: 30
|
||||
# ===================================================================
|
||||
# 并行构建所有平台和架构
|
||||
# ===================================================================
|
||||
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
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
# 设置 pnpm 版本
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
node-version: "20.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: pnpm install
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
- 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: 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@v4
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
build-macos:
|
||||
name: Build for macOS
|
||||
runs-on: macos-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
- 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
|
||||
# 安装项目依赖
|
||||
- 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@v4
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
build-linux:
|
||||
name: Build for Linux
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "20.x"
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
if: runner.os == 'Linux'
|
||||
run: sudo apt-get update
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
# 安装依赖
|
||||
- name: Install RPM & Pacman
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
if [ ! -f .env ]; then
|
||||
cp .env.example .env
|
||||
else
|
||||
echo ".env file already exists. Skipping the copy step."
|
||||
fi
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
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
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Linux x64 & ARM64 App
|
||||
if: runner.os == 'Linux'
|
||||
run: pnpm build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
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: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
name: SPlayer-${{ runner.os }}
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
|
||||
# ===================================================================
|
||||
# 收集并发布 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:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
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
|
||||
|
||||
4
.gitignore
vendored
@@ -15,6 +15,8 @@ coverage
|
||||
*.local
|
||||
out
|
||||
.env
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -26,3 +28,5 @@ out
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env.development
|
||||
.env.production
|
||||
|
||||
4
.npmrc
@@ -1,5 +1,5 @@
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
# ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
ELECTRON_MIRROR=https://registry.npmmirror.com/-/binary/electron/
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
shamefully-hoist=true
|
||||
|
||||
@@ -2,7 +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
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# 是否使用单引号而不是双引号
|
||||
singleQuote: false
|
||||
# 是否在语句末尾使用分号
|
||||
semi: true
|
||||
# 每行的最大打印宽度
|
||||
printWidth: 100
|
||||
# 是否在对象和数组的末尾加上逗号
|
||||
trailingComma: all
|
||||
19
Dockerfile
@@ -1,5 +1,5 @@
|
||||
# build
|
||||
FROM node:18-alpine as builder
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
@@ -11,19 +11,28 @@ 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.25.3-alpine-slim as app
|
||||
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
|
||||
|
||||
RUN apk add --no-cache npm
|
||||
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN npm install -g NeteaseCloudMusicApi
|
||||
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
|
||||
|
||||
CMD nginx && npx NeteaseCloudMusicApi
|
||||
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["npx", "NeteaseCloudMusicApi"]
|
||||
228
README.md
@@ -1,6 +1,12 @@
|
||||
# SPlayer
|
||||
|
||||
> 一个简约的音乐播放器
|
||||
> A simple music player
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||

|
||||
|
||||
@@ -12,11 +18,11 @@
|
||||
>
|
||||
> - 请务必遵守 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可协议
|
||||
> - 在您的修改、演绎、分发或派生项目中,必须同样采用 **AGPL-3.0** 许可协议,**并在适当的位置包含本项目的许可和版权信息**
|
||||
> - **禁止用于售卖或其他商业用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 若发现未遵守 **AGPL-3.0** 许可协议的行为,**本项目将永久停更**
|
||||
> - **禁止用于售卖或其他盈利用途**,如若发现,作者保留追究法律责任的权利
|
||||
> - 禁止在二开项目中修改程序原版权信息( 您可以添加二开作者信息 )
|
||||
> - 感谢您的尊重与理解
|
||||
|
||||
- 本项目采用 [Vue 3](https://cn.vuejs.org/) 全家桶和 [Naïve UI](https://www.naiveui.com/) 组件库及 [Electron](https://www.electronjs.org/zh/docs/latest/) 开发
|
||||
- 本项目采用 [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`,其他平台可自行解决兼容性后进行构建
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
@@ -33,11 +39,14 @@
|
||||
- ✨ 支持扫码登录
|
||||
- 📱 支持手机号登录
|
||||
- 📅 自动进行每日签到及云贝签到
|
||||
- 🎨 封面主题色自适应
|
||||
- 🌚 Light / Dark 模式自动切换
|
||||
- 💻 支持桌面歌词
|
||||
- 💻 支持切换为本地播放器,此模式将不会连接网络
|
||||
- 🎨 封面主题色自适应,支持全站着色
|
||||
- 🌚 Light / Dark / Auto 模式自动切换
|
||||
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
|
||||
- 📁 简易的本地音乐标签编辑及封面修改
|
||||
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- ⬇️ 下载歌曲(最高支持 Hi-Res)
|
||||
- ⬇️ 下载歌曲( 最高支持 Hi-Res,需具有相应会员账号 )
|
||||
- ➕ 新建歌单及歌单编辑
|
||||
- ❤️ 收藏 / 取消收藏歌单或歌手
|
||||
- 🎶 每日推荐歌曲
|
||||
@@ -52,8 +61,7 @@
|
||||
- 🎶 音乐频谱显示
|
||||
- ⏭️ 音乐渐入渐出
|
||||
- 🔄 支持 PWA
|
||||
- 💬 支持评论区及评论点赞
|
||||
- 🌓 明暗模式自动 / 手动切换
|
||||
- 💬 支持评论区
|
||||
- 📱 移动端基础适配
|
||||
- ~~🌐 `i18n` 支持~~
|
||||
|
||||
@@ -115,6 +123,10 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## Snap Store
|
||||
|
||||
[](https://snapcraft.io/splayer)
|
||||
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
@@ -128,7 +140,7 @@
|
||||
docker build -t splayer .
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 splayer
|
||||
docker run -d --name SPlayer -p 25884:25884 splayer
|
||||
# 或使用 Docker Compose
|
||||
docker-compose up -d
|
||||
```
|
||||
@@ -142,10 +154,10 @@ docker pull imsyy/splayer:latest
|
||||
docker pull ghcr.io/imsyy/splayer:latest
|
||||
|
||||
# 运行
|
||||
docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
docker run -d --name SPlayer -p 25884:25884 imsyy/splayer:latest
|
||||
```
|
||||
|
||||
以上步骤成功后,将会在本地 [localhost:7899](http://localhost:7899/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
以上步骤成功后,将会在本地 [localhost:25884](http://localhost:25884/) 启动,如需更换端口,请自行修改命令行中的端口号
|
||||
|
||||
## ⚙️ Vercel 部署
|
||||
|
||||
@@ -154,10 +166,10 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
1. 本程序依赖 [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi) 运行,请确保您已成功部署该项目,并成功取得在线访问地址
|
||||
2. 点击本仓库右上角的 `Fork`,复制本仓库到你的 `GitHub` 账号
|
||||
3. 复制 `/.env.example` 文件并重命名为 `/.env`
|
||||
4. 将 `.env` 文件中的 `RENDERER_VITE_SERVER_URL` 改为第一步得到的 API 地址
|
||||
4. 将 `.env` 文件中的 `VITE_API_URL` 改为第一步得到的 API 地址
|
||||
|
||||
```js
|
||||
RENDERER_VITE_SERVER_URL = "https://example.com";
|
||||
VITE_API_URL = "https://example.com";
|
||||
```
|
||||
|
||||
5. 将 `Build and Output Settings` 中的 `Output Directory` 改为 `out/renderer`
|
||||
@@ -171,19 +183,17 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
> 将链接中的 example/repository.git 替换为你要克隆的实际仓库的地址
|
||||
|
||||
```bash
|
||||
git clone https://github.com/example/repository.git
|
||||
git clone https://github.com/imsyy/SPlayer.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或者
|
||||
# 或
|
||||
yarn install
|
||||
# 或者
|
||||
# 或
|
||||
npm install
|
||||
```
|
||||
|
||||
@@ -191,9 +201,9 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
# 或
|
||||
yarn build
|
||||
# 或者
|
||||
# 或
|
||||
npm build
|
||||
```
|
||||
|
||||
@@ -226,7 +236,10 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [applemusic-like-lyrics](https://github.com/Steve-xmh/applemusic-like-lyrics)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
|
||||
- [material-color-utilities](https://github.com/material-foundation/material-color-utilities)
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
@@ -249,179 +262,6 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## 📂 目录结构
|
||||
|
||||
<details>
|
||||
<summary>查看目录结构详情</summary>
|
||||
|
||||
> ChatGPT 写的,如有错误,请见谅
|
||||
|
||||
```dir
|
||||
├── auto-imports.d.ts # 自动导入
|
||||
├── components.d.ts # 自动导入
|
||||
├── docker-compose.yml # Docker Compose
|
||||
├── Dockerfile # Docker
|
||||
├── electron # Electron
|
||||
│ ├── main # Electron 主进程
|
||||
│ │ ├── index.js # 主进程入口
|
||||
│ │ ├── mainIpcMain.js # 主进程与渲染进程通信
|
||||
│ │ ├── startMainServer.js # 启动主进程服务器
|
||||
│ │ ├── startNcmServer.js # 启动网易云音乐服务
|
||||
│ │ └── utils # 主进程工具函数
|
||||
│ │ ├── checkUpdates.js # 检查更新
|
||||
│ │ ├── createGlobalShortcut.js # 创建全局快捷键
|
||||
│ │ ├── createSystemTray.js # 创建系统托盘
|
||||
│ │ ├── getNeteaseMusicUrl.js # 解灰
|
||||
│ │ ├── kwDES.js # DES加密算法
|
||||
│ │ └── readDirAsync.js # 异步读取目录
|
||||
│ └── preload # Electron 预加载脚本
|
||||
│ └── index.js # 预加载脚本入口文件
|
||||
├── electron-builder.yml # Electron Builder
|
||||
├── electron.vite.config.js # Electron Vite
|
||||
├── index.html # 主页面 HTML
|
||||
├── LICENSE # 项目许可证
|
||||
├── nginx.conf # Nginx 配置
|
||||
├── src # 项目源代码
|
||||
│ ├── api # API 相关
|
||||
│ │ ├── ./..
|
||||
│ ├── App.vue # 根组件
|
||||
│ ├── assets # 静态资源
|
||||
│ │ ├── emoji.json # 表情数据
|
||||
│ │ ├── icon.json # 图标数据
|
||||
│ │ └── themeColor.json # 主题颜色数据
|
||||
│ ├── components # 组件目录
|
||||
│ │ ├── Cover # 封面相关组件目录
|
||||
│ │ │ ├── CoverDropdown.vue # 封面下拉组件
|
||||
│ │ │ ├── MainCover.vue # 主封面组件
|
||||
│ │ │ ├── SpecialCoverCard.vue # 特殊封面卡片组件
|
||||
│ │ │ └── SpecialCover.vue # 特殊封面组件
|
||||
│ │ ├── Global # 全局组件目录
|
||||
│ │ │ ├── MainLayout.vue # 主布局组件
|
||||
│ │ │ ├── Menu.vue # 菜单组件
|
||||
│ │ │ ├── Pagination.vue # 分页组件
|
||||
│ │ │ ├── Playlist.vue # 歌单组件
|
||||
│ │ │ ├── Provider.vue # 全局化配置组件
|
||||
│ │ │ └── SvgIcon.vue # SVG 图标组件
|
||||
│ │ ├── List # 列表组件目录
|
||||
│ │ │ ├── CommentList.vue # 评论列表组件
|
||||
│ │ │ ├── SongListDropdown.vue # 歌曲下拉组件
|
||||
│ │ │ └── SongList.vue # 歌曲列表组件
|
||||
│ │ ├── Modal # 弹窗相关组件目录
|
||||
│ │ │ ├── AddPlaylist.vue # 添加歌单组件
|
||||
│ │ │ ├── CloudSongMatch.vue # 云盘歌曲匹配组件
|
||||
│ │ │ ├── CreatePlaylist.vue # 创建歌单组件
|
||||
│ │ │ ├── DownloadSong.vue # 下载歌曲组件
|
||||
│ │ │ ├── LoginPhone.vue # 手机登录组件
|
||||
│ │ │ ├── LoginQRCode.vue # 二维码登录组件
|
||||
│ │ │ ├── Login.vue # 登录组件
|
||||
│ │ │ ├── PlaylistUpdate.vue # 歌单编辑组件
|
||||
│ │ │ └── UpCloudSong.vue # 上传云盘歌曲组件
|
||||
│ │ ├── Nav # 导航相关组件目录
|
||||
│ │ │ ├── MainNav.vue # 主导航组件
|
||||
│ │ │ └── UserData.vue # 用户数据组件
|
||||
│ │ ├── Player # 播放器相关组件目录
|
||||
│ │ │ ├── CountDown.vue # 倒计时组件
|
||||
│ │ │ ├── FullPlayer.vue # 全屏播放器组件
|
||||
│ │ │ ├── Lyric.vue # 歌词组件
|
||||
│ │ │ ├── MainControl.vue # 主控制组件
|
||||
│ │ │ ├── PlayerControl.vue # 播放器控制组件
|
||||
│ │ │ ├── PlayerCover.vue # 播放器封面组件
|
||||
│ │ │ └── PrivateFm.vue # 私人 FM 组件
|
||||
│ │ ├── Search # 搜索相关组件
|
||||
│ │ │ ├── SearchHot.vue # 热门搜索组件
|
||||
│ │ │ ├── SearchInp.vue # 搜索输入组件
|
||||
│ │ │ └── SearchSuggestions.vue # 搜索建议组件
|
||||
│ │ └── WinDom # 窗口 DOM 相关组件
|
||||
│ │ └── TitleBar.vue # 标题栏组件
|
||||
│ ├── main.js # Vue 应用的入口文件
|
||||
│ ├── router # Vue Router 相关文件夹
|
||||
│ │ ├── index.js # Vue Router 入口文件
|
||||
│ │ └── routes.js # 路由配置文件
|
||||
│ ├── stores # Vuex Store 相关文件夹
|
||||
│ │ ├── indexedDB.js # IndexedDB 数据库相关文件
|
||||
│ │ ├── index.js # Vuex Store 入口文件
|
||||
│ │ ├── musicData.js # 音乐数据相关文件
|
||||
│ │ ├── siteData.js # 网站数据相关文件
|
||||
│ │ ├── siteSettings.js # 网站设置相关文件
|
||||
│ │ └── siteStatus.js # 网站状态相关文件
|
||||
│ ├── style # 样式相关文件夹
|
||||
│ │ ├── animate.scss # 动画样式文件
|
||||
│ │ └── main.scss # 主样式文件
|
||||
│ ├── utils # 工具函数文件夹
|
||||
│ │ ├── auth.js # 认证相关函数
|
||||
│ │ ├── base64.js # Base64编码解码相关函数
|
||||
│ │ ├── color-utils.js # 颜色工具函数
|
||||
│ │ ├── cover-color.js # 封面颜色相关函数
|
||||
│ │ ├── debounce.js # 防抖函数
|
||||
│ │ ├── formatData.js # 数据格式化函数
|
||||
│ │ ├── formRules.js # 表单验证规则
|
||||
│ │ ├── globalEvents.js # 全局事件处理函数
|
||||
│ │ ├── globalShortcut.js # 全局快捷键相关函数
|
||||
│ │ ├── helper.js # 辅助函数
|
||||
│ │ ├── parseLyric.js # 解析歌词函数
|
||||
│ │ ├── Player.js # 播放器控制相关函数
|
||||
│ │ ├── request.js # 网络请求相关函数
|
||||
│ │ ├── throttle.js # 节流函数
|
||||
│ │ ├── timeTools.js # 时间工具函数
|
||||
│ │ └── userSignIn.js # 用户登录相关函数
|
||||
│ └── views # Vue组件文件夹
|
||||
│ ├── Artist # 艺术家相关组件
|
||||
│ │ ├── albums.vue # 艺术家专辑组件
|
||||
│ │ ├── hot.vue # 艺术家热门组件
|
||||
│ │ ├── index.vue # 艺术家主组件
|
||||
│ │ ├── songs.vue # 艺术家歌曲组件
|
||||
│ │ └── videos.vue # 艺术家视频组件
|
||||
│ ├── Cloud.vue # 云盘组件
|
||||
│ ├── Comment.vue # 评论组件
|
||||
│ ├── DailySongs.vue # 每日推荐组件
|
||||
│ ├── Discover # 发现音乐相关组件
|
||||
│ │ ├── artists.vue # 发现音乐艺术家组件
|
||||
│ │ ├── index.vue # 发现音乐主组件
|
||||
│ │ ├── new.vue # 发现音乐新歌组件
|
||||
│ │ ├── playlists.vue # 发现音乐歌单组件
|
||||
│ │ └── toplists.vue # 发现音乐排行榜组件
|
||||
│ ├── History.vue # 历史记录组件
|
||||
│ ├── Home.vue # 主页组件
|
||||
│ ├── Like # 我喜欢的相关组件
|
||||
│ │ ├── albums.vue # 我喜欢的专辑组件
|
||||
│ │ ├── artists.vue # 我喜欢的艺术家组件
|
||||
│ │ ├── index.vue # 我喜欢的主组件
|
||||
│ │ ├── playlists.vue # 我喜欢的歌单组件
|
||||
│ │ └── videos.vue # 我喜欢的视频组件
|
||||
│ ├── List # 列表相关组件
|
||||
│ │ ├── album.vue # 专辑组件
|
||||
│ │ └── playlist.vue # 歌单组件
|
||||
│ │ └── dj.vue # 电台组件
|
||||
│ ├── Local # 本地音乐相关组件
|
||||
│ │ ├── albums.vue # 本地音乐专辑组件
|
||||
│ │ ├── artists.vue # 本地音乐艺术家组件
|
||||
│ │ ├── index.vue # 本地音乐主组件
|
||||
│ │ └── songs.vue # 本地音乐歌曲组件
|
||||
│ ├── Player.vue # 视频播放器组件
|
||||
│ ├── Dj # 电台相关组件
|
||||
│ │ └── index.vue # 电台主组件
|
||||
│ │ └── type.vue # 电台分类组件
|
||||
│ ├── Search # 搜索相关组件
|
||||
│ │ ├── albums.vue # 搜索专辑组件
|
||||
│ │ ├── artists.vue # 搜索艺术家组件
|
||||
│ │ ├── index.vue # 搜索主组件
|
||||
│ │ ├── playlists.vue # 搜索歌单组件
|
||||
│ │ ├── songs.vue # 搜索歌曲组件
|
||||
│ │ └── videos.vue # 搜索视频组件
|
||||
│ │ └── djs.vue # 搜索电台组件
|
||||
│ ├── Setting # 设置相关组件
|
||||
│ │ └── index.vue # 设置主组件
|
||||
│ ├── Song.vue
|
||||
│ ├── State
|
||||
│ │ ├── 403.vue
|
||||
│ │ ├── 404.vue
|
||||
│ │ └── 500.vue
|
||||
│ └── Test.vue
|
||||
└── vercel.json # Vercel 部署配置
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#imsyy/SPlayer&Date)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
241
auto-imports.d.ts
vendored
@@ -3,68 +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, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, 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
@@ -1,112 +1,145 @@
|
||||
/* 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']
|
||||
CloudSongMatch: typeof import('./src/components/Modal/CloudSongMatch.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']
|
||||
CoverPlayBtn: typeof import('./src/components/Cover/CoverPlayBtn.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']
|
||||
LoginPhone: typeof import('./src/components/Modal/LoginPhone.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']
|
||||
MainLayout: typeof import('./src/components/Global/MainLayout.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']
|
||||
NDivider: typeof import('naive-ui')['NDivider']
|
||||
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']
|
||||
NLayoutContent: typeof import('naive-ui')['NLayoutContent']
|
||||
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']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
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']
|
||||
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']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
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']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.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']
|
||||
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']
|
||||
SongListDrawer: typeof import('./src/components/List/SongListDrawer.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']
|
||||
Spectrum: typeof import('./src/components/Player/Spectrum.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
@@ -0,0 +1,3 @@
|
||||
provider: github
|
||||
owner: "imsyy"
|
||||
repo: "SPlayer"
|
||||
@@ -8,5 +8,23 @@ services:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 7899:7899
|
||||
- 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
@@ -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
@@ -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,98 +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/imgs/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target: nsis
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
oneClick: false
|
||||
# 安装程序的生成名称
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
# 创建的桌面快捷方式名称
|
||||
shortcutName: ${productName}
|
||||
# 卸载时显示的名称
|
||||
uninstallDisplayName: ${productName}
|
||||
# 创建桌面图标
|
||||
createDesktopShortcut: always
|
||||
# 是否允许 UAC 提升权限
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/imgs/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/imgs/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/imgs/icons/favicon-512x512.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/imgs/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- AppImage
|
||||
- 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,163 +0,0 @@
|
||||
import { resolve } from "path";
|
||||
import {
|
||||
defineConfig,
|
||||
externalizeDepsPlugin,
|
||||
loadEnv,
|
||||
splitVendorChunkPlugin,
|
||||
} from "electron-vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
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";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 读取环境变量
|
||||
const getEnv = (name) => {
|
||||
return loadEnv(mode, process.cwd())[name];
|
||||
};
|
||||
// 返回配置
|
||||
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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 预渲染
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.mjs"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 渲染进程
|
||||
renderer: {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
"vue",
|
||||
{
|
||||
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
|
||||
},
|
||||
],
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()],
|
||||
}),
|
||||
// viteCompression
|
||||
viteCompression(),
|
||||
// splitVendorChunkPlugin
|
||||
splitVendorChunkPlugin(),
|
||||
// PWA
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
workbox: {
|
||||
clientsClaim: true,
|
||||
skipWaiting: true,
|
||||
cleanupOutdatedCaches: true,
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: /(.*?)\.(woff2|woff|ttf)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "file-cache",
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,
|
||||
handler: "CacheFirst",
|
||||
options: {
|
||||
cacheName: "image-cache",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
manifest: {
|
||||
name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
short_name: getEnv("RENDERER_VITE_SITE_TITLE"),
|
||||
description: getEnv("RENDERER_VITE_SITE_DES"),
|
||||
display: "standalone",
|
||||
start_url: "/",
|
||||
theme_color: "#fff",
|
||||
background_color: "#efefef",
|
||||
icons: [
|
||||
{
|
||||
src: "/imgs/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/imgs/icons/favicon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/imgs/icons/favicon-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/imgs/icons/favicon-512x512.png",
|
||||
sizes: "512x512",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
// 服务器配置
|
||||
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/, ""),
|
||||
},
|
||||
},
|
||||
},
|
||||
// 构建
|
||||
root: ".",
|
||||
build: {
|
||||
minify: "terser",
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
},
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
pure_funcs: ["console.log"],
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
122
electron.vite.config.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { resolve } from "path";
|
||||
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(({ command, mode }) => {
|
||||
// 读取环境变量
|
||||
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: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
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.ts"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 渲染进程
|
||||
renderer: {
|
||||
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(),
|
||||
wasm(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src/"),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["legacy-js-api"],
|
||||
},
|
||||
},
|
||||
},
|
||||
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: {
|
||||
compress: {
|
||||
pure_funcs: ["console.log"],
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
1
electron/main/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="electron-vite/node" />
|
||||
@@ -1,258 +0,0 @@
|
||||
import { join } from "path";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut, nativeImage } from "electron";
|
||||
import { platform, optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import createSystemTray from "@main/utils/createSystemTray";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import Store from "electron-store";
|
||||
import log from "electron-log";
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 配置 log
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
||||
// 设置日志文件的最大大小为 2 MB
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024;
|
||||
// 绑定 console 事件
|
||||
console.error = log.error.bind(log);
|
||||
console.warn = log.warn.bind(log);
|
||||
console.info = log.info.bind(log);
|
||||
console.debug = log.debug.bind(log);
|
||||
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
constructor() {
|
||||
// 主窗口
|
||||
this.mainWindow = null;
|
||||
// 主代理
|
||||
this.mainServer = null;
|
||||
// 网易云 API
|
||||
this.ncmServer = null;
|
||||
// Store
|
||||
this.store = new Store({
|
||||
// 窗口大小
|
||||
windowSize: {
|
||||
width: { type: "number", default: 1280 },
|
||||
height: { type: "number", default: 740 },
|
||||
},
|
||||
});
|
||||
// 设置应用程序名称
|
||||
if (process.platform === "win32") app.setAppUserModelId(app.getName());
|
||||
// 初始化
|
||||
this.checkApp().then(async (lockObtained) => {
|
||||
if (lockObtained) {
|
||||
await this.init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 单例锁
|
||||
async checkApp() {
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
log.error("已有一个程序正在运行,本次启动阻止");
|
||||
app.quit();
|
||||
// 未获得锁
|
||||
return false;
|
||||
}
|
||||
// 聚焦到当前程序
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
});
|
||||
// 获得锁
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化程序
|
||||
async init() {
|
||||
log.info("主进程初始化");
|
||||
|
||||
// 启动网易云 API
|
||||
try {
|
||||
this.ncmServer = await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("启动网易云 API 失败:", error);
|
||||
}
|
||||
|
||||
// 非开发环境启动代理
|
||||
if (!is.dev) {
|
||||
this.mainServer = await startMainServer();
|
||||
}
|
||||
|
||||
// 注册应用协议
|
||||
app.setAsDefaultProtocolClient("SPlayer");
|
||||
// 应用程序准备好之前注册
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: "app", privileges: { secure: true, standard: true } },
|
||||
]);
|
||||
|
||||
// 主应用程序事件
|
||||
this.mainAppEvents();
|
||||
}
|
||||
|
||||
// 创建主窗口
|
||||
createWindow() {
|
||||
// 创建浏览器窗口
|
||||
this.mainWindow = new BrowserWindow({
|
||||
title: app.getName() || "SPlayer",
|
||||
width: this.store.get("windowSize.width") || 1280, // 窗口宽度
|
||||
height: this.store.get("windowSize.height") || 740, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
// transparent: true, // 透明窗口
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/imgs/icons/favicon.png")),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 窗口准备就绪时显示窗口
|
||||
this.mainWindow.once("ready-to-show", () => {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 主窗口事件
|
||||
this.mainWindowEvents();
|
||||
|
||||
// 设置窗口打开处理程序
|
||||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 渲染路径
|
||||
// 在开发模式
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
||||
this.mainWindow.loadURL(process.env.ELECTRON_RENDERER_URL);
|
||||
}
|
||||
// 生产模式
|
||||
else {
|
||||
this.mainWindow.loadURL(`http://127.0.0.1:${import.meta.env.MAIN_VITE_MAIN_PORT ?? 7899}`);
|
||||
}
|
||||
|
||||
// 配置网络代理
|
||||
const proxyRules = this.store.get("proxy");
|
||||
if (proxyRules) {
|
||||
this.mainWindow.webContents.session.setProxy({ proxyRules }, (result) => {
|
||||
console.info("网络代理配置:", result);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 主应用程序事件
|
||||
mainAppEvents() {
|
||||
app.whenReady().then(async () => {
|
||||
// 创建主窗口
|
||||
this.createWindow();
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow, this.store);
|
||||
// 系统托盘
|
||||
createSystemTray(this.mainWindow);
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(this.mainWindow);
|
||||
});
|
||||
|
||||
// 开发环境下 F12 打开控制台
|
||||
app.on("browser-window-created", (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window);
|
||||
});
|
||||
|
||||
// 在 macOS 上,当单击 Dock 图标且没有其他窗口时,通常会重新创建窗口
|
||||
app.on("activate", () => {
|
||||
if (BrowserWindow.getAllWindows().length === 0) this.createWindow();
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
globalShortcut.unregisterAll();
|
||||
});
|
||||
|
||||
// 当所有窗口都关闭时退出应用,macOS 除外
|
||||
app.on("window-all-closed", () => {
|
||||
if (!platform.isMacOS) {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主窗口事件
|
||||
mainWindowEvents() {
|
||||
this.mainWindow.on("show", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("hide", () => {
|
||||
// console.info("窗口隐藏");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("focus", () => {
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
|
||||
// this.mainWindow.on("blur", () => {
|
||||
// console.info("窗口失去焦点");
|
||||
// });
|
||||
|
||||
this.mainWindow.on("maximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", true);
|
||||
});
|
||||
|
||||
this.mainWindow.on("unmaximize", () => {
|
||||
this.mainWindow.webContents.send("windowState", false);
|
||||
});
|
||||
|
||||
this.mainWindow.on("resized", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
this.mainWindow.on("moved", () => {
|
||||
this.store.set("windowSize", this.mainWindow.getBounds());
|
||||
});
|
||||
|
||||
// 窗口关闭
|
||||
this.mainWindow.on("close", (event) => {
|
||||
if (platform.isLinux) {
|
||||
app.quit();
|
||||
} else {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
this.mainWindow.hide();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
new MainProcess();
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,321 +0,0 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { download } from "electron-dl";
|
||||
import { getFonts } from "font-list";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
/**
|
||||
* 监听主进程的 IPC 事件
|
||||
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
|
||||
* @param {Store} store - 存储对象
|
||||
*/
|
||||
|
||||
const mainIpcMain = (win, store) => {
|
||||
// 窗口操作部分
|
||||
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("windowState", 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("check-updates", () => {
|
||||
console.info("开始检查更新");
|
||||
configureAutoUpdater();
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
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 (_, songData, options) => {
|
||||
try {
|
||||
const { url, data, lyric, name, type } = JSON.parse(songData);
|
||||
const { path, downloadMeta, downloadCover, downloadLyrics } = JSON.parse(options);
|
||||
if (fs.access(path)) {
|
||||
console.info("开始下载:", name, url);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, url, {
|
||||
directory: path,
|
||||
filename: `${name}.${type}`,
|
||||
});
|
||||
// 若关闭,则不进行元信息写入
|
||||
if (!downloadMeta) return true;
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, data.cover, {
|
||||
directory: path,
|
||||
filename: `${name}.jpg`,
|
||||
});
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = data.name || "未知曲目";
|
||||
songFile.tag.album = data.album?.name || "未知专辑";
|
||||
songFile.tag.performers = data?.artists?.map((ar) => ar.name) || ["未知艺术家"];
|
||||
if (downloadLyrics) songFile.tag.lyrics = lyric;
|
||||
if (downloadCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
// 删除封面
|
||||
await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} else {
|
||||
console.log(`目录不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("下载文件时出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取系统全部字体
|
||||
ipcMain.handle("getAllFonts", async () => {
|
||||
try {
|
||||
const fonts = await getFonts();
|
||||
return fonts;
|
||||
} catch (error) {
|
||||
console.error("获取系统字体时出错:", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 配置网络代理
|
||||
ipcMain.on("set-proxy", (_, config) => {
|
||||
console.log(config);
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
store.set("proxy", proxyRules);
|
||||
win.webContents.session.setProxy({ proxyRules }, () => {
|
||||
console.info("网络代理配置完成");
|
||||
});
|
||||
});
|
||||
|
||||
// 取消代理
|
||||
ipcMain.on("remove-proxy", () => {
|
||||
store.set("proxy", "");
|
||||
win.webContents.session.setProxy({ proxyRules: "" }, () => {
|
||||
console.info("取消网络代理配置");
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 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
@@ -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 @@
|
||||
import netEaseApi from "NeteaseCloudMusicApi";
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - 服务器配置
|
||||
* @param {number} [options.port=11451] - 服务器端口
|
||||
* @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
@@ -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
@@ -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
@@ -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
@@ -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,65 +0,0 @@
|
||||
import { dialog } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import pkg from "electron-updater";
|
||||
|
||||
const { autoUpdater } = pkg;
|
||||
|
||||
// 更新弹窗
|
||||
const hasNewVersion = (info) => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否立即下载并安装新版本?",
|
||||
buttons: ["立即下载", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 触发手动下载
|
||||
autoUpdater.downloadUpdate();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
if (is.dev) return false;
|
||||
|
||||
// 监听下载进度事件
|
||||
autoUpdater.on("download-progress", (progressObj) => {
|
||||
console.log(`更新下载进度: ${progressObj.percent}%`);
|
||||
});
|
||||
|
||||
// 下载完成
|
||||
autoUpdater.on("update-downloaded", () => {
|
||||
// 显示安装弹窗
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "下载完成",
|
||||
message: "新版本已下载完成,是否现在安装?",
|
||||
buttons: ["是", "稍后"],
|
||||
type: "question",
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 下载失败
|
||||
autoUpdater.on("error", (err) => {
|
||||
console.error("下载更新失败:", err);
|
||||
dialog.showErrorBox("下载更新失败", "请检查网络连接并稍后重试!");
|
||||
});
|
||||
|
||||
// 若有更新
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
hasNewVersion(info);
|
||||
});
|
||||
|
||||
// 检查更新
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
};
|
||||
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,24 +0,0 @@
|
||||
import { globalShortcut } from "electron";
|
||||
|
||||
/**
|
||||
* 注册全局快捷键
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createGlobalShortcut = (win) => {
|
||||
// 刷新程序
|
||||
globalShortcut.register("CmdOrCtrl+Shift+R", () => {
|
||||
if (win && win.isFocused()) win?.reload();
|
||||
});
|
||||
|
||||
// 打开开发者工具
|
||||
globalShortcut.register("CmdOrCtrl+Shift+I", () => {
|
||||
if (win && win.isFocused()) {
|
||||
win?.webContents.openDevTools({
|
||||
mode: "right",
|
||||
activate: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default createGlobalShortcut;
|
||||
@@ -1,139 +0,0 @@
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
import { join } from "path";
|
||||
|
||||
// 当前歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统托盘
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemTray = (win) => {
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(
|
||||
nativeImage
|
||||
.createFromPath(
|
||||
join(
|
||||
__dirname,
|
||||
process.platform === "win32"
|
||||
? "../../public/imgs/icons/favicon.ico"
|
||||
: "../../public/imgs/icons/favicon-32x32.png",
|
||||
),
|
||||
)
|
||||
.resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
}),
|
||||
);
|
||||
// 应用内菜单
|
||||
Menu.setApplicationMenu(createTrayMenu(win));
|
||||
// 默认名称
|
||||
win.setTitle(app.getName());
|
||||
mainTray.setTitle(app.getName());
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 左键事件
|
||||
mainTray.on("click", () => win.show());
|
||||
// 托盘菜单
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
// 系统主题改变
|
||||
nativeTheme.on("updated", () => {
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放歌曲改变
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
win.setTitle(val);
|
||||
mainTray.setTitle(val);
|
||||
mainTray.setToolTip(val);
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
// 播放状态改变
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
mainTray.setContextMenu(createTrayMenu(win));
|
||||
});
|
||||
};
|
||||
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/imgs/icons/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/imgs/icons/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 返回菜单
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
click() {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("showPlayer");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
accelerator: "CmdOrCtrl+Left",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
accelerator: "CmdOrCtrl+Space",
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
accelerator: "CmdOrCtrl+Right",
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("open-setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "退出",
|
||||
icon: createIcon("power"),
|
||||
click: () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
export default createSystemTray;
|
||||
@@ -1,167 +0,0 @@
|
||||
import { encryptQuery } from "@main/utils/kwDES";
|
||||
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) => {
|
||||
try {
|
||||
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) {
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword.split("-");
|
||||
const songName = result.data.musics[0]?.songName;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
return result.data.musics[0].id;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取咪咕音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getMiguSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getMiguSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("咪咕解灰歌曲 ID:", songId);
|
||||
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) {
|
||||
const songUrl = result.data.data.url;
|
||||
console.info("咪咕解灰歌曲 URL:", songUrl);
|
||||
return songUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取咪咕音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 ID
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongId = async (keyword) => {
|
||||
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.toString();
|
||||
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 originalName = keyword.split("-");
|
||||
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
|
||||
if (songName && !songName?.includes(originalName[0])) {
|
||||
return null;
|
||||
}
|
||||
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
|
||||
return songId.slice("MUSIC_".length);
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 ID 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取酷我音乐歌曲 URL
|
||||
* @param {string} keyword - 搜索关键字
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
const getKuwoSongUrl = async (keyword) => {
|
||||
try {
|
||||
const songId = await getKuwoSongId(keyword);
|
||||
if (!songId) return null;
|
||||
console.info("酷我解灰歌曲 ID:", songId);
|
||||
const url = encryptQuery
|
||||
? "http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
|
||||
encryptQuery(
|
||||
"corp=kuwo&source=kwplayer_ar_1.1.9_oppo_118980_320.apk&p2p=1&type=convert_url2&sig=0&format=mp3" +
|
||||
"&rid=" +
|
||||
songId,
|
||||
)
|
||||
: "http://antiserver.kuwo.cn/anti.s?type=convert_url&format=mp3&response=url&rid=MUSIC_" +
|
||||
songId;
|
||||
const result = await axios.get(url, { "user-agent": "okhttp/3.10.0" });
|
||||
if (result.data) {
|
||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||
console.info("酷我解灰歌曲 URL:", urlMatch);
|
||||
return urlMatch;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取酷我音乐歌曲 URL 失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取给定关键字的音乐 URL
|
||||
* @param {string} keyword - 关键字
|
||||
* @returns {Promise<?string>} 音乐 URL
|
||||
*/
|
||||
const getNeteaseMusicUrl = async (keyword) => {
|
||||
try {
|
||||
const [kuwoSongUrl, miguSongUrl] = await Promise.all([
|
||||
getKuwoSongUrl(keyword),
|
||||
getMiguSongUrl(keyword),
|
||||
]);
|
||||
if (kuwoSongUrl) {
|
||||
return kuwoSongUrl;
|
||||
}
|
||||
if (miguSongUrl) {
|
||||
return miguSongUrl;
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error("获取解灰 URL 全部失败:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default getNeteaseMusicUrl;
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable no-undef */
|
||||
/*
|
||||
Thanks to
|
||||
https://github.com/XuShaohua/kwplayer/blob/master/kuwo/DES.py
|
||||
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
@@ -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
@@ -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",
|
||||
},
|
||||
},
|
||||
];
|
||||
36
index.html
@@ -1,23 +1,21 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/imgs/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/imgs/icons/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/imgs/icons/apple-touch-icon.png" />
|
||||
<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%" />
|
||||
<link rel="mask-icon" href="/imgs/icons/safari-pinned-tab.svg" color="#5bbad5" />
|
||||
<meta name="msapplication-TileColor" content="#da532c" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/icon" href="/icons/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
34
nginx.conf
@@ -1,8 +1,9 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 7899;
|
||||
listen [::]:7899;
|
||||
listen 25884;
|
||||
listen [::]:25884;
|
||||
server_name localhost;
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
@@ -14,10 +15,27 @@ server {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_buffers 16 32k;
|
||||
location /api/netease/song/url/v1 {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_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;
|
||||
@@ -25,4 +43,10 @@ server {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
158
package.json
@@ -1,77 +1,135 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.8",
|
||||
"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": ">=18.16.0",
|
||||
"npm": ">=9.6.7",
|
||||
"pnpm": ">=8.14.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": "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": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.19.8",
|
||||
"axios": "^1.7.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.2",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"express": "^4.19.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"font-list": "^1.5.1",
|
||||
"@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.14.0",
|
||||
"node-taglib-sharp": "^5.2.3",
|
||||
"pinia": "^2.1.7",
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"plyr": "^3.7.8",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue-router": "^4.3.2",
|
||||
"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.2",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"ajv": "^8.13.0",
|
||||
"electron": "^28.3.2",
|
||||
"electron-builder": "^24.13.3",
|
||||
"electron-log": "^5.1.4",
|
||||
"electron-vite": "^2.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"naive-ui": "^2.38.2",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.77.2",
|
||||
"terser": "^5.31.0",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.2.11",
|
||||
"@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",
|
||||
"vite-plugin-pwa": "^0.17.5",
|
||||
"vue": "3.4.8"
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
9933
pnpm-lock.yaml
generated
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,13 +0,0 @@
|
||||
@font-face {
|
||||
font-family: "HarmonyOS Sans";
|
||||
src: url("./HarmonyOS_Sans_SC.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "HarmonyOS Sans";
|
||||
src: url("./HarmonyOS_Sans_SC_Bold.woff2") format("woff2");
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
6
public/fonts/font.css
Normal file
@@ -0,0 +1,6 @@
|
||||
@font-face {
|
||||
font-family: "logo";
|
||||
src: url("./logo.woff2") format("woff2");
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
}
|
||||
BIN
public/fonts/logo.woff2
Normal file
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 644 B |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
public/icons/icon.icns
Normal file
BIN
public/icons/logo-icon.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/icons/logo.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
public/icons/thumbar/next-dark.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
public/icons/thumbar/next-light.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/icons/thumbar/pause-dark.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |