mirror of
https://github.com/imsyy/SPlayer.git
synced 2025-11-25 11:29:26 +08:00
Compare commits
170 Commits
v2.0.0-bet
...
v3.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
146af3aeba | ||
|
|
b2ddb9f4e2 | ||
|
|
201186bab2 | ||
|
|
bfcd59daca | ||
|
|
d5c3843c3f | ||
|
|
d3f307eac5 | ||
|
|
675a52b8d1 | ||
|
|
aee90e9c4e | ||
|
|
eb39b81d8d | ||
|
|
436df47104 | ||
|
|
e04e5e34c6 | ||
|
|
b57d685c03 | ||
|
|
6684172592 | ||
|
|
0257e74ff0 | ||
|
|
16c8865651 | ||
|
|
1a61aa2458 | ||
|
|
e543f07d8e | ||
|
|
96a0495a88 | ||
|
|
02befcd8a4 | ||
|
|
15b500806a | ||
|
|
9accf5d27d | ||
|
|
191ab29a44 | ||
|
|
a4d4cd5f70 | ||
|
|
3b07f7346f | ||
|
|
1edceeebdd | ||
|
|
fbf261f80b | ||
|
|
432fa18299 | ||
|
|
b2dcec840b | ||
|
|
6f93d65f02 | ||
|
|
852b901353 | ||
|
|
71a282157d | ||
|
|
49f462cdf5 | ||
|
|
94c0ca70e1 | ||
|
|
813637762c | ||
|
|
eb39b85a8b | ||
|
|
201fd8d687 | ||
|
|
02116d8f0f | ||
|
|
551a190edf | ||
|
|
f5015a4028 | ||
|
|
ea55616bd6 | ||
|
|
2b8eb93404 | ||
|
|
18113d94e9 | ||
|
|
a8f01d5728 | ||
|
|
3a6f8e7462 | ||
|
|
57325bfc46 | ||
|
|
a3e8931460 | ||
|
|
2e0f448f66 | ||
|
|
44cfbbf4e0 | ||
|
|
963343c39e | ||
|
|
a697799f48 | ||
|
|
7c59cc2ccb | ||
|
|
11acbcf7eb | ||
|
|
c35ede2158 | ||
|
|
dc480459eb | ||
|
|
c100bfed8d | ||
|
|
2b48713565 | ||
|
|
528f9b0aa6 | ||
|
|
0e0bde89bb | ||
|
|
acf60b8b75 | ||
|
|
3b5338d582 | ||
|
|
0c82d6a096 | ||
|
|
50374e173e | ||
|
|
62c9dc33db | ||
|
|
2b6d68ecbd | ||
|
|
8a842aa6d6 | ||
|
|
b0a08ce1b8 | ||
|
|
77ab94a59a | ||
|
|
2b65269ba9 | ||
|
|
7ed33919ab | ||
|
|
d20512e662 | ||
|
|
9423d2bf9e | ||
|
|
54fb8e74a8 | ||
|
|
7d3d7696da | ||
|
|
e66ba52889 | ||
|
|
b146dc011e | ||
|
|
ad2422d826 | ||
|
|
6b9ba74c35 | ||
|
|
22e2653e20 | ||
|
|
4cac54c84a | ||
|
|
e5f9ecd7b5 | ||
|
|
d64cfb40ec | ||
|
|
53c30caf00 | ||
|
|
6fcce91b2b | ||
|
|
5a53dfcdf7 | ||
|
|
bbe71bd62d | ||
|
|
ecedf0b7b9 | ||
|
|
f2935f3c1c | ||
|
|
d3e677d494 | ||
|
|
e49f90b0da | ||
|
|
0359b0e470 | ||
|
|
aa2373695b | ||
|
|
cf940ff405 | ||
|
|
e7c17cf531 | ||
|
|
c6a1ca4f42 | ||
|
|
8f5008df69 | ||
|
|
428ce4be86 | ||
|
|
d46c4c4285 | ||
|
|
0b871175b2 | ||
|
|
c34c4fd880 | ||
|
|
ff00f0c283 | ||
|
|
847c2e5810 | ||
|
|
e62c81bb33 | ||
|
|
984fdb3459 | ||
|
|
019b78bf38 | ||
|
|
cf88c7669f | ||
|
|
f4383ba848 | ||
|
|
adbda459ba | ||
|
|
984d747179 | ||
|
|
c012f84064 | ||
|
|
a57a18b9f5 | ||
|
|
309c323a14 | ||
|
|
6a1e606d6d | ||
|
|
af3931847e | ||
|
|
41eadb5843 | ||
|
|
8963d719d9 | ||
|
|
0a7761ffff | ||
|
|
1a63771f2d | ||
|
|
1f9141ba33 | ||
|
|
a341a69d48 | ||
|
|
0cedfe0af3 | ||
|
|
59f492ed8f | ||
|
|
8f416ff841 | ||
|
|
99ab194e4b | ||
|
|
43fb9b48dc | ||
|
|
c61e54d6a3 | ||
|
|
c8d195053f | ||
|
|
8cfe5d0481 | ||
|
|
fcc2f5015f | ||
|
|
9b98a45264 | ||
|
|
3c4e836fb8 | ||
|
|
a8111b9d3f | ||
|
|
8eaeffeda3 | ||
|
|
eed76966c4 | ||
|
|
b095e4eb36 | ||
|
|
3dbdf3e613 | ||
|
|
5ceca058a7 | ||
|
|
a8e867bbf9 | ||
|
|
4cb8eb0213 | ||
|
|
461f216cab | ||
|
|
2756313e4a | ||
|
|
e802a2f574 | ||
|
|
a45940b104 | ||
|
|
ac0ac5f4ea | ||
|
|
883b6d13a5 | ||
|
|
ee9bbf0687 | ||
|
|
693dc65b07 | ||
|
|
354d271582 | ||
|
|
eaaeb0f5d3 | ||
|
|
e4e8deec59 | ||
|
|
3f21704b82 | ||
|
|
7ad2bb8bde | ||
|
|
3123e4f5f8 | ||
|
|
60e43c9f40 | ||
|
|
298813a057 | ||
|
|
2db74f3a39 | ||
|
|
024ff1773e | ||
|
|
6046e5a153 | ||
|
|
3c39dbd87f | ||
|
|
c5747b6a3e | ||
|
|
750d570c3d | ||
|
|
b811b00b9f | ||
|
|
a372570038 | ||
|
|
6d5fa15098 | ||
|
|
b65369a8a6 | ||
|
|
0af0ac3cce | ||
|
|
f0ed78eed5 | ||
|
|
b1cda68c75 | ||
|
|
dd1081cfa2 | ||
|
|
046b8f3a92 | ||
|
|
72650a5419 |
12
.dockerignore
Normal file
12
.dockerignore
Normal file
@@ -0,0 +1,12 @@
|
||||
node_modules
|
||||
npm-debug.log
|
||||
Dockerfile*
|
||||
docker-compose*
|
||||
.dockerignore
|
||||
.git
|
||||
.github
|
||||
.gitignore
|
||||
README.md
|
||||
LICENSE
|
||||
.vscode
|
||||
dist
|
||||
@@ -1,9 +1,18 @@
|
||||
# 根配置文件
|
||||
## 编辑器在查找配置时会停止查找更高层次的配置文件
|
||||
root = true
|
||||
|
||||
# 通配符,匹配所有文件
|
||||
[*]
|
||||
# 设置字符集为 UTF-8,确保文件中的文本使用 UTF-8 编码
|
||||
charset = utf-8
|
||||
# 使用空格作为缩进风格
|
||||
indent_style = space
|
||||
# 设置每个缩进级别的空格数量为 2
|
||||
indent_size = 2
|
||||
# 设置行尾换行符为LF(Line Feed)
|
||||
end_of_line = lf
|
||||
# 在文件的末尾插入一个新行
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
# 删除每一行末尾的尾随空格
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
29
.env
29
.env
@@ -1,29 +0,0 @@
|
||||
# 程序配置
|
||||
## 程序名称
|
||||
MAIN_VITE_TITLE = "SPlayer"
|
||||
## 程序主端口
|
||||
MAIN_VITE_MAIN_PORT = 7899
|
||||
## 程序开发环境运行端口
|
||||
MAIN_VITE_DEV_PORT = 6944
|
||||
|
||||
# 全局 API 配置
|
||||
## API 运行地址
|
||||
MAIN_VITE_SERVER_HOST = 127.0.0.1
|
||||
## API 运行端口
|
||||
MAIN_VITE_SERVER_PORT = 11451
|
||||
## API 在线地址( 网址结尾不要加 / )
|
||||
### 用于非客户端( 浏览器环境 )
|
||||
RENDERER_VITE_SERVER_URL = /api
|
||||
|
||||
# 程序信息
|
||||
RENDERER_VITE_SITE_TITLE = "SPlayer"
|
||||
RENDERER_VITE_SITE_ANTHOR = "無名"
|
||||
RENDERER_VITE_SITE_KEYWORDS = "SPlayer,云音乐,播放器,在线音乐,在线播放器,音乐播放器"
|
||||
RENDERER_VITE_SITE_DES = "一个简约的在线音乐播放器,具有音乐搜索、播放、每日推荐、私人FM、歌词显示、歌曲评论、网易云登录与云盘等功能"
|
||||
RENDERER_VITE_SITE_URL = "imsyy.top"
|
||||
RENDERER_VITE_SITE_LOGO = "/images/logo/favicon.svg"
|
||||
RENDERER_VITE_SITE_APPLE_LOGO = "/images/logo/favicon-apple.png"
|
||||
|
||||
# Cookie
|
||||
## 咪咕音乐 Cookie
|
||||
MAIN_VITE_MIGU_COOKIE = ""
|
||||
6
.env.example
Normal file
6
.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
## WEB 端口
|
||||
VITE_WEB_PORT = 14558
|
||||
## API 端口
|
||||
VITE_SERVER_PORT = 25884
|
||||
## API 地址 - 结尾不要加 /
|
||||
VITE_API_URL = /api/netease
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
dist
|
||||
out
|
||||
.gitignore
|
||||
304
.eslintrc-auto-import.json
Normal file
304
.eslintrc-auto-import.json
Normal file
@@ -0,0 +1,304 @@
|
||||
{
|
||||
"globals": {
|
||||
"Component": true,
|
||||
"ComponentPublicInstance": true,
|
||||
"ComputedRef": true,
|
||||
"DirectiveBinding": true,
|
||||
"EffectScope": true,
|
||||
"ExtractDefaultPropTypes": true,
|
||||
"ExtractPropTypes": true,
|
||||
"ExtractPublicPropTypes": true,
|
||||
"InjectionKey": true,
|
||||
"MaybeRef": true,
|
||||
"MaybeRefOrGetter": true,
|
||||
"PropType": true,
|
||||
"Ref": true,
|
||||
"VNode": true,
|
||||
"WritableComputedRef": true,
|
||||
"asyncComputed": true,
|
||||
"autoResetRef": true,
|
||||
"computed": true,
|
||||
"computedAsync": true,
|
||||
"computedEager": true,
|
||||
"computedInject": true,
|
||||
"computedWithControl": true,
|
||||
"controlledComputed": true,
|
||||
"controlledRef": true,
|
||||
"createApp": true,
|
||||
"createEventHook": true,
|
||||
"createGlobalState": true,
|
||||
"createInjectionState": true,
|
||||
"createReactiveFn": true,
|
||||
"createReusableTemplate": true,
|
||||
"createSharedComposable": true,
|
||||
"createTemplatePromise": true,
|
||||
"createUnrefFn": true,
|
||||
"customRef": true,
|
||||
"debouncedRef": true,
|
||||
"debouncedWatch": true,
|
||||
"defineAsyncComponent": true,
|
||||
"defineComponent": true,
|
||||
"eagerComputed": true,
|
||||
"effectScope": true,
|
||||
"extendRef": true,
|
||||
"getCurrentInstance": true,
|
||||
"getCurrentScope": true,
|
||||
"h": true,
|
||||
"ignorableWatch": true,
|
||||
"inject": true,
|
||||
"injectLocal": true,
|
||||
"isDefined": true,
|
||||
"isProxy": true,
|
||||
"isReactive": true,
|
||||
"isReadonly": true,
|
||||
"isRef": true,
|
||||
"makeDestructurable": true,
|
||||
"markRaw": true,
|
||||
"nextTick": true,
|
||||
"onActivated": true,
|
||||
"onBeforeMount": true,
|
||||
"onBeforeRouteLeave": true,
|
||||
"onBeforeRouteUpdate": true,
|
||||
"onBeforeUnmount": true,
|
||||
"onBeforeUpdate": true,
|
||||
"onClickOutside": true,
|
||||
"onDeactivated": true,
|
||||
"onErrorCaptured": true,
|
||||
"onKeyStroke": true,
|
||||
"onLongPress": true,
|
||||
"onMounted": true,
|
||||
"onRenderTracked": true,
|
||||
"onRenderTriggered": true,
|
||||
"onScopeDispose": true,
|
||||
"onServerPrefetch": true,
|
||||
"onStartTyping": true,
|
||||
"onUnmounted": true,
|
||||
"onUpdated": true,
|
||||
"onWatcherCleanup": true,
|
||||
"pausableWatch": true,
|
||||
"provide": true,
|
||||
"provideLocal": true,
|
||||
"reactify": true,
|
||||
"reactifyObject": true,
|
||||
"reactive": true,
|
||||
"reactiveComputed": true,
|
||||
"reactiveOmit": true,
|
||||
"reactivePick": true,
|
||||
"readonly": true,
|
||||
"ref": true,
|
||||
"refAutoReset": true,
|
||||
"refDebounced": true,
|
||||
"refDefault": true,
|
||||
"refThrottled": true,
|
||||
"refWithControl": true,
|
||||
"resolveComponent": true,
|
||||
"resolveRef": true,
|
||||
"resolveUnref": true,
|
||||
"shallowReactive": true,
|
||||
"shallowReadonly": true,
|
||||
"shallowRef": true,
|
||||
"syncRef": true,
|
||||
"syncRefs": true,
|
||||
"templateRef": true,
|
||||
"throttledRef": true,
|
||||
"throttledWatch": true,
|
||||
"toRaw": true,
|
||||
"toReactive": true,
|
||||
"toRef": true,
|
||||
"toRefs": true,
|
||||
"toValue": true,
|
||||
"triggerRef": true,
|
||||
"tryOnBeforeMount": true,
|
||||
"tryOnBeforeUnmount": true,
|
||||
"tryOnMounted": true,
|
||||
"tryOnScopeDispose": true,
|
||||
"tryOnUnmounted": true,
|
||||
"unref": true,
|
||||
"unrefElement": true,
|
||||
"until": true,
|
||||
"useActiveElement": true,
|
||||
"useAnimate": true,
|
||||
"useArrayDifference": true,
|
||||
"useArrayEvery": true,
|
||||
"useArrayFilter": true,
|
||||
"useArrayFind": true,
|
||||
"useArrayFindIndex": true,
|
||||
"useArrayFindLast": true,
|
||||
"useArrayIncludes": true,
|
||||
"useArrayJoin": true,
|
||||
"useArrayMap": true,
|
||||
"useArrayReduce": true,
|
||||
"useArraySome": true,
|
||||
"useArrayUnique": true,
|
||||
"useAsyncQueue": true,
|
||||
"useAsyncState": true,
|
||||
"useAttrs": true,
|
||||
"useBase64": true,
|
||||
"useBattery": true,
|
||||
"useBluetooth": true,
|
||||
"useBreakpoints": true,
|
||||
"useBroadcastChannel": true,
|
||||
"useBrowserLocation": true,
|
||||
"useCached": true,
|
||||
"useClipboard": true,
|
||||
"useClipboardItems": true,
|
||||
"useCloned": true,
|
||||
"useColorMode": true,
|
||||
"useConfirmDialog": true,
|
||||
"useCounter": true,
|
||||
"useCssModule": true,
|
||||
"useCssVar": true,
|
||||
"useCssVars": true,
|
||||
"useCurrentElement": true,
|
||||
"useCycleList": true,
|
||||
"useDark": true,
|
||||
"useDateFormat": true,
|
||||
"useDebounce": true,
|
||||
"useDebounceFn": true,
|
||||
"useDebouncedRefHistory": true,
|
||||
"useDeviceMotion": true,
|
||||
"useDeviceOrientation": true,
|
||||
"useDevicePixelRatio": true,
|
||||
"useDevicesList": true,
|
||||
"useDialog": true,
|
||||
"useDisplayMedia": true,
|
||||
"useDocumentVisibility": true,
|
||||
"useDraggable": true,
|
||||
"useDropZone": true,
|
||||
"useElementBounding": true,
|
||||
"useElementByPoint": true,
|
||||
"useElementHover": true,
|
||||
"useElementSize": true,
|
||||
"useElementVisibility": true,
|
||||
"useEventBus": true,
|
||||
"useEventListener": true,
|
||||
"useEventSource": true,
|
||||
"useEyeDropper": true,
|
||||
"useFavicon": true,
|
||||
"useFetch": true,
|
||||
"useFileDialog": true,
|
||||
"useFileSystemAccess": true,
|
||||
"useFocus": true,
|
||||
"useFocusWithin": true,
|
||||
"useFps": true,
|
||||
"useFullscreen": true,
|
||||
"useGamepad": true,
|
||||
"useGeolocation": true,
|
||||
"useId": true,
|
||||
"useIdle": true,
|
||||
"useImage": true,
|
||||
"useInfiniteScroll": true,
|
||||
"useIntersectionObserver": true,
|
||||
"useInterval": true,
|
||||
"useIntervalFn": true,
|
||||
"useKeyModifier": true,
|
||||
"useLastChanged": true,
|
||||
"useLink": true,
|
||||
"useLoadingBar": true,
|
||||
"useLocalStorage": true,
|
||||
"useMagicKeys": true,
|
||||
"useManualRefHistory": true,
|
||||
"useMediaControls": true,
|
||||
"useMediaQuery": true,
|
||||
"useMemoize": true,
|
||||
"useMemory": true,
|
||||
"useMessage": true,
|
||||
"useModel": true,
|
||||
"useMounted": true,
|
||||
"useMouse": true,
|
||||
"useMouseInElement": true,
|
||||
"useMousePressed": true,
|
||||
"useMutationObserver": true,
|
||||
"useNavigatorLanguage": true,
|
||||
"useNetwork": true,
|
||||
"useNotification": true,
|
||||
"useNow": true,
|
||||
"useObjectUrl": true,
|
||||
"useOffsetPagination": true,
|
||||
"useOnline": true,
|
||||
"usePageLeave": true,
|
||||
"useParallax": true,
|
||||
"useParentElement": true,
|
||||
"usePerformanceObserver": true,
|
||||
"usePermission": true,
|
||||
"usePointer": true,
|
||||
"usePointerLock": true,
|
||||
"usePointerSwipe": true,
|
||||
"usePreferredColorScheme": true,
|
||||
"usePreferredContrast": true,
|
||||
"usePreferredDark": true,
|
||||
"usePreferredLanguages": true,
|
||||
"usePreferredReducedMotion": true,
|
||||
"usePrevious": true,
|
||||
"useRafFn": true,
|
||||
"useRefHistory": true,
|
||||
"useResizeObserver": true,
|
||||
"useRoute": true,
|
||||
"useRouter": true,
|
||||
"useScreenOrientation": true,
|
||||
"useScreenSafeArea": true,
|
||||
"useScriptTag": true,
|
||||
"useScroll": true,
|
||||
"useScrollLock": true,
|
||||
"useSessionStorage": true,
|
||||
"useShare": true,
|
||||
"useSlots": true,
|
||||
"useSorted": true,
|
||||
"useSpeechRecognition": true,
|
||||
"useSpeechSynthesis": true,
|
||||
"useStepper": true,
|
||||
"useStorage": true,
|
||||
"useStorageAsync": true,
|
||||
"useStyleTag": true,
|
||||
"useSupported": true,
|
||||
"useSwipe": true,
|
||||
"useTemplateRef": true,
|
||||
"useTemplateRefsList": true,
|
||||
"useTextDirection": true,
|
||||
"useTextSelection": true,
|
||||
"useTextareaAutosize": true,
|
||||
"useThrottle": true,
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
"useTimestamp": true,
|
||||
"useTitle": true,
|
||||
"useToNumber": true,
|
||||
"useToString": true,
|
||||
"useToggle": true,
|
||||
"useTransition": true,
|
||||
"useUrlSearchParams": true,
|
||||
"useUserMedia": true,
|
||||
"useVModel": true,
|
||||
"useVModels": true,
|
||||
"useVibrate": true,
|
||||
"useVirtualList": true,
|
||||
"useWakeLock": true,
|
||||
"useWebNotification": true,
|
||||
"useWebSocket": true,
|
||||
"useWebWorker": true,
|
||||
"useWebWorkerFn": true,
|
||||
"useWindowFocus": true,
|
||||
"useWindowScroll": true,
|
||||
"useWindowSize": true,
|
||||
"watch": true,
|
||||
"watchArray": true,
|
||||
"watchAtMost": true,
|
||||
"watchDebounced": true,
|
||||
"watchDeep": true,
|
||||
"watchEffect": true,
|
||||
"watchIgnorable": true,
|
||||
"watchImmediate": true,
|
||||
"watchOnce": true,
|
||||
"watchPausable": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true,
|
||||
"watchThrottled": true,
|
||||
"watchTriggerable": true,
|
||||
"watchWithFilter": true,
|
||||
"whenever": true
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
/* eslint-env node */
|
||||
require("@rushstack/eslint-patch/modern-module-resolution");
|
||||
|
||||
module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:vue/vue3-recommended",
|
||||
"@electron-toolkit",
|
||||
"@vue/eslint-config-prettier",
|
||||
],
|
||||
rules: {
|
||||
"vue/v-on-event-hyphenation": "off",
|
||||
"vue/require-default-prop": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
"vue/attribute-hyphenation": "off",
|
||||
},
|
||||
ignorePatterns: [
|
||||
"node_modules/",
|
||||
"build/",
|
||||
"dist/",
|
||||
"out/",
|
||||
"components.d.ts",
|
||||
"auto-imports.d.ts",
|
||||
],
|
||||
globals: {
|
||||
defineProps: true,
|
||||
defineEmits: true,
|
||||
withDefaults: true,
|
||||
h: true,
|
||||
vue: true,
|
||||
ref: true,
|
||||
reactive: true,
|
||||
computed: true,
|
||||
watch: true,
|
||||
provide: true,
|
||||
inject: true,
|
||||
defineComponent: true,
|
||||
onBeforeMount: true,
|
||||
onBeforeUnmount: true,
|
||||
onUnmounted: true,
|
||||
onMounted: true,
|
||||
nextTick: true,
|
||||
watchEffect: true,
|
||||
electron: true,
|
||||
$message: true,
|
||||
$dialog: true,
|
||||
$loadingBar: true,
|
||||
$changeLogin: true,
|
||||
$notification: true,
|
||||
$changeThemeColor: true,
|
||||
$canNotConnect: true,
|
||||
},
|
||||
};
|
||||
17
.github/ISSUE_TEMPLATE/add.yml
vendored
17
.github/ISSUE_TEMPLATE/add.yml
vendored
@@ -1,17 +0,0 @@
|
||||
name: 添加功能
|
||||
description: 请填写希望添加的功能的具体信息
|
||||
title: "添加功能"
|
||||
labels: [add]
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
validations:
|
||||
required: true
|
||||
attributes:
|
||||
label: "希望添加什么功能?"
|
||||
placeholder: "请填写功能名称"
|
||||
- type: textarea
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "请详细描述希望添加的功能的具体信息"
|
||||
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
4
.github/ISSUE_TEMPLATE/bug.yml
vendored
@@ -1,5 +1,6 @@
|
||||
name: 遇到问题
|
||||
description: 关于使用过程中遇到的问题
|
||||
title: 请填写标题
|
||||
labels: [bug]
|
||||
body:
|
||||
- type: input
|
||||
@@ -30,4 +31,5 @@ body:
|
||||
id: other
|
||||
attributes:
|
||||
label: "具体信息"
|
||||
description: "有需要补充的信息吗?比如控制台的报错什么的"
|
||||
description: "请填写完整的复现步骤和遇到的问题,包括但不限于报错信息、控制台输出、网络请求等"
|
||||
placeholder: "请填写具体的复现步骤和遇到的问题"
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 添加功能
|
||||
url: https://github.com/imsyy/SPlayer/discussions/new?category=%E6%83%B3%E6%B3%95-ideas
|
||||
about: 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 转到讨论区
|
||||
url: https://github.com/imsyy/SPlayer/discussions
|
||||
about: Issues 用于反馈 Bug, 新的功能建议和提问答疑请到讨论区发起
|
||||
- name: 提问的艺术
|
||||
url: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md
|
||||
about: 默认所有 Issues 发起者均已了解此处的内容
|
||||
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
@@ -15,27 +15,50 @@ jobs:
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
node-version: "22.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g 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: npm install
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App
|
||||
run: npm run build:win
|
||||
run: pnpm run build:win
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 清理不必要的构建产物
|
||||
- name: Cleanup Artifacts
|
||||
run: |
|
||||
npx rimraf "dist/!(*.exe)"
|
||||
run: npx del-cli "dist/**/*" "!dist/*.exe"
|
||||
# 上传构建产物
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlayer-dev
|
||||
path: dist
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
name: ${{ github.ref }}-rc
|
||||
body: This version is still under development, currently only provides windows version, non-developers please do not use!
|
||||
draft: false
|
||||
prerelease: true
|
||||
files: dist/*.exe
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
56
.github/workflows/docker.yml
vendored
Normal file
56
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,56 @@
|
||||
name: Publish Docker image
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to multiple registries
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
imsyy/splayer
|
||||
ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: |
|
||||
linux/amd64
|
||||
linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
126
.github/workflows/release.yml
vendored
126
.github/workflows/release.yml
vendored
@@ -6,6 +6,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
# Windows
|
||||
@@ -15,39 +16,44 @@ jobs:
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
node-version: "22.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g 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: npm install
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Windows
|
||||
run: npm run build:win || true
|
||||
shell: bash
|
||||
run: pnpm run build:win || true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Windows artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Win
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.exe
|
||||
dist/*.msi
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Mac
|
||||
@@ -57,39 +63,45 @@ jobs:
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
# 安装项目依赖
|
||||
node-version: "20.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
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
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for macOS
|
||||
run: npm run build:mac || true
|
||||
run: pnpm run build:mac || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload macOS artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Macos
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.dmg
|
||||
dist/*.zip
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
# Linux
|
||||
@@ -99,43 +111,65 @@ jobs:
|
||||
steps:
|
||||
# 检出 Git 仓库
|
||||
- name: Check out git repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
# 安装 Node.js
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v4.0.0
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18.x"
|
||||
node-version: "20.x"
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm
|
||||
# 更新 Ubuntu 软件源
|
||||
- name: Ubuntu Update with sudo
|
||||
run: sudo apt-get update
|
||||
# 安装依赖
|
||||
- name: Install RPM & Pacman
|
||||
run: |
|
||||
sudo apt-get install --no-install-recommends -y rpm &&
|
||||
sudo apt-get install --no-install-recommends -y libarchive-tools &&
|
||||
sudo apt-get install --no-install-recommends -y libopenjp2-tools
|
||||
# 安装 Snapcraft
|
||||
- name: Install Snapcraft
|
||||
uses: samuelmeuli/action-snapcraft@v2
|
||||
with:
|
||||
snapcraft_token: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
# 复制环境变量文件
|
||||
- name: Copy .env.example
|
||||
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
|
||||
run: pnpm install
|
||||
# 构建 Electron App
|
||||
- name: Build Electron App for Linux
|
||||
run: npm run build:linux || true
|
||||
run: pnpm run build:linux || true
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
# 上传 Snap 包到 Snapcraft 商店
|
||||
- name: Publish Snap to Snap Store
|
||||
run: snapcraft upload dist/*.snap --release stable
|
||||
continue-on-error: true
|
||||
env:
|
||||
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
|
||||
# 上传构建产物
|
||||
- name: Upload Linux artifact
|
||||
uses: actions/upload-artifact@v3.1.3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: SPlarer-Linux
|
||||
if-no-files-found: ignore
|
||||
path: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
path: dist/*.*
|
||||
# 创建 GitHub Release
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v0.1.15
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/*.AppImage
|
||||
dist/*.deb
|
||||
dist/*.rpm
|
||||
files: dist/*.*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -14,9 +14,9 @@ dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
out
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
.env
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
@@ -28,3 +28,5 @@ out
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env.development
|
||||
.env.production
|
||||
5
.npmrc
5
.npmrc
@@ -1,2 +1,5 @@
|
||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/
|
||||
registry=https://registry.npmmirror.com
|
||||
disturl=https://registry.npmmirror.com/-/binary/node
|
||||
electron_mirror=https://npmmirror.com/mirrors/electron/
|
||||
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
|
||||
shamefully-hoist=true
|
||||
|
||||
@@ -2,5 +2,7 @@ out
|
||||
dist
|
||||
pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
# tsconfig.json
|
||||
# tsconfig.*.json
|
||||
|
||||
7
.prettierrc.json
Normal file
7
.prettierrc.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
singleQuote: false
|
||||
semi: true
|
||||
printWidth: 100
|
||||
trailingComma: all
|
||||
38
Dockerfile
Normal file
38
Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
# build
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
RUN apk update && apk add --no-cache git
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
# add .env.example to .env
|
||||
RUN [ ! -e ".env" ] && cp .env.example .env || true
|
||||
|
||||
RUN npm run build
|
||||
|
||||
# nginx
|
||||
FROM nginx:1.27-alpine-slim AS app
|
||||
|
||||
COPY --from=builder /app/out/renderer /usr/share/nginx/html
|
||||
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
COPY --from=builder /app/docker-entrypoint.sh /docker-entrypoint.sh
|
||||
|
||||
RUN apk add --no-cache npm python3 youtube-dl \
|
||||
&& npm install -g @unblockneteasemusic/server NeteaseCloudMusicApi \
|
||||
&& wget https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -O /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /usr/local/bin/yt-dlp \
|
||||
&& chmod +x /docker-entrypoint.sh
|
||||
|
||||
ENV NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
|
||||
CMD ["npx", "NeteaseCloudMusicApi"]
|
||||
149
LICENSE
149
LICENSE
@@ -1,23 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
@@ -26,44 +24,34 @@ them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
@@ -72,7 +60,7 @@ modification follow.
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
@@ -549,35 +537,45 @@ to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
@@ -635,40 +633,29 @@ the "copyright" line and a pointer to where the full notice is found.
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
263
README.md
263
README.md
@@ -1,25 +1,34 @@
|
||||
> [!IMPORTANT]
|
||||
> ## 🎉 当前项目正在重构中 🎉
|
||||
>
|
||||
> - 目前版本进入维护模式,仅在遇到重大问题时会进行修复
|
||||
> - 支持客户端与网页端
|
||||
> - 支持现有版本所有功能
|
||||
> - 新增支持播放与管理本地歌曲
|
||||
# SPlayer
|
||||
|
||||
<div align="center">
|
||||
<img alt="logo" height="80" src="./public/images/logo/favicon.png" />
|
||||
<h2>SPlayer</h2>
|
||||
<p>一个简约的音乐播放器</p>
|
||||
<img alt="main" src="./screenshots/main.png" />
|
||||
</div>
|
||||
<br />
|
||||
> A simple music player
|
||||
|
||||

|
||||

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

|
||||

|
||||
|
||||

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

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

|
||||
|
||||
构建完成后可在 `dist` 文件夹中打开可执行文件来完成安装操作
|
||||
6. 点击 `Deploy`,即可成功部署
|
||||
|
||||
## ⚙️ 服务器部署
|
||||
|
||||
1. 重复 `⚙️ Vercel 部署` 中的 1 - 4 步骤
|
||||
2. 克隆仓库
|
||||
|
||||
```bash
|
||||
git clone https://github.com/imsyy/SPlayer.git
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
# 或
|
||||
yarn install
|
||||
# 或
|
||||
npm install
|
||||
```
|
||||
|
||||
4. 编译打包
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
# 或
|
||||
yarn build
|
||||
# 或
|
||||
npm build
|
||||
```
|
||||
|
||||
5. 将站点运行目录设置为 `out/renderer` 目录
|
||||
|
||||
## ⚙️ 本地部署
|
||||
|
||||
1. 本地部署需要用到 `Node.js`。可前往 [Node.js 官网](https://nodejs.org/zh-cn/) 下载安装包,请下载最新稳定版
|
||||
2. 安装 pnpm
|
||||
|
||||
```bash
|
||||
npm install pnpm -g
|
||||
```
|
||||
|
||||
3. 克隆仓库并拉取至本地,此处不再赘述
|
||||
4. 使用 `pnpm install` 安装项目依赖(若安装过程中遇到网络错误,请使用国内镜像源替代,此处不再赘述)
|
||||
5. 复制 `/.env.example` 文件并重命名为 `/.env` 并修改配置
|
||||
6. 打包客户端,请依据你的系统类型来选择,打包成功后,会输出安装包或可执行文件在 `/dist` 目录中,可自行安装
|
||||
|
||||
| 命令 | 系统类型 |
|
||||
| ------------------ | -------- |
|
||||
| `pnpm build:win` | Windows |
|
||||
| `pnpm build:linux` | Linux |
|
||||
| `pnpm build:mac` | MacOS |
|
||||
|
||||
## 😘 鸣谢
|
||||
|
||||
@@ -188,18 +236,10 @@ pnpm build:mac
|
||||
- [NeteaseCloudMusicApi](https://github.com/Binaryify/NeteaseCloudMusicApi)
|
||||
- [YesPlayMusic](https://github.com/qier222/YesPlayMusic)
|
||||
- [UnblockNeteaseMusic](https://github.com/UnblockNeteaseMusic/server)
|
||||
- [BlurLyric](https://github.com/Project-And-Factory/BlurLyric)
|
||||
- [applemusic-like-lyrics](https://github.com/Steve-xmh/applemusic-like-lyrics)
|
||||
- [Vue-mmPlayer](https://github.com/maomao1996/Vue-mmPlayer)
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 GPL Version 3 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 GPL Version 3,并在适当的地方注明原始项目的许可证
|
||||
3. **免责声明:** 根据 GPL Version 3,本项目不提供任何明示或暗示的担保。请详细阅读 GPL Version 3以了解完整的免责声明内容
|
||||
4. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
5. **许可证链接:** 请阅读 [GNU General Public License version 3](https://opensource.org/license/gpl-3-0/) 了解更多详情
|
||||
- [refined-now-playing-netease](https://github.com/solstice23/refined-now-playing-netease)
|
||||
- [material-color-utilities](https://github.com/material-foundation/material-color-utilities)
|
||||
|
||||
## 📢 免责声明
|
||||
|
||||
@@ -210,3 +250,18 @@ pnpm build:mac
|
||||
请使用者在使用本项目时遵守相关法律法规,**不要将本项目用于任何商业及非法用途。如有违反,一切后果由使用者自负。** 同时,使用者应该自行承担因使用本项目而带来的风险和责任。本项目开发者不对本项目所提供的服务和内容做出任何保证
|
||||
|
||||
感谢您的理解
|
||||
|
||||
## 📜 开源许可
|
||||
|
||||
- **本项目仅供个人学习研究使用,禁止用于商业及非法用途**
|
||||
- 本项目基于 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 许可进行开源
|
||||
1. **修改和分发:** 任何对本项目的修改和分发都必须基于 AGPL-3.0 进行,源代码必须一并提供
|
||||
2. **派生作品:** 任何派生作品必须同样采用 AGPL-3.0,并在适当的地方注明原始项目的许可证
|
||||
3. **注明原作者:** 在任何修改、派生作品或其他分发中,必须在适当的位置明确注明原作者及其贡献
|
||||
4. **免责声明:** 根据 AGPL-3.0,本项目不提供任何明示或暗示的担保。请详细阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 以了解完整的免责声明内容
|
||||
5. **社区参与:** 欢迎社区的参与和贡献,我们鼓励开发者一同改进和维护本项目
|
||||
6. **许可证链接:** 请阅读 [GNU Affero General Public License (AGPL-3.0)](https://www.gnu.org/licenses/agpl-3.0.html) 了解更多详情
|
||||
|
||||
## ⭐ Star History
|
||||
|
||||
[](https://star-history.com/#imsyy/SPlayer&Date)
|
||||
|
||||
309
auto-eslint.mjs
Normal file
309
auto-eslint.mjs
Normal file
@@ -0,0 +1,309 @@
|
||||
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,
|
||||
"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,
|
||||
"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,
|
||||
"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,
|
||||
"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
|
||||
}
|
||||
}
|
||||
239
auto-imports.d.ts
vendored
239
auto-imports.d.ts
vendored
@@ -3,67 +3,304 @@
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
// Generated by unplugin-auto-import
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const asyncComputed: typeof import('@vueuse/core')['asyncComputed']
|
||||
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const computedAsync: typeof import('@vueuse/core')['computedAsync']
|
||||
const computedEager: typeof import('@vueuse/core')['computedEager']
|
||||
const computedInject: typeof import('@vueuse/core')['computedInject']
|
||||
const computedWithControl: typeof import('@vueuse/core')['computedWithControl']
|
||||
const controlledComputed: typeof import('@vueuse/core')['controlledComputed']
|
||||
const controlledRef: typeof import('@vueuse/core')['controlledRef']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const createEventHook: typeof import('@vueuse/core')['createEventHook']
|
||||
const createGlobalState: typeof import('@vueuse/core')['createGlobalState']
|
||||
const createInjectionState: typeof import('@vueuse/core')['createInjectionState']
|
||||
const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn']
|
||||
const createRef: typeof import('@vueuse/core')['createRef']
|
||||
const createReusableTemplate: typeof import('@vueuse/core')['createReusableTemplate']
|
||||
const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable']
|
||||
const createTemplatePromise: typeof import('@vueuse/core')['createTemplatePromise']
|
||||
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
|
||||
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const extendRef: typeof import('@vueuse/core')['extendRef']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const 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 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 useTimeout: typeof import('@vueuse/core')['useTimeout']
|
||||
const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn']
|
||||
const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll']
|
||||
const useTimestamp: typeof import('@vueuse/core')['useTimestamp']
|
||||
const useTitle: typeof import('@vueuse/core')['useTitle']
|
||||
const useToNumber: typeof import('@vueuse/core')['useToNumber']
|
||||
const useToString: typeof import('@vueuse/core')['useToString']
|
||||
const useToggle: typeof import('@vueuse/core')['useToggle']
|
||||
const useTransition: typeof import('@vueuse/core')['useTransition']
|
||||
const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams']
|
||||
const useUserMedia: typeof import('@vueuse/core')['useUserMedia']
|
||||
const useVModel: typeof import('@vueuse/core')['useVModel']
|
||||
const useVModels: typeof import('@vueuse/core')['useVModels']
|
||||
const useVibrate: typeof import('@vueuse/core')['useVibrate']
|
||||
const useVirtualList: typeof import('@vueuse/core')['useVirtualList']
|
||||
const useWakeLock: typeof import('@vueuse/core')['useWakeLock']
|
||||
const useWebNotification: typeof import('@vueuse/core')['useWebNotification']
|
||||
const useWebSocket: typeof import('@vueuse/core')['useWebSocket']
|
||||
const useWebWorker: typeof import('@vueuse/core')['useWebWorker']
|
||||
const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn']
|
||||
const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus']
|
||||
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
|
||||
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchArray: typeof import('@vueuse/core')['watchArray']
|
||||
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
|
||||
const watchDebounced: typeof import('@vueuse/core')['watchDebounced']
|
||||
const watchDeep: typeof import('@vueuse/core')['watchDeep']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable']
|
||||
const watchImmediate: typeof import('@vueuse/core')['watchImmediate']
|
||||
const watchOnce: typeof import('@vueuse/core')['watchOnce']
|
||||
const watchPausable: typeof import('@vueuse/core')['watchPausable']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const watchThrottled: typeof import('@vueuse/core')['watchThrottled']
|
||||
const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable']
|
||||
const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter']
|
||||
const whenever: typeof import('@vueuse/core')['whenever']
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
|
||||
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
}
|
||||
|
||||
100
components.d.ts
vendored
100
components.d.ts
vendored
@@ -1,32 +1,54 @@
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AddPlaylist: typeof import('./src/components/Modal/AddPlaylist.vue')['default']
|
||||
AboutSetting: typeof import('./src/components/Setting/AboutSetting.vue')['default']
|
||||
ArtistList: typeof import('./src/components/List/ArtistList.vue')['default']
|
||||
BatchList: typeof import('./src/components/Modal/batchList.vue')['default']
|
||||
CloudMatch: typeof import('./src/components/Modal/CloudMatch.vue')['default']
|
||||
CommentList: typeof import('./src/components/List/CommentList.vue')['default']
|
||||
CountDown: typeof import('./src/components/Player/CountDown.vue')['default']
|
||||
CoverDropdown: typeof import('./src/components/Cover/CoverDropdown.vue')['default']
|
||||
CoverList: typeof import('./src/components/List/CoverList.vue')['default']
|
||||
CoverMenu: typeof import('./src/components/Menu/CoverMenu.vue')['default']
|
||||
CreatePlaylist: typeof import('./src/components/Modal/CreatePlaylist.vue')['default']
|
||||
DownloadSong: typeof import('./src/components/Modal/DownloadSong.vue')['default']
|
||||
ExcludeKeywords: typeof import('./src/components/Modal/ExcludeKeywords.vue')['default']
|
||||
FullPlayer: typeof import('./src/components/Player/FullPlayer.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/LoginQRCode.vue')['default']
|
||||
Lyric: typeof import('./src/components/Player/Lyric.vue')['default']
|
||||
MainControl: typeof import('./src/components/Player/MainControl.vue')['default']
|
||||
MainCover: typeof import('./src/components/Cover/MainCover.vue')['default']
|
||||
MainNav: typeof import('./src/components/Nav/MainNav.vue')['default']
|
||||
Menu: typeof import('./src/components/Global/Menu.vue')['default']
|
||||
GeneralSetting: typeof import('./src/components/Setting/GeneralSetting.vue')['default']
|
||||
JumpArtist: typeof import('./src/components/Modal/JumpArtist.vue')['default']
|
||||
KeyboardSetting: typeof import('./src/components/Setting/KeyboardSetting.vue')['default']
|
||||
LocalSetting: typeof import('./src/components/Setting/LocalSetting.vue')['default']
|
||||
Login: typeof import('./src/components/Modal/Login/Login.vue')['default']
|
||||
LoginCookie: typeof import('./src/components/Modal/Login/LoginCookie.vue')['default']
|
||||
LoginPhone: typeof import('./src/components/Modal/Login/LoginPhone.vue')['default']
|
||||
LoginQRCode: typeof import('./src/components/Modal/Login/LoginQRCode.vue')['default']
|
||||
LoginUID: typeof import('./src/components/Modal/Login/LoginUID.vue')['default']
|
||||
LyricsSetting: typeof import('./src/components/Setting/LyricsSetting.vue')['default']
|
||||
MainAMLyric: typeof import('./src/components/Player/MainAMLyric.vue')['default']
|
||||
MainLyric: typeof import('./src/components/Player/MainLyric.vue')['default']
|
||||
MainPlayer: typeof import('./src/components/Player/MainPlayer.vue')['default']
|
||||
MainPlayList: typeof import('./src/components/Player/MainPlayList.vue')['default']
|
||||
MainSetting: typeof import('./src/components/Setting/MainSetting.vue')['default']
|
||||
Menu: typeof import('./src/components/Layout/Menu.vue')['default']
|
||||
NA: typeof import('naive-ui')['NA']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
Nav: typeof import('./src/components/Layout/Nav.vue')['default']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NBackTop: typeof import('naive-ui')['NBackTop']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NCard: typeof import('naive-ui')['NCard']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
|
||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
@@ -34,41 +56,49 @@ declare module 'vue' {
|
||||
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']
|
||||
NLayoutHeader: typeof import('naive-ui')['NLayoutHeader']
|
||||
NLayoutSider: typeof import('naive-ui')['NLayoutSider']
|
||||
NLi: typeof import('naive-ui')['NLi']
|
||||
NList: typeof import('naive-ui')['NList']
|
||||
NListItem: typeof import('naive-ui')['NListItem']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMenu: typeof import('naive-ui')['NMenu']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPagination: typeof import('naive-ui')['NPagination']
|
||||
NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
|
||||
NOl: typeof import('naive-ui')['NOl']
|
||||
NP: typeof import('naive-ui')['NP']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NProgress: typeof import('naive-ui')['NProgress']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
|
||||
NResult: typeof import('naive-ui')['NResult']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSlider: typeof import('naive-ui')['NSlider']
|
||||
NSpace: typeof import('naive-ui')['NSpace']
|
||||
NSpin: typeof import('naive-ui')['NSpin']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTab: typeof import('naive-ui')['NTab']
|
||||
@@ -77,24 +107,38 @@ declare module 'vue' {
|
||||
NTag: typeof import('naive-ui')['NTag']
|
||||
NText: typeof import('naive-ui')['NText']
|
||||
NThing: typeof import('naive-ui')['NThing']
|
||||
Pagination: typeof import('./src/components/Global/Pagination.vue')['default']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
OtherSetting: typeof import('./src/components/Setting/OtherSetting.vue')['default']
|
||||
PersonalFM: typeof import('./src/components/Player/PersonalFM.vue')['default']
|
||||
PlayerBackground: typeof import('./src/components/Player/PlayerBackground.vue')['default']
|
||||
PlayerComment: typeof import('./src/components/Player/PlayerComment.vue')['default']
|
||||
PlayerControl: typeof import('./src/components/Player/PlayerControl.vue')['default']
|
||||
Playlist: typeof import('./src/components/Global/Playlist.vue')['default']
|
||||
PlaylistUpdate: typeof import('./src/components/Modal/PlaylistUpdate.vue')['default']
|
||||
PrivateFm: typeof import('./src/components/Player/PrivateFm.vue')['default']
|
||||
PlayerCover: typeof import('./src/components/Player/PlayerCover.vue')['default']
|
||||
PlayerData: typeof import('./src/components/Player/PlayerData.vue')['default']
|
||||
PlayerMenu: typeof import('./src/components/Player/PlayerMenu.vue')['default']
|
||||
PlayerSpectrum: typeof import('./src/components/Player/PlayerSpectrum.vue')['default']
|
||||
PlaylistAdd: typeof import('./src/components/Modal/PlaylistAdd.vue')['default']
|
||||
PlaySetting: typeof import('./src/components/Setting/PlaySetting.vue')['default']
|
||||
Provider: typeof import('./src/components/Global/Provider.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchHot: typeof import('./src/components/Search/SearchHot.vue')['default']
|
||||
SearchDefault: typeof import('./src/components/Search/SearchDefault.vue')['default']
|
||||
SearchInp: typeof import('./src/components/Search/SearchInp.vue')['default']
|
||||
SearchSuggestions: typeof import('./src/components/Search/SearchSuggestions.vue')['default']
|
||||
SearchInpMenu: typeof import('./src/components/Menu/SearchInpMenu.vue')['default']
|
||||
SearchSuggest: typeof import('./src/components/Search/SearchSuggest.vue')['default']
|
||||
Sider: typeof import('./src/components/Layout/Sider.vue')['default']
|
||||
SImage: typeof import('./src/components/UI/s-image.vue')['default']
|
||||
SongCard: typeof import('./src/components/Card/SongCard.vue')['default']
|
||||
SongDataCard: typeof import('./src/components/Card/SongDataCard.vue')['default']
|
||||
SongInfoEditor: typeof import('./src/components/Modal/SongInfoEditor.vue')['default']
|
||||
SongList: typeof import('./src/components/List/SongList.vue')['default']
|
||||
SongListDropdown: typeof import('./src/components/List/SongListDropdown.vue')['default']
|
||||
SpecialCover: typeof import('./src/components/Cover/SpecialCover.vue')['default']
|
||||
SpecialCoverCard: typeof import('./src/components/Cover/SpecialCoverCard.vue')['default']
|
||||
SongListCard: typeof import('./src/components/Card/SongListCard.vue')['default']
|
||||
SongListMenu: typeof import('./src/components/Menu/SongListMenu.vue')['default']
|
||||
SvgIcon: typeof import('./src/components/Global/SvgIcon.vue')['default']
|
||||
TitleBar: typeof import('./src/components/WinDom/TitleBar.vue')['default']
|
||||
UpCloudSong: typeof import('./src/components/Modal/UpCloudSong.vue')['default']
|
||||
UserData: typeof import('./src/components/Nav/UserData.vue')['default']
|
||||
TextContainer: typeof import('./src/components/Global/TextContainer.vue')['default']
|
||||
UpdateApp: typeof import('./src/components/Modal/UpdateApp.vue')['default']
|
||||
UpdatePlaylist: typeof import('./src/components/Modal/UpdatePlaylist.vue')['default']
|
||||
User: typeof import('./src/components/Layout/User.vue')['default']
|
||||
UserAgreement: typeof import('./src/components/Modal/UserAgreement.vue')['default']
|
||||
}
|
||||
}
|
||||
|
||||
3
dev-app-update.yml
Normal file
3
dev-app-update.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
provider: github
|
||||
owner: "imsyy"
|
||||
repo: "SPlayer"
|
||||
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
SPlayer:
|
||||
build:
|
||||
context: .
|
||||
image: splayer
|
||||
container_name: SPlayer
|
||||
volumes:
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
ports:
|
||||
- 25884:25884
|
||||
restart: always
|
||||
environment:
|
||||
# 所有变量都不是必填项
|
||||
# 网易云服务端 IP, 可在宿主机通过 ping music.163.com 获得
|
||||
- NETEASE_SERVER_IP=220.197.30.65
|
||||
# UnblockNeteaseMusic 使用的音源, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E9%9F%B3%E6%BA%90%E6%B8%85%E5%8D%95
|
||||
- UNBLOCK_SOURCES=kugou kuwo bilibili
|
||||
# 可添加 UnblockNeteaseMusic 支持的任何环境变量, 支持列表见 https://github.com/UnblockNeteaseMusic/server?tab=readme-ov-file#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F
|
||||
- ENABLE_FLAC=false
|
||||
- ENABLE_HTTPDNS=false
|
||||
- BLOCK_ADS=true
|
||||
- DISABLE_UPGRADE_CHECK=false
|
||||
- DEVELOPMENT=false
|
||||
- FOLLOW_SOURCE_ORDER=true
|
||||
- JSON_LOG=false
|
||||
- NO_CACHE=false
|
||||
- SELECT_MAX_BR=true
|
||||
- LOG_LEVEL=info
|
||||
- SEARCH_ALBUM=true
|
||||
29
docker-entrypoint.sh
Normal file
29
docker-entrypoint.sh
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
# start unblock service in the background
|
||||
npx unblockneteasemusic -p 80:443 -s -f ${NETEASE_SERVER_IP:-220.197.30.65} -o ${UNBLOCK_SOURCES:-kugou kuwo bilibili} 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 "$@"
|
||||
@@ -5,9 +5,12 @@ productName: SPlayer
|
||||
copyright: Copyright © imsyy 2023
|
||||
# 构建资源所在的目录
|
||||
directories:
|
||||
buildResources: build
|
||||
# 包含在最终应用程序构建中的文件列表,这里使用通配符 ! 表示排除不需要的文件
|
||||
buildResources: public
|
||||
# 包含在最终应用程序构建中的文件列表
|
||||
# 使用通配符 ! 表示排除不需要的文件
|
||||
files:
|
||||
- "public/**"
|
||||
- "out/**"
|
||||
- "!**/.vscode/*"
|
||||
- "!src/*"
|
||||
- "!electron.vite.config.{js,ts,mjs,cjs}"
|
||||
@@ -16,21 +19,20 @@ files:
|
||||
# 哪些文件将不会被压缩,而是解压到构建目录
|
||||
asarUnpack:
|
||||
- public/**
|
||||
# Windows 平台配置
|
||||
win:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
# 安装版
|
||||
- nsis
|
||||
# 打包版
|
||||
- portable
|
||||
# 管理员权限
|
||||
requestedExecutionLevel: highestAvailable
|
||||
# NSIS 安装器配置
|
||||
nsis:
|
||||
# 一键式安装程序还是辅助安装程序
|
||||
# 是否一键式安装
|
||||
oneClick: false
|
||||
# 安装程序的生成名称
|
||||
artifactName: ${productName}-${version}-setup.${ext}
|
||||
@@ -44,12 +46,16 @@ nsis:
|
||||
allowElevation: true
|
||||
# 是否允许用户更改安装目录
|
||||
allowToChangeInstallationDirectory: true
|
||||
# 安装包图标
|
||||
installerIcon: public/icons/favicon.ico
|
||||
# 卸载命令图标
|
||||
uninstallerIcon: public/icons/favicon.ico
|
||||
# macOS 平台配置
|
||||
mac:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
executableName: SPlayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_512.png
|
||||
icon: public/icons/favicon-512x512.png
|
||||
# 权限继承的文件路径
|
||||
entitlementsInherit: build/entitlements.mac.plist
|
||||
# 扩展信息,如权限描述
|
||||
@@ -62,22 +68,32 @@ mac:
|
||||
notarize: false
|
||||
darkModeSupport: true
|
||||
category: public.app-category.music
|
||||
target:
|
||||
- target: dmg
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
- target: zip
|
||||
arch:
|
||||
- x64
|
||||
- arm64
|
||||
# macOS 平台的 DMG 配置
|
||||
dmg:
|
||||
# DMG 文件的生成名称
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
# Linux 平台配置
|
||||
linux:
|
||||
# 可执行文件名
|
||||
executableName: splayer
|
||||
# 应用程序的图标文件路径
|
||||
icon: public/images/logo/favicon_256.png
|
||||
icon: public/icons/favicon-512x512.png
|
||||
# 构建类型
|
||||
target:
|
||||
- pacman
|
||||
- AppImage
|
||||
- snap
|
||||
- deb
|
||||
- rpm
|
||||
- snap
|
||||
- tar.gz
|
||||
# 维护者信息
|
||||
maintainer: imsyy.top
|
||||
@@ -86,7 +102,7 @@ linux:
|
||||
# AppImage 配置
|
||||
appImage:
|
||||
# AppImage 文件的生成名称
|
||||
artifactName: ${productName}-${version}.${ext}
|
||||
artifactName: ${name}-${version}.${ext}
|
||||
# 是否在构建之前重新编译原生模块
|
||||
npmRebuild: false
|
||||
# 自动更新的配置
|
||||
|
||||
@@ -1,91 +1,100 @@
|
||||
import { resolve } from "path";
|
||||
import {
|
||||
defineConfig,
|
||||
externalizeDepsPlugin,
|
||||
loadEnv,
|
||||
splitVendorChunkPlugin,
|
||||
} from "electron-vite";
|
||||
import { MainEnv } from "./env";
|
||||
import { defineConfig, externalizeDepsPlugin, loadEnv } from "electron-vite";
|
||||
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import Components from "unplugin-vue-components/vite";
|
||||
import viteCompression from "vite-plugin-compression";
|
||||
import wasm from "vite-plugin-wasm";
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
export default defineConfig(({ command, mode }) => {
|
||||
// 读取环境变量
|
||||
const getEnv = (name) => {
|
||||
const getEnv = (name: keyof MainEnv): string => {
|
||||
return loadEnv(mode, process.cwd())[name];
|
||||
};
|
||||
console.log(command);
|
||||
// 获取端口
|
||||
const webPort: number = Number(getEnv("VITE_WEB_PORT") || 14558);
|
||||
const servePort: number = Number(getEnv("VITE_SERVER_PORT") || 25884);
|
||||
// 返回配置
|
||||
return {
|
||||
// 主进程
|
||||
main: {
|
||||
resolve: {
|
||||
alias: {
|
||||
"@main": resolve(__dirname, "electron/main"),
|
||||
},
|
||||
},
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/main/index.js"),
|
||||
index: resolve(__dirname, "electron/main/index.ts"),
|
||||
lyric: resolve(__dirname, "web/lyric.html"),
|
||||
loading: resolve(__dirname, "web/loading.html"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 预渲染
|
||||
// 预加载
|
||||
preload: {
|
||||
plugins: [externalizeDepsPlugin()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: {
|
||||
index: resolve(__dirname, "electron/preload/index.js"),
|
||||
index: resolve(__dirname, "electron/preload/index.ts"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// 渲染进程
|
||||
renderer: {
|
||||
resolve: {
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src"),
|
||||
},
|
||||
},
|
||||
root: ".",
|
||||
plugins: [
|
||||
vue(),
|
||||
AutoImport({
|
||||
imports: [
|
||||
"vue",
|
||||
"vue-router",
|
||||
"@vueuse/core",
|
||||
{
|
||||
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
|
||||
},
|
||||
],
|
||||
eslintrc: {
|
||||
enabled: true,
|
||||
filepath: "./auto-eslint.mjs",
|
||||
},
|
||||
}),
|
||||
Components({
|
||||
resolvers: [NaiveUiResolver()],
|
||||
}),
|
||||
// viteCompression
|
||||
viteCompression(),
|
||||
// splitVendorChunkPlugin
|
||||
splitVendorChunkPlugin(),
|
||||
wasm(),
|
||||
],
|
||||
// 服务器配置
|
||||
server: {
|
||||
port: getEnv("MAIN_VITE_DEV_PORT"),
|
||||
// 代理
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://${getEnv("MAIN_VITE_SERVER_HOST")}:${getEnv("MAIN_VITE_SERVER_PORT")}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ""),
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": resolve(__dirname, "src/"),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
silenceDeprecations: ["legacy-js-api"],
|
||||
},
|
||||
},
|
||||
},
|
||||
// 构建
|
||||
root: ".",
|
||||
server: {
|
||||
port: webPort,
|
||||
// 代理
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: `http://127.0.0.1:${servePort}`,
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, "/api/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
preview: {
|
||||
port: webPort,
|
||||
},
|
||||
build: {
|
||||
minify: "terser",
|
||||
publicDir: resolve(__dirname, "public"),
|
||||
@@ -93,6 +102,11 @@ export default defineConfig(({ mode }) => {
|
||||
input: {
|
||||
index: resolve(__dirname, "index.html"),
|
||||
},
|
||||
output: {
|
||||
manualChunks: {
|
||||
stores: ["src/stores/data.ts", "src/stores/index.ts"],
|
||||
},
|
||||
},
|
||||
},
|
||||
terserOptions: {
|
||||
compress: {
|
||||
@@ -100,9 +114,6 @@ export default defineConfig(({ mode }) => {
|
||||
},
|
||||
},
|
||||
sourcemap: false,
|
||||
win: {
|
||||
icon: resolve(__dirname, "/public/images/logo/favicon.png"),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
1
electron/main/index.d.ts
vendored
Normal file
1
electron/main/index.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="electron-vite/node" />
|
||||
@@ -1,186 +0,0 @@
|
||||
import { join } from "path";
|
||||
import { app, protocol, shell, BrowserWindow, globalShortcut } from "electron";
|
||||
import { optimizer, is } from "@electron-toolkit/utils";
|
||||
import { startNcmServer } from "@main/startNcmServer";
|
||||
import { startMainServer } from "@main/startMainServer";
|
||||
import { configureAutoUpdater } from "@main/utils/checkUpdates";
|
||||
import createSystemInfo from "@main/utils/createSystemInfo";
|
||||
import createGlobalShortcut from "@main/utils/createGlobalShortcut";
|
||||
import mainIpcMain from "@main/mainIpcMain";
|
||||
import log from "electron-log";
|
||||
|
||||
// 屏蔽报错
|
||||
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.log
|
||||
console.log = log.log.bind(log);
|
||||
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
constructor() {
|
||||
// 主窗口
|
||||
this.mainWindow = null;
|
||||
// 主代理
|
||||
this.mainServer = null;
|
||||
// 网易云 API
|
||||
this.ncmServer = null;
|
||||
// 初始化
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化程序
|
||||
async init() {
|
||||
log.info("主进程初始化");
|
||||
|
||||
// 单例锁
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
app.quit();
|
||||
log.error("已有一个程序正在运行,本次启动阻止");
|
||||
}
|
||||
|
||||
// 启动网易云 API
|
||||
this.ncmServer = await startNcmServer({
|
||||
port: import.meta.env.MAIN_VITE_SERVER_PORT,
|
||||
host: import.meta.env.MAIN_VITE_SERVER_HOST,
|
||||
});
|
||||
|
||||
// 非开发环境启动代理
|
||||
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({
|
||||
width: 1280, // 窗口宽度
|
||||
height: 720, // 窗口高度
|
||||
minHeight: 700, // 最小高度
|
||||
minWidth: 1200, // 最小宽度
|
||||
center: true, // 是否出现在屏幕居中的位置
|
||||
show: false, // 初始时不显示窗口
|
||||
frame: false, // 无边框
|
||||
titleBarStyle: "customButtonsOnHover", // Macos 隐藏菜单栏
|
||||
autoHideMenuBar: true, // 失去焦点后自动隐藏菜单栏
|
||||
// 图标配置
|
||||
icon: join(__dirname, "../../public/images/logo/favicon.png"),
|
||||
// 预加载
|
||||
webPreferences: {
|
||||
// devTools: is.dev, //是否开启 DevTools
|
||||
preload: join(__dirname, "../preload/index.js"),
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
hardwareAcceleration: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 窗口准备就绪时显示窗口
|
||||
this.mainWindow.on("ready-to-show", () => {
|
||||
this.mainWindow.show();
|
||||
// mainWindow.maximize();
|
||||
});
|
||||
|
||||
// 主窗口事件
|
||||
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}`);
|
||||
}
|
||||
|
||||
// 监听关闭
|
||||
this.mainWindow.on("close", (event) => {
|
||||
if (!app.isQuiting) {
|
||||
event.preventDefault();
|
||||
this.mainWindow.hide();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
// 主应用程序事件
|
||||
mainAppEvents() {
|
||||
app.on("ready", async () => {
|
||||
// 创建主窗口
|
||||
this.createWindow();
|
||||
// 检测更新
|
||||
configureAutoUpdater(process.platform);
|
||||
// 创建系统信息
|
||||
createSystemInfo(this.mainWindow);
|
||||
// 引入主 Ipc
|
||||
mainIpcMain(this.mainWindow);
|
||||
// 注册快捷键
|
||||
createGlobalShortcut(this.mainWindow);
|
||||
});
|
||||
// 在开发模式下默认通过 F12 打开或关闭 DevTools
|
||||
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 (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 主窗口事件
|
||||
mainWindowEvents() {
|
||||
this.mainWindow.on("show", () => {
|
||||
console.info("窗口展示");
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
// this.mainWindow.on("hide", () => {
|
||||
// console.info("窗口隐藏");
|
||||
// });
|
||||
this.mainWindow.on("focus", () => {
|
||||
console.info("窗口获得焦点");
|
||||
this.mainWindow.webContents.send("lyricsScroll");
|
||||
});
|
||||
// this.mainWindow.on("blur", () => {
|
||||
// console.info("窗口失去焦点");
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
new MainProcess();
|
||||
292
electron/main/index.ts
Normal file
292
electron/main/index.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { app, shell, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
|
||||
import { electronApp } from "@electron-toolkit/utils";
|
||||
import { join } from "path";
|
||||
import { release, type } from "os";
|
||||
import { isDev, isMac, appName } from "./utils";
|
||||
import { unregisterShortcuts } from "./shortcut";
|
||||
import { initTray, MainTray } from "./tray";
|
||||
import { initThumbar, Thumbar } from "./thumbar";
|
||||
import { type StoreType, initStore } from "./store";
|
||||
import Store from "electron-store";
|
||||
import initAppServer from "../server";
|
||||
import initIpcMain from "./ipcMain";
|
||||
import log from "./logger";
|
||||
// icon
|
||||
import icon from "../../public/icons/favicon.png?asset";
|
||||
|
||||
// 屏蔽报错
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
|
||||
|
||||
// 模拟打包
|
||||
Object.defineProperty(app, "isPackaged", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
// 主进程
|
||||
class MainProcess {
|
||||
// 窗口
|
||||
mainWindow: BrowserWindow | null = null;
|
||||
lyricWindow: BrowserWindow | null = null;
|
||||
loadingWindow: BrowserWindow | null = null;
|
||||
// store
|
||||
store: Store<StoreType> | null = null;
|
||||
// 托盘
|
||||
mainTray: MainTray | null = null;
|
||||
// 工具栏
|
||||
thumbar: Thumbar | null = null;
|
||||
// 是否退出
|
||||
isQuit: boolean = false;
|
||||
constructor() {
|
||||
log.info("🚀 Main process startup");
|
||||
// 禁用 Windows 7 的 GPU 加速功能
|
||||
if (release().startsWith("6.1") && type() == "Windows_NT") app.disableHardwareAcceleration();
|
||||
// 单例锁
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
log.error("❌ There is already a program running and this process is terminated");
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
} else this.showWindow();
|
||||
// 准备就绪
|
||||
app.on("ready", async () => {
|
||||
log.info("🚀 Application Process Startup");
|
||||
// 设置应用程序名称
|
||||
electronApp.setAppUserModelId("com.imsyy.splayer");
|
||||
// 初始化 store
|
||||
this.store = initStore();
|
||||
// 启动主服务进程
|
||||
await initAppServer();
|
||||
// 启动进程
|
||||
this.createLoadingWindow();
|
||||
this.createMainWindow();
|
||||
this.createLyricsWindow();
|
||||
this.handleAppEvents();
|
||||
this.handleWindowEvents();
|
||||
// 注册其他服务
|
||||
this.mainTray = initTray(this.mainWindow!, this.lyricWindow!);
|
||||
this.thumbar = initThumbar(this.mainWindow!);
|
||||
// 注册主进程事件
|
||||
initIpcMain(
|
||||
this.mainWindow,
|
||||
this.lyricWindow,
|
||||
this.loadingWindow,
|
||||
this.mainTray,
|
||||
this.thumbar,
|
||||
this.store,
|
||||
);
|
||||
});
|
||||
}
|
||||
// 创建窗口
|
||||
createWindow(options: BrowserWindowConstructorOptions = {}): BrowserWindow {
|
||||
const defaultOptions: BrowserWindowConstructorOptions = {
|
||||
title: appName,
|
||||
width: 1280,
|
||||
height: 720,
|
||||
frame: false,
|
||||
center: true,
|
||||
// 图标
|
||||
icon,
|
||||
webPreferences: {
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
// 禁用渲染器沙盒
|
||||
sandbox: false,
|
||||
// 禁用同源策略
|
||||
webSecurity: false,
|
||||
// 允许 HTTP
|
||||
allowRunningInsecureContent: true,
|
||||
// 禁用拼写检查
|
||||
spellcheck: false,
|
||||
// 启用 Node.js
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInWorker: true,
|
||||
// 启用上下文隔离
|
||||
contextIsolation: false,
|
||||
},
|
||||
};
|
||||
// 合并参数
|
||||
options = Object.assign(defaultOptions, options);
|
||||
// 创建窗口
|
||||
const win = new BrowserWindow(options);
|
||||
return win;
|
||||
}
|
||||
// 创建主窗口
|
||||
createMainWindow() {
|
||||
// 窗口配置项
|
||||
const options: BrowserWindowConstructorOptions = {
|
||||
width: this.store?.get("window").width,
|
||||
height: this.store?.get("window").height,
|
||||
minHeight: 800,
|
||||
minWidth: 1280,
|
||||
// 菜单栏
|
||||
titleBarStyle: "customButtonsOnHover",
|
||||
// 立即显示窗口
|
||||
show: false,
|
||||
};
|
||||
// 初始化窗口
|
||||
this.mainWindow = this.createWindow(options);
|
||||
|
||||
// 渲染路径
|
||||
if (isDev && process.env["ELECTRON_RENDERER_URL"]) {
|
||||
this.mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]);
|
||||
} else {
|
||||
const port = Number(import.meta.env["VITE_SERVER_PORT"] || 25884);
|
||||
this.mainWindow.loadURL(`http://127.0.0.1:${port}`);
|
||||
}
|
||||
|
||||
// 配置网络代理
|
||||
if (this.store?.get("proxy")) {
|
||||
this.mainWindow.webContents.session.setProxy({ proxyRules: this.store?.get("proxy") });
|
||||
}
|
||||
|
||||
// 窗口打开处理程序
|
||||
this.mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
const { url } = details;
|
||||
if (url.startsWith("https://") || url.startsWith("http://")) {
|
||||
shell.openExternal(url);
|
||||
}
|
||||
return { action: "deny" };
|
||||
});
|
||||
}
|
||||
// 创建加载窗口
|
||||
createLoadingWindow() {
|
||||
// 初始化窗口
|
||||
this.loadingWindow = this.createWindow({
|
||||
width: 800,
|
||||
height: 560,
|
||||
maxWidth: 800,
|
||||
maxHeight: 560,
|
||||
resizable: false,
|
||||
});
|
||||
// 渲染路径
|
||||
this.loadingWindow.loadFile(join(__dirname, "../main/web/loading.html"));
|
||||
}
|
||||
// 创建桌面歌词窗口
|
||||
createLyricsWindow() {
|
||||
// 初始化窗口
|
||||
this.lyricWindow = this.createWindow({
|
||||
width: this.store?.get("lyric").width || 800,
|
||||
height: this.store?.get("lyric").height || 180,
|
||||
minWidth: 440,
|
||||
minHeight: 120,
|
||||
maxWidth: 1600,
|
||||
maxHeight: 300,
|
||||
// 窗口位置
|
||||
x: this.store?.get("lyric").x,
|
||||
y: this.store?.get("lyric").y,
|
||||
transparent: true,
|
||||
backgroundColor: "rgba(0, 0, 0, 0)",
|
||||
alwaysOnTop: true,
|
||||
resizable: true,
|
||||
movable: true,
|
||||
// 不在任务栏显示
|
||||
skipTaskbar: true,
|
||||
// 窗口不能最小化
|
||||
minimizable: false,
|
||||
// 窗口不能最大化
|
||||
maximizable: false,
|
||||
// 窗口不能进入全屏状态
|
||||
fullscreenable: false,
|
||||
show: false,
|
||||
});
|
||||
// 渲染路径
|
||||
this.lyricWindow.loadFile(join(__dirname, "../main/web/lyric.html"));
|
||||
}
|
||||
// 应用程序事件
|
||||
handleAppEvents() {
|
||||
// 窗口被关闭时
|
||||
app.on("window-all-closed", () => {
|
||||
if (!isMac) app.quit();
|
||||
this.mainWindow = null;
|
||||
this.loadingWindow = null;
|
||||
});
|
||||
|
||||
// 应用被激活
|
||||
app.on("activate", () => {
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
if (allWindows.length) {
|
||||
allWindows[0].focus();
|
||||
} else {
|
||||
this.createMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// 新增 session
|
||||
app.on("second-instance", () => {
|
||||
this.showWindow();
|
||||
});
|
||||
|
||||
// 自定义协议
|
||||
app.on("open-url", (_, url) => {
|
||||
console.log("Received custom protocol URL:", url);
|
||||
});
|
||||
|
||||
// 将要退出
|
||||
app.on("will-quit", () => {
|
||||
// 注销全部快捷键
|
||||
unregisterShortcuts();
|
||||
});
|
||||
|
||||
// 退出前
|
||||
app.on("before-quit", () => {
|
||||
this.isQuit = true;
|
||||
});
|
||||
}
|
||||
// 窗口事件
|
||||
handleWindowEvents() {
|
||||
this.mainWindow?.on("ready-to-show", () => {
|
||||
if (!this.mainWindow) return;
|
||||
this.thumbar = initThumbar(this.mainWindow);
|
||||
});
|
||||
this.mainWindow?.on("show", () => {
|
||||
// this.mainWindow?.webContents.send("lyricsScroll");
|
||||
});
|
||||
this.mainWindow?.on("focus", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
// 移动或缩放
|
||||
this.mainWindow?.on("resized", () => {
|
||||
// 若处于全屏则不保存
|
||||
if (this.mainWindow?.isFullScreen()) return;
|
||||
this.saveBounds();
|
||||
});
|
||||
this.mainWindow?.on("moved", () => {
|
||||
this.saveBounds();
|
||||
});
|
||||
|
||||
// 歌词窗口缩放
|
||||
this.lyricWindow?.on("resized", () => {
|
||||
const bounds = this.lyricWindow?.getBounds();
|
||||
if (bounds) {
|
||||
const { width, height } = bounds;
|
||||
this.store?.set("lyric", { ...this.store?.get("lyric"), width, height });
|
||||
}
|
||||
});
|
||||
|
||||
// 窗口关闭
|
||||
this.mainWindow?.on("close", (event) => {
|
||||
event.preventDefault();
|
||||
if (this.isQuit) {
|
||||
app.exit();
|
||||
} else {
|
||||
this.mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
// 更新窗口大小
|
||||
saveBounds() {
|
||||
if (this.mainWindow?.isFullScreen()) return;
|
||||
const bounds = this.mainWindow?.getBounds();
|
||||
if (bounds) this.store?.set("window", bounds);
|
||||
}
|
||||
// 显示窗口
|
||||
showWindow() {
|
||||
if (this.mainWindow) {
|
||||
this.mainWindow.show();
|
||||
if (this.mainWindow.isMinimized()) this.mainWindow.restore();
|
||||
this.mainWindow.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MainProcess();
|
||||
720
electron/main/ipcMain.ts
Normal file
720
electron/main/ipcMain.ts
Normal file
@@ -0,0 +1,720 @@
|
||||
import {
|
||||
app,
|
||||
ipcMain,
|
||||
BrowserWindow,
|
||||
powerSaveBlocker,
|
||||
screen,
|
||||
shell,
|
||||
dialog,
|
||||
net,
|
||||
session,
|
||||
} from "electron";
|
||||
import { File, Picture, Id3v2Settings } from "node-taglib-sharp";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { getFonts } from "font-list";
|
||||
import { MainTray } from "./tray";
|
||||
import { Thumbar } from "./thumbar";
|
||||
import { StoreType } from "./store";
|
||||
import { isDev, getFileID, getFileMD5 } from "./utils";
|
||||
import { isShortcutRegistered, registerShortcut, unregisterShortcuts } from "./shortcut";
|
||||
import { join, basename, resolve, relative, isAbsolute } from "path";
|
||||
import { download } from "electron-dl";
|
||||
import { checkUpdate, startDownloadUpdate } from "./update";
|
||||
import fs from "fs/promises";
|
||||
import log from "../main/logger";
|
||||
import Store from "electron-store";
|
||||
import fg from "fast-glob";
|
||||
import openLoginWin from "./loginWin";
|
||||
|
||||
// 注册 ipcMain
|
||||
const initIpcMain = (
|
||||
win: BrowserWindow | null,
|
||||
lyricWin: BrowserWindow | null,
|
||||
loadingWin: BrowserWindow | null,
|
||||
tray: MainTray | null,
|
||||
thumbar: Thumbar | null,
|
||||
store: Store<StoreType>,
|
||||
) => {
|
||||
initWinIpcMain(win, loadingWin, lyricWin, store);
|
||||
initLyricIpcMain(lyricWin, win, store);
|
||||
initTrayIpcMain(tray, win, lyricWin);
|
||||
initThumbarIpcMain(thumbar);
|
||||
initStoreIpcMain(store);
|
||||
initOtherIpcMain(win);
|
||||
};
|
||||
|
||||
// win
|
||||
const initWinIpcMain = (
|
||||
win: BrowserWindow | null,
|
||||
loadingWin: BrowserWindow | null,
|
||||
lyricWin: BrowserWindow | null,
|
||||
store: Store<StoreType>,
|
||||
) => {
|
||||
let preventId: number | null = null;
|
||||
|
||||
// 当前窗口状态
|
||||
ipcMain.on("win-state", (ev) => {
|
||||
ev.returnValue = win?.isMaximized();
|
||||
});
|
||||
|
||||
// 加载完成
|
||||
ipcMain.on("win-loaded", () => {
|
||||
if (loadingWin && !loadingWin.isDestroyed()) loadingWin.close();
|
||||
win?.show();
|
||||
win?.focus();
|
||||
});
|
||||
|
||||
// 最小化
|
||||
ipcMain.on("win-min", (ev) => {
|
||||
ev.preventDefault();
|
||||
win?.minimize();
|
||||
});
|
||||
// 最大化
|
||||
ipcMain.on("win-max", () => {
|
||||
win?.maximize();
|
||||
});
|
||||
// 还原
|
||||
ipcMain.on("win-restore", () => {
|
||||
win?.restore();
|
||||
});
|
||||
// 关闭
|
||||
ipcMain.on("win-close", (ev) => {
|
||||
ev.preventDefault();
|
||||
win?.close();
|
||||
app.quit();
|
||||
});
|
||||
// 隐藏
|
||||
ipcMain.on("win-hide", () => {
|
||||
win?.hide();
|
||||
});
|
||||
// 显示
|
||||
ipcMain.on("win-show", () => {
|
||||
win?.show();
|
||||
});
|
||||
// 重启
|
||||
ipcMain.on("win-reload", () => {
|
||||
app.quit();
|
||||
app.relaunch();
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
ipcMain.on("set-bar", (_, val: number | "none" | "indeterminate" | "error" | "paused") => {
|
||||
switch (val) {
|
||||
case "none":
|
||||
win?.setProgressBar(-1);
|
||||
break;
|
||||
case "indeterminate":
|
||||
win?.setProgressBar(2, { mode: "indeterminate" });
|
||||
break;
|
||||
case "error":
|
||||
win?.setProgressBar(1, { mode: "error" });
|
||||
break;
|
||||
case "paused":
|
||||
win?.setProgressBar(1, { mode: "paused" });
|
||||
break;
|
||||
default:
|
||||
if (typeof val === "number") {
|
||||
win?.setProgressBar(val / 100);
|
||||
} else {
|
||||
win?.setProgressBar(-1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 开启控制台
|
||||
ipcMain.on("open-dev-tools", () => {
|
||||
win?.webContents.openDevTools({
|
||||
title: "SPlayer DevTools",
|
||||
mode: isDev ? "right" : "detach",
|
||||
});
|
||||
});
|
||||
|
||||
// 获取系统全部字体
|
||||
ipcMain.handle("get-all-fonts", async () => {
|
||||
try {
|
||||
const fonts = await getFonts();
|
||||
return fonts;
|
||||
} catch (error) {
|
||||
log.error(`❌ Failed to get all system fonts: ${error}`);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
// 切换桌面歌词
|
||||
ipcMain.on("change-desktop-lyric", (_, val: boolean) => {
|
||||
if (val) {
|
||||
lyricWin?.show();
|
||||
lyricWin?.setAlwaysOnTop(true, "screen-saver");
|
||||
} else lyricWin?.hide();
|
||||
});
|
||||
|
||||
// 是否阻止系统息屏
|
||||
ipcMain.on("prevent-sleep", (_, val: boolean) => {
|
||||
if (val) {
|
||||
preventId = powerSaveBlocker.start("prevent-display-sleep");
|
||||
log.info("⏾ System sleep prevention started");
|
||||
} else {
|
||||
if (preventId !== null) {
|
||||
powerSaveBlocker.stop(preventId);
|
||||
log.info("✅ System sleep prevention stopped");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 默认文件夹
|
||||
ipcMain.handle(
|
||||
"get-default-dir",
|
||||
(_, 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 fg("**/*.{mp3,wav,flac}", { cwd: filePath });
|
||||
// 解析元信息
|
||||
const metadataPromises = musicFiles.map(async (file) => {
|
||||
const filePath = join(dirPath, file);
|
||||
// 处理元信息
|
||||
const { common, format } = await parseFile(filePath);
|
||||
// 获取文件大小
|
||||
const { size } = await fs.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],
|
||||
duration: (format?.duration ?? 0) * 1000,
|
||||
size: (size / (1024 * 1024)).toFixed(2),
|
||||
path: filePath,
|
||||
quality,
|
||||
};
|
||||
});
|
||||
const metadataArray = await Promise.all(metadataPromises);
|
||||
return metadataArray;
|
||||
} catch (error) {
|
||||
log.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 fs.stat(filePath)).size / (1024 * 1024),
|
||||
// 元信息
|
||||
common,
|
||||
// 音质信息
|
||||
format,
|
||||
// md5
|
||||
md5: await getFileMD5(filePath),
|
||||
};
|
||||
} catch (error) {
|
||||
log.error("❌ Error fetching music metadata:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 获取音乐歌词
|
||||
ipcMain.handle("get-music-lyric", async (_, path: string): Promise<string> => {
|
||||
try {
|
||||
const filePath = resolve(path).replace(/\\/g, "/");
|
||||
const { common } = await parseFile(filePath);
|
||||
const lyric = common?.lyrics;
|
||||
if (lyric && lyric.length > 0) return String(lyric[0]);
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
const lrcFilePath = filePath.replace(/\.[^.]+$/, ".lrc");
|
||||
try {
|
||||
await fs.access(lrcFilePath);
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
return lrcData || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.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 fs.access(coverFilePath);
|
||||
const coverData = await fs.readFile(coverFilePath);
|
||||
return { data: coverData, format: "image/jpeg" };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error fetching music cover:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 删除文件
|
||||
ipcMain.handle("delete-file", async (_, path: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const resolvedPath = resolve(path);
|
||||
// 检查文件是否存在
|
||||
try {
|
||||
await fs.access(resolvedPath);
|
||||
} catch {
|
||||
throw new Error("❌ File not found");
|
||||
}
|
||||
// 删除文件
|
||||
await fs.unlink(resolvedPath);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("❌ File delete error", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 打开文件夹
|
||||
ipcMain.on("open-folder", async (_, path: string) => {
|
||||
try {
|
||||
// 规范化路径
|
||||
const resolvedPath = resolve(path);
|
||||
// 检查文件夹是否存在
|
||||
try {
|
||||
await fs.access(resolvedPath);
|
||||
} catch {
|
||||
throw new Error("❌ Folder not found");
|
||||
}
|
||||
// 打开文件夹
|
||||
shell.showItemInFolder(resolvedPath);
|
||||
} catch (error) {
|
||||
log.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) {
|
||||
log.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) {
|
||||
log.error("❌ Path choose error", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 修改音乐元信息
|
||||
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) {
|
||||
log.error("❌ Error setting music metadata:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 下载文件
|
||||
ipcMain.handle(
|
||||
"download-file",
|
||||
async (
|
||||
_,
|
||||
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 {
|
||||
if (!win) return false;
|
||||
// 获取配置
|
||||
const {
|
||||
fileName,
|
||||
fileType,
|
||||
path,
|
||||
lyric,
|
||||
downloadMeta,
|
||||
downloadCover,
|
||||
downloadLyric,
|
||||
saveMetaFile,
|
||||
songData,
|
||||
} = options;
|
||||
// 规范化路径
|
||||
const downloadPath = resolve(path);
|
||||
// 检查文件夹是否存在
|
||||
try {
|
||||
await fs.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 fs.writeFile(lrcPath, lyric, "utf-8");
|
||||
}
|
||||
// 是否删除封面
|
||||
if (!saveMetaFile || !downloadCover) await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error("❌ Error downloading file:", error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// 取消代理
|
||||
ipcMain.on("remove-proxy", () => {
|
||||
store.set("proxy", "");
|
||||
win?.webContents.session.setProxy({ proxyRules: "" });
|
||||
log.info("✅ Remove proxy successfully");
|
||||
});
|
||||
|
||||
// 配置网络代理
|
||||
ipcMain.on("set-proxy", (_, config) => {
|
||||
const proxyRules = `${config.protocol}://${config.server}:${config.port}`;
|
||||
store.set("proxy", proxyRules);
|
||||
win?.webContents.session.setProxy({ proxyRules });
|
||||
log.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) {
|
||||
log.info("✅ Proxy test successful");
|
||||
resolve(true);
|
||||
} else {
|
||||
log.error(`❌ Proxy test failed with status code: ${response.statusCode}`);
|
||||
resolve(false);
|
||||
}
|
||||
});
|
||||
request.on("error", (error) => {
|
||||
log.error("❌ Error testing proxy:", error);
|
||||
resolve(false);
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
} catch (error) {
|
||||
log.error("❌ Error testing proxy:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 重置全部设置
|
||||
ipcMain.on("reset-setting", () => {
|
||||
store.reset();
|
||||
log.info("✅ Reset setting successfully");
|
||||
});
|
||||
|
||||
// 检查更新
|
||||
ipcMain.on("check-update", (_, showTip) => checkUpdate(win!, showTip));
|
||||
|
||||
// 开始下载更新
|
||||
ipcMain.on("start-download-update", () => startDownloadUpdate());
|
||||
|
||||
// 新建窗口
|
||||
ipcMain.on("open-login-web", () => openLoginWin(win!));
|
||||
};
|
||||
|
||||
// lyric
|
||||
const initLyricIpcMain = (
|
||||
lyricWin: BrowserWindow | null,
|
||||
mainWin: BrowserWindow | null,
|
||||
store: Store<StoreType>,
|
||||
): void => {
|
||||
// 音乐名称更改
|
||||
ipcMain.on("play-song-change", (_, title) => {
|
||||
if (!title) return;
|
||||
lyricWin?.webContents.send("play-song-change", title);
|
||||
});
|
||||
|
||||
// 音乐歌词更改
|
||||
ipcMain.on("play-lyric-change", (_, lyricData) => {
|
||||
if (!lyricData) return;
|
||||
lyricWin?.webContents.send("play-lyric-change", lyricData);
|
||||
});
|
||||
|
||||
// 获取窗口位置
|
||||
ipcMain.handle("get-window-bounds", () => {
|
||||
return lyricWin?.getBounds();
|
||||
});
|
||||
|
||||
// 获取屏幕尺寸
|
||||
ipcMain.handle("get-screen-size", () => {
|
||||
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
|
||||
return { width, height };
|
||||
});
|
||||
|
||||
// 移动窗口
|
||||
ipcMain.on("move-window", (_, x, y, width, height) => {
|
||||
lyricWin?.setBounds({ x, y, width, height });
|
||||
// 保存配置
|
||||
store.set("lyric", { ...store.get("lyric"), x, y, width, height });
|
||||
// 保持置顶
|
||||
lyricWin?.setAlwaysOnTop(true, "screen-saver");
|
||||
});
|
||||
|
||||
// 更新高度
|
||||
ipcMain.on("update-window-height", (_, height) => {
|
||||
if (!lyricWin) return;
|
||||
const { width } = lyricWin.getBounds();
|
||||
// 更新窗口高度
|
||||
lyricWin.setBounds({ width, height });
|
||||
});
|
||||
|
||||
// 获取配置
|
||||
ipcMain.handle("get-desktop-lyric-option", () => {
|
||||
return store.get("lyric");
|
||||
});
|
||||
|
||||
// 保存配置
|
||||
ipcMain.on("set-desktop-lyric-option", (_, option, callback: boolean = false) => {
|
||||
store.set("lyric", option);
|
||||
// 触发窗口更新
|
||||
if (callback && lyricWin) {
|
||||
lyricWin.webContents.send("desktop-lyric-option-change", option);
|
||||
}
|
||||
mainWin?.webContents.send("desktop-lyric-option-change", option);
|
||||
});
|
||||
|
||||
// 发送主程序事件
|
||||
ipcMain.on("send-main-event", (_, name, val) => {
|
||||
mainWin?.webContents.send(name, val);
|
||||
});
|
||||
|
||||
// 关闭桌面歌词
|
||||
ipcMain.on("closeDesktopLyric", () => {
|
||||
lyricWin?.hide();
|
||||
mainWin?.webContents.send("closeDesktopLyric");
|
||||
});
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
||||
if (!lyricWin) return;
|
||||
// 是否穿透
|
||||
if (isLock) {
|
||||
lyricWin.setIgnoreMouseEvents(true, { forward: true });
|
||||
} else {
|
||||
lyricWin.setIgnoreMouseEvents(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);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// tray
|
||||
const initTrayIpcMain = (
|
||||
tray: MainTray | null,
|
||||
win: BrowserWindow | null,
|
||||
lyricWin: BrowserWindow | null,
|
||||
): void => {
|
||||
// 音乐播放状态更改
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
tray?.setPlayState(playStatus ? "play" : "pause");
|
||||
lyricWin?.webContents.send("play-status-change", playStatus);
|
||||
});
|
||||
|
||||
// 音乐名称更改
|
||||
ipcMain.on("play-song-change", (_, title) => {
|
||||
if (!title) return;
|
||||
// 更改标题
|
||||
win?.setTitle(title);
|
||||
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("change-desktop-lyric", (_, val: boolean) => {
|
||||
tray?.setDesktopLyricShow(val);
|
||||
});
|
||||
|
||||
// 锁定/解锁桌面歌词
|
||||
ipcMain.on("toogleDesktopLyricLock", (_, isLock: boolean) => {
|
||||
tray?.setDesktopLyricLock(isLock);
|
||||
});
|
||||
};
|
||||
|
||||
// thumbar
|
||||
const initThumbarIpcMain = (thumbar: Thumbar | null): void => {
|
||||
if (!thumbar) return;
|
||||
// 更新工具栏
|
||||
ipcMain.on("play-status-change", (_, playStatus: boolean) => {
|
||||
thumbar?.updateThumbar(playStatus);
|
||||
});
|
||||
};
|
||||
|
||||
// store
|
||||
const initStoreIpcMain = (store: Store<StoreType>): void => {
|
||||
if (!store) return;
|
||||
};
|
||||
|
||||
// other
|
||||
const initOtherIpcMain = (mainWin: BrowserWindow | null): void => {
|
||||
// 快捷键是否被注册
|
||||
ipcMain.handle("is-shortcut-registered", (_, shortcut: string) => isShortcutRegistered(shortcut));
|
||||
|
||||
// 注册快捷键
|
||||
ipcMain.handle("register-all-shortcut", (_, allShortcuts: any): string[] | false => {
|
||||
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 initIpcMain;
|
||||
31
electron/main/logger.ts
Normal file
31
electron/main/logger.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// 日志输出
|
||||
import { join } from "path";
|
||||
import { app } from "electron";
|
||||
import { isDev } from "./utils";
|
||||
import log from "electron-log";
|
||||
|
||||
// 绑定事件
|
||||
Object.assign(console, log.functions);
|
||||
|
||||
// 日志配置
|
||||
log.transports.file.level = "info";
|
||||
log.transports.file.maxSize = 2 * 1024 * 1024; // 2M
|
||||
if (log.transports.ipc) log.transports.ipc.level = false;
|
||||
|
||||
// 控制台输出
|
||||
log.transports.console.useStyles = true;
|
||||
|
||||
// 文件输出
|
||||
log.transports.file.format = "{y}-{m}-{d} {h}:{i}:{s}:{ms} {text}";
|
||||
|
||||
// 本地输出
|
||||
if (!isDev) {
|
||||
log.transports.file.resolvePathFn = () =>
|
||||
join(app.getPath("documents"), "/SPlayer/SPlayer-log.txt");
|
||||
} else {
|
||||
log.transports.file.level = false;
|
||||
}
|
||||
|
||||
log.info("📃 logger initialized");
|
||||
|
||||
export default log;
|
||||
72
electron/main/loginWin.ts
Normal file
72
electron/main/loginWin.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { BrowserWindow, session } from "electron";
|
||||
import icon from "../../public/icons/favicon.png?asset";
|
||||
import { join } from "path";
|
||||
|
||||
const openLoginWin = async (mainWin: BrowserWindow) => {
|
||||
let loginTimer: NodeJS.Timeout;
|
||||
const loginSession = session.fromPartition("persist:login");
|
||||
// 清除 Cookie
|
||||
await loginSession.clearStorageData({
|
||||
storages: ["cookies", "localstorage"],
|
||||
});
|
||||
const loginWin = new BrowserWindow({
|
||||
parent: mainWin,
|
||||
title: "登录网易云音乐( 若遇到无响应请关闭后重试 )",
|
||||
width: 1280,
|
||||
height: 800,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
icon,
|
||||
// resizable: false,
|
||||
// movable: false,
|
||||
// minimizable: false,
|
||||
// maximizable: false,
|
||||
webPreferences: {
|
||||
session: loginSession,
|
||||
sandbox: false,
|
||||
webSecurity: false,
|
||||
preload: join(__dirname, "../preload/index.mjs"),
|
||||
},
|
||||
});
|
||||
|
||||
// 打开网易云
|
||||
loginWin.loadURL("https://music.163.com/#/login/");
|
||||
|
||||
// 阻止新窗口创建
|
||||
loginWin.webContents.setWindowOpenHandler(() => {
|
||||
return { action: "deny" };
|
||||
});
|
||||
|
||||
// 检查是否登录
|
||||
const checkLogin = async () => {
|
||||
try {
|
||||
loginWin.webContents.executeJavaScript(
|
||||
"document.title = '登录网易云音乐( 若遇到无响应请关闭后重试 )'",
|
||||
);
|
||||
// 是否登录?判断 MUSIC_U
|
||||
const MUSIC_U = await loginSession.cookies.get({
|
||||
name: "MUSIC_U",
|
||||
});
|
||||
if (MUSIC_U && MUSIC_U?.length > 0) {
|
||||
if (loginTimer) clearInterval(loginTimer);
|
||||
const value = `MUSIC_U=${MUSIC_U[0].value};`;
|
||||
// 发送回主进程
|
||||
mainWin?.webContents.send("send-cookies", value);
|
||||
loginWin.destroy();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
// 循环检查
|
||||
loginWin.webContents.once("did-finish-load", () => {
|
||||
loginWin.show();
|
||||
loginTimer = setInterval(checkLogin, 1000);
|
||||
loginWin.on("closed", () => {
|
||||
clearInterval(loginTimer);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export default openLoginWin;
|
||||
@@ -1,278 +0,0 @@
|
||||
import { ipcMain, dialog, app, clipboard, shell } from "electron";
|
||||
import { readDirAsync } from "@main/utils/readDirAsync";
|
||||
import { parseFile } from "music-metadata";
|
||||
import { write } from "node-id3";
|
||||
import { download } from "electron-dl";
|
||||
import getNeteaseMusicUrl from "@main/utils/getNeteaseMusicUrl";
|
||||
import axios from "axios";
|
||||
import fs from "fs/promises";
|
||||
|
||||
/**
|
||||
* 监听主进程的 IPC 事件
|
||||
* @param {BrowserWindow} win - 要监听 IPC 事件的程序窗口
|
||||
*/
|
||||
|
||||
const mainIpcMain = (win) => {
|
||||
// 窗口操作部分
|
||||
ipcMain.on("window-min", (ev) => {
|
||||
// 阻止最小化
|
||||
ev.preventDefault();
|
||||
// 最小化
|
||||
win.minimize();
|
||||
});
|
||||
ipcMain.on("window-maxOrRestore", (ev) => {
|
||||
const winSizeState = win.isMaximized();
|
||||
winSizeState ? win.restore() : win.maximize();
|
||||
ev.reply("window-state", win.isMaximized());
|
||||
});
|
||||
ipcMain.on("window-restore", () => {
|
||||
win.restore();
|
||||
});
|
||||
ipcMain.on("window-hide", () => {
|
||||
win.hide();
|
||||
});
|
||||
ipcMain.on("window-close", () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
});
|
||||
ipcMain.on("window-relaunch", () => {
|
||||
app.isQuiting = true;
|
||||
app.relaunch();
|
||||
app.quit();
|
||||
});
|
||||
|
||||
// 显示进度
|
||||
ipcMain.on("setProgressBar", (_, val) => {
|
||||
if (val === "close") {
|
||||
win.setProgressBar(-1);
|
||||
return false;
|
||||
}
|
||||
win.setProgressBar(val / 100);
|
||||
});
|
||||
|
||||
// 解灰
|
||||
ipcMain.handle("getMusicNumUrl", async (_, data) => {
|
||||
// 解析传入数据
|
||||
const songData = JSON.parse(data);
|
||||
const songName = `${songData?.name}-${songData?.artists?.[0].name}`;
|
||||
console.log("开始解灰:", songName);
|
||||
const url = await getNeteaseMusicUrl(songName);
|
||||
console.log("解灰地址:", url);
|
||||
return url;
|
||||
});
|
||||
|
||||
// bili 链接解析
|
||||
ipcMain.handle("getBiliUrlData", async (_, url) => {
|
||||
const data = await getBiliUrlBase64(url);
|
||||
return data;
|
||||
});
|
||||
|
||||
// 默认音乐文件夹
|
||||
ipcMain.handle("getdefaultMusicPath", async () => {
|
||||
const path = app.getPath("music");
|
||||
return path;
|
||||
});
|
||||
|
||||
// 选择文件夹
|
||||
ipcMain.handle("selectDir", async (_, isChooseDl = false) => {
|
||||
try {
|
||||
const { canceled, filePaths } = await dialog.showOpenDialog({
|
||||
title: isChooseDl ? "选择下载目录" : "选择添加目录",
|
||||
defaultPath: isChooseDl ? app.getPath("downloads") : app.getPath("music"),
|
||||
properties: ["openDirectory", "createDirectory"],
|
||||
buttonLabel: "选择文件夹",
|
||||
});
|
||||
if (!canceled) {
|
||||
const selectedDirectory = filePaths[0];
|
||||
return selectedDirectory;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("选择文件夹时发生错误:", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取文件夹内容
|
||||
ipcMain.handle("getDirContents", async (_, selectedDir) => {
|
||||
try {
|
||||
// 使用 readDirAsync 函数递归地读取文件夹内容
|
||||
const directoryContents = await readDirAsync(selectedDir);
|
||||
return directoryContents;
|
||||
} catch (err) {
|
||||
console.error("读取文件夹内容时发生错误:", err);
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取音乐歌词
|
||||
ipcMain.handle("getMusicLyric", async (_, path) => {
|
||||
try {
|
||||
const data = await parseFile(path);
|
||||
const lyric = data.common.lyrics;
|
||||
if (lyric && lyric.length > 0) {
|
||||
return lyric[0];
|
||||
}
|
||||
// 如果歌词数据不存在,尝试读取同名的 lrc 文件
|
||||
else {
|
||||
const lrcFilePath = path.replace(/\.[^.]+$/, ".lrc");
|
||||
const lrcData = await fs.readFile(lrcFilePath, "utf-8");
|
||||
// 返回读取的 lrc 数据,如果没有则返回 null
|
||||
return lrcData || null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("读取音乐歌词出错:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 读取音乐封面
|
||||
ipcMain.handle("getMusicCover", async (_, path) => {
|
||||
try {
|
||||
const data = await parseFile(path);
|
||||
const picture = data.common.picture;
|
||||
if (picture && picture.length > 0) {
|
||||
const coverData = picture[0].data;
|
||||
const coverFormat = picture[0].format;
|
||||
return { coverData, coverFormat };
|
||||
}
|
||||
// 如果封面数据不存在,尝试读取同名的封面图片文件
|
||||
else {
|
||||
const coverFilePath = path.replace(/\.[^.]+$/, ".jpg");
|
||||
const coverData = await fs.readFile(coverFilePath);
|
||||
// 返回读取的封面图片数据,如果没有则返回 null
|
||||
return coverData ? { coverData, coverFormat: "jpg" } : null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("读取音乐封面出错:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
// 执行复制操作
|
||||
ipcMain.handle("copyData", async (_, data) => {
|
||||
try {
|
||||
clipboard.writeText(data);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("复制操作出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 本地磁盘文件删除
|
||||
ipcMain.handle("deleteFile", async (_, path) => {
|
||||
try {
|
||||
// 检查文件是否存在
|
||||
if (fs.access(path)) {
|
||||
// 尝试删除文件
|
||||
fs.unlink(path);
|
||||
console.log(`文件已删除:${path}`);
|
||||
return true;
|
||||
} else {
|
||||
console.log(`文件不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`文件删除操作出错:${path}`, err);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 打开歌曲目录
|
||||
ipcMain.on("openSongLocal", (_, path) => {
|
||||
try {
|
||||
if (fs.access(path)) {
|
||||
shell.showItemInFolder(path);
|
||||
} else {
|
||||
console.log(`文件不存在:${path}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("打开歌曲目录时出错:", error);
|
||||
}
|
||||
});
|
||||
|
||||
// 下载文件至指定目录
|
||||
ipcMain.handle("downloadFile", async (_, data, song, songName, songType, path) => {
|
||||
try {
|
||||
if (fs.access(path)) {
|
||||
const songData = JSON.parse(song);
|
||||
console.info("开始下载:", songData, data);
|
||||
// 下载歌曲
|
||||
const songDownload = await download(win, data.url, {
|
||||
directory: path,
|
||||
filename: `${songName}.${songType}`,
|
||||
});
|
||||
// 下载封面
|
||||
const coverDownload = await download(win, songData.cover, {
|
||||
directory: path,
|
||||
filename: `${songName}.jpg`,
|
||||
});
|
||||
// 生成歌曲文件的元数据
|
||||
const songTag = {
|
||||
title: songData.name,
|
||||
artist: Array.isArray(songData.artists)
|
||||
? songData.artists.map((ar) => ar.name).join(" / ")
|
||||
: songData.artists || "未知歌手",
|
||||
album: songData.album?.name || songData.album,
|
||||
image: coverDownload.getSavePath(),
|
||||
};
|
||||
// 保存修改后的元数据
|
||||
write(songTag, songDownload.getSavePath());
|
||||
// 删除封面
|
||||
await fs.unlink(coverDownload.getSavePath());
|
||||
return true;
|
||||
} else {
|
||||
console.log(`目录不存在:${path}`);
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("下载文件时出错:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 从 Bilibili 视频中获取文件的 Base64 数据
|
||||
*
|
||||
* @param {string} url - 要获取的文件的 URL
|
||||
* @returns {Promise<string>} - 文件的 Base64 数据
|
||||
*/
|
||||
const getBiliUrlBase64 = async (url) => {
|
||||
try {
|
||||
const response = await axios.get(url, {
|
||||
headers: {
|
||||
Referer: "https://www.bilibili.com/",
|
||||
"User-Agent": "okhttp/3.4.1",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
withCredentials: false,
|
||||
});
|
||||
// 将二进制数据转换为缓冲区
|
||||
const buffer = toBuffer(response.data);
|
||||
// 将缓冲区中的数据转换为 Base64 编码的字符串
|
||||
const encodedData = buffer.toString("base64");
|
||||
// 返回 Base64 编码的文件数据
|
||||
return encodedData;
|
||||
} catch (error) {
|
||||
console.error("获取文件数据时发生错误:" + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 将数据转换为缓冲区( Buffer )
|
||||
*
|
||||
* @param {ArrayBuffer|Buffer|Uint8Array} data - 要转换的数据
|
||||
* @returns {Buffer} - 转换后的缓冲区
|
||||
*/
|
||||
const toBuffer = (data) => {
|
||||
if (data instanceof Buffer) {
|
||||
return data;
|
||||
} else {
|
||||
return Buffer.from(data);
|
||||
}
|
||||
};
|
||||
|
||||
export default mainIpcMain;
|
||||
30
electron/main/shortcut.ts
Normal file
30
electron/main/shortcut.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { globalShortcut } from "electron";
|
||||
import log from "../main/logger";
|
||||
|
||||
// 注册快捷键并检查
|
||||
export const registerShortcut = (shortcut: string, callback: () => void): boolean => {
|
||||
try {
|
||||
const success = globalShortcut.register(shortcut, callback);
|
||||
if (!success) {
|
||||
log.error(`❌ Failed to register shortcut: ${shortcut}`);
|
||||
return false;
|
||||
} else {
|
||||
log.info(`✅ Shortcut registered: ${shortcut}`);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`ℹ️ Error registering shortcut ${shortcut}:`, error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// 检查快捷键是否被注册
|
||||
export const isShortcutRegistered = (shortcut: string): boolean => {
|
||||
return globalShortcut.isRegistered(shortcut);
|
||||
};
|
||||
|
||||
// 卸载所有快捷键
|
||||
export const unregisterShortcuts = () => {
|
||||
globalShortcut.unregisterAll();
|
||||
log.info("🚫 All shortcuts unregistered.");
|
||||
};
|
||||
@@ -1,19 +0,0 @@
|
||||
import { join } from "path";
|
||||
import express from "express";
|
||||
import expressProxy from "express-http-proxy";
|
||||
|
||||
/**
|
||||
* 启动主服务器
|
||||
* @returns {import('http').Server} HTTP 服务器实例
|
||||
*/
|
||||
export const startMainServer = async () => {
|
||||
const { MAIN_VITE_MAIN_PORT, MAIN_VITE_SERVER_HOST, MAIN_VITE_SERVER_PORT } = import.meta.env;
|
||||
const port = MAIN_VITE_MAIN_PORT ?? 7899;
|
||||
const apiHost = `http://${MAIN_VITE_SERVER_HOST}:${MAIN_VITE_SERVER_PORT}`;
|
||||
const expressApp = express();
|
||||
// 代理
|
||||
expressApp.use("/", express.static(join(__dirname, "../renderer/")));
|
||||
expressApp.use("/api", expressProxy(apiHost));
|
||||
// 启动 Express 应用服务器,并监听指定端口
|
||||
return expressApp.listen(port, "127.0.0.1");
|
||||
};
|
||||
@@ -1,20 +0,0 @@
|
||||
const netEaseApi = require("NeteaseCloudMusicApi");
|
||||
|
||||
/**
|
||||
* 启动网易云音乐 API 服务器
|
||||
*
|
||||
* @async
|
||||
* @param {Object} options - 服务器配置
|
||||
* @param {number} [options.port=12141] - 服务器端口
|
||||
* @param {string} [options.host="127.0.0.1"] - 服务器主机地址
|
||||
* @returns {Promise<void>} 返回一个 Promise,在 API 服务器成功启动后 resolve
|
||||
*/
|
||||
export const startNcmServer = async (
|
||||
options = {
|
||||
port: 11451,
|
||||
host: "127.0.0.1",
|
||||
},
|
||||
) => {
|
||||
console.log(options);
|
||||
return await netEaseApi.serveNcmApi(options);
|
||||
};
|
||||
47
electron/main/store.ts
Normal file
47
electron/main/store.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import Store from "electron-store";
|
||||
import { screen } from "electron";
|
||||
import log from "./logger";
|
||||
|
||||
log.info("🌱 Store init");
|
||||
|
||||
export interface StoreType {
|
||||
window: {
|
||||
width: number;
|
||||
height: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
};
|
||||
lyric: {
|
||||
fontSize: number;
|
||||
mainColor: string;
|
||||
shadowColor: string;
|
||||
// 窗口位置
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
proxy: string;
|
||||
}
|
||||
|
||||
// 初始化仓库
|
||||
export const initStore = () => {
|
||||
return new Store<StoreType>({
|
||||
defaults: {
|
||||
window: {
|
||||
width: 1280,
|
||||
height: 800,
|
||||
},
|
||||
lyric: {
|
||||
fontSize: 30,
|
||||
mainColor: "#fff",
|
||||
shadowColor: "rgba(0, 0, 0, 0.5)",
|
||||
x: screen.getPrimaryDisplay().workAreaSize.width / 2 - 400,
|
||||
y: screen.getPrimaryDisplay().workAreaSize.height - 90,
|
||||
width: 800,
|
||||
height: 180,
|
||||
},
|
||||
proxy: "",
|
||||
},
|
||||
});
|
||||
};
|
||||
99
electron/main/thumbar.ts
Normal file
99
electron/main/thumbar.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { BrowserWindow, nativeImage, nativeTheme, ThumbarButton } from "electron";
|
||||
import { join } from "path";
|
||||
import { isWin } from "./utils";
|
||||
import log 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;
|
||||
}
|
||||
|
||||
// 工具栏图标
|
||||
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;
|
||||
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();
|
||||
}
|
||||
// 更新工具栏
|
||||
updateThumbar(playing: boolean = false, clean: boolean = false) {
|
||||
if (clean) return this.clearThumbar();
|
||||
this._win.setThumbarButtons([this._prev, playing ? this._pause : this._play, this._next]);
|
||||
}
|
||||
// 清除工具栏
|
||||
clearThumbar() {
|
||||
this._win.setThumbarButtons([]);
|
||||
}
|
||||
}
|
||||
|
||||
export const initThumbar = (win: BrowserWindow) => {
|
||||
try {
|
||||
// 若非 Win
|
||||
if (!isWin) return null;
|
||||
log.info("🚀 ThumbarButtons Startup");
|
||||
return new createThumbar(win);
|
||||
} catch (error) {
|
||||
log.error("❌ ThumbarButtons Error", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
278
electron/main/tray.ts
Normal file
278
electron/main/tray.ts
Normal file
@@ -0,0 +1,278 @@
|
||||
import {
|
||||
app,
|
||||
Tray,
|
||||
Menu,
|
||||
MenuItemConstructorOptions,
|
||||
BrowserWindow,
|
||||
nativeImage,
|
||||
nativeTheme,
|
||||
} from "electron";
|
||||
import { isWin, appName } from "./utils";
|
||||
import { join } from "path";
|
||||
import log from "./logger";
|
||||
|
||||
// 播放模式
|
||||
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;
|
||||
}
|
||||
|
||||
// 托盘图标
|
||||
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,
|
||||
lyricWin: 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: () => lyricWin.webContents.send("toogleDesktopLyricLock", !desktopLyricLock),
|
||||
},
|
||||
{
|
||||
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 _lyricWin: BrowserWindow;
|
||||
// 托盘
|
||||
private _tray: Tray;
|
||||
// 菜单
|
||||
private _menu: MenuItemConstructorOptions[];
|
||||
private _contextMenu: Menu;
|
||||
|
||||
constructor(win: BrowserWindow, lyricWin: BrowserWindow) {
|
||||
// 托盘图标
|
||||
const icon = trayIcon(isWin ? "tray.ico" : "tray@32.png").resize({
|
||||
height: 32,
|
||||
width: 32,
|
||||
});
|
||||
// 初始化数据
|
||||
this._win = win;
|
||||
this._lyricWin = lyricWin;
|
||||
this._tray = new Tray(icon);
|
||||
this._menu = createTrayMenu(this._win, this._lyricWin);
|
||||
this._contextMenu = Menu.buildFromTemplate(this._menu);
|
||||
// 初始化事件
|
||||
this.initTrayMenu();
|
||||
this.initEvents();
|
||||
this.setTitle(appName);
|
||||
}
|
||||
// 托盘菜单
|
||||
private initTrayMenu() {
|
||||
this._menu = createTrayMenu(this._win, this._lyricWin);
|
||||
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();
|
||||
});
|
||||
}
|
||||
// 设置标题
|
||||
setTitle(title: string) {
|
||||
this._tray.setTitle(title);
|
||||
this._tray.setToolTip(title);
|
||||
}
|
||||
// 设置播放名称
|
||||
setPlayName(name: string) {
|
||||
// 超长处理
|
||||
if (name.length > 20) name = name.slice(0, 20) + "...";
|
||||
playName = name;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
// 设置播放状态
|
||||
setPlayState(state: PlayState) {
|
||||
playState = state;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
// 设置播放模式
|
||||
setPlayMode(mode: PlayMode) {
|
||||
playMode = mode;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
// 设置喜欢状态
|
||||
setLikeState(like: boolean) {
|
||||
likeSong = like;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
// 桌面歌词开关
|
||||
setDesktopLyricShow(show: boolean) {
|
||||
desktopLyricShow = show;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
// 锁定桌面歌词
|
||||
setDesktopLyricLock(lock: boolean) {
|
||||
desktopLyricLock = lock;
|
||||
// 更新菜单
|
||||
this.initTrayMenu();
|
||||
}
|
||||
// 销毁托盘
|
||||
destroyTray() {
|
||||
this._tray.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export const initTray = (win: BrowserWindow, lyricWin: BrowserWindow) => {
|
||||
try {
|
||||
log.info("🚀 Tray Process Startup");
|
||||
return new CreateTray(win, lyricWin);
|
||||
} catch (error) {
|
||||
log.error("❌ Tray Process Error", error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
76
electron/main/update.ts
Normal file
76
electron/main/update.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { type BrowserWindow } from "electron";
|
||||
import electronUpdater from "electron-updater";
|
||||
import log from "./logger";
|
||||
|
||||
// import
|
||||
const { autoUpdater } = electronUpdater;
|
||||
|
||||
// 更新源
|
||||
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);
|
||||
log.info(`🚀 New version available: ${info.version}`);
|
||||
});
|
||||
|
||||
// 更新下载进度
|
||||
autoUpdater.on("download-progress", (progress) => {
|
||||
win.webContents.send("download-progress", progress);
|
||||
log.info(`🚀 Downloading: ${progress.percent}%`);
|
||||
});
|
||||
|
||||
// 当下载完成时
|
||||
autoUpdater.on("update-downloaded", (info) => {
|
||||
win.webContents.send("update-downloaded", info);
|
||||
log.info(`🚀 Update downloaded: ${info.version}`);
|
||||
// 安装更新
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
// 当没有新版本时
|
||||
autoUpdater.on("update-not-available", (info) => {
|
||||
if (isShowTip) win.webContents.send("update-not-available", info);
|
||||
log.info(`✅ No new version available: ${info.version}`);
|
||||
});
|
||||
|
||||
// 更新错误
|
||||
autoUpdater.on("error", (err) => {
|
||||
win.webContents.send("update-error", err);
|
||||
log.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();
|
||||
};
|
||||
32
electron/main/utils.ts
Normal file
32
electron/main/utils.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { app } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import fs from "fs/promises";
|
||||
import crypto from "crypto";
|
||||
|
||||
// 系统判断
|
||||
export const isDev = is.dev;
|
||||
export const isWin = process.platform === "win32";
|
||||
export const isMac = process.platform === "darwin";
|
||||
export const isLinux = process.platform === "linux";
|
||||
|
||||
// 程序名称
|
||||
export const appName = app.getName() || "SPlayer";
|
||||
|
||||
// 生成唯一ID
|
||||
export const getFileID = (filePath: string): number => {
|
||||
// SHA-256
|
||||
const hash = crypto.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
|
||||
export const getFileMD5 = async (path: string): Promise<string> => {
|
||||
const data = await fs.readFile(path);
|
||||
const hash = crypto.createHash("md5");
|
||||
hash.update(data);
|
||||
return hash.digest("hex");
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import { dialog, shell } from "electron";
|
||||
import { is } from "@electron-toolkit/utils";
|
||||
import { autoUpdater } from "electron-updater";
|
||||
|
||||
// 更新弹窗
|
||||
const hasNewVersion = (info) => {
|
||||
dialog
|
||||
.showMessageBox({
|
||||
title: "发现新版本 v" + info.version,
|
||||
message: "发现新版本 v" + info.version,
|
||||
detail: "是否前往 GitHub 下载新版本安装包?",
|
||||
buttons: ["前往", "取消"],
|
||||
type: "question",
|
||||
noLink: true,
|
||||
})
|
||||
.then((result) => {
|
||||
if (result.response === 0) {
|
||||
shell.openExternal("https://github.com/imsyy/SPlayer/releases");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const configureAutoUpdater = () => {
|
||||
if (is.dev) return false;
|
||||
autoUpdater.checkForUpdatesAndNotify();
|
||||
// 若有更新
|
||||
autoUpdater.on("update-available", (info) => {
|
||||
hasNewVersion(info);
|
||||
});
|
||||
};
|
||||
@@ -1,14 +0,0 @@
|
||||
import { globalShortcut } from "electron";
|
||||
|
||||
/**
|
||||
* 注册全局快捷键
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createGlobalShortcut = (win) => {
|
||||
// 刷新程序
|
||||
globalShortcut.register("CommandOrControl+R", () => {
|
||||
win?.reload();
|
||||
});
|
||||
};
|
||||
|
||||
export default createGlobalShortcut;
|
||||
@@ -1,115 +0,0 @@
|
||||
import { join } from "path";
|
||||
import { Tray, Menu, app, ipcMain, nativeImage, nativeTheme } from "electron";
|
||||
|
||||
// 当前播放歌曲数据
|
||||
let playSongName = "当前暂无播放歌曲";
|
||||
let playSongState = false;
|
||||
|
||||
/**
|
||||
* 创建系统自定义信息
|
||||
* @param {BrowserWindow} win - 程序窗口
|
||||
*/
|
||||
const createSystemInfo = (win) => {
|
||||
// 弹出列表
|
||||
app.setUserTasks([]);
|
||||
// 系统托盘
|
||||
const mainTray = new Tray(join(__dirname, "../../public/images/logo/favicon.png"));
|
||||
// 给托盘图标设置气球提示
|
||||
mainTray.setToolTip(app.getName());
|
||||
// 歌曲数据改变时
|
||||
ipcMain.on("songNameChange", (_, val) => {
|
||||
playSongName = val;
|
||||
// 托盘图标标题
|
||||
mainTray.setToolTip(val);
|
||||
// 更改应用标题
|
||||
win.setTitle(val);
|
||||
});
|
||||
ipcMain.on("songStateChange", (_, val) => {
|
||||
playSongState = val;
|
||||
});
|
||||
// 左键事件
|
||||
mainTray.on("click", () => {
|
||||
// 显示窗口
|
||||
win.show();
|
||||
});
|
||||
// 右键事件
|
||||
mainTray.on("right-click", () => {
|
||||
mainTray.popUpContextMenu(Menu.buildFromTemplate(createTrayMenu(win)));
|
||||
});
|
||||
};
|
||||
|
||||
// 生成右键菜单
|
||||
const createTrayMenu = (win) => {
|
||||
// 系统是否为暗色
|
||||
const isDarkMode = nativeTheme.shouldUseDarkColors;
|
||||
// 生成图标
|
||||
const createIcon = (name) => {
|
||||
return nativeImage
|
||||
.createFromPath(
|
||||
isDarkMode
|
||||
? join(__dirname, `../../public/images/icon/${name}-dark.png`)
|
||||
: join(__dirname, `../../public/images/icon/${name}-light.png`),
|
||||
)
|
||||
.resize({ width: 16, height: 16 });
|
||||
};
|
||||
// 返回菜单
|
||||
return [
|
||||
{
|
||||
label: playSongName,
|
||||
icon: createIcon("open"),
|
||||
click() {
|
||||
win.show();
|
||||
win.focus();
|
||||
win.webContents.send("showPlayer");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "上一曲",
|
||||
icon: createIcon("prev"),
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "prev");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: playSongState ? "暂停" : "播放",
|
||||
icon: createIcon(playSongState ? "pause" : "play"),
|
||||
click: () => {
|
||||
win.webContents.send("playOrPause");
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "下一曲",
|
||||
icon: createIcon("next"),
|
||||
click: () => {
|
||||
win.webContents.send("playNextOrPrev", "next");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "全局设置",
|
||||
icon: createIcon("setting"),
|
||||
click: () => {
|
||||
win.webContents.send("setting");
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
},
|
||||
{
|
||||
label: "退出",
|
||||
icon: createIcon("power"),
|
||||
click: () => {
|
||||
win.close();
|
||||
app.isQuiting = true;
|
||||
app.quit();
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export default createSystemInfo;
|
||||
@@ -1,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;
|
||||
@@ -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;
|
||||
};
|
||||
8
electron/preload/index.d.ts
vendored
Normal file
8
electron/preload/index.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
import { ElectronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
electron: ElectronAPI;
|
||||
api: unknown;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { contextBridge } from "electron";
|
||||
import { electronAPI } from "@electron-toolkit/preload";
|
||||
|
||||
// 如果启用了上下文隔离,使用 `contextBridge` 将 Electron API 暴露给渲染进程
|
||||
// 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 暴露 electronAPI 到渲染进程的全局对象中
|
||||
contextBridge.exposeInMainWorld("electron", electronAPI);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
// 如果上下文隔离未启用,将 electronAPI 添加到 DOM 全局对象
|
||||
// @ts-expect-error (define in dts)
|
||||
window.electron = electronAPI;
|
||||
}
|
||||
59
electron/server/index.ts
Normal file
59
electron/server/index.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { join } from "path";
|
||||
import { isDev } from "../main/utils";
|
||||
import initNcmAPI from "./netease";
|
||||
import initUnblockAPI from "./unblock";
|
||||
import fastifyCookie from "@fastify/cookie";
|
||||
import fastifyMultipart from "@fastify/multipart";
|
||||
import fastifyStatic from "@fastify/static";
|
||||
import fastify from "fastify";
|
||||
import log from "../main/logger";
|
||||
|
||||
const initAppServer = async () => {
|
||||
try {
|
||||
const server = fastify({
|
||||
// 忽略尾随斜杠
|
||||
ignoreTrailingSlash: true,
|
||||
});
|
||||
// 注册插件
|
||||
server.register(fastifyCookie);
|
||||
server.register(fastifyMultipart);
|
||||
// 生产环境启用静态文件
|
||||
if (!isDev) {
|
||||
log.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" });
|
||||
// 启动端口
|
||||
const port = Number(process.env["VITE_SERVER_PORT"] || 25884);
|
||||
await server.listen({ port });
|
||||
log.info(`🌐 Starting AppServer on port ${port}`);
|
||||
return server;
|
||||
} catch (error) {
|
||||
log.error("🚫 AppServer failed to start");
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export default initAppServer;
|
||||
66
electron/server/netease/index.ts
Normal file
66
electron/server/netease/index.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { pathCase } from "change-case";
|
||||
import NeteaseCloudMusicApi from "@neteaseapireborn/api";
|
||||
import log from "../../main/logger";
|
||||
|
||||
// 获取数据
|
||||
const getHandler = (name: string, neteaseApi: (params: any) => any) => {
|
||||
return async (
|
||||
req: FastifyRequest<{ Querystring: { [key: string]: string } }>,
|
||||
reply: FastifyReply,
|
||||
) => {
|
||||
log.info("🌐 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) {
|
||||
log.error("❌ NcmAPI Error:", error);
|
||||
if ([400, 301].includes(error.status)) {
|
||||
return reply.status(error.status).send(error.body);
|
||||
}
|
||||
return reply.status(500);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 初始化 NcmAPI
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
log.info("🌐 Register NcmAPI successfully");
|
||||
};
|
||||
|
||||
export default initNcmAPI;
|
||||
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;
|
||||
68
electron/server/unblock/index.ts
Normal file
68
electron/server/unblock/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
|
||||
import { SongUrlResult } from "./unblock";
|
||||
import getKuwoSongUrl from "./kuwo";
|
||||
import log from "../../main/logger";
|
||||
import axios from "axios";
|
||||
|
||||
/**
|
||||
* 直接获取 网易云云盘 链接
|
||||
* 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;
|
||||
log.info("🔗 NeteaseSongUrl URL:", songUrl);
|
||||
return { code: 200, url: songUrl };
|
||||
} catch (error) {
|
||||
log.error("❌ Get NeteaseSongUrl Error:", error);
|
||||
return { code: 404, url: null };
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化 UnblockAPI
|
||||
const UnblockAPI = 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);
|
||||
},
|
||||
);
|
||||
|
||||
log.info("🌐 Register UnblockAPI successfully");
|
||||
};
|
||||
|
||||
export default UnblockAPI;
|
||||
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 log 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) {
|
||||
log.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];
|
||||
log.info("🔗 KuwoSong URL:", urlMatch);
|
||||
return { code: 200, url: urlMatch };
|
||||
}
|
||||
return { code: 404, url: null };
|
||||
} catch (error) {
|
||||
log.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;
|
||||
}
|
||||
69
eslint.config.mjs
Normal file
69
eslint.config.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
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",
|
||||
"plugin:vue/vue3-essential",
|
||||
),
|
||||
{
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
vue,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
...autoEslint.globals,
|
||||
},
|
||||
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
parser: "@typescript-eslint/parser",
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/.eslintrc.{js,cjs}"],
|
||||
|
||||
languageOptions: {
|
||||
globals: { ...globals.node },
|
||||
ecmaVersion: 5,
|
||||
sourceType: "commonjs",
|
||||
},
|
||||
},
|
||||
];
|
||||
19
index.html
19
index.html
@@ -3,22 +3,19 @@
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="%RENDERER_VITE_SITE_LOGO%" />
|
||||
<link rel="apple-touch-icon" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="bookmark" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="apple-touch-icon-precomposed" sizes="200x200" href="%RENDERER_VITE_SITE_APPLE_LOGO%" />
|
||||
<link rel="icon" type="image/icon" href="/icons/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%RENDERER_VITE_SITE_TITLE%</title>
|
||||
<meta name="apple-mobile-web-app-title" content="%RENDERER_VITE_SITE_TITLE%" />
|
||||
<meta name="author" content="%RENDERER_VITE_SITE_ANTHOR%" />
|
||||
<meta name="keywords" content="%RENDERER_VITE_SITE_KEYWORDS%" />
|
||||
<meta name="description" content="%RENDERER_VITE_SITE_DES%" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<title>SPlayer</title>
|
||||
<!-- font -->
|
||||
<link rel="stylesheet" href="/fonts/font.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
51
nginx.conf
Normal file
51
nginx.conf
Normal file
@@ -0,0 +1,51 @@
|
||||
server {
|
||||
gzip on;
|
||||
listen 25884;
|
||||
listen [::]:25884;
|
||||
server_name localhost;
|
||||
client_max_body_size 100M;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location @rewrites {
|
||||
rewrite ^(.*)$ /index.html last;
|
||||
}
|
||||
|
||||
location /api/netease/song/url/v1 {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/song/url/v1;
|
||||
|
||||
sub_filter '"url":"https://music.163.com' '"url":"/music/unblock';
|
||||
sub_filter_types application/json;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
location /api/netease/ {
|
||||
proxy_buffers 16 64k;
|
||||
proxy_buffer_size 128k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Host $remote_addr;
|
||||
proxy_set_header X-NginX-Proxy true;
|
||||
proxy_pass http://localhost:3000/;
|
||||
}
|
||||
|
||||
location /music/unblock/ {
|
||||
proxy_pass https://music.163.com/;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
}
|
||||
152
package.json
152
package.json
@@ -1,68 +1,130 @@
|
||||
{
|
||||
"name": "splayer",
|
||||
"version": "2.0.0-beta.3",
|
||||
"productName": "SPlayer",
|
||||
"version": "3.0.0-beta.2",
|
||||
"description": "A minimalist music player",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "imsyy",
|
||||
"home": "https://imsyy.top",
|
||||
"github": "https://github.com/imsyy/SPlayer",
|
||||
"blog": "https://blog.imsyy.top",
|
||||
"repository": "github:imsyy/SPlayer",
|
||||
"license": "AGPL-3.0",
|
||||
"license-file": "LICENSE",
|
||||
"engines": {
|
||||
"node": ">=16.16.0"
|
||||
"node": ">=20",
|
||||
"npm": ">=10"
|
||||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts,.vue --fix",
|
||||
"lint": "npx eslint . --fix",
|
||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||
"typecheck:web": "vue-tsc --noEmit -p tsconfig.web.json --composite false",
|
||||
"typecheck": "npm run typecheck:node && npm run typecheck:web",
|
||||
"start": "electron-vite preview",
|
||||
"dev": "chcp 65001 && electron-vite dev --watch",
|
||||
"build": "electron-vite build",
|
||||
"dev": "electron-vite dev",
|
||||
"build": "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",
|
||||
"build:mac": "npm run build && electron-builder --mac",
|
||||
"build:linux": "npm run build && electron-builder --linux"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"@material/material-color-utilities": "^0.2.7",
|
||||
"NeteaseCloudMusicApi": "^4.13.5",
|
||||
"axios": "^1.4.0",
|
||||
"colorthief": "^2.4.0",
|
||||
"electron-dl": "^3.5.1",
|
||||
"electron-updater": "^6.1.7",
|
||||
"express": "^4.18.2",
|
||||
"express-http-proxy": "^1.6.3",
|
||||
"howler": "^2.2.3",
|
||||
"@applemusic-like-lyrics/core": "^0.1.3",
|
||||
"@applemusic-like-lyrics/lyric": "^0.2.4",
|
||||
"@applemusic-like-lyrics/vue": "^0.1.5",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@imsyy/color-utils": "^1.0.2",
|
||||
"@material/material-color-utilities": "^0.3.0",
|
||||
"@neteaseapireborn/api": "^4.29.2",
|
||||
"@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": "^12.8.2",
|
||||
"axios": "^1.11.0",
|
||||
"change-case": "^5.4.4",
|
||||
"dayjs": "^1.11.13",
|
||||
"electron-dl": "^4.0.0",
|
||||
"electron-store": "^8.2.0",
|
||||
"electron-updater": "^6.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"font-list": "^1.5.1",
|
||||
"get-port": "^7.1.0",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"howler": "^2.2.4",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jss": "^10.10.0",
|
||||
"jss-preset-default": "^10.10.0",
|
||||
"localforage": "^1.10.0",
|
||||
"music-metadata": "7.13.4",
|
||||
"node-id3": "^0.2.6",
|
||||
"pinia": "^2.1.6",
|
||||
"pinia-plugin-persistedstate": "^3.2.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^15.0.8",
|
||||
"md5": "^2.3.0",
|
||||
"music-metadata": "10.5.1",
|
||||
"pinia": "^2.3.1",
|
||||
"pinia-plugin-persistedstate": "^4.2.0",
|
||||
"plyr": "^3.7.8",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"screenfull": "^6.0.2",
|
||||
"vue-router": "^4.2.4",
|
||||
"vue-slider-component": "4.1.0-beta.7"
|
||||
"vue-virt-list": "^1.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.1",
|
||||
"@rushstack/eslint-patch": "^1.3.3",
|
||||
"@vitejs/plugin-vue": "^4.3.1",
|
||||
"@vue/eslint-config-prettier": "^8.0.0",
|
||||
"electron": "^25.6.0",
|
||||
"electron-builder": "^24.6.4",
|
||||
"electron-log": "^5.0.1",
|
||||
"electron-vite": "^1.0.27",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-vue": "^9.17.0",
|
||||
"naive-ui": "^2.34.4",
|
||||
"prettier": "^3.0.2",
|
||||
"sass": "^1.66.1",
|
||||
"terser": "^5.19.2",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-vue-components": "^0.25.1",
|
||||
"vite": "^4.4.9",
|
||||
"@electron-toolkit/preload": "^3.0.1",
|
||||
"@electron-toolkit/tsconfig": "^1.0.1",
|
||||
"@electron-toolkit/utils": "^3.0.0",
|
||||
"@fastify/cookie": "^11.0.2",
|
||||
"@fastify/http-proxy": "^11.1.2",
|
||||
"@fastify/multipart": "^9.0.3",
|
||||
"@fastify/static": "^8.1.1",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/howler": "^2.2.12",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@types/md5": "^2.3.5",
|
||||
"@types/node": "^22.14.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.30.1",
|
||||
"@typescript-eslint/parser": "^8.30.1",
|
||||
"@vitejs/plugin-vue": "^5.2.3",
|
||||
"ajv": "^8.17.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"electron": "^35.1.5",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-log": "^5.3.4",
|
||||
"electron-vite": "^3.1.0",
|
||||
"eslint": "^9.24.0",
|
||||
"eslint-plugin-vue": "^9.33.0",
|
||||
"fast-glob": "^3.3.3",
|
||||
"fastify": "^5.3.1",
|
||||
"naive-ui": "^2.42.0",
|
||||
"node-taglib-sharp": "^6.0.1",
|
||||
"prettier": "^3.5.3",
|
||||
"sass": "^1.86.3",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "^5.8.3",
|
||||
"unplugin-auto-import": "^0.19.0",
|
||||
"unplugin-vue-components": "^28.5.0",
|
||||
"vite": "^5.4.18",
|
||||
"vite-plugin-compression": "^0.5.1",
|
||||
"vue": "^3.3.4"
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"vue": "^3.5.18",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue-tsc": "^3.0.5"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"dmg-builder": "25.1.8",
|
||||
"electron-builder-squirrel-windows": "25.1.8"
|
||||
},
|
||||
"onlyBuiltDependencies": [
|
||||
"@parcel/watcher",
|
||||
"core-js",
|
||||
"electron",
|
||||
"esbuild",
|
||||
"vue-demi"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
11820
pnpm-lock.yaml
generated
11820
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
1
public/font/font.min.css
vendored
1
public/font/font.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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.
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