mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 03:14:57 +08:00
Compare commits
286 Commits
v2.0.2
...
ea822f91e8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea822f91e8 | ||
|
|
5e260ffc0d | ||
|
|
e3dcde71e8 | ||
|
|
39c35e8a31 | ||
|
|
772f6552e7 | ||
|
|
c9f3553806 | ||
|
|
7de1355f18 | ||
|
|
392c64f06b | ||
|
|
0d66ced637 | ||
|
|
ae1ee71e75 | ||
|
|
84d9c999eb | ||
|
|
1a36fbf1d5 | ||
|
|
e0e62cd906 | ||
|
|
6d367f1fd5 | ||
|
|
2ed6acf934 | ||
|
|
236ee0a345 | ||
|
|
fe0f7a0f25 | ||
|
|
8529663ea5 | ||
|
|
100bae7488 | ||
|
|
91927b8d76 | ||
|
|
8be209837d | ||
|
|
9ae09b5711 | ||
|
|
3509d73ecc | ||
|
|
0bdb9c07b7 | ||
|
|
3218ab05f1 | ||
|
|
e40343f91f | ||
|
|
fcf1c235c3 | ||
|
|
ef0433c645 | ||
|
|
207f84fcd7 | ||
|
|
2985aa5977 | ||
|
|
ffd7aeff49 | ||
|
|
7f4a88daa1 | ||
|
|
91171465b2 | ||
|
|
1744a5a678 | ||
|
|
ce3d469547 | ||
|
|
7cc5b85fc5 | ||
|
|
3b3f321a3d | ||
|
|
58e3c6e21c | ||
|
|
68756f2502 | ||
|
|
cea9f7b025 | ||
|
|
6b653bc5e8 | ||
|
|
1b2985892b | ||
|
|
eb0094c189 | ||
|
|
5edbd66398 | ||
|
|
4e731e976c | ||
|
|
bd6e23435e | ||
|
|
8ec91e1392 | ||
|
|
8f69b56378 | ||
|
|
ccd0c6bdeb | ||
|
|
1c9109af73 | ||
|
|
a556a2e102 | ||
|
|
9fcd5b0e98 | ||
|
|
a1be1e16b2 | ||
|
|
fc7fc08a6e | ||
|
|
d74515142d | ||
|
|
0aae10e8a0 | ||
|
|
242c6f2ca7 | ||
|
|
413b74bf9a | ||
|
|
aad5c9461f | ||
|
|
23cccbb660 | ||
|
|
ba7666526c | ||
|
|
82ea11b6fa | ||
|
|
9771cea25f | ||
|
|
6f4afd85d7 | ||
|
|
fe82fa623b | ||
|
|
28b521a192 | ||
|
|
bdfbb4d2b1 | ||
|
|
6937a93d17 | ||
|
|
2bc4fca4dd | ||
|
|
92428593e2 | ||
|
|
95e3f74301 | ||
|
|
9145eb034b | ||
|
|
829dc3b591 | ||
|
|
c4c6178b9e | ||
|
|
9967bcb102 | ||
|
|
feb186db65 | ||
|
|
84999338e6 | ||
|
|
7943b56dbb | ||
|
|
6a1657cf20 | ||
|
|
0eaf32b8a7 | ||
|
|
424c1e76f0 | ||
|
|
e854dbb816 | ||
|
|
5e37678348 | ||
|
|
9fcb5ca633 | ||
|
|
55aedc33c3 | ||
|
|
1d4aa43185 | ||
|
|
8014eedd72 | ||
|
|
4f018775ba | ||
|
|
4e7bd95366 | ||
|
|
3035056ded | ||
|
|
df42292f0a | ||
|
|
ca2ff2a890 | ||
|
|
bd06c3f8f4 | ||
|
|
a44cd904dc | ||
|
|
09838e632d | ||
|
|
7f77ccb18a | ||
|
|
f3f05aa72b | ||
|
|
6beb9c78e1 | ||
|
|
2efc0a5228 | ||
|
|
bb0cae2be1 | ||
|
|
22a6fdd0c4 | ||
|
|
d257a441c2 | ||
|
|
49c81b6afd | ||
|
|
045497a7c8 | ||
|
|
521970b8a1 | ||
|
|
a1e9dd7746 | ||
|
|
61d2467916 | ||
|
|
f3adf773a7 | ||
|
|
ad807cf478 | ||
|
|
a1194232af | ||
|
|
9fc1746495 | ||
|
|
103bf7948d | ||
|
|
12c2dea226 | ||
|
|
7fbe8ac6a5 | ||
|
|
ef6868fd40 | ||
|
|
f238845f9b | ||
|
|
d4d16b71ae | ||
|
|
9baf571478 | ||
|
|
6370ac77e6 | ||
|
|
2bf3f2a5c6 | ||
|
|
e8bf42891e | ||
|
|
c21f970b86 | ||
|
|
56045cd338 | ||
|
|
dbf6121098 | ||
|
|
103b2fe923 | ||
|
|
c70a266b22 | ||
|
|
a7b34ca4b0 | ||
|
|
caa6fa838f | ||
|
|
e98140d0ac | ||
|
|
ac47b38773 | ||
|
|
fef6e4f6fa | ||
|
|
0215e5944d | ||
|
|
1b6ad163e3 | ||
|
|
080de35545 | ||
|
|
163423222d | ||
|
|
fc49b7ad00 | ||
|
|
b83cd99d8c | ||
|
|
74a5de96c0 | ||
|
|
fcf78cdd08 | ||
|
|
98fbc81d2f | ||
|
|
5ad562ab1c | ||
|
|
10dc011f9a | ||
|
|
3df6e91f95 | ||
|
|
31ae15d242 | ||
|
|
a2cfbb5e52 | ||
|
|
c4bd94daae | ||
|
|
e18828cd08 | ||
|
|
82f6b4607b | ||
|
|
3874ab3483 | ||
|
|
fc297cb198 | ||
|
|
aec06c5a55 | ||
|
|
87ce076f26 | ||
|
|
45b1bf130d | ||
|
|
3966578015 | ||
|
|
fed7b3678b | ||
|
|
ac6ce257b8 | ||
|
|
b8b8f747d3 | ||
|
|
53468b2e3a | ||
|
|
ecadf6ade7 | ||
|
|
8187b09fcb | ||
|
|
89117d4198 | ||
|
|
6158dd2750 | ||
|
|
86f33d054a | ||
|
|
52e8458590 | ||
|
|
554cf45500 | ||
|
|
60f751713a | ||
|
|
edd9b38cfc | ||
|
|
9a87d73289 | ||
|
|
317763e2c3 | ||
|
|
aa3f9d2ca8 | ||
|
|
e64a5ba1bc | ||
|
|
00e6f7bb60 | ||
|
|
883ef05ab4 | ||
|
|
d0b5eb3371 | ||
|
|
590ef96aa7 | ||
|
|
172ca5a2f3 | ||
|
|
6e1e56c1bd | ||
|
|
105fed4bd0 | ||
|
|
8bd3dc56d8 | ||
|
|
8e88bf64b1 | ||
|
|
244a832c52 | ||
|
|
146af3aeba | ||
|
|
143e8e29d7 | ||
|
|
c702e6e01a | ||
|
|
b2ddb9f4e2 | ||
|
|
201186bab2 | ||
|
|
bfcd59daca | ||
|
|
d5c3843c3f | ||
|
|
d3f307eac5 | ||
|
|
675a52b8d1 | ||
|
|
aee90e9c4e | ||
|
|
eb39b81d8d | ||
|
|
436df47104 | ||
|
|
e04e5e34c6 | ||
|
|
b57d685c03 | ||
|
|
6684172592 | ||
|
|
0257e74ff0 | ||
|
|
16c8865651 | ||
|
|
1a61aa2458 | ||
|
|
e543f07d8e | ||
|
|
96a0495a88 | ||
|
|
02befcd8a4 | ||
|
|
15b500806a | ||
|
|
9accf5d27d | ||
|
|
191ab29a44 | ||
|
|
a4d4cd5f70 | ||
|
|
3b07f7346f | ||
|
|
1edceeebdd | ||
|
|
fbf261f80b | ||
|
|
432fa18299 | ||
|
|
b2dcec840b | ||
|
|
6f93d65f02 | ||
|
|
852b901353 | ||
|
|
71a282157d | ||
|
|
49f462cdf5 | ||
|
|
94c0ca70e1 | ||
|
|
813637762c | ||
|
|
eb39b85a8b | ||
|
|
201fd8d687 | ||
|
|
02116d8f0f | ||
|
|
551a190edf | ||
|
|
f5015a4028 | ||
|
|
ea55616bd6 | ||
|
|
2b8eb93404 | ||
|
|
18113d94e9 | ||
|
|
a8f01d5728 | ||
|
|
3a6f8e7462 | ||
|
|
57325bfc46 | ||
|
|
a3e8931460 | ||
|
|
2e0f448f66 | ||
|
|
44cfbbf4e0 | ||
|
|
963343c39e | ||
|
|
a697799f48 | ||
|
|
7c59cc2ccb | ||
|
|
11acbcf7eb | ||
|
|
c35ede2158 | ||
|
|
dc480459eb | ||
|
|
c100bfed8d | ||
|
|
2b48713565 | ||
|
|
528f9b0aa6 | ||
|
|
0e0bde89bb | ||
|
|
acf60b8b75 | ||
|
|
3b5338d582 | ||
|
|
0c82d6a096 | ||
|
|
50374e173e | ||
|
|
62c9dc33db | ||
|
|
2b6d68ecbd | ||
|
|
8a842aa6d6 | ||
|
|
b0a08ce1b8 | ||
|
|
77ab94a59a | ||
|
|
2b65269ba9 | ||
|
|
7ed33919ab | ||
|
|
d20512e662 | ||
|
|
9423d2bf9e | ||
|
|
54fb8e74a8 | ||
|
|
7d3d7696da | ||
|
|
e66ba52889 | ||
|
|
b146dc011e | ||
|
|
ad2422d826 | ||
|
|
6b9ba74c35 | ||
|
|
22e2653e20 | ||
|
|
4cac54c84a | ||
|
|
e5f9ecd7b5 | ||
|
|
d64cfb40ec | ||
|
|
53c30caf00 | ||
|
|
6fcce91b2b | ||
|
|
5a53dfcdf7 | ||
|
|
bbe71bd62d | ||
|
|
ecedf0b7b9 | ||
|
|
f2935f3c1c | ||
|
|
d3e677d494 | ||
|
|
e49f90b0da | ||
|
|
0359b0e470 | ||
|
|
aa2373695b | ||
|
|
cf940ff405 | ||
|
|
e7c17cf531 | ||
|
|
c6a1ca4f42 | ||
|
|
8f5008df69 | ||
|
|
428ce4be86 | ||
|
|
d46c4c4285 | ||
|
|
0b871175b2 | ||
|
|
c34c4fd880 | ||
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 |
44
.env.example
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
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,
|
||||
},
|
||||
};
|
||||
57
.github/ISSUE_TEMPLATE/bug.yml
vendored
57
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,34 +1,75 @@
|
||||
name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
title: 请填写标题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
---
|
||||
在此之前,我们默认你已经知道该如何提问。简而言之,**你要精确地描述问题并提供充足的信息**
|
||||
|
||||
此外,如果需要,你可以使用 <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> 打开开发者工具为我们提供信息
|
||||
|
||||
- type: checkboxes
|
||||
validations:
|
||||
required: false
|
||||
attributes:
|
||||
label: "检查清单"
|
||||
description: |
|
||||
我们需要了解一些信息,你需要检查下面的检查项 <br />
|
||||
**这里并不是每一个检查项都必要,根据你的真实情况勾选即可**
|
||||
options:
|
||||
- label: "我已检索仓库中所有的 Issues,确保我**没有重复提交问题**;或有相似 Issue,但我觉得我的情况不包含在那个相似 Issue 之内"
|
||||
- label: "我已经找到了可以复现这个问题的方法,并且写在了下面的「具体信息」中"
|
||||
- label: "此问题可以在我的设备和当前环境中**稳定复现**"
|
||||
- label: "此问题可以在最新版本 (Latest Release) 中复现"
|
||||
- label: "此问题是在我更新到当前版本后**才**出现的"
|
||||
|
||||
- type: dropdown
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "是网页端还是客户端"
|
||||
placeholder: "客户端"
|
||||
options:
|
||||
- "客户端"
|
||||
- "网页端"
|
||||
default: 0
|
||||
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前系统环境"
|
||||
placeholder: "win11"
|
||||
placeholder: "如:Windows 11"
|
||||
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
attributes:
|
||||
label: "当前 Node.js 及 npm 版本"
|
||||
placeholder: "v18.16.0 / v9.6.7"
|
||||
placeholder: "如:v18.16.0 / v9.6.7 (选填)"
|
||||
|
||||
- type: input
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "当前版本"
|
||||
placeholder: "v1.0.0"
|
||||
description: |
|
||||
填写关于软件里的或 Releases 中版本号即可 <br />
|
||||
如果是自行构建或从 GitHub Actions 下载的开发版,还需要提供 Commit ID
|
||||
placeholder: "如:v1.0.0"
|
||||
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "有需要补充的信息吗?比如控制台的报错什么的( Ctrl + Shift + i 打开控制台 )"
|
||||
description: |
|
||||
请填写完整的复现步骤和遇到的问题,还请尽量提供所有可能的信息,提供包括但不限于:
|
||||
|
||||
- 分步的复现步骤,可以使用 `1. xxx` (换行) `2. xxx` 的格式
|
||||
- 截图(如果结合复现步骤还是无法详细表述,甚至可以录屏)
|
||||
- 开发者工具中的:控制台输出报错、网络请求等
|
||||
- 出现问题的在线歌曲链接、出现问题的文件(本地歌曲、歌词等)的下载链接
|
||||
|
||||
等信息,以更好地帮助我们解决你的问题
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
|
||||
49
.github/workflows/build.yml
vendored
49
.github/workflows/build.yml
vendored
@@ -1,49 +0,0 @@
|
||||
# Dev 分支推送部署预览
|
||||
## 仅部署 Win 端
|
||||
name: Build Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Build and electron app
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 复制环境变量文件
|
||||
- name: 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@v3.1.3
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: dist
|
||||
67
.github/workflows/dev.yml
vendored
Normal file
67
.github/workflows/dev.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# Dev 分支推送部署预览
|
||||
## 部署 Windows x64 和 ARM64 版本
|
||||
name: Build Dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
|
||||
env:
|
||||
NODE_VERSION: 22.x
|
||||
PNPM_VERSION: 8
|
||||
|
||||
jobs:
|
||||
# Windows x64 架构
|
||||
build-win:
|
||||
name: Build Electron App for Windows
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4
|
||||
# 设置 pnpm 版本
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
run: |
|
||||
if (-not (Test-Path .env)) {
|
||||
Copy-Item .env.example .env
|
||||
} else {
|
||||
Write-Host ".env file already exists. Skipping the copy step."
|
||||
}
|
||||
# 安装项目依赖
|
||||
- name: Install Dependencies
|
||||
run: pnpm install
|
||||
# 清理旧的构建产物
|
||||
- name: Clean dist folder
|
||||
run: |
|
||||
if (Test-Path dist) {
|
||||
Remove-Item -Recurse -Force dist
|
||||
}
|
||||
# 构建 Electron App (x64)
|
||||
- name: Build Electron App for Windows x64
|
||||
# 仅 x64
|
||||
run: pnpm run build:win -- --arch=x64
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 清理不必要的构建产物(保留 .exe 和 .blockmap 文件)
|
||||
- name: Cleanup Artifacts
|
||||
run: npx del-cli "dist/**/*.yaml" "dist/**/*.yml"
|
||||
# 上传构建产物(仅上传 x64 架构的 .exe 文件)
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: |
|
||||
dist/**.exe
|
||||
10
.github/workflows/docker.yml
vendored
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 }}
|
||||
|
||||
261
.github/workflows/release.yml
vendored
261
.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.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/checkout@v4
|
||||
# 设置 pnpm 版本
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
node-version: "18.x"
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
# 安装 Node.js
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
cache: "pnpm"
|
||||
# 清理旧的构建产物
|
||||
- name: Clean workspace on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
Write-Host "🧹 Cleaning workspace, node_modules, and Electron caches..."
|
||||
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
||||
if (Test-Path out) { Remove-Item -Recurse -Force out }
|
||||
if (Test-Path node_modules) { Remove-Item -Recurse -Force node_modules }
|
||||
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron-builder") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron-builder" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
if (Test-Path "$env:LOCALAPPDATA\electron") {
|
||||
Remove-Item "$env:LOCALAPPDATA\electron" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
- name: Clean workspace on macOS & Linux
|
||||
if: runner.os == 'macOS' || runner.os == 'Linux'
|
||||
run: |
|
||||
echo "🧹 Cleaning workspace, node_modules, and Electron caches..."
|
||||
rm -rf dist out node_modules ~/.cache/electron-builder ~/.cache/electron
|
||||
# 安装项目依赖
|
||||
- name: Install dependencies
|
||||
run: 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@v3.1.3
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
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.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.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@v3.1.3
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
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.1.1
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
with:
|
||||
node-version: "18.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
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: dist/*.*
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Build Linux x64 & ARM64 App
|
||||
if: runner.os == 'Linux'
|
||||
run: pnpm build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 上传 Snap 包到 Snapcraft 商店
|
||||
- name: Publish Snap to Snap Store
|
||||
if: runner.os == 'Linux'
|
||||
run: snapcraft upload dist/*.snap --release stable
|
||||
continue-on-error: true
|
||||
# 合并所有构建
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-${{ runner.os }}
|
||||
path: dist/*.*
|
||||
|
||||
# ===================================================================
|
||||
# 收集并发布 Release
|
||||
# ===================================================================
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
# 需要写入权限
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# 将所有产物下载到 artifacts 文件夹
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: artifacts
|
||||
# 创建 GitHub Release 并上传所有产物
|
||||
- name: Create GitHub Release and Upload Assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# 自动生成 Release 说明
|
||||
generate_release_notes: true
|
||||
# 发布为草稿
|
||||
draft: false
|
||||
# 发布为预发布
|
||||
prerelease: false
|
||||
# 全部上传
|
||||
files: |
|
||||
!artifacts/**/*-unpacked/**
|
||||
artifacts/**/*.exe
|
||||
artifacts/**/*.dmg
|
||||
artifacts/**/*.zip
|
||||
artifacts/**/*.AppImage
|
||||
artifacts/**/*.deb
|
||||
artifacts/**/*.rpm
|
||||
artifacts/**/*.pacman
|
||||
artifacts/**/*.snap
|
||||
artifacts/**/*.tar.gz
|
||||
artifacts/**/*.yml
|
||||
artifacts/**/*.blockmap
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -15,9 +15,8 @@ coverage
|
||||
*.local
|
||||
out
|
||||
.env
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -29,3 +28,5 @@ out
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env.development
|
||||
.env.production
|
||||
|
||||
4
.npmrc
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
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
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"]
|
||||
241
README.md
241
README.md
@@ -1,10 +1,14 @@
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/icons/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
# SPlayer
|
||||
|
||||
> A simple music player
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||

|
||||
|
||||
## 说明
|
||||
|
||||
@@ -14,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`,其他平台可自行解决兼容性后进行构建
|
||||
- 仅对移动端做了基础适配,**不保证功能全部可用**
|
||||
|
||||
@@ -35,10 +39,14 @@
|
||||
- ✨ 支持扫码登录
|
||||
- 📱 支持手机号登录
|
||||
- 📅 自动进行每日签到及云贝签到
|
||||
- 🎨 封面主题色自适应
|
||||
- 💻 支持桌面歌词
|
||||
- 💻 支持切换为本地播放器,此模式将不会连接网络
|
||||
- 🎨 封面主题色自适应,支持全站着色
|
||||
- 🌚 Light / Dark / Auto 模式自动切换
|
||||
- 📁 本地歌曲管理及分类(建议先使用 [音乐标签](https://www.cnblogs.com/vinlxc/p/11347744.html) 进行匹配后再使用)
|
||||
- 📁 简易的本地音乐标签编辑及封面修改
|
||||
- 🎵 **支持播放部分无版权歌曲(可能会与原曲不匹配,客户端独占功能)**
|
||||
- ⬇️ 下载歌曲(最高支持 Hi-Res)
|
||||
- ⬇️ 下载歌曲( 最高支持 Hi-Res,需具有相应会员账号 )
|
||||
- ➕ 新建歌单及歌单编辑
|
||||
- ❤️ 收藏 / 取消收藏歌单或歌手
|
||||
- 🎶 每日推荐歌曲
|
||||
@@ -53,12 +61,11 @@
|
||||
- 🎶 音乐频谱显示
|
||||
- ⏭️ 音乐渐入渐出
|
||||
- 🔄 支持 PWA
|
||||
- 💬 支持评论区及评论点赞
|
||||
- 🌓 明暗模式自动 / 手动切换
|
||||
- 💬 支持评论区
|
||||
- 📱 移动端基础适配
|
||||
- ~~🌐 `i18n` 支持~~
|
||||
|
||||
## 🖼️ Screenshots
|
||||
## 🖼️ screenshots
|
||||
|
||||
> 开发中,仅供参考
|
||||
|
||||
@@ -116,6 +123,10 @@
|
||||
|
||||
[Dev Workflow](https://github.com/imsyy/SPlayer/actions/workflows/build.yml)
|
||||
|
||||
## Snap Store
|
||||
|
||||
[](https://snapcraft.io/splayer)
|
||||
|
||||
## ⚙️ Docker 部署
|
||||
|
||||
> 安装及配置 `Docker` 将不在此处说明,请自行解决
|
||||
@@ -129,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
|
||||
```
|
||||
@@ -143,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 部署
|
||||
|
||||
@@ -155,15 +166,15 @@ 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`
|
||||
|
||||

|
||||

|
||||
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
@@ -172,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
|
||||
```
|
||||
|
||||
@@ -192,9 +201,9 @@ docker run -d --name SPlayer -p 7899:7899 imsyy/splayer:latest
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或者
|
||||
# 或
|
||||
yarn build
|
||||
# 或者
|
||||
# 或
|
||||
npm build
|
||||
```
|
||||
|
||||
@@ -227,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)
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
@@ -250,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
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
|
||||
}
|
||||
}
|
||||
353
auto-imports.d.ts
vendored
353
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 computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
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 markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
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 onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
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 unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useDialog: typeof import('naive-ui')['useDialog']
|
||||
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
|
||||
const useMessage: typeof import('naive-ui')['useMessage']
|
||||
const useNotification: typeof import('naive-ui')['useNotification']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
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')
|
||||
}
|
||||
|
||||
108
components.d.ts
vendored
108
components.d.ts
vendored
@@ -1,72 +1,104 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// biome-ignore lint: disable
|
||||
// oxlint-disable
|
||||
// ------
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
|
||||
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']
|
||||
NDynamicTags: typeof import('naive-ui')['NDynamicTags']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NFloatButton: typeof import('naive-ui')['NFloatButton']
|
||||
NFloatButtonGroup: typeof import('naive-ui')['NFloatButtonGroup']
|
||||
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']
|
||||
NH2: typeof import('naive-ui')['NH2']
|
||||
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']
|
||||
@@ -86,27 +118,39 @@ declare module 'vue' {
|
||||
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
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
29
docker-entrypoint.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# start unblock service in the background
|
||||
npx unblockneteasemusic -p 80:443 -s -f ${NETEASE_SERVER_IP:-220.197.30.65} -o ${UNBLOCK_SOURCES:-kugou bodian pyncmd} 2>&1 &
|
||||
|
||||
# point the neteasemusic address to the unblock service
|
||||
if ! grep -q "music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
if ! grep -q "interface3.music.163.com.163jiasu.com" /etc/hosts; then
|
||||
echo "127.0.0.1 interface3.music.163.com.163jiasu.com" >> /etc/hosts
|
||||
fi
|
||||
|
||||
# start the nginx daemon
|
||||
nginx
|
||||
|
||||
# start the main process
|
||||
exec "$@"
|
||||
173
electron-builder.config.ts
Normal file
173
electron-builder.config.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
import type { Configuration } from "electron-builder";
|
||||
|
||||
const config: Configuration = {
|
||||
// 应用程序的唯一标识符
|
||||
appId: "com.imsyy.splayer",
|
||||
// 应用程序的产品名称
|
||||
productName: "SPlayer",
|
||||
copyright: "Copyright © imsyy 2023",
|
||||
// 构建资源所在的目录
|
||||
directories: {
|
||||
buildResources: "build",
|
||||
},
|
||||
// 包含在最终应用程序构建中的文件列表
|
||||
// 使用通配符 ! 表示排除不需要的文件
|
||||
files: [
|
||||
"public/**",
|
||||
"out/**",
|
||||
"!**/.vscode/*",
|
||||
"!src/*",
|
||||
"!electron.vite.config.{js,ts,mjs,cjs}",
|
||||
"!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}",
|
||||
"!{.env,.env.*,.npmrc,pnpm-lock.yaml}",
|
||||
],
|
||||
// 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack: ["public/**"],
|
||||
win: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/logo.ico",
|
||||
// Windows 平台全局文件名模板
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}",
|
||||
// 是否对可执行文件进行签名和编辑
|
||||
// signAndEditExecutable: false,
|
||||
// 构建类型(架构由命令行参数 --x64 或 --arm64 指定)
|
||||
target: [
|
||||
// 安装版
|
||||
{
|
||||
target: "nsis",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// 打包版
|
||||
{
|
||||
target: "portable",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
// NSIS 安装器配置
|
||||
nsis: {
|
||||
// 是否一键式安装
|
||||
oneClick: false,
|
||||
// 安装程序的生成名称
|
||||
artifactName: "${productName}-${version}-${arch}-setup.${ext}",
|
||||
// 创建的桌面快捷方式名称
|
||||
shortcutName: "${productName}",
|
||||
// 卸载时显示的名称
|
||||
uninstallDisplayName: "${productName}",
|
||||
// 创建桌面图标
|
||||
createDesktopShortcut: "always",
|
||||
// 是否允许 UAC 提升权限
|
||||
allowElevation: true,
|
||||
// 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true,
|
||||
// 安装包图标
|
||||
installerIcon: "public/icons/favicon.ico",
|
||||
// 卸载命令图标
|
||||
uninstallerIcon: "public/icons/favicon.ico",
|
||||
},
|
||||
// Portable 便携版配置
|
||||
portable: {
|
||||
// 便携版文件名
|
||||
artifactName: "${productName}-${version}-${arch}-portable.${ext}",
|
||||
},
|
||||
// macOS 平台配置
|
||||
mac: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/favicon-512x512.png",
|
||||
// 权限继承的文件路径
|
||||
entitlementsInherit: "build/entitlements.mac.plist",
|
||||
// macOS 平台全局文件名模板
|
||||
artifactName: "${productName}-${version}-${arch}.${ext}",
|
||||
// 扩展信息,如权限描述
|
||||
extendInfo: {
|
||||
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
||||
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
||||
NSDocumentsFolderUsageDescription:
|
||||
"Application requests access to the user's Documents folder.",
|
||||
NSDownloadsFolderUsageDescription:
|
||||
"Application requests access to the user's Downloads folder.",
|
||||
},
|
||||
// 是否启用应用程序的 Notarization(苹果的安全审核)
|
||||
notarize: false,
|
||||
darkModeSupport: true,
|
||||
category: "public.app-category.music",
|
||||
target: [
|
||||
// DMG 安装版
|
||||
{
|
||||
target: "dmg",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// 压缩包安装版
|
||||
{
|
||||
target: "zip",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
},
|
||||
// Linux 平台配置
|
||||
linux: {
|
||||
// 可执行文件名
|
||||
executableName: "SPlayer",
|
||||
// 应用程序的图标文件路径
|
||||
icon: "public/icons/favicon-512x512.png",
|
||||
// Linux 所有格式的统一文件名模板
|
||||
artifactName: "${name}-${version}-${arch}.${ext}",
|
||||
// 构建类型 - 支持 x64 和 ARM64 架构
|
||||
target: [
|
||||
// Pacman 包管理器
|
||||
{
|
||||
target: "pacman",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// AppImage 格式
|
||||
{
|
||||
target: "AppImage",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// Debian 包管理器
|
||||
{
|
||||
target: "deb",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// RPM 包管理器
|
||||
{
|
||||
target: "rpm",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
// Snap 包管理器(仅支持 x64 架构)
|
||||
{
|
||||
target: "snap",
|
||||
arch: ["x64"],
|
||||
},
|
||||
// 压缩包格式
|
||||
{
|
||||
target: "tar.gz",
|
||||
arch: ["x64", "arm64"],
|
||||
},
|
||||
],
|
||||
// 维护者信息
|
||||
maintainer: "imsyy.top",
|
||||
// 应用程序类别
|
||||
category: "Audio;Music",
|
||||
},
|
||||
// AppImage 特定配置
|
||||
appImage: {
|
||||
// AppImage 文件的生成名称
|
||||
artifactName: "${name}-${version}-${arch}.${ext}",
|
||||
},
|
||||
// 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false,
|
||||
// Electron 下载镜像配置
|
||||
electronDownload: {
|
||||
mirror: "https://npmmirror.com/mirrors/electron/",
|
||||
},
|
||||
// 发布配置
|
||||
// 先留空,不自动上传
|
||||
publish: [],
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -1,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/images/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/images/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/images/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/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/images/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: "/images/icons/favicon-32x32.png",
|
||||
sizes: "32x32",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-96x96.png",
|
||||
sizes: "96x96",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/icons/favicon-256x256.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
{
|
||||
src: "/images/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
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
1
electron/main/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="electron-vite/node" />
|
||||
@@ -1,252 +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 { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
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, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: nativeImage.createFromPath(join(__dirname, "../../public/images/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}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 主应用程序事件
|
||||
mainAppEvents() {
|
||||
app.whenReady().then(async () => {
|
||||
// 创建主窗口
|
||||
this.createWindow();
|
||||
// 检测更新
|
||||
configureAutoUpdater();
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow);
|
||||
// 系统托盘
|
||||
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
86
electron/main/index.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { electronApp } from "@electron-toolkit/utils";
|
||||
import { release, type } from "os";
|
||||
import { isMac } from "./utils/config";
|
||||
import { unregisterShortcuts } from "./shortcut";
|
||||
import { initTray, MainTray } from "./tray";
|
||||
import { processLog } from "./logger";
|
||||
import initAppServer from "../server";
|
||||
import { initSingleLock } from "./utils/single-lock";
|
||||
import loadWindow from "./windows/load-window";
|
||||
import mainWindow from "./windows/main-window";
|
||||
import initIpc from "./ipc";
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
// 窗口
|
||||
mainWindow: BrowserWindow | null = null;
|
||||
loadWindow: BrowserWindow | null = null;
|
||||
// 托盘
|
||||
mainTray: MainTray | null = null;
|
||||
// 是否退出
|
||||
isQuit: boolean = false;
|
||||
constructor() {
|
||||
processLog.info("🚀 Main process startup");
|
||||
// 程序单例锁
|
||||
initSingleLock();
|
||||
// 禁用 Windows 7 的 GPU 加速功能
|
||||
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
|
||||
// 监听应用事件
|
||||
this.handleAppEvents();
|
||||
// Electron 初始化完成后
|
||||
// 某些API只有在此事件发生后才能使用
|
||||
app.whenReady().then(async () => {
|
||||
processLog.info("🚀 Application Process Startup");
|
||||
// 设置应用程序名称
|
||||
electronApp.setAppUserModelId("com.imsyy.splayer");
|
||||
// 启动主服务进程
|
||||
await initAppServer();
|
||||
// 启动窗口
|
||||
this.loadWindow = loadWindow.create();
|
||||
this.mainWindow = mainWindow.create();
|
||||
// 注册其他服务
|
||||
this.mainTray = initTray(this.mainWindow!);
|
||||
// 注册 IPC 通信
|
||||
initIpc();
|
||||
});
|
||||
}
|
||||
// 应用程序事件
|
||||
handleAppEvents() {
|
||||
// 窗口被关闭时
|
||||
app.on("window-all-closed", () => {
|
||||
if (!isMac) app.quit();
|
||||
this.mainWindow = null;
|
||||
this.loadWindow = null;
|
||||
});
|
||||
|
||||
// 应用被激活
|
||||
app.on("activate", () => {
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
if (allWindows.length) {
|
||||
allWindows[0].focus();
|
||||
}
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
processLog.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
unregisterShortcuts();
|
||||
});
|
||||
|
||||
// 退出前
|
||||
app.on("before-quit", () => {
|
||||
this.isQuit = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new MainProcess();
|
||||
27
electron/main/ipc/index.ts
Normal file
27
electron/main/ipc/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import initFileIpc from "./ipc-file";
|
||||
import initLyricIpc from "./ipc-lyric";
|
||||
import initShortcutIpc from "./ipc-shortcut";
|
||||
import initStoreIpc from "./ipc-store";
|
||||
import initSystemIpc from "./ipc-system";
|
||||
import initThumbarIpc from "./ipc-thumbar";
|
||||
import initTrayIpc from "./ipc-tray";
|
||||
import initUpdateIpc from "./ipc-update";
|
||||
import initWindowsIpc from "./ipc-window";
|
||||
|
||||
/**
|
||||
* 初始化全部 IPC 通信
|
||||
* @returns void
|
||||
*/
|
||||
const initIpc = (): void => {
|
||||
initSystemIpc();
|
||||
initWindowsIpc();
|
||||
initUpdateIpc();
|
||||
initFileIpc();
|
||||
initTrayIpc();
|
||||
initLyricIpc();
|
||||
initStoreIpc();
|
||||
initThumbarIpc();
|
||||
initShortcutIpc();
|
||||
};
|
||||
|
||||
export default initIpc;
|
||||
420
electron/main/ipc/ipc-file.ts
Normal file
420
electron/main/ipc/ipc-file.ts
Normal file
@@ -0,0 +1,420 @@
|
||||
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 (_, lyricDirs: string[], id: number): Promise<{ lrc: string; ttml: string }> => {
|
||||
const result = { lrc: "", ttml: "" };
|
||||
|
||||
try {
|
||||
// 定义需要查找的模式
|
||||
const patterns = {
|
||||
ttml: `**/${id}.ttml`,
|
||||
lrc: `**/${id}.lrc`,
|
||||
};
|
||||
|
||||
// 遍历每一个目录
|
||||
for (const dir of lyricDirs) {
|
||||
try {
|
||||
// 查找 ttml
|
||||
if (!result.ttml) {
|
||||
const ttmlFiles = await FastGlob(patterns.ttml, { cwd: dir });
|
||||
if (ttmlFiles.length > 0) {
|
||||
const filePath = join(dir, ttmlFiles[0]);
|
||||
await access(filePath);
|
||||
result.ttml = await readFile(filePath, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
// 查找 lrc
|
||||
if (!result.lrc) {
|
||||
const lrcFiles = await FastGlob(patterns.lrc, { cwd: dir });
|
||||
if (lrcFiles.length > 0) {
|
||||
const filePath = join(dir, lrcFiles[0]);
|
||||
await access(filePath);
|
||||
result.lrc = await readFile(filePath, "utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果两种文件都找到了就提前结束搜索
|
||||
if (result.ttml && result.lrc) break;
|
||||
} catch {
|
||||
// 某个路径异常,跳过
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* 忽略错误 */
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
);
|
||||
|
||||
// 删除文件
|
||||
ipcMain.handle("delete-file", async (_, path: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const resolvedPath = resolve(path);
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await access(resolvedPath);
|
||||
} catch {
|
||||
throw new Error("❌ File not found");
|
||||
}
|
||||
// 删除文件
|
||||
await unlink(resolvedPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ File delete error", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 打开文件夹
|
||||
ipcMain.on("open-folder", async (_, path: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const resolvedPath = resolve(path);
|
||||
// 检查文件夹是否存在
|
||||
try {
|
||||
await access(resolvedPath);
|
||||
} catch {
|
||||
throw new Error("❌ Folder not found");
|
||||
}
|
||||
// 打开文件夹
|
||||
shell.showItemInFolder(resolvedPath);
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Folder open error", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 图片选择窗口
|
||||
ipcMain.handle("choose-image", async () => {
|
||||
try {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
properties: ["openFile"],
|
||||
filters: [{ name: "Images", extensions: ["jpg", "jpeg", "png"] }],
|
||||
});
|
||||
if (!filePaths || filePaths.length === 0) return null;
|
||||
return filePaths[0];
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Image choose error", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 路径选择窗口
|
||||
ipcMain.handle("choose-path", async () => {
|
||||
try {
|
||||
const { filePaths } = await dialog.showOpenDialog({
|
||||
title: "选择文件夹",
|
||||
defaultPath: app.getPath("downloads"),
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
buttonLabel: "选择文件夹",
|
||||
});
|
||||
if (!filePaths || filePaths.length === 0) return null;
|
||||
return filePaths[0];
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Path choose error", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
ipcMain.handle(
|
||||
"download-file",
|
||||
async (
|
||||
event,
|
||||
url: string,
|
||||
options: {
|
||||
fileName: string;
|
||||
fileType: string;
|
||||
path: string;
|
||||
downloadMeta?: boolean;
|
||||
downloadCover?: boolean;
|
||||
downloadLyric?: boolean;
|
||||
saveMetaFile?: boolean;
|
||||
lyric?: string;
|
||||
songData?: any;
|
||||
} = {
|
||||
fileName: "未知文件名",
|
||||
fileType: "mp3",
|
||||
path: app.getPath("downloads"),
|
||||
},
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
// 获取窗口
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
if (!win) return false;
|
||||
// 获取配置
|
||||
const {
|
||||
fileName,
|
||||
fileType,
|
||||
path,
|
||||
lyric,
|
||||
downloadMeta,
|
||||
downloadCover,
|
||||
downloadLyric,
|
||||
saveMetaFile,
|
||||
songData,
|
||||
} = options;
|
||||
// 规范化路径
|
||||
const downloadPath = resolve(path);
|
||||
// 检查文件夹是否存在
|
||||
try {
|
||||
await access(downloadPath);
|
||||
} catch {
|
||||
throw new Error("❌ Folder not found");
|
||||
}
|
||||
// 下载文件
|
||||
const songDownload = await download(win, url, {
|
||||
directory: downloadPath,
|
||||
filename: `${fileName}.${fileType}`,
|
||||
});
|
||||
if (!downloadMeta || !songData?.cover) return true;
|
||||
// 下载封面
|
||||
const coverUrl = songData?.coverSize?.l || songData.cover;
|
||||
const coverDownload = await download(win, coverUrl, {
|
||||
directory: downloadPath,
|
||||
filename: `${fileName}.jpg`,
|
||||
});
|
||||
// 读取歌曲文件
|
||||
const songFile = File.createFromPath(songDownload.getSavePath());
|
||||
// 生成图片信息
|
||||
const songCover = Picture.fromPath(coverDownload.getSavePath());
|
||||
// 保存修改后的元数据
|
||||
Id3v2Settings.forceDefaultVersion = true;
|
||||
Id3v2Settings.defaultVersion = 3;
|
||||
songFile.tag.title = songData?.name || "未知曲目";
|
||||
songFile.tag.album = songData?.album?.name || "未知专辑";
|
||||
songFile.tag.performers = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
||||
songFile.tag.albumArtists = songData?.artists?.map((ar: any) => ar.name) || ["未知艺术家"];
|
||||
if (lyric && downloadLyric) songFile.tag.lyrics = lyric;
|
||||
if (songCover && downloadCover) songFile.tag.pictures = [songCover];
|
||||
// 保存元信息
|
||||
songFile.save();
|
||||
songFile.dispose();
|
||||
// 创建同名歌词文件
|
||||
if (lyric && saveMetaFile && downloadLyric) {
|
||||
const lrcPath = join(downloadPath, `${fileName}.lrc`);
|
||||
await writeFile(lrcPath, lyric, "utf-8");
|
||||
}
|
||||
// 是否删除封面
|
||||
if (!saveMetaFile || !downloadCover) await unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error downloading file:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 检查是否是子文件夹
|
||||
ipcMain.handle("check-if-subfolder", (_, localFilesPath: string[], selectedDir: string) => {
|
||||
const resolvedSelectedDir = resolve(selectedDir);
|
||||
const allPaths = localFilesPath.map((p) => resolve(p));
|
||||
return allPaths.some((existingPath) => {
|
||||
const relativePath = relative(existingPath, resolvedSelectedDir);
|
||||
return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default initFileIpc;
|
||||
201
electron/main/ipc/ipc-lyric.ts
Normal file
201
electron/main/ipc/ipc-lyric.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { BrowserWindow, ipcMain, screen } from "electron";
|
||||
import { useStore } from "../store";
|
||||
import lyricWindow from "../windows/lyric-window";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 歌词相关 IPC
|
||||
*/
|
||||
const initLyricIpc = (): void => {
|
||||
const store = useStore();
|
||||
|
||||
// 歌词窗口
|
||||
let lyricWin: BrowserWindow | null = null;
|
||||
|
||||
/**
|
||||
* 窗口是否存活
|
||||
* @param win 窗口实例
|
||||
* @returns 是否存活
|
||||
*/
|
||||
const isWinAlive = (win: BrowserWindow | null): win is BrowserWindow =>
|
||||
!!win && !win.isDestroyed();
|
||||
|
||||
// 切换桌面歌词
|
||||
ipcMain.on("toggle-desktop-lyric", (_event, val: boolean) => {
|
||||
if (val) {
|
||||
if (!isWinAlive(lyricWin)) {
|
||||
lyricWin = lyricWindow.create();
|
||||
// 监听关闭,置空引用,防止后续调用报错
|
||||
lyricWin?.on("closed", () => {
|
||||
lyricWin = null;
|
||||
});
|
||||
// 设置位置
|
||||
const { x, y } = store.get("lyric");
|
||||
const xPos = Number(x);
|
||||
const yPos = Number(y);
|
||||
if (Number.isFinite(xPos) && Number.isFinite(yPos)) {
|
||||
lyricWin?.setPosition(Math.round(xPos), Math.round(yPos));
|
||||
}
|
||||
} else {
|
||||
lyricWin.show();
|
||||
}
|
||||
if (isWinAlive(lyricWin)) {
|
||||
lyricWin.setAlwaysOnTop(true, "screen-saver");
|
||||
}
|
||||
} else {
|
||||
// 关闭:不销毁窗口,直接隐藏,保留位置与状态
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
lyricWin.hide();
|
||||
}
|
||||
});
|
||||
|
||||
// 更新歌词窗口数据
|
||||
ipcMain.on("update-desktop-lyric-data", (_, lyricData) => {
|
||||
if (!lyricData || !isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
|
||||
});
|
||||
|
||||
// 更新歌词窗口配置
|
||||
ipcMain.on("update-desktop-lyric-option", (_, option, callback: boolean = false) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!option || !isWinAlive(lyricWin)) return;
|
||||
// 增量更新
|
||||
const prevOption = store.get("lyric.config");
|
||||
if (prevOption) {
|
||||
option = { ...prevOption, ...option };
|
||||
}
|
||||
store.set("lyric.config", option);
|
||||
// 触发窗口更新
|
||||
if (callback && isWinAlive(lyricWin)) {
|
||||
lyricWin.webContents.send("update-desktop-lyric-option", option);
|
||||
}
|
||||
if (isWinAlive(mainWin)) {
|
||||
mainWin?.webContents.send("update-desktop-lyric-option", option);
|
||||
}
|
||||
});
|
||||
|
||||
// 播放状态更改
|
||||
ipcMain.on("play-status-change", (_, status) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", { playStatus: status });
|
||||
});
|
||||
|
||||
// 音乐名称更改
|
||||
ipcMain.on("play-song-change", (_, title) => {
|
||||
if (!title || !isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", { playName: title });
|
||||
});
|
||||
|
||||
// 音乐歌词更改
|
||||
ipcMain.on("play-lyric-change", (_, lyricData) => {
|
||||
if (!lyricData || !isWinAlive(lyricWin)) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-data", lyricData);
|
||||
});
|
||||
|
||||
// 获取窗口位置
|
||||
ipcMain.handle("get-window-bounds", () => {
|
||||
if (!isWinAlive(lyricWin)) return {};
|
||||
return lyricWin.getBounds();
|
||||
});
|
||||
|
||||
// 获取屏幕尺寸
|
||||
ipcMain.handle("get-screen-size", () => {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||
return { width, height };
|
||||
});
|
||||
|
||||
// 获取多屏虚拟边界(支持负坐标)
|
||||
ipcMain.handle("get-virtual-screen-bounds", () => {
|
||||
const displays = screen.getAllDisplays();
|
||||
const bounds = displays.map((d) => d.workArea);
|
||||
const minX = Math.min(...bounds.map((b) => b.x));
|
||||
const minY = Math.min(...bounds.map((b) => b.y));
|
||||
const maxX = Math.max(...bounds.map((b) => b.x + b.width));
|
||||
const maxY = Math.max(...bounds.map((b) => b.y + b.height));
|
||||
return { minX, minY, maxX, maxY };
|
||||
});
|
||||
|
||||
// 移动窗口
|
||||
ipcMain.on("move-window", (_, x, y, width, height) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
lyricWin.setBounds({ x, y, width, height });
|
||||
// 保存配置
|
||||
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
|
||||
});
|
||||
|
||||
// 更新歌词窗口宽高
|
||||
ipcMain.on("update-lyric-size", (_, width, height) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
// 更新窗口宽度
|
||||
lyricWin.setBounds({ width, height });
|
||||
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||
});
|
||||
|
||||
// 更新高度
|
||||
ipcMain.on("update-window-height", (_, height) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
const store = useStore();
|
||||
const { width } = lyricWin.getBounds();
|
||||
// 更新窗口高度
|
||||
lyricWin.setBounds({ width, height });
|
||||
store.set("lyric", { ...store.get("lyric"), height });
|
||||
});
|
||||
|
||||
// 是否固定当前最大宽高
|
||||
ipcMain.on(
|
||||
"toggle-fixed-max-size",
|
||||
(_, options: { width: number; height: number; fixed: boolean }) => {
|
||||
if (!isWinAlive(lyricWin)) return;
|
||||
const { width, height, fixed } = options;
|
||||
if (fixed) {
|
||||
lyricWin.setMaximumSize(width, height);
|
||||
} else {
|
||||
lyricWin.setMaximumSize(1400, 360);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 请求歌词数据
|
||||
ipcMain.on("request-desktop-lyric-data", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||
// 触发窗口更新
|
||||
mainWin?.webContents.send("request-desktop-lyric-data");
|
||||
});
|
||||
|
||||
// 请求歌词配置
|
||||
ipcMain.handle("request-desktop-lyric-option", () => {
|
||||
const config = store.get("lyric.config");
|
||||
if (isWinAlive(lyricWin)) {
|
||||
lyricWin.webContents.send("update-desktop-lyric-option", config);
|
||||
}
|
||||
return config;
|
||||
});
|
||||
|
||||
// 关闭桌面歌词
|
||||
ipcMain.on("closeDesktopLyric", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||
lyricWin.hide();
|
||||
mainWin?.webContents.send("closeDesktopLyric");
|
||||
});
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean, isTemp: boolean = false) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!isWinAlive(lyricWin) || !isWinAlive(mainWin)) return;
|
||||
// 是否穿透
|
||||
if (isLock) {
|
||||
lyricWin.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
lyricWin.setIgnoreMouseEvents(false);
|
||||
}
|
||||
if (isTemp) return;
|
||||
store.set("lyric.config", { ...store.get("lyric.config"), isLock });
|
||||
// 触发窗口更新
|
||||
const config = store.get("lyric.config");
|
||||
mainWin?.webContents.send("update-desktop-lyric-option", config);
|
||||
});
|
||||
};
|
||||
|
||||
export default initLyricIpc;
|
||||
36
electron/main/ipc/ipc-shortcut.ts
Normal file
36
electron/main/ipc/ipc-shortcut.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "../shortcut";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 初始化快捷键 IPC 主进程
|
||||
* @returns void
|
||||
*/
|
||||
const initShortcutIpc = (): void => {
|
||||
// 快捷键是否被注册
|
||||
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
|
||||
|
||||
// 注册快捷键
|
||||
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin || !allShortcuts) return false;
|
||||
// 卸载所有快捷键
|
||||
unregisterShortcuts();
|
||||
// 注册快捷键
|
||||
const failedShortcuts: string[] = [];
|
||||
for (const key in allShortcuts) {
|
||||
const shortcut = allShortcuts[key].globalShortcut;
|
||||
if (!shortcut) continue;
|
||||
// 快捷键回调
|
||||
const callback = () => mainWin.webContents.send(key);
|
||||
const isSuccess = registerShortcut(shortcut, callback);
|
||||
if (!isSuccess) failedShortcuts.push(shortcut);
|
||||
}
|
||||
return failedShortcuts;
|
||||
});
|
||||
|
||||
// 卸载所有快捷键
|
||||
ipcMain.on("unregister-all-shortcut", () => unregisterShortcuts());
|
||||
};
|
||||
|
||||
export default initShortcutIpc;
|
||||
45
electron/main/ipc/ipc-store.ts
Normal file
45
electron/main/ipc/ipc-store.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { useStore } from "../store";
|
||||
import type { StoreType } from "../store";
|
||||
|
||||
/**
|
||||
* 初始化 store IPC 主进程
|
||||
*/
|
||||
const initStoreIpc = (): void => {
|
||||
const store = useStore();
|
||||
if (!store) return;
|
||||
|
||||
// 获取配置项
|
||||
ipcMain.handle("store-get", (_event, key: keyof StoreType) => {
|
||||
return store.get(key as any);
|
||||
});
|
||||
|
||||
// 设置配置项
|
||||
ipcMain.handle("store-set", (_event, key: keyof StoreType, value: unknown) => {
|
||||
store.set(key as any, value as any);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 判断配置项是否存在
|
||||
ipcMain.handle("store-has", (_event, key: keyof StoreType) => {
|
||||
return store.has(key as any);
|
||||
});
|
||||
|
||||
// 删除配置项
|
||||
ipcMain.handle("store-delete", (_event, key: keyof StoreType) => {
|
||||
store.delete(key as any);
|
||||
return true;
|
||||
});
|
||||
|
||||
// 重置配置(支持指定 keys 或全部重置)
|
||||
ipcMain.handle("store-reset", (_event, keys?: (keyof StoreType)[]) => {
|
||||
if (keys && keys.length > 0) {
|
||||
store.reset(...(keys as any));
|
||||
} else {
|
||||
store.reset();
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
export default initStoreIpc;
|
||||
105
electron/main/ipc/ipc-system.ts
Normal file
105
electron/main/ipc/ipc-system.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { app, ipcMain, net, powerSaveBlocker, session } from "electron";
|
||||
import { ipcLog } from "../logger";
|
||||
import { getFonts } from "font-list";
|
||||
import { useStore } from "../store";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 初始化系统 IPC 通信
|
||||
* @returns void
|
||||
*/
|
||||
const initSystemIpc = (): void => {
|
||||
const store = useStore();
|
||||
|
||||
/** 阻止系统息屏 ID */
|
||||
let preventId: number | null = null;
|
||||
|
||||
// 是否阻止系统息屏
|
||||
ipcMain.on("prevent-sleep", (_event, val: boolean) => {
|
||||
if (val) {
|
||||
preventId = powerSaveBlocker.start("prevent-display-sleep");
|
||||
ipcLog.info("⏾ System sleep prevention started");
|
||||
} else {
|
||||
if (preventId !== null) {
|
||||
powerSaveBlocker.stop(preventId);
|
||||
ipcLog.info("✅ System sleep prevention stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 退出应用
|
||||
ipcMain.on("quit-app", () => {
|
||||
app.exit(0);
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 获取系统全部字体
|
||||
ipcMain.handle("get-all-fonts", async () => {
|
||||
try {
|
||||
const fonts = await getFonts();
|
||||
return fonts;
|
||||
} catch (error) {
|
||||
ipcLog.error(`❌ Failed to get all system fonts: ${error}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 取消代理
|
||||
ipcMain.on("remove-proxy", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
store.set("proxy", "");
|
||||
if (mainWin) {
|
||||
mainWin?.webContents.session.setProxy({ proxyRules: "" });
|
||||
}
|
||||
ipcLog.info("✅ Remove proxy successfully");
|
||||
});
|
||||
|
||||
// 配置网络代理
|
||||
ipcMain.on("set-proxy", (_, config) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
store.set("proxy", proxyRules);
|
||||
mainWin?.webContents.session.setProxy({ proxyRules });
|
||||
ipcLog.info("✅ Set proxy successfully:", proxyRules);
|
||||
});
|
||||
|
||||
// 代理测试
|
||||
ipcMain.handle("test-proxy", async (_, config) => {
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
try {
|
||||
// 设置代理
|
||||
const ses = session.defaultSession;
|
||||
await ses.setProxy({ proxyRules });
|
||||
// 测试请求
|
||||
const request = net.request({ url: "https://www.baidu.com" });
|
||||
return new Promise((resolve) => {
|
||||
request.on("response", (response) => {
|
||||
if (response.statusCode === 200) {
|
||||
ipcLog.info("✅ Proxy test successful");
|
||||
resolve(true);
|
||||
} else {
|
||||
ipcLog.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
request.on("error", (error) => {
|
||||
ipcLog.error("❌ Error testing proxy:", error);
|
||||
resolve(false);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
} catch (error) {
|
||||
ipcLog.error("❌ Error testing proxy:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 重置全部设置
|
||||
ipcMain.on("reset-setting", () => {
|
||||
store.reset();
|
||||
ipcLog.info("✅ Reset setting successfully");
|
||||
});
|
||||
};
|
||||
|
||||
export default initSystemIpc;
|
||||
15
electron/main/ipc/ipc-thumbar.ts
Normal file
15
electron/main/ipc/ipc-thumbar.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { getThumbar } from "../thumbar";
|
||||
|
||||
const initThumbarIpc = (): void => {
|
||||
// 更新工具栏
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
const thumbar = getThumbar();
|
||||
if (!thumbar) {
|
||||
return;
|
||||
}
|
||||
thumbar.updateThumbar(playStatus);
|
||||
});
|
||||
};
|
||||
|
||||
export default initThumbarIpc;
|
||||
48
electron/main/ipc/ipc-tray.ts
Normal file
48
electron/main/ipc/ipc-tray.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { getMainTray } from "../tray";
|
||||
import lyricWindow from "../windows/lyric-window";
|
||||
|
||||
/**
|
||||
* 托盘 IPC
|
||||
*/
|
||||
const initTrayIpc = (): void => {
|
||||
const tray = getMainTray();
|
||||
|
||||
// 音乐播放状态更改
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
const lyricWin = lyricWindow.getWin();
|
||||
tray?.setPlayState(playStatus ? "play" : "pause");
|
||||
if (!lyricWin) return;
|
||||
lyricWin.webContents.send("play-status-change", playStatus);
|
||||
});
|
||||
|
||||
// 音乐名称更改
|
||||
ipcMain.on("play-song-change", (_, title) => {
|
||||
if (!title) return;
|
||||
// 更改标题
|
||||
tray?.setTitle(title);
|
||||
tray?.setPlayName(title);
|
||||
});
|
||||
|
||||
// 播放模式切换
|
||||
ipcMain.on("play-mode-change", (_, mode) => {
|
||||
tray?.setPlayMode(mode);
|
||||
});
|
||||
|
||||
// 喜欢状态切换
|
||||
ipcMain.on("like-status-change", (_, likeStatus: boolean) => {
|
||||
tray?.setLikeState(likeStatus);
|
||||
});
|
||||
|
||||
// 桌面歌词开关
|
||||
ipcMain.on("toggle-desktop-lyric", (_, val: boolean) => {
|
||||
tray?.setDesktopLyricShow(val);
|
||||
});
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
||||
tray?.setDesktopLyricLock(isLock);
|
||||
});
|
||||
};
|
||||
|
||||
export default initTrayIpc;
|
||||
17
electron/main/ipc/ipc-update.ts
Normal file
17
electron/main/ipc/ipc-update.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { checkUpdate, startDownloadUpdate } from "../update";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
const initUpdateIpc = () => {
|
||||
// 检查更新
|
||||
ipcMain.on("check-update", (_event, showTip) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
checkUpdate(mainWin, showTip);
|
||||
});
|
||||
|
||||
// 开始下载更新
|
||||
ipcMain.on("start-download-update", () => startDownloadUpdate());
|
||||
};
|
||||
|
||||
export default initUpdateIpc;
|
||||
152
electron/main/ipc/ipc-window.ts
Normal file
152
electron/main/ipc/ipc-window.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
import { app, ipcMain } from "electron";
|
||||
import { useStore } from "../store";
|
||||
import { isDev } from "../utils/config";
|
||||
import { initThumbar } from "../thumbar";
|
||||
import mainWindow from "../windows/main-window";
|
||||
import loadWindow from "../windows/load-window";
|
||||
import loginWindow from "../windows/login-window";
|
||||
|
||||
/**
|
||||
* 窗口 IPC 通信
|
||||
* @returns void
|
||||
*/
|
||||
const initWindowsIpc = (): void => {
|
||||
// store
|
||||
const store = useStore();
|
||||
|
||||
// 当前窗口状态
|
||||
ipcMain.on("win-state", (event) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
event.returnValue = mainWin?.isMaximized();
|
||||
});
|
||||
|
||||
// 加载完成
|
||||
ipcMain.on("win-loaded", () => {
|
||||
const loadWin = loadWindow.getWin();
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (loadWin && !loadWin.isDestroyed()) loadWin.destroy();
|
||||
const isMaximized = store.get("window")?.maximized;
|
||||
if (isMaximized) mainWin?.maximize();
|
||||
if (!mainWin) return;
|
||||
mainWin?.show();
|
||||
mainWin?.focus();
|
||||
// 解决窗口不立即显示
|
||||
mainWin?.setAlwaysOnTop(true);
|
||||
// 100ms 后取消置顶
|
||||
const timer = setTimeout(() => {
|
||||
if (mainWin && !mainWin.isDestroyed()) {
|
||||
mainWin.setAlwaysOnTop(false);
|
||||
mainWin.focus();
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}, 100);
|
||||
// 初始化缩略图工具栏
|
||||
if (mainWin) initThumbar(mainWin);
|
||||
});
|
||||
|
||||
// 最小化
|
||||
ipcMain.on("win-min", (event) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
event.preventDefault();
|
||||
mainWin?.minimize();
|
||||
});
|
||||
|
||||
// 最大化
|
||||
ipcMain.on("win-max", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.maximize();
|
||||
});
|
||||
|
||||
// 还原
|
||||
ipcMain.on("win-restore", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.restore();
|
||||
});
|
||||
|
||||
// 隐藏
|
||||
ipcMain.on("win-hide", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.hide();
|
||||
});
|
||||
|
||||
// 显示
|
||||
ipcMain.on("win-show", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.show();
|
||||
mainWin?.focus();
|
||||
});
|
||||
|
||||
// 重启
|
||||
ipcMain.on("win-reload", () => {
|
||||
app.quit();
|
||||
app.relaunch();
|
||||
});
|
||||
|
||||
// 向主窗口发送事件
|
||||
ipcMain.on("send-to-mainWin", (_, eventName, ...args) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin || mainWin.isDestroyed() || mainWin.webContents.isDestroyed()) return;
|
||||
mainWin.webContents.send(eventName, ...args);
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
ipcMain.on("set-bar", (_event, val: number | "none" | "indeterminate" | "error" | "paused") => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
switch (val) {
|
||||
case "none":
|
||||
mainWin?.setProgressBar(-1);
|
||||
break;
|
||||
case "indeterminate":
|
||||
mainWin?.setProgressBar(2, { mode: "indeterminate" });
|
||||
break;
|
||||
case "error":
|
||||
mainWin?.setProgressBar(1, { mode: "error" });
|
||||
break;
|
||||
case "paused":
|
||||
mainWin?.setProgressBar(1, { mode: "paused" });
|
||||
break;
|
||||
default:
|
||||
if (typeof val === "number") {
|
||||
mainWin?.setProgressBar(val / 100);
|
||||
} else {
|
||||
mainWin?.setProgressBar(-1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 开启控制台
|
||||
ipcMain.on("open-dev-tools", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.webContents.openDevTools({
|
||||
title: "SPlayer DevTools",
|
||||
mode: isDev ? "right" : "detach",
|
||||
});
|
||||
});
|
||||
|
||||
// 开启登录窗口
|
||||
ipcMain.on("open-login-web", () => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
loginWindow.create(mainWin);
|
||||
});
|
||||
|
||||
// 开启设置
|
||||
ipcMain.on("open-setting", (_, type) => {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) return;
|
||||
mainWin?.show();
|
||||
mainWin?.focus();
|
||||
mainWin?.webContents.send("openSetting", type);
|
||||
});
|
||||
};
|
||||
|
||||
export default initWindowsIpc;
|
||||
47
electron/main/logger/index.ts
Normal file
47
electron/main/logger/index.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
// 日志输出
|
||||
import { existsSync, mkdirSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { app } from "electron";
|
||||
import log from "electron-log";
|
||||
|
||||
// 日志文件路径
|
||||
const logDir = join(app.getPath("logs"));
|
||||
|
||||
// 是否存在日志目录
|
||||
if (!existsSync(logDir)) mkdirSync(logDir);
|
||||
|
||||
// 获取日期 - YYYY-MM-DD
|
||||
const dateString = new Date().toISOString().slice(0, 10);
|
||||
const logFilePath = join(logDir, `${dateString}.log`);
|
||||
|
||||
// 配置日志系统
|
||||
log.transports.console.useStyles = true; // 颜色输出
|
||||
log.transports.file.level = "info"; // 仅记录 info 及以上级别
|
||||
log.transports.file.resolvePathFn = (): string => logFilePath; // 日志文件路径
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024; // 文件最大 2MB
|
||||
|
||||
// 日志格式化
|
||||
// log.transports.file.format = "[{y}-{m}-{d} {h}:{i}:{s}] [{level}] [{scope}] {text}";
|
||||
|
||||
// 绑定默认事件
|
||||
const defaultLog = log.scope("default");
|
||||
console.log = defaultLog.log;
|
||||
console.info = defaultLog.info;
|
||||
console.warn = defaultLog.warn;
|
||||
console.error = defaultLog.error;
|
||||
|
||||
// 分作用域导出
|
||||
export { defaultLog };
|
||||
export const ipcLog = log.scope("ipc");
|
||||
export const trayLog = log.scope("tray");
|
||||
export const thumbarLog = log.scope("thumbar");
|
||||
export const storeLog = log.scope("store");
|
||||
export const updateLog = log.scope("update");
|
||||
export const systemLog = log.scope("system");
|
||||
export const configLog = log.scope("config");
|
||||
export const windowsLog = log.scope("windows");
|
||||
export const processLog = log.scope("process");
|
||||
export const preloadLog = log.scope("preload");
|
||||
export const rendererLog = log.scope("renderer");
|
||||
export const shortcutLog = log.scope("shortcut");
|
||||
export const serverLog = log.scope("server");
|
||||
@@ -1,285 +0,0 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
/**
|
||||
* 监听主进程的 IPC 事件
|
||||
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
|
||||
*/
|
||||
|
||||
const mainIpcMain = (win) => {
|
||||
// 窗口操作部分
|
||||
ipcMain.on("window-min", (ev) => {
|
||||
// 阻止最小化
|
||||
ev.preventDefault();
|
||||
// 最小化
|
||||
win.minimize();
|
||||
});
|
||||
ipcMain.on("window-maxOrRestore", (ev) => {
|
||||
const winSizeState = win.isMaximized();
|
||||
winSizeState ? win.restore() : win.maximize();
|
||||
ev.reply("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("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;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 Bilibili 视频中获取文件的 Base64 数据
|
||||
*
|
||||
* @param {string} url - 要获取的文件的 URL
|
||||
* @returns {Promise<string>} - 文件的 Base64 数据
|
||||
*/
|
||||
const getBiliUrlBase64 = async (url) => {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
Referer: "https://www.bilibili.com/",
|
||||
"User-Agent": "okhttp/3.4.1",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
withCredentials: false,
|
||||
});
|
||||
// 将二进制数据转换为缓冲区
|
||||
const buffer = toBuffer(response.data);
|
||||
// 将缓冲区中的数据转换为 Base64 编码的字符串
|
||||
const encodedData = buffer.toString("base64");
|
||||
// 返回 Base64 编码的文件数据
|
||||
return encodedData;
|
||||
} catch (error) {
|
||||
console.error("获取文件数据时发生错误:" + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数据转换为缓冲区( Buffer )
|
||||
*
|
||||
* @param {ArrayBuffer|Buffer|Uint8Array} data - 要转换的数据
|
||||
* @returns {Buffer} - 转换后的缓冲区
|
||||
*/
|
||||
const toBuffer = (data) => {
|
||||
if (data instanceof Buffer) {
|
||||
return data;
|
||||
} else {
|
||||
return Buffer.from(data);
|
||||
}
|
||||
};
|
||||
|
||||
export default mainIpcMain;
|
||||
30
electron/main/shortcut/index.ts
Normal file
30
electron/main/shortcut/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import { shortcutLog } from "../logger";
|
||||
|
||||
// 注册快捷键并检查
|
||||
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
|
||||
try {
|
||||
const success = globalShortcut.register(shortcut, callback);
|
||||
if (!success) {
|
||||
shortcutLog.error(`❌ Failed to register shortcut: ${shortcut}`);
|
||||
return false;
|
||||
} else {
|
||||
shortcutLog.info(`✅ Shortcut registered: ${shortcut}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
shortcutLog.error(`ℹ️ Error registering shortcut ${shortcut}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查快捷键是否被注册
|
||||
export const isShortcutRegistered = (shortcut: string): boolean => {
|
||||
return globalShortcut.isRegistered(shortcut);
|
||||
};
|
||||
|
||||
// 卸载所有快捷键
|
||||
export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
shortcutLog.info("🚫 All shortcuts unregistered.");
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { join } from "path";
|
||||
import express from "express";
|
||||
import expressProxy from "express-http-proxy";
|
||||
|
||||
/**
|
||||
* 启动主服务器
|
||||
* @returns {import('http').Server} HTTP 服务器实例
|
||||
*/
|
||||
export const startMainServer = async () => {
|
||||
const { MAIN_VITE_MAIN_PORT, MAIN_VITE_SERVER_HOST, MAIN_VITE_SERVER_PORT } = import.meta.env;
|
||||
const port = MAIN_VITE_MAIN_PORT ?? 7899;
|
||||
const apiHost = `http://${MAIN_VITE_SERVER_HOST}:${MAIN_VITE_SERVER_PORT}`;
|
||||
const expressApp = express();
|
||||
// 代理
|
||||
expressApp.use("/", express.static(join(__dirname, "../renderer/")));
|
||||
expressApp.use("/api", expressProxy(apiHost));
|
||||
// 启动 Express 应用服务器,并监听指定端口
|
||||
return expressApp.listen(port, "127.0.0.1");
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
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
52
electron/main/store/index.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { screen } from "electron";
|
||||
import { storeLog } from "../logger";
|
||||
import type { LyricConfig } from "../../../src/types/desktop-lyric";
|
||||
import defaultLyricConfig from "../../../src/assets/data/lyricConfig";
|
||||
import Store from "electron-store";
|
||||
|
||||
storeLog.info("🌱 Store init");
|
||||
|
||||
export interface StoreType {
|
||||
window: {
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
maximized?: boolean;
|
||||
};
|
||||
lyric: {
|
||||
// 窗口位置
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
// 配置
|
||||
config?: LyricConfig;
|
||||
};
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用 Store
|
||||
* @returns Store<StoreType>
|
||||
*/
|
||||
export const useStore = () => {
|
||||
// 获取主屏幕
|
||||
const screenData = screen.getPrimaryDisplay();
|
||||
return new Store<StoreType>({
|
||||
defaults: {
|
||||
window: {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
},
|
||||
lyric: {
|
||||
x: screenData.workAreaSize.width / 2 - 400,
|
||||
y: screenData.workAreaSize.height - 90,
|
||||
width: 800,
|
||||
height: 136,
|
||||
config: defaultLyricConfig,
|
||||
},
|
||||
proxy: "",
|
||||
},
|
||||
});
|
||||
};
|
||||
141
electron/main/thumbar/index.ts
Normal file
141
electron/main/thumbar/index.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
|
||||
import { join } from "path";
|
||||
import { isWin } from "../utils/config";
|
||||
import { thumbarLog } from "../logger";
|
||||
|
||||
enum ThumbarKeys {
|
||||
Play = "play",
|
||||
Pause = "pause",
|
||||
Prev = "prev",
|
||||
Next = "next",
|
||||
}
|
||||
|
||||
type ThumbarMap = Map<ThumbarKeys, ThumbarButton>;
|
||||
|
||||
export interface Thumbar {
|
||||
clearThumbar(): void;
|
||||
updateThumbar(playing: boolean, clean?: boolean): void;
|
||||
}
|
||||
|
||||
// 缩略图单例
|
||||
let thumbar: Thumbar | null = null;
|
||||
|
||||
// 工具栏图标
|
||||
const thumbarIcon = (filename: string) => {
|
||||
// 是否为暗色
|
||||
const isDark = nativeTheme.shouldUseDarkColors;
|
||||
// 返回图标
|
||||
return nativeImage.createFromPath(
|
||||
join(__dirname, `../../public/icons/thumbar/${filename}-${isDark ? "dark" : "light"}.png`),
|
||||
);
|
||||
};
|
||||
|
||||
// 缩略图工具栏
|
||||
const createThumbarButtons = (win: BrowserWindow): ThumbarMap => {
|
||||
return new Map<ThumbarKeys, ThumbarButton>()
|
||||
.set(ThumbarKeys.Prev, {
|
||||
tooltip: "上一曲",
|
||||
icon: thumbarIcon("prev"),
|
||||
click: () => win.webContents.send("playPrev"),
|
||||
})
|
||||
.set(ThumbarKeys.Next, {
|
||||
tooltip: "下一曲",
|
||||
icon: thumbarIcon("next"),
|
||||
click: () => win.webContents.send("playNext"),
|
||||
})
|
||||
.set(ThumbarKeys.Play, {
|
||||
tooltip: "播放",
|
||||
icon: thumbarIcon("play"),
|
||||
click: () => win.webContents.send("play"),
|
||||
})
|
||||
.set(ThumbarKeys.Pause, {
|
||||
tooltip: "暂停",
|
||||
icon: thumbarIcon("pause"),
|
||||
click: () => win.webContents.send("pause"),
|
||||
});
|
||||
};
|
||||
|
||||
// 创建缩略图工具栏
|
||||
class createThumbar implements Thumbar {
|
||||
// 窗口
|
||||
private _win: BrowserWindow;
|
||||
// 工具栏
|
||||
private _thumbar: ThumbarMap;
|
||||
// 工具栏按钮
|
||||
private _prev: ThumbarButton;
|
||||
private _next: ThumbarButton;
|
||||
private _play: ThumbarButton;
|
||||
private _pause: ThumbarButton;
|
||||
// 当前播放状态
|
||||
private _isPlaying: boolean = false;
|
||||
|
||||
constructor(win: BrowserWindow) {
|
||||
// 初始化数据
|
||||
this._win = win;
|
||||
this._thumbar = createThumbarButtons(win);
|
||||
// 工具栏按钮
|
||||
this._play = this._thumbar.get(ThumbarKeys.Play)!;
|
||||
this._pause = this._thumbar.get(ThumbarKeys.Pause)!;
|
||||
this._prev = this._thumbar.get(ThumbarKeys.Prev)!;
|
||||
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
||||
// 初始化工具栏
|
||||
this.updateThumbar();
|
||||
// 监听主题变化
|
||||
this.initThemeListener();
|
||||
}
|
||||
|
||||
// 初始化主题监听器
|
||||
private initThemeListener() {
|
||||
nativeTheme.on("updated", () => {
|
||||
this.refreshThumbarButtons();
|
||||
});
|
||||
}
|
||||
|
||||
// 刷新工具栏按钮(主题变化时)
|
||||
private refreshThumbarButtons() {
|
||||
// 重新创建按钮
|
||||
this._thumbar = createThumbarButtons(this._win);
|
||||
this._play = this._thumbar.get(ThumbarKeys.Play)!;
|
||||
this._pause = this._thumbar.get(ThumbarKeys.Pause)!;
|
||||
this._prev = this._thumbar.get(ThumbarKeys.Prev)!;
|
||||
this._next = this._thumbar.get(ThumbarKeys.Next)!;
|
||||
// 更新工具栏
|
||||
this.updateThumbar(this._isPlaying);
|
||||
}
|
||||
|
||||
// 更新工具栏
|
||||
updateThumbar(playing: boolean = false, clean: boolean = false) {
|
||||
this._isPlaying = playing;
|
||||
if (clean) return this.clearThumbar();
|
||||
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
|
||||
}
|
||||
|
||||
// 清除工具栏
|
||||
clearThumbar() {
|
||||
this._win.setThumbarButtons([]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化缩略图工具栏
|
||||
* @param win 窗口
|
||||
* @returns 缩略图工具栏
|
||||
*/
|
||||
export const initThumbar = (win: BrowserWindow) => {
|
||||
try {
|
||||
// 若非 Win
|
||||
if (!isWin) return null;
|
||||
thumbarLog.info("🚀 ThumbarButtons Startup");
|
||||
thumbar = new createThumbar(win);
|
||||
return thumbar;
|
||||
} catch (error) {
|
||||
thumbarLog.error("❌ ThumbarButtons Error", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取缩略图工具栏
|
||||
* @returns 缩略图工具栏
|
||||
*/
|
||||
export const getThumbar = () => thumbar;
|
||||
327
electron/main/tray/index.ts
Normal file
327
electron/main/tray/index.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
app,
|
||||
Tray,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
BrowserWindow,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
} from "electron";
|
||||
import { isWin, appName } from "../utils/config";
|
||||
import { join } from "path";
|
||||
import { trayLog } from "../logger";
|
||||
import { useStore } from "../store";
|
||||
import lyricWindow from "../windows/lyric-window";
|
||||
|
||||
// 播放模式
|
||||
type PlayMode = "repeat" | "repeat-once" | "shuffle";
|
||||
type PlayState = "play" | "pause" | "loading";
|
||||
|
||||
// 全局数据
|
||||
let playMode: PlayMode = "repeat";
|
||||
let playState: PlayState = "pause";
|
||||
let playName: string = "未播放歌曲";
|
||||
let likeSong: boolean = false;
|
||||
let desktopLyricShow: boolean = false;
|
||||
let desktopLyricLock: boolean = false;
|
||||
|
||||
export interface MainTray {
|
||||
setTitle(title: string): void;
|
||||
setPlayMode(mode: PlayMode): void;
|
||||
setLikeState(like: boolean): void;
|
||||
setPlayState(state: PlayState): void;
|
||||
setPlayName(name: string): void;
|
||||
setDesktopLyricShow(show: boolean): void;
|
||||
setDesktopLyricLock(lock: boolean): void;
|
||||
destroyTray(): void;
|
||||
}
|
||||
|
||||
// 托盘单例
|
||||
let mainTrayInstance: MainTray | null = null;
|
||||
|
||||
// 托盘图标
|
||||
const trayIcon = (filename: string) => {
|
||||
// const rootPath = isDev
|
||||
// ? join(__dirname, "../../public/icons/tray")
|
||||
// : join(app.getAppPath(), "../../public/icons/tray");
|
||||
// return nativeImage.createFromPath(join(rootPath, filename));
|
||||
return nativeImage.createFromPath(join(__dirname, `../../public/icons/tray/${filename}`));
|
||||
};
|
||||
|
||||
// 托盘菜单
|
||||
const createTrayMenu = (win: BrowserWindow): MenuItemConstructorOptions[] => {
|
||||
// 区分明暗图标
|
||||
const showIcon = (iconName: string) => {
|
||||
const isDark = nativeTheme.shouldUseDarkColors;
|
||||
return trayIcon(`${iconName}${isDark ? "-dark" : "-light"}.png`).resize({
|
||||
width: 16,
|
||||
height: 16,
|
||||
});
|
||||
};
|
||||
// 菜单
|
||||
const menu: MenuItemConstructorOptions[] = [
|
||||
{
|
||||
id: "name",
|
||||
label: playName,
|
||||
icon: showIcon("music"),
|
||||
click: () => {
|
||||
win.show();
|
||||
win.focus();
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "toogleLikeSong",
|
||||
label: likeSong ? "从我喜欢中移除" : "添加到我喜欢",
|
||||
icon: showIcon(likeSong ? "like" : "unlike"),
|
||||
click: () => win.webContents.send("toogleLikeSong"),
|
||||
},
|
||||
{
|
||||
id: "changeMode",
|
||||
label:
|
||||
playMode === "repeat" ? "列表循环" : playMode === "repeat-once" ? "单曲循环" : "随机播放",
|
||||
icon: showIcon(playMode),
|
||||
submenu: [
|
||||
{
|
||||
id: "repeat",
|
||||
label: "列表循环",
|
||||
icon: showIcon("repeat"),
|
||||
checked: playMode === "repeat",
|
||||
type: "radio",
|
||||
click: () => win.webContents.send("changeMode", "repeat"),
|
||||
},
|
||||
{
|
||||
id: "repeat-once",
|
||||
label: "单曲循环",
|
||||
icon: showIcon("repeat-once"),
|
||||
checked: playMode === "repeat-once",
|
||||
type: "radio",
|
||||
click: () => win.webContents.send("changeMode", "repeat-once"),
|
||||
},
|
||||
{
|
||||
id: "shuffle",
|
||||
label: "随机播放",
|
||||
icon: showIcon("shuffle"),
|
||||
checked: playMode === "shuffle",
|
||||
type: "radio",
|
||||
click: () => win.webContents.send("changeMode", "shuffle"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "playNext",
|
||||
label: "上一曲",
|
||||
icon: showIcon("prev"),
|
||||
click: () => win.webContents.send("playPrev"),
|
||||
},
|
||||
{
|
||||
id: "playOrPause",
|
||||
label: playState === "pause" ? "播放" : "暂停",
|
||||
icon: showIcon(playState === "pause" ? "play" : "pause"),
|
||||
click: () => win.webContents.send(playState === "pause" ? "play" : "pause"),
|
||||
},
|
||||
{
|
||||
id: "playNext",
|
||||
label: "下一曲",
|
||||
icon: showIcon("next"),
|
||||
click: () => win.webContents.send("playNext"),
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "toogleDesktopLyric",
|
||||
label: `${desktopLyricShow ? "关闭" : "开启"}桌面歌词`,
|
||||
icon: showIcon("lyric"),
|
||||
click: () => win.webContents.send("toogleDesktopLyric"),
|
||||
},
|
||||
{
|
||||
id: "toogleDesktopLyricLock",
|
||||
label: `${desktopLyricLock ? "解锁" : "锁定"}桌面歌词`,
|
||||
icon: showIcon(desktopLyricLock ? "lock" : "unlock"),
|
||||
visible: desktopLyricShow,
|
||||
click: () => {
|
||||
const store = useStore();
|
||||
// 更新锁定状态
|
||||
store.set("lyric.config", { ...store.get("lyric.config"), isLock: !desktopLyricLock });
|
||||
// 触发窗口更新
|
||||
const config = store.get("lyric.config");
|
||||
const lyricWin = lyricWindow.getWin();
|
||||
if (!lyricWin) return;
|
||||
lyricWin.webContents.send("update-desktop-lyric-option", config);
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "setting",
|
||||
label: "全局设置",
|
||||
icon: showIcon("setting"),
|
||||
click: () => {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("openSetting");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
id: "exit",
|
||||
label: "退出",
|
||||
icon: showIcon("power"),
|
||||
click: () => {
|
||||
// win.close();
|
||||
app.exit(0);
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
return menu;
|
||||
};
|
||||
|
||||
// 创建托盘
|
||||
class CreateTray implements MainTray {
|
||||
// 窗口
|
||||
private _win: BrowserWindow;
|
||||
// 托盘
|
||||
private _tray: Tray;
|
||||
// 菜单
|
||||
private _menu: MenuItemConstructorOptions[];
|
||||
private _contextMenu: Menu;
|
||||
|
||||
constructor(win: BrowserWindow) {
|
||||
// 托盘图标
|
||||
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
});
|
||||
// 初始化数据
|
||||
this._win = win;
|
||||
this._tray = new Tray(icon);
|
||||
this._menu = createTrayMenu(this._win);
|
||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||
// 初始化事件
|
||||
this.initTrayMenu();
|
||||
this.initEvents();
|
||||
this.setTitle(appName);
|
||||
}
|
||||
// 托盘菜单
|
||||
private initTrayMenu() {
|
||||
this._menu = createTrayMenu(this._win);
|
||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||
this._tray.setContextMenu(this._contextMenu);
|
||||
}
|
||||
// 托盘事件
|
||||
private initEvents() {
|
||||
// 点击
|
||||
this._tray.on("click", () => this._win.show());
|
||||
// 明暗变化
|
||||
nativeTheme.on("updated", () => {
|
||||
this.initTrayMenu();
|
||||
});
|
||||
}
|
||||
// 设置标题
|
||||
/**
|
||||
* 设置标题
|
||||
* @param title 标题
|
||||
*/
|
||||
setTitle(title: string) {
|
||||
this._win.setTitle(title);
|
||||
this._tray.setTitle(title);
|
||||
this._tray.setToolTip(title);
|
||||
}
|
||||
/**
|
||||
* 设置播放名称
|
||||
* @param name 播放名称
|
||||
*/
|
||||
setPlayName(name: string) {
|
||||
// 超长处理
|
||||
if (name.length > 20) name = name.slice(0, 20) + "...";
|
||||
playName = name;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 设置播放状态
|
||||
* @param state 播放状态
|
||||
*/
|
||||
setPlayState(state: PlayState) {
|
||||
playState = state;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 设置播放模式
|
||||
* @param mode 播放模式
|
||||
*/
|
||||
setPlayMode(mode: PlayMode) {
|
||||
playMode = mode;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 设置喜欢状态
|
||||
* @param like 喜欢状态
|
||||
*/
|
||||
setLikeState(like: boolean) {
|
||||
likeSong = like;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 桌面歌词开关
|
||||
* @param show 桌面歌词开关状态
|
||||
*/
|
||||
setDesktopLyricShow(show: boolean) {
|
||||
desktopLyricShow = show;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 锁定桌面歌词
|
||||
* @param lock 锁定桌面歌词状态
|
||||
*/
|
||||
setDesktopLyricLock(lock: boolean) {
|
||||
desktopLyricLock = lock;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
/**
|
||||
* 销毁托盘
|
||||
*/
|
||||
destroyTray() {
|
||||
this._tray.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化托盘
|
||||
* @param win 主窗口
|
||||
* @param lyricWin 歌词窗口
|
||||
* @returns 托盘实例
|
||||
*/
|
||||
export const initTray = (win: BrowserWindow) => {
|
||||
try {
|
||||
trayLog.info("🚀 Tray Process Startup");
|
||||
const tray = new CreateTray(win);
|
||||
// 保存单例实例
|
||||
mainTrayInstance = tray;
|
||||
return tray;
|
||||
} catch (error) {
|
||||
trayLog.error("❌ Tray Process Error", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取托盘实例
|
||||
* @returns 托盘实例
|
||||
*/
|
||||
export const getMainTray = (): MainTray | null => mainTrayInstance;
|
||||
84
electron/main/update/index.ts
Normal file
84
electron/main/update/index.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { app, type BrowserWindow } from "electron";
|
||||
import { updateLog } from "../logger";
|
||||
import electronUpdater from "electron-updater";
|
||||
import { isDev } from "../utils/config";
|
||||
|
||||
// import
|
||||
const { autoUpdater } = electronUpdater;
|
||||
|
||||
// 开发环境启用
|
||||
if (isDev) {
|
||||
Object.defineProperty(app, "isPackaged", {
|
||||
get: () => true,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新源
|
||||
autoUpdater.setFeedURL({
|
||||
provider: "github",
|
||||
owner: "imsyy",
|
||||
repo: "SPlayer",
|
||||
});
|
||||
|
||||
// 禁用自动下载
|
||||
autoUpdater.autoDownload = false;
|
||||
|
||||
// 是否初始化
|
||||
let isInit: boolean = false;
|
||||
|
||||
// 是否提示
|
||||
let isShowTip: boolean = false;
|
||||
|
||||
// 事件监听
|
||||
const initUpdaterListeners = (win: BrowserWindow) => {
|
||||
if (isInit) return;
|
||||
|
||||
// 当有新版本可用时
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
win.webContents.send("update-available", info);
|
||||
updateLog.info(`🚀 New version available: ${info.version}`);
|
||||
});
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
win.webContents.send("download-progress", progress);
|
||||
updateLog.info(`🚀 Downloading: ${progress.percent}%`);
|
||||
});
|
||||
|
||||
// 当下载完成时
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
win.webContents.send("update-downloaded", info);
|
||||
updateLog.info(`🚀 Update downloaded: ${info.version}`);
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
// 当没有新版本时
|
||||
autoUpdater.on("update-not-available", (info) => {
|
||||
if (isShowTip) win.webContents.send("update-not-available", info);
|
||||
updateLog.info(`✅ No new version available: ${info.version}`);
|
||||
});
|
||||
|
||||
// 更新错误
|
||||
autoUpdater.on("error", (err) => {
|
||||
win.webContents.send("update-error", err);
|
||||
updateLog.error(`❌ Update error: ${err.message}`);
|
||||
});
|
||||
|
||||
isInit = true;
|
||||
};
|
||||
|
||||
// 检查更新
|
||||
export const checkUpdate = (win: BrowserWindow, showTip: boolean = false) => {
|
||||
// 初始化事件监听器
|
||||
initUpdaterListeners(win);
|
||||
// 更改提示
|
||||
isShowTip = showTip;
|
||||
// 检查更新
|
||||
autoUpdater.checkForUpdates();
|
||||
};
|
||||
|
||||
// 开始下载
|
||||
export const startDownloadUpdate = () => {
|
||||
autoUpdater.downloadUpdate();
|
||||
};
|
||||
@@ -1,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
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/images/icons/favicon.ico"
|
||||
: "../../public/images/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/images/icons/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/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_8.5.5.0_apk_keluze.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
58
electron/main/utils/helper.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createHash } from "crypto";
|
||||
import { readFile } from "fs/promises";
|
||||
|
||||
/**
|
||||
* 生成文件唯一ID
|
||||
* @param filePath 文件路径
|
||||
* @returns 唯一ID
|
||||
*/
|
||||
export const getFileID = (filePath: string): number => {
|
||||
// SHA-256
|
||||
const hash = createHash("sha256");
|
||||
hash.update(filePath);
|
||||
const digest = hash.digest("hex");
|
||||
// 将哈希值的前 16 位转换为十进制数字
|
||||
const uniqueId = parseInt(digest.substring(0, 16), 16);
|
||||
return Number(uniqueId.toString().padStart(16, "0"));
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成文件 MD5
|
||||
* @param path 文件路径
|
||||
* @returns MD5值
|
||||
*/
|
||||
export const getFileMD5 = async (path: string): Promise<string> => {
|
||||
const data = await readFile(path);
|
||||
const hash = createHash("md5");
|
||||
hash.update(data);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
|
||||
/**
|
||||
* 将 music-metadata 库中的歌词数组转换为LRC格式字符串
|
||||
* @param lyrics 歌词数组,每个元素包含时间戳(毫秒)和歌词文本
|
||||
* @returns LRC格式的字符串
|
||||
*/
|
||||
export const metaDataLyricsArrayToLrc = (
|
||||
lyrics: {
|
||||
text: string;
|
||||
timestamp?: number;
|
||||
}[],
|
||||
): string => {
|
||||
return lyrics
|
||||
.map(({ timestamp, text }) => {
|
||||
if (!timestamp) return "";
|
||||
const totalSeconds = Math.floor(timestamp / 1000);
|
||||
const minutes = Math.floor(totalSeconds / 60);
|
||||
const seconds = totalSeconds % 60;
|
||||
const centiseconds = Math.floor((timestamp % 1000) / 10);
|
||||
|
||||
// 格式化为两位数字
|
||||
const mm = String(minutes).padStart(2, "0");
|
||||
const ss = String(seconds).padStart(2, "0");
|
||||
const cs = String(centiseconds).padStart(2, "0");
|
||||
|
||||
return `[${mm}:${ss}.${cs}]${text}`;
|
||||
})
|
||||
.join("\n");
|
||||
};
|
||||
@@ -1,151 +0,0 @@
|
||||
import { parseFile } from "music-metadata";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
/**
|
||||
* 从指定文件夹中递归读取音乐文件,并将它们以数组形式返回
|
||||
* @param {string} directoryPath - 要读取的文件夹路径
|
||||
* @param {number} fileLimit - 返回的音乐文件数量限制
|
||||
* @returns {Array} - 包含音乐文件信息的数组
|
||||
*/
|
||||
export const readDirAsync = async (directoryPath, fileLimit = 5000) => {
|
||||
const result = [];
|
||||
// 递归读取文件夹中的项目
|
||||
const readItem = async (item) => {
|
||||
const itemPath = path.join(directoryPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
// 若为音频文件
|
||||
if (stats.isFile() && isAudioFile(itemPath)) {
|
||||
try {
|
||||
const { common, format } = await parseFile(itemPath);
|
||||
// 音乐文件信息
|
||||
const fileInfo = {
|
||||
id: generateId(itemPath),
|
||||
name: common.title,
|
||||
path: itemPath,
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2),
|
||||
time: stats.mtime?.getTime(),
|
||||
artists: common.artists?.[0],
|
||||
album: common.album,
|
||||
alia: common.comment?.[0],
|
||||
duration: formatDuration(format.duration),
|
||||
};
|
||||
result.push(fileInfo);
|
||||
} catch (error) {
|
||||
console.error("解析音乐文件元数据时出错:", error);
|
||||
}
|
||||
}
|
||||
// 若为文件夹
|
||||
if (stats.isDirectory()) {
|
||||
// 读取子文件夹中的项目
|
||||
const subItems = await fs.readdir(itemPath);
|
||||
for (const subItem of subItems) {
|
||||
await readItem(path.join(item, subItem));
|
||||
}
|
||||
}
|
||||
};
|
||||
// 从根目录开始读取
|
||||
await readItem("");
|
||||
// 返回不超过上限的音乐文件列表
|
||||
return result.slice(0, fileLimit);
|
||||
};
|
||||
|
||||
/**
|
||||
* 递归地读取文件夹内容,包括文件和子文件夹的信息
|
||||
* @param {string} directoryPath - 要读取的文件夹路径
|
||||
* @param {number} depth - 递归深度(默认为 -1,无限递归)
|
||||
* @param {number} fileLimit - 文件总数
|
||||
* @returns {Promise<Array>} 包含文件和子文件夹信息的树形数组
|
||||
*/
|
||||
export const readDirTreeAsync = async (directoryPath, depth = -1, fileLimit = 5000) => {
|
||||
const result = [];
|
||||
|
||||
const readItem = async (item) => {
|
||||
const itemPath = path.join(directoryPath, item);
|
||||
const stats = await fs.stat(itemPath);
|
||||
|
||||
const fileInfo = {
|
||||
id: generateId(item),
|
||||
name: item,
|
||||
path: itemPath,
|
||||
type: stats.isFile() ? "song" : "dir",
|
||||
size: (stats.size / (1024 * 1024)).toFixed(2), // 文件大小
|
||||
modified: stats.mtime, // 修改日期
|
||||
};
|
||||
|
||||
if (stats.isFile() && isAudioFile(itemPath)) {
|
||||
try {
|
||||
const { common, format } = await parseFile(itemPath);
|
||||
fileInfo.metadata = {
|
||||
name: common.title,
|
||||
artists: common.artists,
|
||||
album: common.album,
|
||||
date: common.date,
|
||||
alia: common.comment?.[0],
|
||||
year: common.year,
|
||||
duration: formatDuration(format.duration),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("解析音乐文件元数据时出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (stats.isDirectory() && (depth === -1 || depth > 0)) {
|
||||
// 如果是文件夹且未达到递归深度限制,且文件数量未达到上限,则递归读取文件夹内容
|
||||
if (fileInfo.type === "dir" && result.length < fileLimit) {
|
||||
fileInfo.children = await readDirAsync(
|
||||
itemPath,
|
||||
depth === -1 ? -1 : depth - 1,
|
||||
fileLimit - result.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
result.push(fileInfo);
|
||||
};
|
||||
|
||||
const items = await fs.readdir(directoryPath);
|
||||
await Promise.all(items.map(readItem));
|
||||
|
||||
return result.slice(0, fileLimit); // 返回不超过上限的文件列表
|
||||
};
|
||||
|
||||
/**
|
||||
* 歌曲时长时间戳转换
|
||||
* @param {number} mss 毫秒数
|
||||
* @returns {string} 格式为 "mm:ss" 的字符串
|
||||
*/
|
||||
const formatDuration = (seconds) => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
const formattedMinutes = minutes < 10 ? `0${minutes}` : `${minutes}`;
|
||||
const formattedSeconds = remainingSeconds < 10 ? `0${remainingSeconds}` : `${remainingSeconds}`;
|
||||
return `${formattedMinutes}:${formattedSeconds}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 判断文件是否为音频文件
|
||||
* @param {string} filePath - 文件路径
|
||||
* @returns {boolean} - 是否为音频文件
|
||||
*/
|
||||
const isAudioFile = (filePath) => {
|
||||
const audioExtensions = [".flac", ".mp3"];
|
||||
const extension = path.extname(filePath).toLowerCase();
|
||||
return audioExtensions.includes(extension);
|
||||
};
|
||||
|
||||
/**
|
||||
* 从文件名生成数字 ID
|
||||
* @param {string} fileName - 文件名
|
||||
* @returns {number} - 生成的数字ID
|
||||
*/
|
||||
const generateId = (fileName) => {
|
||||
// 将文件名转换为哈希值
|
||||
let hash = 0;
|
||||
for (let i = 0; i < fileName.length; i++) {
|
||||
hash = (hash << 5) - hash + fileName.charCodeAt(i);
|
||||
}
|
||||
// 将哈希值转换为正整数
|
||||
const numericId = Math.abs(hash % 10000000000);
|
||||
return numericId;
|
||||
};
|
||||
25
electron/main/utils/single-lock.ts
Normal file
25
electron/main/utils/single-lock.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { app } from "electron";
|
||||
import { systemLog } from "../logger";
|
||||
import mainWindow from "../windows/main-window";
|
||||
|
||||
/**
|
||||
* 初始化单实例锁
|
||||
* @returns 如果当前实例获得了锁,返回 true;否则返回 false
|
||||
*/
|
||||
export const initSingleLock = (): boolean => {
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
// 如果未获得锁,退出当前实例
|
||||
if (!gotTheLock) {
|
||||
app.quit();
|
||||
systemLog.warn("❌ 已有一个实例正在运行");
|
||||
return false;
|
||||
}
|
||||
// 当第二个实例启动时触发
|
||||
else {
|
||||
app.on("second-instance", () => {
|
||||
systemLog.warn("❌ 第二个实例将要启动");
|
||||
mainWindow.getWin()?.show();
|
||||
});
|
||||
}
|
||||
return true;
|
||||
};
|
||||
43
electron/main/windows/index.ts
Normal file
43
electron/main/windows/index.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import { windowsLog } from "../logger";
|
||||
import { appName } from "../utils/config";
|
||||
import { join } from "path";
|
||||
import icon from "../../../public/icons/favicon.png?asset";
|
||||
|
||||
export const createWindow = (
|
||||
options: BrowserWindowConstructorOptions = {},
|
||||
): BrowserWindow | null => {
|
||||
try {
|
||||
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||
title: appName,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frame: false, // 是否显示窗口边框
|
||||
center: true, // 窗口居中
|
||||
icon, // 窗口图标
|
||||
autoHideMenuBar: true, // 隐藏菜单栏
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
// 禁用渲染器沙盒
|
||||
sandbox: false,
|
||||
// 禁用同源策略
|
||||
webSecurity: false,
|
||||
// 允许 HTTP
|
||||
allowRunningInsecureContent: true,
|
||||
// 禁用拼写检查
|
||||
spellcheck: false,
|
||||
// 启用 Node.js
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
},
|
||||
};
|
||||
// 合并参数
|
||||
options = Object.assign(defaultOptions, options);
|
||||
// 创建窗口
|
||||
const win = new BrowserWindow(options);
|
||||
return win;
|
||||
} catch (error) {
|
||||
windowsLog.error(error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
60
electron/main/windows/load-window.ts
Normal file
60
electron/main/windows/load-window.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { loadWinUrl } from "../utils/config";
|
||||
|
||||
class LoadWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
private winURL: string;
|
||||
constructor() {
|
||||
this.winURL = loadWinUrl;
|
||||
}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return;
|
||||
// 准备好显示
|
||||
this.win.on("ready-to-show", () => {
|
||||
this.win?.show();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
this.win = createWindow({
|
||||
width: 800,
|
||||
height: 560,
|
||||
maxWidth: 800,
|
||||
maxHeight: 560,
|
||||
resizable: false,
|
||||
alwaysOnTop: true,
|
||||
// 不在任务栏显示
|
||||
skipTaskbar: true,
|
||||
// 窗口不能最小化
|
||||
minimizable: false,
|
||||
// 窗口不能最大化
|
||||
maximizable: false,
|
||||
// 窗口不能进入全屏状态
|
||||
fullscreenable: false,
|
||||
show: false,
|
||||
});
|
||||
if (!this.win) return null;
|
||||
// 加载地址
|
||||
this.win.loadURL(this.winURL);
|
||||
// 窗口事件
|
||||
this.event();
|
||||
return this.win;
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
return this.win;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LoadWindow();
|
||||
99
electron/main/windows/login-window.ts
Normal file
99
electron/main/windows/login-window.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { app, BrowserWindow, session } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { join } from "path";
|
||||
|
||||
class LoginWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
private loginTimer: NodeJS.Timeout | null = null;
|
||||
private loginSession: Electron.Session | null = null;
|
||||
|
||||
constructor() {}
|
||||
|
||||
private getLoginSession(): Electron.Session {
|
||||
if (!this.loginSession) {
|
||||
this.loginSession = session.fromPartition("persist:login");
|
||||
}
|
||||
return this.loginSession;
|
||||
}
|
||||
// 事件绑定
|
||||
private event(mainWin: BrowserWindow): void {
|
||||
if (!this.win) return;
|
||||
// 阻止新窗口创建
|
||||
this.win.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
||||
// 加载完成后显示并开始轮询登录状态
|
||||
this.win.webContents.once("did-finish-load", () => {
|
||||
this.win?.show();
|
||||
this.loginTimer = setInterval(() => this.checkLogin(mainWin), 1000);
|
||||
this.win?.on("closed", () => {
|
||||
if (this.loginTimer) clearInterval(this.loginTimer);
|
||||
this.loginTimer = null;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否已登录
|
||||
private async checkLogin(mainWin: BrowserWindow) {
|
||||
if (!this.win) return;
|
||||
try {
|
||||
this.win.webContents.executeJavaScript(
|
||||
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'",
|
||||
);
|
||||
// 判断 MUSIC_U
|
||||
const MUSIC_U = await this.getLoginSession().cookies.get({ name: "MUSIC_U" });
|
||||
if (MUSIC_U && MUSIC_U.length > 0) {
|
||||
if (this.loginTimer) clearInterval(this.loginTimer);
|
||||
this.loginTimer = null;
|
||||
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||
// 发送回主进程
|
||||
mainWin?.webContents.send("send-cookies", value);
|
||||
this.win.destroy();
|
||||
this.win = null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建登录窗口
|
||||
async create(mainWin: BrowserWindow): Promise<BrowserWindow | null> {
|
||||
await app.whenReady();
|
||||
const loginSession = this.getLoginSession();
|
||||
// 清理登录会话存储
|
||||
await loginSession.clearStorageData({
|
||||
storages: ["cookies", "localstorage"],
|
||||
});
|
||||
|
||||
this.win = createWindow({
|
||||
parent: mainWin,
|
||||
title: "登录网易云音乐( 若遇到无响应请关闭后重试 )",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
allowRunningInsecureContent: true,
|
||||
spellcheck: false,
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
session: loginSession,
|
||||
},
|
||||
});
|
||||
|
||||
if (!this.win) return null;
|
||||
// 加载登录地址
|
||||
this.win.loadURL("https://music.163.com/#/login/");
|
||||
// 绑定事件
|
||||
this.event(mainWin);
|
||||
return this.win;
|
||||
}
|
||||
|
||||
// 获取窗口
|
||||
getWin(): BrowserWindow | null {
|
||||
return this.win;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LoginWindow();
|
||||
89
electron/main/windows/lyric-window.ts
Normal file
89
electron/main/windows/lyric-window.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { BrowserWindow } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { useStore } from "../store";
|
||||
import { lyricWinUrl } from "../utils/config";
|
||||
import mainWindow from "./main-window";
|
||||
|
||||
class LyricWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
constructor() {}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return;
|
||||
// 准备好显示
|
||||
this.win.on("ready-to-show", () => {
|
||||
this.win?.show();
|
||||
});
|
||||
// 歌词窗口缩放
|
||||
this.win?.on("resized", () => {
|
||||
const store = useStore();
|
||||
const bounds = this.win?.getBounds();
|
||||
if (bounds) {
|
||||
const { width, height } = bounds;
|
||||
store.set("lyric", { ...store.get("lyric"), width, height });
|
||||
}
|
||||
});
|
||||
// 歌词窗口关闭
|
||||
this.win?.on("close", () => {
|
||||
this.win = null;
|
||||
const mainWin = mainWindow?.getWin();
|
||||
if (mainWin) {
|
||||
mainWin?.webContents.send("closeDesktopLyric");
|
||||
}
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建主窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
const store = useStore();
|
||||
const { width, height, x, y } = store.get("lyric");
|
||||
this.win = createWindow({
|
||||
width: width || 800,
|
||||
height: height || 180,
|
||||
minWidth: 640,
|
||||
minHeight: 140,
|
||||
maxWidth: 1400,
|
||||
maxHeight: 360,
|
||||
// 没有指定位置时居中显示
|
||||
center: !(x && y),
|
||||
// 窗口位置
|
||||
x,
|
||||
y,
|
||||
transparent: true,
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
alwaysOnTop: true,
|
||||
resizable: true,
|
||||
movable: true,
|
||||
show: false,
|
||||
// 不在任务栏显示
|
||||
skipTaskbar: true,
|
||||
// 窗口不能最小化
|
||||
minimizable: false,
|
||||
// 窗口不能最大化
|
||||
maximizable: false,
|
||||
// 窗口不能进入全屏状态
|
||||
fullscreenable: false,
|
||||
});
|
||||
if (!this.win) return null;
|
||||
// 加载地址
|
||||
this.win.loadURL(lyricWinUrl);
|
||||
// 窗口事件
|
||||
this.event();
|
||||
return this.win;
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
if (this.win && !this.win?.isDestroyed()) return this.win;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default new LyricWindow();
|
||||
134
electron/main/windows/main-window.ts
Normal file
134
electron/main/windows/main-window.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { BrowserWindow, shell } from "electron";
|
||||
import { createWindow } from "./index";
|
||||
import { mainWinUrl } from "../utils/config";
|
||||
import { useStore } from "../store";
|
||||
import { isLinux } from "../utils/config";
|
||||
|
||||
class MainWindow {
|
||||
private win: BrowserWindow | null = null;
|
||||
private winURL: string;
|
||||
constructor() {
|
||||
this.winURL = mainWinUrl;
|
||||
}
|
||||
/**
|
||||
* 保存窗口大小和状态
|
||||
*/
|
||||
private saveBounds() {
|
||||
if (this.win?.isFullScreen()) return;
|
||||
const store = useStore();
|
||||
const bounds = this.win?.getBounds();
|
||||
if (bounds) {
|
||||
const maximized = this.win?.isMaximized();
|
||||
store.set("window", { ...bounds, maximized });
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 主窗口事件
|
||||
* @returns void
|
||||
*/
|
||||
private event(): void {
|
||||
if (!this.win) return;
|
||||
const store = useStore();
|
||||
// 配置网络代理
|
||||
if (store.get("proxy")) {
|
||||
this.win.webContents.session.setProxy({ proxyRules: store.get("proxy") });
|
||||
}
|
||||
|
||||
// 窗口打开处理程序
|
||||
this.win.webContents.setWindowOpenHandler((details) => {
|
||||
const { url } = details;
|
||||
if (url.startsWith("https://") || url.startsWith("http://")) {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
return { action: "deny" };
|
||||
});
|
||||
// 窗口显示时
|
||||
this.win?.on("show", () => {
|
||||
this.win?.webContents.send("lyricsScroll");
|
||||
});
|
||||
// 窗口获得焦点时
|
||||
this.win?.on("focus", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
// 窗口大小改变时
|
||||
this.win?.on("resized", () => {
|
||||
// 若处于全屏则不保存
|
||||
if (this.win?.isFullScreen()) return;
|
||||
this.saveBounds();
|
||||
});
|
||||
// 窗口位置改变时
|
||||
this.win?.on("moved", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
// 窗口最大化时
|
||||
this.win?.on("maximize", () => {
|
||||
this.saveBounds();
|
||||
this.win?.webContents.send("win-state-change", true);
|
||||
});
|
||||
// 窗口取消最大化时
|
||||
this.win?.on("unmaximize", () => {
|
||||
this.saveBounds();
|
||||
this.win?.webContents.send("win-state-change", false);
|
||||
});
|
||||
// Linux 无法使用 resized 和 moved
|
||||
if (isLinux) {
|
||||
this.win?.on("resize", () => {
|
||||
// 若处于全屏则不保存
|
||||
if (this.win?.isFullScreen()) return;
|
||||
this.saveBounds();
|
||||
});
|
||||
this.win?.on("move", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
}
|
||||
// 窗口关闭
|
||||
this.win?.on("close", (event) => {
|
||||
event.preventDefault();
|
||||
this.win?.hide();
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 创建窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
create(): BrowserWindow | null {
|
||||
const store = useStore();
|
||||
const { width, height } = store.get("window");
|
||||
this.win = createWindow({
|
||||
// 菜单栏
|
||||
titleBarStyle: "customButtonsOnHover",
|
||||
width,
|
||||
height,
|
||||
minHeight: 600,
|
||||
minWidth: 800,
|
||||
show: false,
|
||||
});
|
||||
if (!this.win) return null;
|
||||
// 加载地址
|
||||
this.win.loadURL(this.winURL);
|
||||
// 窗口事件
|
||||
this.event();
|
||||
return this.win;
|
||||
}
|
||||
/**
|
||||
* 获取窗口
|
||||
* @returns BrowserWindow | null
|
||||
*/
|
||||
getWin(): BrowserWindow | null {
|
||||
if (this.win && !this.win.isDestroyed()) {
|
||||
return this.win;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 显示主窗口
|
||||
*/
|
||||
showWindow() {
|
||||
if (this.win) {
|
||||
this.win.show();
|
||||
if (this.win.isMinimized()) this.win.restore();
|
||||
this.win.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
export default new MainWindow();
|
||||
17
electron/preload/index.d.ts
vendored
Normal file
17
electron/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
import { ElectronAPI } from "@electron-toolkit/preload";
|
||||
import type { StoreType } from "../main/store";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI;
|
||||
api: {
|
||||
store: {
|
||||
get<K extends keyof StoreType>(key: K): Promise<StoreType[K]>;
|
||||
set<K extends keyof StoreType>(key: K, value: StoreType[K]): Promise<boolean>;
|
||||
has(key: keyof StoreType): Promise<boolean>;
|
||||
delete(key: keyof StoreType): Promise<boolean>;
|
||||
reset(keys?: (keyof StoreType)[]): Promise<boolean>;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { contextBridge } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
// 如果启用了上下文隔离,使用 `contextBridge` 将 Electron API 暴露给渲染进程
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
// 使用 contextBridge 暴露 electronAPI 到渲染进程的全局对象中
|
||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// 如果上下文隔离未启用,将 electronAPI 添加到 DOM 全局对象
|
||||
window.electron = electronAPI;
|
||||
}
|
||||
23
electron/preload/index.ts
Normal file
23
electron/preload/index.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { contextBridge, ipcRenderer } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
// Use `contextBridge` APIs to expose Electron APIs to
|
||||
// renderer only if context isolation is enabled, otherwise
|
||||
// just add to the DOM global.
|
||||
if (process.contextIsolated) {
|
||||
try {
|
||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||
// Expose store API via preload
|
||||
contextBridge.exposeInMainWorld("api", {
|
||||
store: {
|
||||
get: (key: string) => ipcRenderer.invoke("store-get", key),
|
||||
set: (key: string, value: unknown) => ipcRenderer.invoke("store-set", key, value),
|
||||
has: (key: string) => ipcRenderer.invoke("store-has", key),
|
||||
delete: (key: string) => ipcRenderer.invoke("store-delete", key),
|
||||
reset: (keys?: string[]) => ipcRenderer.invoke("store-reset", keys),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
187
electron/server/control/index.ts
Normal file
187
electron/server/control/index.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import mainWindow from "../../main/windows/main-window";
|
||||
|
||||
/**
|
||||
* 播放控制接口
|
||||
* @param fastify Fastify 实例
|
||||
*/
|
||||
export const initControlAPI = async (fastify: FastifyInstance) => {
|
||||
// 播放控制路由前缀
|
||||
await fastify.register(
|
||||
async (fastify) => {
|
||||
// 播放
|
||||
fastify.get("/play", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("play");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "播放命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "播放失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 暂停
|
||||
fastify.get("/pause", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("pause");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "暂停命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "暂停失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 播放/暂停切换
|
||||
fastify.get("/toggle", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 这里可以根据当前播放状态来决定发送 play 还是 pause
|
||||
// 暂时先发送 toggle 事件,如果渲染进程支持的话
|
||||
mainWin.webContents.send("toggle");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "播放/暂停切换命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "播放/暂停切换失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 下一曲
|
||||
fastify.get("/next", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("playNext");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "下一曲命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "下一曲失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 上一曲
|
||||
fastify.get("/prev", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
mainWin.webContents.send("playPrev");
|
||||
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "上一曲命令已发送",
|
||||
data: null,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "上一曲失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取播放状态(可选功能)
|
||||
fastify.get("/status", async (_request, reply) => {
|
||||
try {
|
||||
const mainWin = mainWindow.getWin();
|
||||
if (!mainWin) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "主窗口未找到",
|
||||
data: null,
|
||||
});
|
||||
}
|
||||
|
||||
// 这里可以通过 IPC 获取当前播放状态
|
||||
// 暂时返回基本信息
|
||||
return reply.send({
|
||||
code: 200,
|
||||
message: "获取状态成功",
|
||||
data: {
|
||||
connected: true,
|
||||
window: "available",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(500).send({
|
||||
code: 500,
|
||||
message: "获取状态失败",
|
||||
data: error,
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ prefix: "/control" },
|
||||
);
|
||||
};
|
||||
63
electron/server/index.ts
Normal file
63
electron/server/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { join } from "path";
|
||||
import { isDev } from "../main/utils/config";
|
||||
import { serverLog } from "../main/logger";
|
||||
import { initNcmAPI } from "./netease";
|
||||
import { initUnblockAPI } from "./unblock";
|
||||
import { initControlAPI } from "./control";
|
||||
import fastifyCookie from "@fastify/cookie";
|
||||
import fastifyMultipart from "@fastify/multipart";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import fastify from "fastify";
|
||||
|
||||
const initAppServer = async () => {
|
||||
try {
|
||||
const server = fastify({
|
||||
routerOptions: {
|
||||
// 忽略尾随斜杠
|
||||
ignoreTrailingSlash: true,
|
||||
},
|
||||
});
|
||||
// 注册插件
|
||||
server.register(fastifyCookie);
|
||||
server.register(fastifyMultipart);
|
||||
// 生产环境启用静态文件
|
||||
if (!isDev) {
|
||||
serverLog.info("📂 Serving static files from /renderer");
|
||||
server.register(fastifyStatic, {
|
||||
root: join(__dirname, "../renderer"),
|
||||
});
|
||||
}
|
||||
// 声明
|
||||
server.get("/api", (_, reply) => {
|
||||
reply.send({
|
||||
name: "SPlayer API",
|
||||
description: "SPlayer API service",
|
||||
author: "@imsyy",
|
||||
list: [
|
||||
{
|
||||
name: "NeteaseCloudMusicApi",
|
||||
url: "/api/netease",
|
||||
},
|
||||
{
|
||||
name: "UnblockAPI",
|
||||
url: "/api/unblock",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
// 注册接口
|
||||
server.register(initNcmAPI, { prefix: "/api" });
|
||||
server.register(initUnblockAPI, { prefix: "/api" });
|
||||
server.register(initControlAPI, { prefix: "/api" });
|
||||
// 启动端口
|
||||
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
|
||||
await server.listen({ port });
|
||||
serverLog.info(`🌐 Starting AppServer on port ${port}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
serverLog.error("🚫 AppServer failed to start");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default initAppServer;
|
||||
87
electron/server/netease/index.ts
Normal file
87
electron/server/netease/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { pathCase } from "change-case";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import NeteaseCloudMusicApi from "@neteasecloudmusicapienhanced/api";
|
||||
|
||||
// 获取数据
|
||||
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
||||
return async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
serverLog.log("🌐 Request NcmAPI:", name);
|
||||
// 获取 NcmAPI 数据
|
||||
try {
|
||||
const result = await neteaseApi({
|
||||
...req.query,
|
||||
...(req.body as Record<string, any>),
|
||||
cookie: req.cookies,
|
||||
});
|
||||
return reply.send(result.body);
|
||||
} catch (error: any) {
|
||||
serverLog.error("❌ NcmAPI Error:", error);
|
||||
if ([400, 301].includes(error.status)) {
|
||||
return reply.status(error.status).send(error.body);
|
||||
}
|
||||
return reply.status(500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化 NcmAPI
|
||||
export const initNcmAPI = async (fastify: FastifyInstance) => {
|
||||
// 主信息
|
||||
fastify.get("/netease", (_, reply) => {
|
||||
reply.send({
|
||||
name: "@neteaseapireborn/api",
|
||||
version: "4.29.2",
|
||||
description: "网易云音乐 API Enhanced",
|
||||
author: "@MoeFurina",
|
||||
license: "MIT",
|
||||
url: "https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced",
|
||||
});
|
||||
});
|
||||
|
||||
// 注册 NeteaseCloudMusicApi 所有接口
|
||||
Object.entries(NeteaseCloudMusicApi).forEach(([routerName, neteaseApi]: [string, any]) => {
|
||||
// 例外
|
||||
if (["serveNcmApi", "getModulesDefinitions"].includes(routerName)) return;
|
||||
// 路由名称
|
||||
const pathName = pathCase(routerName);
|
||||
// 获取数据
|
||||
const handler = getHandler(pathName, neteaseApi);
|
||||
// 注册路由
|
||||
fastify.get(`/netease/${pathName}`, handler);
|
||||
fastify.post(`/netease/${pathName}`, handler);
|
||||
// 兼容路由 - 中间具有 _ 的路由
|
||||
if (routerName.includes("_")) {
|
||||
fastify.get(`/netease/${routerName}`, handler);
|
||||
fastify.post(`/netease/${routerName}`, handler);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取 TTML 歌词
|
||||
fastify.get(
|
||||
"/netease/lyric/ttml",
|
||||
async (req: FastifyRequest<{ Querystring: { id: string } }>, reply: FastifyReply) => {
|
||||
const { id } = req.query;
|
||||
if (!id) {
|
||||
return reply.status(400).send({ error: "id is required" });
|
||||
}
|
||||
const url = `https://amll-ttml-db.stevexmh.net/ncm/${id}`;
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (response.status !== 200) {
|
||||
return reply.send(null);
|
||||
}
|
||||
const data = await response.text();
|
||||
return reply.send(data);
|
||||
} catch (error) {
|
||||
serverLog.error("❌ TTML Lyric Fetch Error:", error);
|
||||
return reply.send(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
serverLog.info("🌐 Register NcmAPI successfully");
|
||||
};
|
||||
14
electron/server/port.ts
Normal file
14
electron/server/port.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import getPort from "get-port";
|
||||
|
||||
// 默认端口
|
||||
let webPort: number;
|
||||
let servePort: number;
|
||||
|
||||
const getSafePort = async () => {
|
||||
if (webPort && servePort) return { webPort, servePort };
|
||||
webPort = await getPort({ port: 14558 });
|
||||
servePort = await getPort({ port: 25884 });
|
||||
return { webPort, servePort };
|
||||
};
|
||||
|
||||
export default getSafePort;
|
||||
161
electron/server/unblock/bodian.ts
Normal file
161
electron/server/unblock/bodian.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import { createHash } from "crypto";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 生成随机设备 ID
|
||||
* @returns 随机设备 ID
|
||||
*/
|
||||
const getRandomDeviceId = () => {
|
||||
const min = 0;
|
||||
const max = 100000000000;
|
||||
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
return randomNum.toString();
|
||||
};
|
||||
|
||||
/** 随机设备 ID */
|
||||
const deviceId = getRandomDeviceId();
|
||||
|
||||
/**
|
||||
* 格式化歌曲信息
|
||||
* @param song 歌曲信息
|
||||
* @returns 格式化后的歌曲信息
|
||||
*/
|
||||
const format = (song: any) => ({
|
||||
id: song.MUSICRID.split("_").pop(),
|
||||
name: song.SONGNAME,
|
||||
duration: song.DURATION * 1000,
|
||||
album: { id: song.ALBUMID, name: song.ALBUM },
|
||||
artists: song.ARTIST.split("&").map((name: any, index: any) => ({
|
||||
id: index ? null : song.ARTISTID,
|
||||
name,
|
||||
})),
|
||||
});
|
||||
|
||||
/**
|
||||
* 生成签名
|
||||
* @param str 请求字符串
|
||||
* @returns 包含签名的请求字符串
|
||||
*/
|
||||
const generateSign = (str: string) => {
|
||||
const url = new URL(str);
|
||||
|
||||
const currentTime = Date.now();
|
||||
str += `×tamp=${currentTime}`;
|
||||
|
||||
const filteredChars = str
|
||||
.substring(str.indexOf("?") + 1)
|
||||
.replace(/[^a-zA-Z0-9]/g, "")
|
||||
.split("")
|
||||
.sort();
|
||||
|
||||
const dataToEncrypt = `kuwotest${filteredChars.join("")}${url.pathname}`;
|
||||
const md5 = createHash("md5").update(dataToEncrypt).digest("hex");
|
||||
return `${str}&sign=${md5}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索歌曲
|
||||
* @param keyword 搜索关键词
|
||||
* @returns 歌曲 ID 或 null
|
||||
*/
|
||||
const search = async (info: string): Promise<string | null> => {
|
||||
try {
|
||||
const keyword = encodeURIComponent(info.replace(" - ", " "));
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&vipver=1&stype=comprehensive&encoding=utf8" +
|
||||
"&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword;
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// 获取歌曲信息
|
||||
const list = result.data.content[1].musicpage.abslist.map(format);
|
||||
if (list[0] && !list[0]?.id) return null;
|
||||
return list[0].id;
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get BodianSongId Error:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 发送广告免费请求
|
||||
* @returns 包含广告免费响应的 Promise
|
||||
*/
|
||||
const sendAdFreeRequest = () => {
|
||||
try {
|
||||
const adurl =
|
||||
"http://bd-api.kuwo.cn/api/service/advert/watch?uid=-1&token=×tamp=1724306124436&sign=15a676d66285117ad714e8c8371691da";
|
||||
|
||||
const headers = {
|
||||
"user-agent": "Dart/2.19 (dart:io)",
|
||||
plat: "ar",
|
||||
channel: "aliopen",
|
||||
devid: deviceId,
|
||||
ver: "3.9.0",
|
||||
host: "bd-api.kuwo.cn",
|
||||
qimei36: "1e9970cbcdc20a031dee9f37100017e1840e",
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
};
|
||||
|
||||
const data = JSON.stringify({
|
||||
type: 5,
|
||||
subType: 5,
|
||||
musicId: 0,
|
||||
adToken: "",
|
||||
});
|
||||
return axios.post(adurl, data, { headers });
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get Bodian Ad Free Error:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取波点音乐歌曲 URL
|
||||
* @param keyword 搜索关键词
|
||||
* @returns 包含歌曲 URL 的结果对象
|
||||
*/
|
||||
const getBodianSongUrl = async (keyword: string): Promise<SongUrlResult> => {
|
||||
try {
|
||||
if (!keyword) return { code: 404, url: null };
|
||||
const songId = await search(keyword);
|
||||
if (!songId) return { code: 404, url: null };
|
||||
// 请求地址
|
||||
const headers = {
|
||||
"user-agent": "Dart/2.19 (dart:io)",
|
||||
plat: "ar",
|
||||
channel: "aliopen",
|
||||
devid: deviceId,
|
||||
ver: "3.9.0",
|
||||
host: "bd-api.kuwo.cn",
|
||||
"X-Forwarded-For": "1.0.1.114",
|
||||
};
|
||||
let audioUrl = `http://bd-api.kuwo.cn/api/play/music/v2/audioUrl?&br=${"320kmp3"}&musicId=${songId}`;
|
||||
// 生成签名
|
||||
audioUrl = generateSign(audioUrl);
|
||||
// 获取广告
|
||||
await sendAdFreeRequest();
|
||||
// 获取歌曲地址
|
||||
const result = await axios.get(audioUrl, { headers });
|
||||
if (typeof result.data === "object") {
|
||||
const urlMatch = result.data.data.audioUrl;
|
||||
serverLog.log("🔗 BodianSong URL:", urlMatch);
|
||||
return { code: 200, url: urlMatch };
|
||||
}
|
||||
return { code: 404, url: null };
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get BodianSong URL Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
export default getBodianSongUrl;
|
||||
79
electron/server/unblock/index.ts
Normal file
79
electron/server/unblock/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import getKuwoSongUrl from "./kuwo";
|
||||
import axios from "axios";
|
||||
import getBodianSongUrl from "./bodian";
|
||||
|
||||
/**
|
||||
* 直接获取 网易云云盘 链接
|
||||
* Thank @939163156
|
||||
* Power by GD音乐台(music.gdstudio.xyz)
|
||||
*/
|
||||
const getNeteaseSongUrl = async (id: number | string): Promise<SongUrlResult> => {
|
||||
try {
|
||||
if (!id) return { code: 404, url: null };
|
||||
const baseUrl = "https://music-api.gdstudio.xyz/api.php";
|
||||
const result = await axios.get(baseUrl, {
|
||||
params: { types: "url", id },
|
||||
});
|
||||
const songUrl = result.data.url;
|
||||
serverLog.log("🔗 NeteaseSongUrl URL:", songUrl);
|
||||
return { code: 200, url: songUrl };
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get NeteaseSongUrl Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化 UnblockAPI
|
||||
export const initUnblockAPI = async (fastify: FastifyInstance) => {
|
||||
// 主信息
|
||||
fastify.get("/unblock", (_, reply) => {
|
||||
reply.send({
|
||||
name: "UnblockAPI",
|
||||
description: "SPlayer UnblockAPI service",
|
||||
author: "@imsyy",
|
||||
content:
|
||||
"部分接口采用 @939163156 by GD音乐台(music.gdstudio.xyz),仅供本人学习使用,不可传播下载内容,不可用于商业用途。",
|
||||
});
|
||||
});
|
||||
// netease
|
||||
fastify.get(
|
||||
"/unblock/netease",
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const { id } = req.query;
|
||||
const result = await getNeteaseSongUrl(id);
|
||||
return reply.send(result);
|
||||
},
|
||||
);
|
||||
// kuwo
|
||||
fastify.get(
|
||||
"/unblock/kuwo",
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const { keyword } = req.query;
|
||||
const result = await getKuwoSongUrl(keyword);
|
||||
return reply.send(result);
|
||||
},
|
||||
);
|
||||
// bodian
|
||||
fastify.get(
|
||||
"/unblock/bodian",
|
||||
async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
const { keyword } = req.query;
|
||||
const result = await getBodianSongUrl(keyword);
|
||||
return reply.send(result);
|
||||
},
|
||||
);
|
||||
|
||||
serverLog.info("🌐 Register UnblockAPI successfully");
|
||||
};
|
||||
66
electron/server/unblock/kuwo.ts
Normal file
66
electron/server/unblock/kuwo.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { encryptQuery } from "./kwDES";
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import { serverLog } from "../../main/logger";
|
||||
import axios from "axios";
|
||||
|
||||
// 获取酷我音乐歌曲 ID
|
||||
const getKuwoSongId = async (keyword: string): Promise<string | null> => {
|
||||
try {
|
||||
const url =
|
||||
"http://search.kuwo.cn/r.s?&correct=1&stype=comprehensive&encoding=utf8&rformat=json&mobi=1&show_copyright_off=1&searchapi=6&all=" +
|
||||
keyword;
|
||||
const result = await axios.get(url);
|
||||
if (
|
||||
!result.data ||
|
||||
result.data.content.length < 2 ||
|
||||
!result.data.content[1].musicpage ||
|
||||
result.data.content[1].musicpage.abslist.length < 1
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
// 获取歌曲信息
|
||||
const songId = result.data.content[1].musicpage.abslist[0].MUSICRID;
|
||||
const songName = result.data.content[1].musicpage.abslist[0]?.SONGNAME;
|
||||
// 是否与原曲吻合
|
||||
const originalName = keyword?.split("-") ?? keyword;
|
||||
if (songName && !songName?.includes(originalName[0])) return null;
|
||||
return songId.slice("MUSIC_".length);
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get KuwoSongId Error:", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 获取酷我音乐歌曲 URL
|
||||
const getKuwoSongUrl = async (keyword: string): Promise<SongUrlResult> => {
|
||||
try {
|
||||
if (!keyword) return { code: 404, url: null };
|
||||
const songId = await getKuwoSongId(keyword);
|
||||
if (!songId) return { code: 404, url: null };
|
||||
// 请求地址
|
||||
const PackageName = "kwplayer_ar_5.1.0.0_B_jiakong_vh.apk";
|
||||
const url =
|
||||
"http://mobi.kuwo.cn/mobi.s?f=kuwo&q=" +
|
||||
encryptQuery(
|
||||
`corp=kuwo&source=${PackageName}&p2p=1&type=convert_url2&sig=0&format=mp3` +
|
||||
"&rid=" +
|
||||
songId,
|
||||
);
|
||||
const result = await axios.get(url, {
|
||||
headers: {
|
||||
"User-Agent": "okhttp/3.10.0",
|
||||
},
|
||||
});
|
||||
if (result.data) {
|
||||
const urlMatch = result.data.match(/http[^\s$"]+/)[0];
|
||||
serverLog.log("🔗 KuwoSong URL:", urlMatch);
|
||||
return { code: 200, url: urlMatch };
|
||||
}
|
||||
return { code: 404, url: null };
|
||||
} catch (error) {
|
||||
serverLog.error("❌ Get KuwoSong URL Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
export default getKuwoSongUrl;
|
||||
@@ -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
4
electron/server/unblock/unblock.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
export type SongUrlResult = {
|
||||
code: number;
|
||||
url: string | null;
|
||||
};
|
||||
5
env.d.ts
vendored
Normal file
5
env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface MainEnv {
|
||||
readonly VITE_WEB_PORT: string;
|
||||
readonly VITE_SERVER_PORT: string;
|
||||
readonly VITE_API_URL: string;
|
||||
}
|
||||
65
eslint.config.mjs
Normal file
65
eslint.config.mjs
Normal file
@@ -0,0 +1,65 @@
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
import vue from "eslint-plugin-vue";
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import path from "node:path";
|
||||
import autoEslint from "./auto-eslint.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all,
|
||||
});
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
"**/node_modules",
|
||||
"**/dist",
|
||||
"**/out",
|
||||
"**/.gitignore",
|
||||
"**/auto-imports.d.ts",
|
||||
"**/components.d.ts",
|
||||
],
|
||||
},
|
||||
...compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
|
||||
{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
vue,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...autoEslint.globals,
|
||||
},
|
||||
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/.eslintrc.{js,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: { ...globals.node },
|
||||
ecmaVersion: 5,
|
||||
sourceType: "commonjs",
|
||||
},
|
||||
},
|
||||
];
|
||||
36
index.html
36
index.html
@@ -1,23 +1,21 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/images/icons/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/images/icons/favicon-16x16.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/images/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="/images/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
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;
|
||||
}
|
||||
}
|
||||
|
||||
159
package.json
159
package.json
@@ -1,76 +1,137 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.2",
|
||||
"productName": "SPlayer",
|
||||
"version": "3.0.0-beta.5",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"blog": "https://blog.imsyy.top",
|
||||
"repository": "github:imsyy/SPlayer",
|
||||
"license": "AGPL-3.0",
|
||||
"license-file": "LICENSE",
|
||||
"engines": {
|
||||
"node": ">=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.0",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.14.0",
|
||||
"axios": "^1.6.5",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.1.7",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^2.0.0",
|
||||
"@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.17",
|
||||
"@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.19",
|
||||
"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.2.5",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^16.4.2",
|
||||
"md5": "^2.3.0",
|
||||
"music-metadata": "^11.10.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"plyr": "^3.8.3",
|
||||
"vue-virt-list": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.2",
|
||||
"@rushstack/eslint-patch": "^1.6.1",
|
||||
"@vitejs/plugin-vue": "^5.0.2",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"ajv": "^8.12.0",
|
||||
"electron": "^28.1.2",
|
||||
"electron-builder": "^24.9.1",
|
||||
"electron-log": "^5.0.3",
|
||||
"electron-vite": "^2.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"naive-ui": "^2.37.3",
|
||||
"prettier": "^3.1.1",
|
||||
"sass": "^1.69.7",
|
||||
"terser": "^5.26.0",
|
||||
"unplugin-auto-import": "^0.17.3",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "^5.0.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.3.0",
|
||||
"@fastify/static": "^8.3.0",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/md5": "^2.3.6",
|
||||
"@types/node": "^24.10.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.4",
|
||||
"@typescript-eslint/parser": "^8.46.4",
|
||||
"@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.39.1",
|
||||
"eslint-plugin-vue": "^10.5.1",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.6.2",
|
||||
"naive-ui": "^2.43.1",
|
||||
"node-taglib-sharp": "^6.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"sass": "^1.94.0",
|
||||
"terser": "^5.44.1",
|
||||
"typescript": "^5.9.3",
|
||||
"unplugin-auto-import": "^20.2.0",
|
||||
"unplugin-vue-components": "^29.2.0",
|
||||
"vite": "^7.2.2",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vite-plugin-pwa": "^0.17.4",
|
||||
"vue": "3.4.8"
|
||||
"vite-plugin-vue-devtools": "^8.0.3",
|
||||
"vite-plugin-wasm": "^3.5.0",
|
||||
"vue": "^3.5.24",
|
||||
"vue-router": "^4.6.3",
|
||||
"vue-tsc": "^3.1.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"dmg-builder": "26.0.12",
|
||||
"electron-builder-squirrel-windows": "26.0.12"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@applemusic-like-lyrics/core",
|
||||
"@applemusic-like-lyrics/lyric",
|
||||
"@applemusic-like-lyrics/vue",
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"electron",
|
||||
"electron-winstaller",
|
||||
"esbuild",
|
||||
"sharp",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
13680
pnpm-lock.yaml
generated
13680
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
723
public/font/font.min.css
vendored
723
public/font/font.min.css
vendored
@@ -1,723 +0,0 @@
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.a.woff2) format("woff2");
|
||||
unicode-range: U+9aa2-ffe5;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.b.woff2) format("woff2");
|
||||
unicode-range: U+8983-9aa0;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.c.woff2) format("woff2");
|
||||
unicode-range: U+78f2-897b;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.d.woff2) format("woff2");
|
||||
unicode-range: U+646d-78d9;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.e.woff2) format("woff2");
|
||||
unicode-range: U+30e0-6445;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.f.woff2) format("woff2");
|
||||
unicode-range: U+101-30df;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.g.woff2) format("woff2");
|
||||
unicode-range: U+9aa8, U+9ab8, U+9ad3, U+9ad8, U+9b03, U+9b3c, U+9b41-9b42, U+9b44, U+9b4f, U+9b54,
|
||||
U+9c7c, U+9c81, U+9c8d, U+9c9c, U+9ca4, U+9cb8, U+9cc3, U+9cd6, U+9cde, U+9e1f, U+9e21, U+9e23,
|
||||
U+9e25-9e26, U+9e2d, U+9e2f, U+9e33, U+9e35, U+9e3d, U+9e3f, U+9e43, U+9e45, U+9e4a, U+9e4f,
|
||||
U+9e64, U+9e70, U+9e7f, U+9e93, U+9ea6, U+9ebb, U+9ec4, U+9ecd-9ece, U+9ed1, U+9ed4, U+9ed8,
|
||||
U+9f0e, U+9f13, U+9f20, U+9f3b, U+9f50, U+9f7f, U+9f84, U+9f8b, U+9f99-9f9a, U+9f9f, U+ff01,
|
||||
U+ff08-ff09, U+ff0c, U+ff1a-ff1b, U+ff1f;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.h.woff2) format("woff2");
|
||||
unicode-range: U+975b, U+975e, U+9760-9762, U+9769, U+9773-9774, U+9776, U+978b, U+978d, U+9798,
|
||||
U+97a0, U+97ad, U+97e6-97e7, U+97e9, U+97ed, U+97f3, U+97f5-97f6, U+9875-9877, U+9879-987b,
|
||||
U+987d-987f, U+9881-9882, U+9884-9888, U+988a, U+9890-9891, U+9893, U+9896-9898, U+989c-989d,
|
||||
U+98a0, U+98a4, U+98a7, U+98ce, U+98d8, U+98de-98df, U+9910, U+9965, U+996d-9972, U+9975-9976,
|
||||
U+997a, U+997c, U+997f, U+9981, U+9985-9986, U+9988, U+998b, U+998f, U+9992, U+9996, U+9999,
|
||||
U+9a6c-9a71, U+9a73-9a74, U+9a76, U+9a79, U+9a7b-9a7c, U+9a7e, U+9a82, U+9a84, U+9a86-9a87,
|
||||
U+9a8b-9a8c, U+9a8f, U+9a91, U+9a97, U+9a9a, U+9aa1, U+9aa4;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.i.woff2) format("woff2");
|
||||
unicode-range: U+9570, U+9576, U+957f, U+95e8, U+95ea, U+95ed-95f0, U+95f2, U+95f4, U+95f7-95fb,
|
||||
U+95fd, U+9600-9602, U+9605, U+9609, U+960e, U+9610-9611, U+9614, U+961c, U+961f, U+962e,
|
||||
U+9632-9636, U+963b, U+963f-9640, U+9644-9648, U+964b-964d, U+9650, U+9655, U+965b, U+9661-9662,
|
||||
U+9664, U+9668-966a, U+9675-9677, U+9685-9686, U+968b, U+968f-9690, U+9694, U+9698-9699, U+969c,
|
||||
U+96a7, U+96b6, U+96be, U+96c0-96c1, U+96c4-96c7, U+96cc-96cd, U+96cf, U+96d5, U+96e8, U+96ea,
|
||||
U+96f6-96f7, U+96f9, U+96fe, U+9700, U+9704, U+9707, U+9709, U+970d, U+9713, U+9716, U+971c,
|
||||
U+971e, U+9732, U+9738-9739, U+9752, U+9756, U+9759;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.j.woff2) format("woff2");
|
||||
unicode-range: U+9179, U+917f, U+9187, U+9189, U+918b, U+918d, U+9190, U+9192, U+919a-919b, U+91ba,
|
||||
U+91c7, U+91c9-91ca, U+91cc-91cf, U+91d1, U+91dc, U+9274, U+93d6, U+9488-9489, U+948e,
|
||||
U+9492-9493, U+9497, U+9499, U+949d-94a3, U+94a5-94a9, U+94ae, U+94b1, U+94b3, U+94b5, U+94bb,
|
||||
U+94be, U+94c0-94c3, U+94c5-94c6, U+94dc-94dd, U+94e1, U+94e3, U+94ec-94ed, U+94f0-94f2, U+94f6,
|
||||
U+94f8, U+94fa, U+94fe, U+9500-9501, U+9504-9505, U+9508, U+950b-950c, U+9510-9511, U+9517,
|
||||
U+9519-951a, U+9521, U+9523-9526, U+9528, U+952d-9530, U+9539, U+953b, U+9540-9541, U+9547,
|
||||
U+954a, U+954d, U+9550-9551, U+955c, U+9563, U+956d;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.k.woff2) format("woff2");
|
||||
unicode-range: U+9001-9003, U+9005-9006, U+9009-900a, U+900d, U+900f-9012, U+9014, U+9017,
|
||||
U+901a-901b, U+901d-9022, U+902e, U+9038, U+903b-903c, U+903e, U+9041-9042, U+9044, U+9047,
|
||||
U+904d, U+904f-9053, U+9057, U+905b, U+9062-9063, U+9065, U+9068, U+906d-906e, U+9075, U+907d,
|
||||
U+907f-9080, U+9082-9083, U+908b, U+9091, U+9093, U+9099, U+90a2-90a3, U+90a6, U+90aa,
|
||||
U+90ae-90af, U+90b1, U+90b5, U+90b8-90b9, U+90bb, U+90c1, U+90ca, U+90ce, U+90d1, U+90dd, U+90e1,
|
||||
U+90e7-90e8, U+90ed, U+90f4, U+90f8, U+90fd, U+9102, U+9119, U+9149, U+914b-914d, U+9152, U+9157,
|
||||
U+915a, U+915d-915e, U+9161, U+9163, U+9165, U+916a, U+916c, U+916e, U+9171, U+9175-9178;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.l.woff2) format("woff2");
|
||||
unicode-range: U+8e44, U+8e47-8e48, U+8e4a-8e4b, U+8e51, U+8e59, U+8e66, U+8e6c-8e6d, U+8e6f,
|
||||
U+8e72, U+8e74, U+8e76, U+8e7f, U+8e81, U+8e87, U+8e8f, U+8eab-8eac, U+8eaf, U+8eb2, U+8eba,
|
||||
U+8f66-8f69, U+8f6c, U+8f6e-8f72, U+8f74, U+8f7b, U+8f7d, U+8f7f, U+8f83-8f8a, U+8f8d-8f8e,
|
||||
U+8f90-8f91, U+8f93, U+8f95-8f99, U+8f9b-8f9c, U+8f9e-8f9f, U+8fa3, U+8fa8-8fa9, U+8fab,
|
||||
U+8fb0-8fb1, U+8fb9, U+8fbd-8fbe, U+8fc1-8fc2, U+8fc4-8fc5, U+8fc7-8fc8, U+8fce, U+8fd0-8fd1,
|
||||
U+8fd3-8fd5, U+8fd8-8fd9, U+8fdb-8fdf, U+8fe2, U+8fe6, U+8fe8, U+8fea-8feb, U+8fed, U+8ff0,
|
||||
U+8ff3, U+8ff7-8ff9, U+8ffd, U+9000;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.m.woff2) format("woff2");
|
||||
unicode-range: U+8d24-8d31, U+8d34-8d35, U+8d37-8d3f, U+8d41-8d45, U+8d48, U+8d4a-8d4c,
|
||||
U+8d4e-8d50, U+8d54, U+8d56, U+8d58, U+8d5a-8d5b, U+8d5d-8d5e, U+8d60-8d64, U+8d66-8d67, U+8d6b,
|
||||
U+8d70, U+8d74-8d77, U+8d81, U+8d85, U+8d8a-8d8b, U+8d9f, U+8da3, U+8db3-8db4, U+8db8,
|
||||
U+8dbe-8dbf, U+8dc3-8dc4, U+8dcb-8dcc, U+8dd1, U+8dd7, U+8ddb, U+8ddd, U+8ddf, U+8de4, U+8de8,
|
||||
U+8dea, U+8def, U+8df3, U+8df5, U+8df7, U+8dfa-8dfb, U+8e09-8e0a, U+8e0c, U+8e0f, U+8e1d-8e1e,
|
||||
U+8e22, U+8e29-8e2a, U+8e2e, U+8e31, U+8e35, U+8e39, U+8e42;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.n.woff2) format("woff2");
|
||||
unicode-range: U+8bc9-8bcd, U+8bcf, U+8bd1, U+8bd3, U+8bd5, U+8bd7-8bd8, U+8bda-8bdb, U+8bdd-8bde,
|
||||
U+8be0-8be9, U+8beb-8bf5, U+8bf7-8bf8, U+8bfa-8bfb, U+8bfd-8c01, U+8c03-8c06, U+8c08,
|
||||
U+8c0a-8c0b, U+8c0d-8c13, U+8c15, U+8c17, U+8c19-8c1c, U+8c22-8c24, U+8c26-8c2a, U+8c2c-8c2d,
|
||||
U+8c30-8c35, U+8c37, U+8c41, U+8c46, U+8c4c, U+8c61-8c62, U+8c6a-8c6b, U+8c79-8c7a, U+8c82,
|
||||
U+8c89, U+8c8c, U+8d1d-8d1f, U+8d21-8d23;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.o.woff2) format("woff2");
|
||||
unicode-range: U+889c, U+88a4, U+88ab, U+88ad, U+88b1, U+88c1-88c2, U+88c5-88c6, U+88c9,
|
||||
U+88d4-88d5, U+88d8-88d9, U+88df, U+88e3-88e4, U+88e8, U+88f1, U+88f3-88f4, U+88f8-88f9, U+88fe,
|
||||
U+8902, U+8910, U+8912-8913, U+891a-891b, U+8921, U+8925, U+892a-892b, U+8934, U+8936, U+8941,
|
||||
U+8944, U+895e-895f, U+8966, U+897f, U+8981, U+8986, U+89c1-89c2, U+89c4-89c6, U+89c8-89cb,
|
||||
U+89ce, U+89d0-89d2, U+89e3, U+89e5-89e6, U+8a00, U+8a07, U+8a79, U+8a89-8a8a, U+8a93, U+8b66,
|
||||
U+8b6c, U+8ba1-8bab, U+8bad-8bb0, U+8bb2-8bb3, U+8bb6-8bba, U+8bbc-8bc1, U+8bc4-8bc6, U+8bc8;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.p.woff2) format("woff2");
|
||||
unicode-range: U+8695, U+869c, U+86a3-86a4, U+86a7, U+86aa, U+86af, U+86b1, U+86c0, U+86c6-86c7,
|
||||
U+86ca-86cb, U+86d0, U+86d4, U+86d9, U+86db, U+86df, U+86e4, U+86ee, U+86f0, U+86f9, U+86fe,
|
||||
U+8700, U+8702-8703, U+8708-8709, U+870d, U+8712-8713, U+8715, U+8717-8718, U+871a, U+871c,
|
||||
U+8721, U+8725, U+8734, U+8737, U+873b, U+873f, U+8747, U+8749, U+874c, U+874e, U+8757, U+8759,
|
||||
U+8760, U+8763, U+8774, U+8776, U+877c, U+8782-8783, U+8785, U+878d, U+8793, U+879f, U+87af,
|
||||
U+87b3, U+87ba, U+87c6, U+87ca, U+87d1-87d2, U+87e0, U+87e5, U+87f9, U+87fe, U+8815, U+8822,
|
||||
U+8839, U+8840, U+8845, U+884c-884d, U+8854, U+8857, U+8859, U+8861, U+8863, U+8865, U+8868,
|
||||
U+886b-886c, U+8870, U+8877, U+887d-887f, U+8881-8882, U+8884-8885, U+8888, U+888b, U+888d,
|
||||
U+8892, U+8896;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.q.woff2) format("woff2");
|
||||
unicode-range: U+83dc-83dd, U+83e0, U+83e9, U+83f1-83f2, U+8403-8404, U+840b-840e, U+841d,
|
||||
U+8424-8428, U+843d, U+8451, U+8457, U+8459, U+845b, U+8461, U+8463, U+8469, U+846b-846c, U+8471,
|
||||
U+8475, U+847a, U+8482, U+848b, U+8499, U+849c, U+84b2, U+84b8, U+84bf, U+84c4, U+84c9, U+84d1,
|
||||
U+84d6, U+84dd, U+84df, U+84e6, U+84ec, U+8511, U+8513, U+8517, U+851a, U+851f, U+8521,
|
||||
U+852b-852c, U+8537, U+853b-853d, U+8549-854a, U+8559, U+8574, U+857e, U+8584, U+8587, U+858f,
|
||||
U+859b, U+85aa, U+85af-85b0, U+85c9, U+85cf-85d0, U+85d3, U+85d5, U+85e4, U+85e9, U+85fb, U+8611,
|
||||
U+8638, U+864e-8651, U+8654, U+865a, U+865e, U+866b-866c, U+8671, U+8679, U+867d-867e,
|
||||
U+8680-8682, U+868a, U+868c-868d, U+8693;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.r.woff2) format("woff2");
|
||||
unicode-range: U+8273, U+827a, U+827e, U+8282, U+828a-828b, U+828d, U+8292, U+8299, U+829c-829d,
|
||||
U+82a5-82a6, U+82a9, U+82ab-82ad, U+82af, U+82b1, U+82b3, U+82b7-82b9, U+82bd, U+82c7, U+82cd,
|
||||
U+82cf, U+82d1, U+82d3-82d4, U+82d7, U+82db, U+82de-82df, U+82e3, U+82e5-82e6, U+82eb, U+82ef,
|
||||
U+82f1, U+82f9, U+82fb, U+8301-8305, U+8309, U+830e, U+8314, U+8317, U+8327-8328, U+832b-832c,
|
||||
U+832f, U+8335-8336, U+8338-8339, U+8340, U+8346-8347, U+8349, U+834f-8352, U+8354, U+835a,
|
||||
U+835c, U+8361, U+8363-8364, U+8367, U+836b, U+836f, U+8377, U+837c, U+8386, U+8389, U+838e,
|
||||
U+8393, U+839e, U+83a0, U+83ab, U+83b1-83b4, U+83b7, U+83b9-83ba, U+83bd, U+83c1, U+83c5, U+83c7,
|
||||
U+83ca, U+83cc, U+83cf;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.s.woff2) format("woff2");
|
||||
unicode-range: U+80de, U+80e1, U+80e7, U+80ea-80eb, U+80ed, U+80ef-80f0, U+80f3-80f4, U+80f6,
|
||||
U+80f8, U+80fa, U+80fd, U+8102, U+8106, U+8109-810a, U+810d, U+810f-8111, U+8113-8114, U+8116,
|
||||
U+8118, U+811a, U+812f, U+8131, U+8138, U+813e, U+8146, U+814a-814c, U+8150-8151, U+8154-8155,
|
||||
U+8165, U+816e, U+8170, U+8174, U+8179-817c, U+817e-8180, U+818a, U+818f, U+8198, U+819b-819d,
|
||||
U+81a8, U+81b3, U+81ba-81bb, U+81c0, U+81c2-81c3, U+81c6, U+81ca, U+81e3, U+81ea, U+81ec-81ed,
|
||||
U+81f3-81f4, U+81fb-81fc, U+81fe, U+8200, U+8205-8206, U+820c-820d, U+8210, U+8212, U+8214,
|
||||
U+821c, U+821e-821f, U+822a-822c, U+8230-8231, U+8235-8239, U+8247, U+8258, U+826f-8270, U+8272;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.t.woff2) format("woff2");
|
||||
unicode-range: U+7f72, U+7f81, U+7f8a, U+7f8c, U+7f8e, U+7f94, U+7f9a, U+7f9e, U+7fa1, U+7fa4,
|
||||
U+7fb2, U+7fb8-7fb9, U+7fbd, U+7fc1, U+7fc5, U+7fcc, U+7fce, U+7fd4-7fd5, U+7fd8, U+7fdf-7fe1,
|
||||
U+7fe6, U+7fe9, U+7ff0-7ff1, U+7ff3, U+7ffb-7ffc, U+8000-8001, U+8003, U+8005, U+800c-800d,
|
||||
U+8010, U+8012, U+8015, U+8017-8019, U+8027, U+802a, U+8033, U+8036-8038, U+803b, U+803d, U+803f,
|
||||
U+8042, U+8046, U+804a-804c, U+8052, U+8054, U+8058, U+805a, U+806a, U+807f, U+8083-8084,
|
||||
U+8086-8087, U+8089, U+808b-808c, U+8096, U+8098, U+809a-809b, U+809d, U+80a0-80a2, U+80a4-80a5,
|
||||
U+80a9-80aa, U+80ae-80af, U+80b2, U+80b4, U+80ba, U+80be-80c1, U+80c3-80c4, U+80c6, U+80cc,
|
||||
U+80ce, U+80d6, U+80da-80dc;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.u.woff2) format("woff2");
|
||||
unicode-range: U+7eb5-7eba, U+7ebd, U+7ebf, U+7ec2-7eca, U+7ecd-7ed5, U+7ed8-7edf, U+7ee1-7ee3,
|
||||
U+7ee5-7ee7, U+7ee9-7eeb, U+7eed, U+7eef-7ef0, U+7ef3-7ef8, U+7efc-7efd, U+7eff-7f00,
|
||||
U+7f04-7f09, U+7f0e-7f0f, U+7f13-7f16, U+7f18, U+7f1a, U+7f1c-7f1d, U+7f1f-7f22, U+7f24-7f26,
|
||||
U+7f28-7f2a, U+7f2d-7f2e, U+7f30, U+7f34, U+7f38, U+7f3a, U+7f42, U+7f50-7f51, U+7f54-7f55,
|
||||
U+7f57, U+7f5a, U+7f61-7f62, U+7f69-7f6a, U+7f6e;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.v.woff2) format("woff2");
|
||||
unicode-range: U+7b4c, U+7b4f-7b52, U+7b54, U+7b56, U+7b5b, U+7b5d, U+7b75, U+7b77, U+7b79, U+7b7e,
|
||||
U+7b80, U+7b8d, U+7b94-7b95, U+7b97, U+7ba1, U+7ba9-7bab, U+7bad, U+7bb1, U+7bb8, U+7bc6-7bc7,
|
||||
U+7bd1, U+7bd3, U+7bd9, U+7bdd, U+7be1, U+7bee, U+7bf1, U+7bf7, U+7bfe, U+7c07, U+7c0c, U+7c27,
|
||||
U+7c2a, U+7c38, U+7c3f, U+7c41, U+7c4d, U+7c73, U+7c7b, U+7c7d, U+7c89, U+7c92, U+7c95,
|
||||
U+7c97-7c98, U+7c9f, U+7ca4-7ca5, U+7caa, U+7cae, U+7cb1, U+7cb3, U+7cb9, U+7cbc-7cbe, U+7cc5,
|
||||
U+7cca, U+7cd5-7cd7, U+7cd9, U+7cdc, U+7cdf-7ce0, U+7cef, U+7cfb, U+7d0a, U+7d20, U+7d22, U+7d27,
|
||||
U+7d2b, U+7d2f, U+7d6e, U+7e41, U+7e82, U+7ea0-7ea4, U+7ea6-7ea8, U+7eaa-7ead, U+7eaf-7eb3;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.w.woff2) format("woff2");
|
||||
unicode-range: U+7981, U+7984-7985, U+798f, U+79b9, U+79bb, U+79bd-79be, U+79c0-79c1, U+79c3,
|
||||
U+79c6, U+79c9, U+79cb, U+79cd, U+79d1-79d2, U+79d8, U+79df, U+79e3-79e4, U+79e6-79e7, U+79e9,
|
||||
U+79ef-79f0, U+79f8, U+79fb, U+79fd, U+7a00, U+7a0b, U+7a0d-7a0e, U+7a14, U+7a17, U+7a1a, U+7a20,
|
||||
U+7a33, U+7a37, U+7a39, U+7a3b-7a3d, U+7a3f, U+7a46, U+7a51, U+7a57, U+7a74, U+7a76-7a77,
|
||||
U+7a79-7a7a, U+7a7f, U+7a81, U+7a83-7a84, U+7a88, U+7a8d, U+7a91-7a92, U+7a95-7a98, U+7a9c-7a9d,
|
||||
U+7a9f, U+7aa5-7aa6, U+7abf, U+7acb, U+7ad6, U+7ad9, U+7ade-7ae0, U+7ae3, U+7ae5-7ae6, U+7aed,
|
||||
U+7aef, U+7af9, U+7afd, U+7aff, U+7b03, U+7b06, U+7b08, U+7b0b, U+7b11, U+7b14, U+7b19, U+7b1b,
|
||||
U+7b20, U+7b26, U+7b28, U+7b2c, U+7b3a, U+7b3c, U+7b49, U+7b4b;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.x.woff2) format("woff2");
|
||||
unicode-range: U+77aa, U+77ac, U+77b0, U+77b3, U+77b5, U+77bb, U+77bf, U+77d7, U+77db-77dc,
|
||||
U+77e2-77e3, U+77e5, U+77e9, U+77eb, U+77ed-77ee, U+77f3, U+77fd-77ff, U+7801-7802, U+780c-780d,
|
||||
U+7812, U+7814, U+7816, U+781a, U+781d, U+7823, U+7825, U+7827, U+7830, U+7834, U+7837-7838,
|
||||
U+783a, U+783e, U+7840, U+7845, U+784c, U+7852, U+7855, U+785d, U+786b-786c, U+786e, U+787c,
|
||||
U+7887, U+7889, U+788c-788e, U+7891, U+7897-7898, U+789c, U+789f, U+78a5, U+78a7, U+78b0-78b1,
|
||||
U+78b3-78b4, U+78be, U+78c1, U+78c5, U+78ca-78cb, U+78d0, U+78d5, U+78e8, U+78ec, U+78f7, U+78fa,
|
||||
U+7901, U+7934, U+793a, U+793c, U+793e, U+7940-7941, U+7948, U+7956-7957, U+795a-795b,
|
||||
U+795d-7960, U+7965, U+7968, U+796d, U+796f, U+7977-7978, U+797a, U+7980;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.y.woff2) format("woff2");
|
||||
unicode-range: U+761f, U+7624, U+7626, U+7629-762b, U+7634-7635, U+7638, U+763e, U+764c, U+7656,
|
||||
U+765e, U+7663, U+766b, U+7678, U+767b, U+767d-767e, U+7682, U+7684, U+7686-7688, U+768b, U+768e,
|
||||
U+7691, U+7693, U+7696, U+7699, U+76ae, U+76b1, U+76b4, U+76bf, U+76c2, U+76c5-76c6, U+76c8,
|
||||
U+76ca, U+76ce-76d2, U+76d4, U+76d6-76d8, U+76db, U+76df, U+76ee-76ef, U+76f2, U+76f4,
|
||||
U+76f8-76f9, U+76fc, U+76fe, U+7701, U+7708-7709, U+770b, U+771f-7720, U+7726, U+7728-7729,
|
||||
U+772f, U+7736-7738, U+773a, U+773c, U+7740-7741, U+7750-7751, U+775a-775b, U+7761, U+7763,
|
||||
U+7765-7766, U+7768, U+776b-776c, U+7779, U+777d, U+777f, U+7784-7785, U+778c, U+778e,
|
||||
U+7791-7792, U+779f-77a0, U+77a5, U+77a7, U+77a9;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.z.woff2) format("woff2");
|
||||
unicode-range: U+7435-7436, U+743c, U+7455, U+7459-745a, U+745c, U+745e-745f, U+7470, U+7476,
|
||||
U+7480, U+7483, U+7487, U+749c, U+749e, U+74a7-74a8, U+74dc, U+74e2-74e4, U+74e6, U+74ee,
|
||||
U+74f6-74f7, U+7504, U+7518, U+751a, U+751c, U+751f, U+7525, U+7528-7529, U+752b-752d,
|
||||
U+7530-7533, U+7535, U+7537-7538, U+753b, U+7545, U+754c, U+754f, U+7554, U+7559, U+755c,
|
||||
U+7565-7566, U+756a, U+7574, U+7578, U+7583, U+7586, U+758f, U+7591, U+7597, U+7599-759a, U+759f,
|
||||
U+75a1, U+75a4-75a5, U+75ab, U+75ae-75b2, U+75b4-75b5, U+75b9, U+75bc-75be, U+75c5, U+75c7-75ca,
|
||||
U+75cd, U+75d2, U+75d4-75d5, U+75d8, U+75db, U+75de, U+75e2-75e3, U+75e8, U+75ea, U+75f0, U+75f4,
|
||||
U+75f9, U+7600-7601;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.aa.woff2) format("woff2");
|
||||
unicode-range: U+725f, U+7261-7262, U+7267, U+7269, U+7272, U+7275, U+7279-727a, U+7280-7281,
|
||||
U+7284, U+728a, U+7292, U+729f, U+72ac, U+72af, U+72b6-72b9, U+72c1-72c2, U+72c4, U+72c8, U+72ce,
|
||||
U+72d0, U+72d2, U+72d7, U+72d9, U+72de, U+72e0-72e1, U+72e9, U+72ec-72f2, U+72f7-72f8, U+72fc,
|
||||
U+730a, U+730e, U+7316, U+731b-731d, U+7322, U+7325, U+7329-732c, U+732e, U+7334, U+733e-733f,
|
||||
U+7350, U+7357, U+7360, U+736d, U+7384, U+7387, U+7389, U+738b, U+7396, U+739b, U+73a9, U+73ab,
|
||||
U+73af-73b0, U+73b2, U+73b7, U+73ba-73bb, U+73c0, U+73c8, U+73ca, U+73cd, U+73d0-73d1, U+73d9,
|
||||
U+73e0, U+73ed, U+7403, U+7405-7406, U+7409-740a, U+740f-7410, U+741a, U+7422, U+7425, U+742a,
|
||||
U+7433-7434;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ab.woff2) format("woff2");
|
||||
unicode-range: U+706d, U+706f-7070, U+7075-7076, U+7078, U+707c, U+707e-707f, U+7089-708a, U+708e,
|
||||
U+7092, U+7094-7096, U+7099, U+70ab-70af, U+70b1, U+70b3, U+70b8-70b9, U+70bc-70bd, U+70c1-70c3,
|
||||
U+70c8, U+70ca, U+70d8-70d9, U+70db, U+70df, U+70e4, U+70e6-70e7, U+70e9, U+70eb-70ed, U+70ef,
|
||||
U+70f7, U+70f9, U+70fd, U+7109-710a, U+7115, U+7119-711a, U+7126, U+7130-7131, U+7136, U+714c,
|
||||
U+714e, U+715e, U+7164, U+7166-7168, U+716e, U+7172-7173, U+717d, U+7184, U+718a, U+718f, U+7194,
|
||||
U+7198-7199, U+719f-71a0, U+71a8, U+71ac, U+71b9, U+71c3, U+71ce, U+71d5, U+71e5, U+7206, U+722a,
|
||||
U+722c, U+7231, U+7235-7239, U+723d, U+7247-7248, U+724c-724d, U+7252, U+7259, U+725b;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ac.woff2) format("woff2");
|
||||
unicode-range: U+6df7, U+6df9, U+6dfb, U+6e05, U+6e0a, U+6e0d-6e0e, U+6e10, U+6e14, U+6e17, U+6e1a,
|
||||
U+6e1d, U+6e20-6e21, U+6e23-6e25, U+6e29, U+6e2d, U+6e2f, U+6e32, U+6e34, U+6e38, U+6e3a, U+6e43,
|
||||
U+6e4d, U+6e56, U+6e58, U+6e5b, U+6e6e, U+6e7e-6e7f, U+6e83, U+6e85, U+6e89, U+6e90, U+6e9c,
|
||||
U+6ea2, U+6ea5, U+6eaa, U+6eaf, U+6eb6, U+6eba, U+6ec1, U+6ec7, U+6ecb, U+6ed1, U+6ed3-6ed5,
|
||||
U+6eda, U+6ede, U+6ee1, U+6ee4-6ee6, U+6ee8-6ee9, U+6ef4, U+6f02, U+6f06, U+6f09, U+6f0f,
|
||||
U+6f13-6f15, U+6f20, U+6f29-6f2b, U+6f31, U+6f33, U+6f3e, U+6f46-6f47, U+6f4d, U+6f58, U+6f5c,
|
||||
U+6f5e, U+6f62, U+6f66, U+6f6d-6f6e, U+6f84, U+6f88-6f89, U+6f8e, U+6f9c, U+6fa1, U+6fb3, U+6fb9,
|
||||
U+6fc0, U+6fd1-6fd2, U+6fe1, U+7011, U+701a, U+7023, U+704c, U+706b;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ad.woff2) format("woff2");
|
||||
unicode-range: U+6ccc, U+6cd3, U+6cd5, U+6cdb, U+6cde, U+6ce1-6ce3, U+6ce5, U+6ce8, U+6cea-6ceb,
|
||||
U+6cef-6cf1, U+6cf3, U+6cf5, U+6cfb-6cfe, U+6d01, U+6d0b, U+6d12, U+6d17, U+6d1b, U+6d1e, U+6d25,
|
||||
U+6d27, U+6d2a, U+6d31-6d32, U+6d3b-6d3e, U+6d41, U+6d43, U+6d45-6d47, U+6d4a-6d4b, U+6d4e-6d4f,
|
||||
U+6d51, U+6d53, U+6d59-6d5a, U+6d63, U+6d66, U+6d69-6d6a, U+6d6e, U+6d74, U+6d77-6d78, U+6d82,
|
||||
U+6d85, U+6d88-6d89, U+6d8c, U+6d8e, U+6d93, U+6d95, U+6d9b, U+6d9d, U+6d9f-6da1, U+6da3-6da4,
|
||||
U+6da6-6daa, U+6dae-6daf, U+6db2, U+6db5, U+6db8, U+6dc0, U+6dc4-6dc7, U+6dcb-6dcc, U+6dd1,
|
||||
U+6dd6, U+6dd8-6dd9, U+6de1, U+6de4, U+6deb-6dec, U+6dee, U+6df1, U+6df3;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ae.woff2) format("woff2");
|
||||
unicode-range: U+6b92, U+6b96, U+6b9a, U+6ba1, U+6bb4-6bb5, U+6bb7, U+6bbf, U+6bc1, U+6bc5, U+6bcb,
|
||||
U+6bcd, U+6bcf, U+6bd2, U+6bd4-6bd7, U+6bd9, U+6bdb, U+6be1, U+6beb, U+6bef, U+6c05, U+6c0f,
|
||||
U+6c11, U+6c13-6c14, U+6c16, U+6c1b, U+6c1f, U+6c22, U+6c24, U+6c26-6c28, U+6c2e-6c30, U+6c32,
|
||||
U+6c34, U+6c38, U+6c3d, U+6c40-6c42, U+6c47, U+6c49, U+6c50, U+6c55, U+6c57, U+6c5b, U+6c5d-6c61,
|
||||
U+6c64, U+6c68-6c6a, U+6c70, U+6c72, U+6c76, U+6c79, U+6c7d-6c7e, U+6c81-6c83, U+6c86,
|
||||
U+6c88-6c89, U+6c8c, U+6c8f-6c90, U+6c93, U+6c99, U+6c9b, U+6c9f, U+6ca1, U+6ca4-6ca7,
|
||||
U+6caa-6cab, U+6cae, U+6cb3, U+6cb8-6cb9, U+6cbb-6cbf, U+6cc4-6cc5, U+6cc9-6cca;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.af.woff2) format("woff2");
|
||||
unicode-range: U+68ad, U+68af-68b0, U+68b3, U+68b5, U+68c0, U+68c2, U+68c9, U+68cb, U+68cd, U+68d2,
|
||||
U+68d5, U+68d8, U+68da, U+68e0, U+68ee, U+68f1, U+68f5, U+68fa, U+6905, U+690d-690e, U+6912,
|
||||
U+692d, U+6930, U+693d, U+693f, U+6942, U+6954, U+6957, U+695a, U+695e, U+6963, U+696b,
|
||||
U+6977-6978, U+697c, U+6982, U+6984, U+6986, U+6994, U+699c, U+69a8, U+69ad, U+69b4, U+69b7,
|
||||
U+69bb, U+69c1, U+69cc, U+69d0, U+69db, U+69fd, U+69ff, U+6a0a, U+6a1f, U+6a21, U+6a2a, U+6a31,
|
||||
U+6a35, U+6a3d, U+6a44, U+6a47, U+6a58-6a59, U+6a61, U+6a71, U+6a80, U+6a84, U+6a8e, U+6a90,
|
||||
U+6aac, U+6b20-6b23, U+6b27, U+6b32, U+6b3a, U+6b3e, U+6b47, U+6b49, U+6b4c, U+6b62-6b67, U+6b6a,
|
||||
U+6b79, U+6b7b-6b7c, U+6b81, U+6b83-6b84, U+6b86-6b87, U+6b89-6b8b;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ag.woff2) format("woff2");
|
||||
unicode-range: U+6756, U+675c, U+675e-6761, U+6765, U+6768, U+676d, U+676f-6770, U+6773, U+6775,
|
||||
U+6777, U+677c, U+677e-677f, U+6781, U+6784, U+6787, U+6789, U+6790, U+6795, U+6797, U+679a,
|
||||
U+679c-679d, U+67a2-67a3, U+67aa-67ab, U+67ad, U+67af-67b0, U+67b6-67b7, U+67c4, U+67cf-67d4,
|
||||
U+67d9-67da, U+67dc, U+67de, U+67e0, U+67e5, U+67e9, U+67ec, U+67ef, U+67f1, U+67f3-67f4,
|
||||
U+67ff-6800, U+6805, U+6807-6808, U+680b, U+680f, U+6811, U+6813, U+6816-6817, U+6821,
|
||||
U+6829-682a, U+6837-6839, U+683c-683d, U+6840, U+6842-6843, U+6845-6846, U+6848, U+684c,
|
||||
U+6850-6851, U+6853-6854, U+6863, U+6865, U+6868-6869, U+6874, U+6876, U+6881, U+6885-6886,
|
||||
U+6893, U+6897, U+68a2, U+68a6-68a8;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ah.woff2) format("woff2");
|
||||
unicode-range: U+65f7, U+65fa, U+6602, U+6606, U+660a, U+660c, U+660e-660f, U+6613-6614, U+6619,
|
||||
U+661d, U+661f-6620, U+6625, U+6627-6628, U+662d, U+662f, U+6631, U+6635, U+663c, U+663e, U+6643,
|
||||
U+664b-664c, U+664f, U+6652-6653, U+6655-6657, U+665a, U+6664, U+6666, U+6668, U+666e-6670,
|
||||
U+6674, U+6676-6677, U+667a, U+667e, U+6682, U+6684, U+6687, U+668c, U+6691, U+6696-6697, U+669d,
|
||||
U+66a7, U+66ae, U+66b4, U+66d9, U+66dc-66dd, U+66e6, U+66f0, U+66f2-66f4, U+66f9, U+66fc,
|
||||
U+66fe-6700, U+6708-6709, U+670b, U+670d, U+6714-6715, U+6717, U+671b, U+671d, U+671f, U+6726,
|
||||
U+6728, U+672a-672d, U+672f, U+6731, U+6734-6735, U+673a, U+673d, U+6740, U+6742-6743, U+6746,
|
||||
U+6748-6749, U+674e-6751;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ai.woff2) format("woff2");
|
||||
unicode-range: U+6467, U+6469, U+6478-6479, U+6482, U+6485, U+6487, U+6491-6492, U+6495, U+649e,
|
||||
U+64a4, U+64a9, U+64ac-64ae, U+64b0, U+64b5, U+64b8, U+64ba, U+64bc, U+64c2, U+64c5, U+64cd-64ce,
|
||||
U+64d2, U+64d8, U+64de, U+64e2, U+64e6, U+6500, U+6512, U+6518, U+6525, U+652b, U+652f, U+6536,
|
||||
U+6538-6539, U+653b, U+653e-653f, U+6545, U+6548, U+654c, U+654f, U+6551, U+6555-6556, U+6559,
|
||||
U+655b, U+655d-655e, U+6562-6563, U+6566, U+656c, U+6570, U+6572, U+6574, U+6577, U+6587,
|
||||
U+658b-658c, U+6590-6591, U+6593, U+6597, U+6599, U+659c, U+659f, U+65a1, U+65a4-65a5, U+65a7,
|
||||
U+65a9, U+65ab, U+65ad, U+65af-65b0, U+65b9, U+65bd, U+65c1, U+65c4-65c5, U+65cb-65cc, U+65cf,
|
||||
U+65d7, U+65e0, U+65e2, U+65e5-65e9, U+65ec-65ed, U+65f1, U+65f6;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.aj.woff2) format("woff2");
|
||||
unicode-range: U+6323-6325, U+6328, U+632a-632b, U+632f, U+6332, U+633a, U+633d, U+6342,
|
||||
U+6345-6346, U+6349, U+634b-6350, U+6355, U+635e-635f, U+6361-6363, U+6367, U+636e, U+6371,
|
||||
U+6376-6377, U+637a-637b, U+6380, U+6382, U+6387-6389, U+638c, U+638f-6390, U+6392, U+6396,
|
||||
U+6398, U+63a0, U+63a2-63a3, U+63a5, U+63a7-63aa, U+63ac, U+63b0, U+63b3-63b4, U+63b7-63b8,
|
||||
U+63ba, U+63c4, U+63c9, U+63cd, U+63cf-63d0, U+63d2, U+63d6, U+63e1, U+63e3, U+63e9-63ea, U+63ed,
|
||||
U+63f4, U+63f6, U+63fd, U+6400-6402, U+6405, U+640f-6410, U+6413-6414, U+641c, U+641e, U+6421,
|
||||
U+642a, U+642c-642d, U+643a, U+643d, U+6441, U+6444, U+6446-6448, U+644a, U+6452, U+6454, U+6458,
|
||||
U+645e;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ak.woff2) format("woff2");
|
||||
unicode-range: U+6258, U+625b, U+6263, U+6266-6267, U+6269-6270, U+6273, U+6276, U+6279, U+627c,
|
||||
U+627e-6280, U+6284, U+6289-628a, U+6291-6293, U+6295-6298, U+629a-629b, U+62a0-62a2,
|
||||
U+62a4-62a5, U+62a8, U+62ab-62ac, U+62b1, U+62b5, U+62b9, U+62bc-62bd, U+62bf, U+62c2,
|
||||
U+62c4-62ca, U+62cc-62ce, U+62d0, U+62d2-62d4, U+62d6-62d9, U+62db-62dc, U+62df, U+62e2-62e3,
|
||||
U+62e5-62e9, U+62ec-62ed, U+62ef, U+62f1, U+62f3-62f4, U+62f7, U+62fc-62ff, U+6301-6302, U+6307,
|
||||
U+6309, U+630e, U+6311, U+6316, U+631a-631b, U+631d-6321;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.al.woff2) format("woff2");
|
||||
unicode-range: U+60cb, U+60d1, U+60d5, U+60d8, U+60da, U+60dc, U+60df-60e0, U+60e6-60e9,
|
||||
U+60eb-60f0, U+60f3-60f4, U+60f6, U+60f9-60fa, U+6101, U+6108-6109, U+610e-610f, U+6115, U+611a,
|
||||
U+611f-6120, U+6123-6124, U+6127, U+612b, U+613f, U+6148, U+614a, U+614c, U+614e, U+6151, U+6155,
|
||||
U+6162, U+6167-6168, U+6170, U+6175, U+6177, U+618b, U+618e, U+6194, U+61a7-61a9, U+61ac, U+61be,
|
||||
U+61c2, U+61c8, U+61ca, U+61d1-61d2, U+61d4, U+61e6, U+61f5, U+61ff, U+6208, U+620a, U+620c-6212,
|
||||
U+6216, U+6218, U+621a-621b, U+621f, U+622a, U+622c, U+622e, U+6233-6234, U+6237, U+623e-6241,
|
||||
U+6247-6249, U+624b, U+624d-624e, U+6251-6254;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.am.woff2) format("woff2");
|
||||
unicode-range: U+5fcc-5fcd, U+5fcf-5fd2, U+5fd6-5fd9, U+5fdd, U+5fe0-5fe1, U+5fe4, U+5fe7,
|
||||
U+5fea-5feb, U+5ff1, U+5ff5, U+5ffb, U+5ffd-6002, U+6005-6006, U+600d-600f, U+6012, U+6014-6016,
|
||||
U+6019, U+601c-601d, U+6020-6021, U+6025-6028, U+602a, U+602f, U+6035, U+603b-603c, U+6041,
|
||||
U+6043, U+604b, U+604d, U+6050, U+6052, U+6055, U+6059-605a, U+6062-6064, U+6068-606d,
|
||||
U+606f-6070, U+6073, U+6076, U+6078-607c, U+607f, U+6084, U+6089, U+608c-608d, U+6094, U+6096,
|
||||
U+609a, U+609f-60a0, U+60a3, U+60a6, U+60a8, U+60ac, U+60af, U+60b1-60b2, U+60b4, U+60b8,
|
||||
U+60bb-60bc, U+60c5-60c6, U+60ca;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.an.woff2) format("woff2");
|
||||
unicode-range: U+5e7f, U+5e84, U+5e86-5e87, U+5e8a, U+5e8f-5e90, U+5e93-5e97, U+5e99-5e9a, U+5e9c,
|
||||
U+5e9e-5e9f, U+5ea6-5ea7, U+5ead, U+5eb5-5eb8, U+5ec9-5eca, U+5ed1, U+5ed3, U+5ed6, U+5ef6-5ef7,
|
||||
U+5efa, U+5f00, U+5f02-5f04, U+5f08, U+5f0a-5f0b, U+5f0f, U+5f11, U+5f13, U+5f15, U+5f17-5f18,
|
||||
U+5f1b, U+5f1f-5f20, U+5f25-5f27, U+5f29, U+5f2f, U+5f31, U+5f39-5f3a, U+5f52-5f53, U+5f55,
|
||||
U+5f57, U+5f5d, U+5f62, U+5f64, U+5f66, U+5f69-5f6a, U+5f6c-5f6d, U+5f70-5f71, U+5f77, U+5f79,
|
||||
U+5f7b-5f7c, U+5f80-5f81, U+5f84-5f85, U+5f87-5f8b, U+5f90, U+5f92, U+5f95, U+5f97-5f98, U+5fa1,
|
||||
U+5fa8, U+5faa, U+5fad-5fae, U+5fb5, U+5fb7, U+5fbc-5fbd, U+5fc3, U+5fc5-5fc6;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ao.woff2) format("woff2");
|
||||
unicode-range: U+5c7f, U+5c81-5c82, U+5c8c, U+5c94, U+5c96-5c97, U+5c9a-5c9b, U+5ca9, U+5cad,
|
||||
U+5cb3, U+5cb8, U+5cbf, U+5ccb, U+5cd9, U+5ce1, U+5ce5-5ce6, U+5ce8, U+5cea, U+5ced, U+5cf0,
|
||||
U+5cfb, U+5d02, U+5d07, U+5d0e, U+5d14, U+5d16, U+5d1b, U+5d24, U+5d29, U+5d2d, U+5d34, U+5d3d,
|
||||
U+5d4c, U+5d58, U+5d6c, U+5d82, U+5d99, U+5dc5, U+5dcd, U+5ddd-5dde, U+5de1-5de2, U+5de5-5de9,
|
||||
U+5deb, U+5dee, U+5df1-5df4, U+5df7, U+5dfe, U+5e01-5e03, U+5e05-5e06, U+5e08, U+5e0c,
|
||||
U+5e10-5e11, U+5e15-5e16, U+5e18, U+5e1a-5e1d, U+5e26-5e27, U+5e2d-5e2e, U+5e37-5e38,
|
||||
U+5e3c-5e3d, U+5e42, U+5e44-5e45, U+5e4c, U+5e54-5e55, U+5e61-5e62, U+5e72-5e74, U+5e76, U+5e78,
|
||||
U+5e7a-5e7d;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ap.woff2) format("woff2");
|
||||
unicode-range: U+5b85, U+5b87-5b89, U+5b8b-5b8c, U+5b8f, U+5b95, U+5b97-5b9e, U+5ba0-5ba4, U+5ba6,
|
||||
U+5baa-5bab, U+5bb0, U+5bb3-5bb6, U+5bb9, U+5bbd-5bbf, U+5bc2, U+5bc4-5bc7, U+5bcc, U+5bd0,
|
||||
U+5bd2-5bd3, U+5bdd-5bdf, U+5be1, U+5be4-5be5, U+5be8, U+5bf0, U+5bf8-5bfc, U+5bff, U+5c01,
|
||||
U+5c04, U+5c06, U+5c09-5c0a, U+5c0f, U+5c11, U+5c14, U+5c16, U+5c18, U+5c1a, U+5c1d, U+5c24,
|
||||
U+5c27, U+5c2c, U+5c31, U+5c34, U+5c38-5c3a, U+5c3c-5c42, U+5c45, U+5c48-5c4b, U+5c4e-5c51,
|
||||
U+5c55, U+5c5e, U+5c60-5c61, U+5c65, U+5c6f, U+5c71, U+5c79;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.aq.woff2) format("woff2");
|
||||
unicode-range: U+5996, U+5999, U+599e, U+59a5, U+59a8-59aa, U+59ae, U+59b2, U+59b9, U+59bb, U+59be,
|
||||
U+59c6, U+59cb, U+59d0-59d1, U+59d3-59d4, U+59d7-59d8, U+59da, U+59dc-59dd, U+59e3, U+59e5,
|
||||
U+59e8, U+59ec, U+59f9, U+59fb, U+59ff, U+5a01, U+5a03-5a04, U+5a06-5a07, U+5a11, U+5a13, U+5a18,
|
||||
U+5a1c, U+5a1f-5a20, U+5a25, U+5a29, U+5a31-5a32, U+5a34, U+5a36, U+5a3c, U+5a40, U+5a46,
|
||||
U+5a49-5a4a, U+5a5a, U+5a62, U+5a6a, U+5a74, U+5a76-5a77, U+5a7f, U+5a92, U+5a9a-5a9b,
|
||||
U+5ab2-5ab3, U+5ac1-5ac2, U+5ac9, U+5acc, U+5ad4, U+5ad6, U+5ae1, U+5ae3, U+5ae6, U+5ae9, U+5b09,
|
||||
U+5b34, U+5b37, U+5b40, U+5b50, U+5b54-5b55, U+5b57-5b59, U+5b5c-5b5d, U+5b5f, U+5b63-5b64,
|
||||
U+5b66, U+5b69-5b6a, U+5b6c, U+5b70-5b71, U+5b75, U+5b7a, U+5b7d, U+5b81, U+5b83;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ar.woff2) format("woff2");
|
||||
unicode-range: U+57ce, U+57d4, U+57df-57e0, U+57f9-57fa, U+5800, U+5802, U+5806, U+5811, U+5815,
|
||||
U+5821, U+5824, U+582a, U+5830, U+5835, U+584c, U+5851, U+5854, U+5858, U+585e, U+586b, U+587e,
|
||||
U+5883, U+5885, U+5892-5893, U+5899, U+589e-589f, U+58a8-58a9, U+58c1, U+58d1, U+58d5, U+58e4,
|
||||
U+58eb-58ec, U+58ee, U+58f0, U+58f3, U+58f6, U+58f9, U+5904, U+5907, U+590d, U+590f, U+5915-5916,
|
||||
U+5919-591a, U+591c, U+591f, U+5927, U+5929-592b, U+592d-592f, U+5931, U+5934, U+5937-593a,
|
||||
U+5942, U+5944, U+5947-5949, U+594b, U+594e-594f, U+5951, U+5954-5957, U+595a, U+5960, U+5962,
|
||||
U+5965, U+5973-5974, U+5976, U+5978-5979, U+597d, U+5981-5984, U+5986-5988, U+598a, U+598d,
|
||||
U+5992-5993;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.as.woff2) format("woff2");
|
||||
unicode-range: U+561b, U+561e-561f, U+5624, U+562d, U+5631-5632, U+5634, U+5636, U+5639, U+563b,
|
||||
U+563f, U+564c, U+564e, U+5654, U+5657, U+5659, U+565c, U+5662, U+5664, U+5668-566c, U+5676,
|
||||
U+567c, U+5685, U+568e-568f, U+5693, U+56a3, U+56b7, U+56bc, U+56ca, U+56d4, U+56da-56db, U+56de,
|
||||
U+56e0, U+56e2, U+56e4, U+56ed, U+56f0-56f1, U+56f4, U+56f9-56fa, U+56fd-56ff, U+5703, U+5706,
|
||||
U+5708-5709, U+571f, U+5723, U+5728, U+572d, U+5730, U+573a, U+573e, U+5740, U+5747, U+574a,
|
||||
U+574d-5751, U+5757, U+575a-575b, U+575d-5761, U+5764, U+5766, U+5768, U+576a, U+576f, U+5773,
|
||||
U+5777, U+5782-5784, U+578b, U+5792, U+579b, U+57a0, U+57a2-57a3, U+57a6, U+57ab, U+57ae,
|
||||
U+57c2-57c3, U+57cb;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.at.woff2) format("woff2");
|
||||
unicode-range: U+54e5-54ea, U+54ed-54ee, U+54f2, U+54fa, U+54fc-54fd, U+5501, U+5506-5507, U+5509,
|
||||
U+550f-5510, U+5514, U+5520, U+5522, U+5524, U+5527, U+552c, U+552e-5531, U+5533, U+553e-553f,
|
||||
U+5543-5544, U+5546, U+554a, U+5550, U+5555-5556, U+555c, U+5561, U+5564-5567, U+556a, U+556c,
|
||||
U+556e, U+5575, U+5577-5578, U+557b-557c, U+557e, U+5580, U+5582-5584, U+5587, U+5589-558b,
|
||||
U+558f, U+5591, U+5594, U+5598-5599, U+559c-559d, U+559f, U+55a7, U+55b3, U+55b7, U+55bb, U+55bd,
|
||||
U+55c5, U+55d1-55d4, U+55d6, U+55dc-55dd, U+55df, U+55e1, U+55e3-55e6, U+55e8, U+55eb-55ec,
|
||||
U+55ef, U+55f7, U+55fd, U+5600-5601, U+5608-5609, U+560e, U+5618;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.au.woff2) format("woff2");
|
||||
unicode-range: U+5411, U+5413, U+5415, U+5417, U+541b, U+541d-5420, U+5426-5429, U+542b-542f,
|
||||
U+5431, U+5434-5435, U+5438-5439, U+543b-543c, U+543e, U+5440, U+5443, U+5446, U+5448, U+544a,
|
||||
U+5450, U+5453, U+5455, U+5457-5458, U+545b-545c, U+5462, U+5464, U+5466, U+5468, U+5471-5473,
|
||||
U+5475, U+5478, U+547b-547d, U+5480, U+5482, U+5484, U+5486, U+548b-548c, U+548e-5490, U+5492,
|
||||
U+5494-5496, U+5499-549b, U+54a4, U+54a6-54ad, U+54af, U+54b1, U+54b3, U+54b8, U+54bb, U+54bd,
|
||||
U+54bf-54c2, U+54c4, U+54c6-54c9, U+54cd-54ce, U+54d0-54d2, U+54d5, U+54d7, U+54da, U+54dd,
|
||||
U+54df;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.av.woff2) format("woff2");
|
||||
unicode-range: U+5348-534a, U+534e-534f, U+5351-5353, U+5355-5357, U+535a, U+535c, U+535e-5362,
|
||||
U+5364, U+5366-5367, U+536b, U+536f-5371, U+5373-5375, U+5377-5378, U+537f, U+5382, U+5384-5386,
|
||||
U+5389, U+538b-538c, U+5395, U+5398, U+539a, U+539f, U+53a2, U+53a5-53a6, U+53a8-53a9, U+53ae,
|
||||
U+53bb, U+53bf, U+53c1-53c2, U+53c8-53cd, U+53d1, U+53d4, U+53d6-53d9, U+53db, U+53df-53e0,
|
||||
U+53e3-53e6, U+53e8-53f3, U+53f6-53f9, U+53fc-53fd, U+5401, U+5403-5404, U+5408-540a,
|
||||
U+540c-5410;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.aw.woff2) format("woff2");
|
||||
unicode-range: U+5207, U+520a, U+520d-520e, U+5211-5212, U+5217-521b, U+521d, U+5220, U+5224,
|
||||
U+5228-5229, U+522b, U+522d-522e, U+5230, U+5236-523b, U+523d, U+5241-5243, U+524a, U+524c-524d,
|
||||
U+5250-5251, U+5254, U+5256, U+525c, U+5265, U+5267, U+5269-526a, U+526f, U+5272, U+527d, U+527f,
|
||||
U+5288, U+529b, U+529d-52a1, U+52a3, U+52a8-52ab, U+52ad, U+52b1-52b3, U+52be-52bf, U+52c3,
|
||||
U+52c7, U+52c9, U+52cb, U+52d0, U+52d2, U+52d8, U+52df, U+52e4, U+52fa, U+52fe-5300, U+5305-5306,
|
||||
U+5308, U+530d, U+5310, U+5315-5317, U+5319, U+531d, U+5320-5321, U+5323, U+532a, U+532e,
|
||||
U+5339-533b, U+533e-533f, U+5341, U+5343, U+5347;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ax.woff2) format("woff2");
|
||||
unicode-range: U+50cf, U+50d6, U+50da, U+50e7, U+50ee, U+50f3, U+50f5, U+50fb, U+5106, U+510b,
|
||||
U+5112, U+5121, U+513f-5141, U+5143-5146, U+5148-5149, U+514b, U+514d, U+5151, U+5154, U+515a,
|
||||
U+515c, U+5162, U+5165, U+5168, U+516b-516e, U+5170-5171, U+5173-5179, U+517b-517d, U+5180,
|
||||
U+5185, U+5188-5189, U+518c-518d, U+5192, U+5195, U+5197, U+5199, U+519b-519c, U+51a0, U+51a2,
|
||||
U+51a4-51a5, U+51ac, U+51af-51b0, U+51b2-51b3, U+51b5-51b7, U+51bb, U+51bd, U+51c0, U+51c4,
|
||||
U+51c6, U+51c9, U+51cb-51cc, U+51cf, U+51d1, U+51db, U+51dd, U+51e0-51e1, U+51e4, U+51ed,
|
||||
U+51ef-51f0, U+51f3, U+51f6, U+51f8-51fb, U+51fd, U+51ff-5201, U+5203, U+5206;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.ay.woff2) format("woff2");
|
||||
unicode-range: U+4f60, U+4f63, U+4f65, U+4f69, U+4f6c, U+4f6f-4f70, U+4f73-4f74, U+4f7b-4f7c,
|
||||
U+4f7f, U+4f83-4f84, U+4f88, U+4f8b, U+4f8d, U+4f97, U+4f9b, U+4f9d, U+4fa0, U+4fa3, U+4fa5-4faa,
|
||||
U+4fac, U+4fae-4faf, U+4fb5, U+4fbf, U+4fc3-4fc5, U+4fca, U+4fce-4fd1, U+4fd7-4fd8, U+4fda,
|
||||
U+4fdd-4fde, U+4fe1, U+4fe6, U+4fe8-4fe9, U+4fed-4fef, U+4ff1, U+4ff8, U+4ffa, U+4ffe,
|
||||
U+500c-500d, U+500f, U+5012, U+5014, U+5018-501a, U+501c, U+501f, U+5021, U+5026, U+5028-502a,
|
||||
U+502d, U+503a, U+503c, U+503e, U+5043, U+5047-5048, U+504c, U+504e-504f, U+5055, U+505a, U+505c,
|
||||
U+5065, U+5076-5077, U+507b, U+507f-5080, U+5085, U+5088, U+508d, U+50a3, U+50a5, U+50a8, U+50ac,
|
||||
U+50b2, U+50bb;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.az.woff2) format("woff2");
|
||||
unicode-range: U+4e94-4e95, U+4e98, U+4e9a-4e9b, U+4e9f, U+4ea1-4ea2, U+4ea4-4ea9, U+4eab-4eae,
|
||||
U+4eb2, U+4eb5, U+4eba, U+4ebf-4ec1, U+4ec3-4ec7, U+4eca-4ecb, U+4ecd-4ece, U+4ed1, U+4ed3-4ed9,
|
||||
U+4ede-4edf, U+4ee3-4ee5, U+4ee8, U+4eea, U+4eec, U+4ef0, U+4ef2, U+4ef5-4ef7, U+4efb, U+4efd,
|
||||
U+4eff, U+4f01, U+4f0a, U+4f0d-4f11, U+4f17-4f1a, U+4f1e-4f20, U+4f22, U+4f24-4f26, U+4f2a-4f2b,
|
||||
U+4f2f-4f30, U+4f34, U+4f36, U+4f38, U+4f3a, U+4f3c-4f3d, U+4f43, U+4f46, U+4f4d-4f51, U+4f53,
|
||||
U+4f55, U+4f58-4f59, U+4f5b-4f5e;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.a0.woff2) format("woff2");
|
||||
unicode-range: U+d7, U+e0-e1, U+e8-ea, U+ec-ed, U+f2-f3, U+f7, U+f9-fa, U+fc, U+2014, U+2018-2019,
|
||||
U+201c-201d, U+3001-3002, U+300a-300b, U+3010-3011, U+4e00-4e01, U+4e03, U+4e07-4e0b,
|
||||
U+4e0d-4e0e, U+4e10-4e11, U+4e13-4e14, U+4e16, U+4e18-4e1e, U+4e22, U+4e24-4e25, U+4e27,
|
||||
U+4e2a-4e2b, U+4e2d, U+4e30, U+4e32, U+4e34, U+4e38-4e3b, U+4e3d-4e3e, U+4e43, U+4e45,
|
||||
U+4e48-4e49, U+4e4b-4e50, U+4e52-4e54, U+4e56, U+4e58-4e59, U+4e5c-4e61, U+4e66, U+4e70-4e71,
|
||||
U+4e73, U+4e7e, U+4e86, U+4e88-4e89, U+4e8b-4e8c, U+4e8e-4e8f, U+4e91-4e93;
|
||||
}
|
||||
@font-face {
|
||||
font-family: HarmonyOS_Regular;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
src: url(font_files/HarmonyOS_Regular.a1.woff2) format("woff2");
|
||||
unicode-range: U+21-7e, U+a4, U+a7-a8, U+b0-b1, U+b7;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user