mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 11:29:42 +08:00
feat:缓存路径支持自定义
下载路径支持自定义;fix:播放页面唱针可以拖动问题 播放按钮加载中 因为自动下一曲 导致动画变形问题 SMTC 功能 系统显示未知应用问题 播放页歌词字体粗细偶现丢失问题
This commit is contained in:
31
.vitepress/cache/deps/_metadata.json
vendored
31
.vitepress/cache/deps/_metadata.json
vendored
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"hash": "23b978c5",
|
|
||||||
"configHash": "c96c5ee9",
|
|
||||||
"lockfileHash": "603038da",
|
|
||||||
"browserHash": "b1457114",
|
|
||||||
"optimized": {
|
|
||||||
"vue": {
|
|
||||||
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
|
||||||
"file": "vue.js",
|
|
||||||
"fileHash": "7c4217d1",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"vitepress > @vue/devtools-api": {
|
|
||||||
"src": "../../../node_modules/vitepress/node_modules/@vue/devtools-api/dist/index.js",
|
|
||||||
"file": "vitepress___@vue_devtools-api.js",
|
|
||||||
"fileHash": "dc8e5ae9",
|
|
||||||
"needsInterop": false
|
|
||||||
},
|
|
||||||
"vitepress > @vueuse/core": {
|
|
||||||
"src": "../../../node_modules/@vueuse/core/index.mjs",
|
|
||||||
"file": "vitepress___@vueuse_core.js",
|
|
||||||
"fileHash": "74c34320",
|
|
||||||
"needsInterop": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chunks": {
|
|
||||||
"chunk-TH7GRLUQ": {
|
|
||||||
"file": "chunk-TH7GRLUQ.js"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
12833
.vitepress/cache/deps/chunk-TH7GRLUQ.js
vendored
12833
.vitepress/cache/deps/chunk-TH7GRLUQ.js
vendored
File diff suppressed because it is too large
Load Diff
7
.vitepress/cache/deps/chunk-TH7GRLUQ.js.map
vendored
7
.vitepress/cache/deps/chunk-TH7GRLUQ.js.map
vendored
File diff suppressed because one or more lines are too long
3
.vitepress/cache/deps/package.json
vendored
3
.vitepress/cache/deps/package.json
vendored
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
4140
.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
4140
.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
9923
.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
9923
.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
342
.vitepress/cache/deps/vue.js
vendored
342
.vitepress/cache/deps/vue.js
vendored
@@ -1,342 +0,0 @@
|
|||||||
import {
|
|
||||||
BaseTransition,
|
|
||||||
BaseTransitionPropsValidators,
|
|
||||||
Comment,
|
|
||||||
DeprecationTypes,
|
|
||||||
EffectScope,
|
|
||||||
ErrorCodes,
|
|
||||||
ErrorTypeStrings,
|
|
||||||
Fragment,
|
|
||||||
KeepAlive,
|
|
||||||
ReactiveEffect,
|
|
||||||
Static,
|
|
||||||
Suspense,
|
|
||||||
Teleport,
|
|
||||||
Text,
|
|
||||||
TrackOpTypes,
|
|
||||||
Transition,
|
|
||||||
TransitionGroup,
|
|
||||||
TriggerOpTypes,
|
|
||||||
VueElement,
|
|
||||||
assertNumber,
|
|
||||||
callWithAsyncErrorHandling,
|
|
||||||
callWithErrorHandling,
|
|
||||||
camelize,
|
|
||||||
capitalize,
|
|
||||||
cloneVNode,
|
|
||||||
compatUtils,
|
|
||||||
compile,
|
|
||||||
computed,
|
|
||||||
createApp,
|
|
||||||
createBaseVNode,
|
|
||||||
createBlock,
|
|
||||||
createCommentVNode,
|
|
||||||
createElementBlock,
|
|
||||||
createHydrationRenderer,
|
|
||||||
createPropsRestProxy,
|
|
||||||
createRenderer,
|
|
||||||
createSSRApp,
|
|
||||||
createSlots,
|
|
||||||
createStaticVNode,
|
|
||||||
createTextVNode,
|
|
||||||
createVNode,
|
|
||||||
customRef,
|
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
defineCustomElement,
|
|
||||||
defineEmits,
|
|
||||||
defineExpose,
|
|
||||||
defineModel,
|
|
||||||
defineOptions,
|
|
||||||
defineProps,
|
|
||||||
defineSSRCustomElement,
|
|
||||||
defineSlots,
|
|
||||||
devtools,
|
|
||||||
effect,
|
|
||||||
effectScope,
|
|
||||||
getCurrentInstance,
|
|
||||||
getCurrentScope,
|
|
||||||
getCurrentWatcher,
|
|
||||||
getTransitionRawChildren,
|
|
||||||
guardReactiveProps,
|
|
||||||
h,
|
|
||||||
handleError,
|
|
||||||
hasInjectionContext,
|
|
||||||
hydrate,
|
|
||||||
hydrateOnIdle,
|
|
||||||
hydrateOnInteraction,
|
|
||||||
hydrateOnMediaQuery,
|
|
||||||
hydrateOnVisible,
|
|
||||||
initCustomFormatter,
|
|
||||||
initDirectivesForSSR,
|
|
||||||
inject,
|
|
||||||
isMemoSame,
|
|
||||||
isProxy,
|
|
||||||
isReactive,
|
|
||||||
isReadonly,
|
|
||||||
isRef,
|
|
||||||
isRuntimeOnly,
|
|
||||||
isShallow,
|
|
||||||
isVNode,
|
|
||||||
markRaw,
|
|
||||||
mergeDefaults,
|
|
||||||
mergeModels,
|
|
||||||
mergeProps,
|
|
||||||
nextTick,
|
|
||||||
normalizeClass,
|
|
||||||
normalizeProps,
|
|
||||||
normalizeStyle,
|
|
||||||
onActivated,
|
|
||||||
onBeforeMount,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onBeforeUpdate,
|
|
||||||
onDeactivated,
|
|
||||||
onErrorCaptured,
|
|
||||||
onMounted,
|
|
||||||
onRenderTracked,
|
|
||||||
onRenderTriggered,
|
|
||||||
onScopeDispose,
|
|
||||||
onServerPrefetch,
|
|
||||||
onUnmounted,
|
|
||||||
onUpdated,
|
|
||||||
onWatcherCleanup,
|
|
||||||
openBlock,
|
|
||||||
popScopeId,
|
|
||||||
provide,
|
|
||||||
proxyRefs,
|
|
||||||
pushScopeId,
|
|
||||||
queuePostFlushCb,
|
|
||||||
reactive,
|
|
||||||
readonly,
|
|
||||||
ref,
|
|
||||||
registerRuntimeCompiler,
|
|
||||||
render,
|
|
||||||
renderList,
|
|
||||||
renderSlot,
|
|
||||||
resolveComponent,
|
|
||||||
resolveDirective,
|
|
||||||
resolveDynamicComponent,
|
|
||||||
resolveFilter,
|
|
||||||
resolveTransitionHooks,
|
|
||||||
setBlockTracking,
|
|
||||||
setDevtoolsHook,
|
|
||||||
setTransitionHooks,
|
|
||||||
shallowReactive,
|
|
||||||
shallowReadonly,
|
|
||||||
shallowRef,
|
|
||||||
ssrContextKey,
|
|
||||||
ssrUtils,
|
|
||||||
stop,
|
|
||||||
toDisplayString,
|
|
||||||
toHandlerKey,
|
|
||||||
toHandlers,
|
|
||||||
toRaw,
|
|
||||||
toRef,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
transformVNodeArgs,
|
|
||||||
triggerRef,
|
|
||||||
unref,
|
|
||||||
useAttrs,
|
|
||||||
useCssModule,
|
|
||||||
useCssVars,
|
|
||||||
useHost,
|
|
||||||
useId,
|
|
||||||
useModel,
|
|
||||||
useSSRContext,
|
|
||||||
useShadowRoot,
|
|
||||||
useSlots,
|
|
||||||
useTemplateRef,
|
|
||||||
useTransitionState,
|
|
||||||
vModelCheckbox,
|
|
||||||
vModelDynamic,
|
|
||||||
vModelRadio,
|
|
||||||
vModelSelect,
|
|
||||||
vModelText,
|
|
||||||
vShow,
|
|
||||||
version,
|
|
||||||
warn,
|
|
||||||
watch,
|
|
||||||
watchEffect,
|
|
||||||
watchPostEffect,
|
|
||||||
watchSyncEffect,
|
|
||||||
withAsyncContext,
|
|
||||||
withCtx,
|
|
||||||
withDefaults,
|
|
||||||
withDirectives,
|
|
||||||
withKeys,
|
|
||||||
withMemo,
|
|
||||||
withModifiers,
|
|
||||||
withScopeId
|
|
||||||
} from './chunk-TH7GRLUQ.js'
|
|
||||||
export {
|
|
||||||
BaseTransition,
|
|
||||||
BaseTransitionPropsValidators,
|
|
||||||
Comment,
|
|
||||||
DeprecationTypes,
|
|
||||||
EffectScope,
|
|
||||||
ErrorCodes,
|
|
||||||
ErrorTypeStrings,
|
|
||||||
Fragment,
|
|
||||||
KeepAlive,
|
|
||||||
ReactiveEffect,
|
|
||||||
Static,
|
|
||||||
Suspense,
|
|
||||||
Teleport,
|
|
||||||
Text,
|
|
||||||
TrackOpTypes,
|
|
||||||
Transition,
|
|
||||||
TransitionGroup,
|
|
||||||
TriggerOpTypes,
|
|
||||||
VueElement,
|
|
||||||
assertNumber,
|
|
||||||
callWithAsyncErrorHandling,
|
|
||||||
callWithErrorHandling,
|
|
||||||
camelize,
|
|
||||||
capitalize,
|
|
||||||
cloneVNode,
|
|
||||||
compatUtils,
|
|
||||||
compile,
|
|
||||||
computed,
|
|
||||||
createApp,
|
|
||||||
createBlock,
|
|
||||||
createCommentVNode,
|
|
||||||
createElementBlock,
|
|
||||||
createBaseVNode as createElementVNode,
|
|
||||||
createHydrationRenderer,
|
|
||||||
createPropsRestProxy,
|
|
||||||
createRenderer,
|
|
||||||
createSSRApp,
|
|
||||||
createSlots,
|
|
||||||
createStaticVNode,
|
|
||||||
createTextVNode,
|
|
||||||
createVNode,
|
|
||||||
customRef,
|
|
||||||
defineAsyncComponent,
|
|
||||||
defineComponent,
|
|
||||||
defineCustomElement,
|
|
||||||
defineEmits,
|
|
||||||
defineExpose,
|
|
||||||
defineModel,
|
|
||||||
defineOptions,
|
|
||||||
defineProps,
|
|
||||||
defineSSRCustomElement,
|
|
||||||
defineSlots,
|
|
||||||
devtools,
|
|
||||||
effect,
|
|
||||||
effectScope,
|
|
||||||
getCurrentInstance,
|
|
||||||
getCurrentScope,
|
|
||||||
getCurrentWatcher,
|
|
||||||
getTransitionRawChildren,
|
|
||||||
guardReactiveProps,
|
|
||||||
h,
|
|
||||||
handleError,
|
|
||||||
hasInjectionContext,
|
|
||||||
hydrate,
|
|
||||||
hydrateOnIdle,
|
|
||||||
hydrateOnInteraction,
|
|
||||||
hydrateOnMediaQuery,
|
|
||||||
hydrateOnVisible,
|
|
||||||
initCustomFormatter,
|
|
||||||
initDirectivesForSSR,
|
|
||||||
inject,
|
|
||||||
isMemoSame,
|
|
||||||
isProxy,
|
|
||||||
isReactive,
|
|
||||||
isReadonly,
|
|
||||||
isRef,
|
|
||||||
isRuntimeOnly,
|
|
||||||
isShallow,
|
|
||||||
isVNode,
|
|
||||||
markRaw,
|
|
||||||
mergeDefaults,
|
|
||||||
mergeModels,
|
|
||||||
mergeProps,
|
|
||||||
nextTick,
|
|
||||||
normalizeClass,
|
|
||||||
normalizeProps,
|
|
||||||
normalizeStyle,
|
|
||||||
onActivated,
|
|
||||||
onBeforeMount,
|
|
||||||
onBeforeUnmount,
|
|
||||||
onBeforeUpdate,
|
|
||||||
onDeactivated,
|
|
||||||
onErrorCaptured,
|
|
||||||
onMounted,
|
|
||||||
onRenderTracked,
|
|
||||||
onRenderTriggered,
|
|
||||||
onScopeDispose,
|
|
||||||
onServerPrefetch,
|
|
||||||
onUnmounted,
|
|
||||||
onUpdated,
|
|
||||||
onWatcherCleanup,
|
|
||||||
openBlock,
|
|
||||||
popScopeId,
|
|
||||||
provide,
|
|
||||||
proxyRefs,
|
|
||||||
pushScopeId,
|
|
||||||
queuePostFlushCb,
|
|
||||||
reactive,
|
|
||||||
readonly,
|
|
||||||
ref,
|
|
||||||
registerRuntimeCompiler,
|
|
||||||
render,
|
|
||||||
renderList,
|
|
||||||
renderSlot,
|
|
||||||
resolveComponent,
|
|
||||||
resolveDirective,
|
|
||||||
resolveDynamicComponent,
|
|
||||||
resolveFilter,
|
|
||||||
resolveTransitionHooks,
|
|
||||||
setBlockTracking,
|
|
||||||
setDevtoolsHook,
|
|
||||||
setTransitionHooks,
|
|
||||||
shallowReactive,
|
|
||||||
shallowReadonly,
|
|
||||||
shallowRef,
|
|
||||||
ssrContextKey,
|
|
||||||
ssrUtils,
|
|
||||||
stop,
|
|
||||||
toDisplayString,
|
|
||||||
toHandlerKey,
|
|
||||||
toHandlers,
|
|
||||||
toRaw,
|
|
||||||
toRef,
|
|
||||||
toRefs,
|
|
||||||
toValue,
|
|
||||||
transformVNodeArgs,
|
|
||||||
triggerRef,
|
|
||||||
unref,
|
|
||||||
useAttrs,
|
|
||||||
useCssModule,
|
|
||||||
useCssVars,
|
|
||||||
useHost,
|
|
||||||
useId,
|
|
||||||
useModel,
|
|
||||||
useSSRContext,
|
|
||||||
useShadowRoot,
|
|
||||||
useSlots,
|
|
||||||
useTemplateRef,
|
|
||||||
useTransitionState,
|
|
||||||
vModelCheckbox,
|
|
||||||
vModelDynamic,
|
|
||||||
vModelRadio,
|
|
||||||
vModelSelect,
|
|
||||||
vModelText,
|
|
||||||
vShow,
|
|
||||||
version,
|
|
||||||
warn,
|
|
||||||
watch,
|
|
||||||
watchEffect,
|
|
||||||
watchPostEffect,
|
|
||||||
watchSyncEffect,
|
|
||||||
withAsyncContext,
|
|
||||||
withCtx,
|
|
||||||
withDefaults,
|
|
||||||
withDirectives,
|
|
||||||
withKeys,
|
|
||||||
withMemo,
|
|
||||||
withModifiers,
|
|
||||||
withScopeId
|
|
||||||
}
|
|
||||||
7
.vitepress/cache/deps/vue.js.map
vendored
7
.vitepress/cache/deps/vue.js.map
vendored
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 3,
|
|
||||||
"sources": [],
|
|
||||||
"sourcesContent": [],
|
|
||||||
"mappings": "",
|
|
||||||
"names": []
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
provider: generic
|
|
||||||
url: https://update.ceru.shiqianjiang.cn
|
|
||||||
updaterCacheDirName: ceru-music-updater
|
|
||||||
@@ -32,7 +32,8 @@ export default defineConfig({
|
|||||||
{ text: '音乐播放列表', link: '/guide/used/playList' },
|
{ text: '音乐播放列表', link: '/guide/used/playList' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{ text: '软件设计文档', link: '/guide/design' }
|
{ text: '软件设计文档', link: '/guide/design' },
|
||||||
|
{ text: '更新日志', link: '/guide/updateLog' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -163,20 +163,20 @@ html.dark #app {
|
|||||||
--check-line: 1;
|
--check-line: 1;
|
||||||
|
|
||||||
/* 自动编号格式设置 无需自动编号可全部注释掉或部分注释掉*/
|
/* 自动编号格式设置 无需自动编号可全部注释掉或部分注释掉*/
|
||||||
--autonum-h1: counter(h1) ". ";
|
// --autonum-h1: counter(h1) ". ";
|
||||||
--autonum-h2: counter(h1) "." counter(h2) ". ";
|
// --autonum-h2: counter(h1) "." counter(h2) ". ";
|
||||||
--autonum-h3: counter(h1) "." counter(h2) "." counter(h3) ". ";
|
// --autonum-h3: counter(h1) "." counter(h2) "." counter(h3) ". ";
|
||||||
--autonum-h4: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
|
// --autonum-h4: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
|
||||||
--autonum-h5: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
|
// --autonum-h5: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) ". ";
|
||||||
--autonum-h6: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
|
// --autonum-h6: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) "." counter(h5) "." counter(h6) ". ";
|
||||||
|
|
||||||
/* 下面是文章内Toc目录自动编号,与上面一样即可 */
|
/* 下面是文章内Toc目录自动编号,与上面一样即可 */
|
||||||
--autonum-h1toc: counter(h1toc) ". ";
|
// --autonum-h1toc: counter(h1toc) ". ";
|
||||||
--autonum-h2toc: counter(h1toc) "." counter(h2toc) ". ";
|
// --autonum-h2toc: counter(h1toc) "." counter(h2toc) ". ";
|
||||||
--autonum-h3toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) ". ";
|
// --autonum-h3toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) ". ";
|
||||||
--autonum-h4toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) ". ";
|
// --autonum-h4toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) ". ";
|
||||||
--autonum-h5toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) ". ";
|
// --autonum-h5toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) ". ";
|
||||||
--autonum-h6toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) "." counter(h6toc) ". ";
|
// --autonum-h6toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) "." counter(h5toc) "." counter(h6toc) ". ";
|
||||||
|
|
||||||
/* 主题颜色 */
|
/* 主题颜色 */
|
||||||
|
|
||||||
|
|||||||
15
docs/guide/updateLog.md
Normal file
15
docs/guide/updateLog.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 澜音版本更新日志
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## 日志
|
||||||
|
|
||||||
|
- 2025-9-17 **(V1.3.1)**
|
||||||
|
1. **设置功能页**
|
||||||
|
- 缓存路径支持自定义
|
||||||
|
- 下载路径支持自定义
|
||||||
|
2. **debug**
|
||||||
|
- 播放页面唱针可以拖动问题
|
||||||
|
- 播放按钮加载中 因为自动下一曲 导致动画变形问题
|
||||||
|
- **SMTC** 功能 系统显示**未知应用**问题
|
||||||
|
- 播放页歌词**字体粗细**偶现丢失问题
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ceru-music",
|
"name": "ceru-music",
|
||||||
"version": "1.3.0",
|
"version": "1.3.1",
|
||||||
"description": "一款简洁优雅的音乐播放器",
|
"description": "一款简洁优雅的音乐播放器",
|
||||||
"main": "./out/main/index.js",
|
"main": "./out/main/index.js",
|
||||||
"author": "sqj,wldss,star",
|
"author": "sqj,wldss,star",
|
||||||
@@ -45,6 +45,7 @@
|
|||||||
"@pixi/filter-color-matrix": "^7.4.3",
|
"@pixi/filter-color-matrix": "^7.4.3",
|
||||||
"@pixi/sprite": "^7.4.3",
|
"@pixi/sprite": "^7.4.3",
|
||||||
"@types/needle": "^3.3.0",
|
"@types/needle": "^3.3.0",
|
||||||
|
"NeteaseCloudMusicApi": "^4.27.0",
|
||||||
"animate.css": "^4.1.1",
|
"animate.css": "^4.1.1",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"color-extraction": "^1.0.8",
|
"color-extraction": "^1.0.8",
|
||||||
@@ -61,7 +62,6 @@
|
|||||||
"marked": "^16.1.2",
|
"marked": "^16.1.2",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"needle": "^3.3.1",
|
"needle": "^3.3.1",
|
||||||
"NeteaseCloudMusicApi": "^4.27.0",
|
|
||||||
"node-fetch": "2",
|
"node-fetch": "2",
|
||||||
"pinia": "^3.0.3",
|
"pinia": "^3.0.3",
|
||||||
"tdesign-vue-next": "^1.15.2",
|
"tdesign-vue-next": "^1.15.2",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@types/node-fetch": "^2.6.13",
|
"@types/node-fetch": "^2.6.13",
|
||||||
"@vitejs/plugin-vue": "^6.0.0",
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
"electron": "^37.3.1",
|
"electron": "^38.1.0",
|
||||||
"electron-builder": "^25.1.8",
|
"electron-builder": "^25.1.8",
|
||||||
"electron-icon-builder": "^2.0.1",
|
"electron-icon-builder": "^2.0.1",
|
||||||
"electron-vite": "^4.0.0",
|
"electron-vite": "^4.0.0",
|
||||||
|
|||||||
206
src/main/events/directorySettings.ts
Normal file
206
src/main/events/directorySettings.ts
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import { ipcMain, dialog, app } from 'electron'
|
||||||
|
import { join } from 'path'
|
||||||
|
import fs from 'fs'
|
||||||
|
import { promisify } from 'util'
|
||||||
|
|
||||||
|
const mkdir = promisify(fs.mkdir)
|
||||||
|
const access = promisify(fs.access)
|
||||||
|
|
||||||
|
export const CONFIG_NAME = 'sqj_config.json'
|
||||||
|
|
||||||
|
// 默认目录配置
|
||||||
|
const getDefaultDirectories = () => {
|
||||||
|
const userDataPath = app.getPath('userData')
|
||||||
|
return {
|
||||||
|
cacheDir: join(userDataPath, 'music-cache'),
|
||||||
|
downloadDir: join(app.getPath('music'), 'CeruMusic/songs')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
const ensureDirectoryExists = async (dirPath: string) => {
|
||||||
|
try {
|
||||||
|
await access(dirPath)
|
||||||
|
} catch {
|
||||||
|
await mkdir(dirPath, { recursive: true })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前目录配置
|
||||||
|
ipcMain.handle('directory-settings:get-directories', async () => {
|
||||||
|
try {
|
||||||
|
const defaults = getDefaultDirectories()
|
||||||
|
|
||||||
|
// 从配置文件读取用户设置的目录
|
||||||
|
const configPath = join(app.getPath('userData'), CONFIG_NAME)
|
||||||
|
let userConfig: any = {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf-8')
|
||||||
|
userConfig = JSON.parse(configData)
|
||||||
|
} catch {
|
||||||
|
// 配置文件不存在或读取失败,使用默认配置
|
||||||
|
}
|
||||||
|
|
||||||
|
const directories = {
|
||||||
|
cacheDir: userConfig.cacheDir || defaults.cacheDir,
|
||||||
|
downloadDir: userConfig.downloadDir || defaults.downloadDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
await ensureDirectoryExists(directories.cacheDir)
|
||||||
|
await ensureDirectoryExists(directories.downloadDir)
|
||||||
|
|
||||||
|
return directories
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取目录配置失败:', error)
|
||||||
|
const defaults = getDefaultDirectories()
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择缓存目录
|
||||||
|
ipcMain.handle('directory-settings:select-cache-dir', async () => {
|
||||||
|
try {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
title: '选择缓存目录',
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
buttonLabel: '选择目录'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const selectedPath = result.filePaths[0]
|
||||||
|
await ensureDirectoryExists(selectedPath)
|
||||||
|
return { success: true, path: selectedPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: '用户取消选择' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择缓存目录失败:', error)
|
||||||
|
return { success: false, message: '选择目录失败' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 选择下载目录
|
||||||
|
ipcMain.handle('directory-settings:select-download-dir', async () => {
|
||||||
|
try {
|
||||||
|
const result = await dialog.showOpenDialog({
|
||||||
|
title: '选择下载目录',
|
||||||
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
|
buttonLabel: '选择目录'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!result.canceled && result.filePaths.length > 0) {
|
||||||
|
const selectedPath = result.filePaths[0]
|
||||||
|
await ensureDirectoryExists(selectedPath)
|
||||||
|
return { success: true, path: selectedPath }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: false, message: '用户取消选择' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择下载目录失败:', error)
|
||||||
|
return { success: false, message: '选择目录失败' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存目录配置
|
||||||
|
ipcMain.handle('directory-settings:save-directories', async (_, directories) => {
|
||||||
|
try {
|
||||||
|
const configPath = join(app.getPath('userData'), CONFIG_NAME)
|
||||||
|
|
||||||
|
// 确保目录存在
|
||||||
|
await ensureDirectoryExists(directories.cacheDir)
|
||||||
|
await ensureDirectoryExists(directories.downloadDir)
|
||||||
|
|
||||||
|
// 保存配置
|
||||||
|
fs.writeFileSync(configPath, JSON.stringify(directories, null, 2))
|
||||||
|
|
||||||
|
return { success: true, message: '目录配置已保存' }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存目录配置失败:', error)
|
||||||
|
return { success: false, message: '保存配置失败' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置为默认目录
|
||||||
|
ipcMain.handle('directory-settings:reset-directories', async () => {
|
||||||
|
try {
|
||||||
|
const defaults = getDefaultDirectories()
|
||||||
|
const configPath = join(app.getPath('userData'), CONFIG_NAME)
|
||||||
|
|
||||||
|
// 删除配置文件
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(configPath)
|
||||||
|
} catch {
|
||||||
|
// 文件不存在,忽略错误
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保默认目录存在
|
||||||
|
await ensureDirectoryExists(defaults.cacheDir)
|
||||||
|
await ensureDirectoryExists(defaults.downloadDir)
|
||||||
|
|
||||||
|
return { success: true, directories: defaults }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置目录配置失败:', error)
|
||||||
|
return { success: false, message: '重置配置失败' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 打开目录
|
||||||
|
ipcMain.handle('directory-settings:open-directory', async (_, dirPath) => {
|
||||||
|
try {
|
||||||
|
const { shell } = require('electron')
|
||||||
|
await shell.openPath(dirPath)
|
||||||
|
return { success: true }
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开目录失败:', error)
|
||||||
|
return { success: false, message: '打开目录失败' }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取目录大小
|
||||||
|
ipcMain.handle('directory-settings:get-directory-size', async (_, dirPath) => {
|
||||||
|
try {
|
||||||
|
const getDirectorySize = (dirPath: string): number => {
|
||||||
|
let totalSize = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = fs.readdirSync(dirPath)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = join(dirPath, item)
|
||||||
|
const stats = fs.statSync(itemPath)
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
totalSize += getDirectorySize(itemPath)
|
||||||
|
} else {
|
||||||
|
totalSize += stats.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略无法访问的文件/目录
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalSize
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = getDirectorySize(dirPath)
|
||||||
|
|
||||||
|
// 格式化大小
|
||||||
|
const formatSize = (bytes: number): string => {
|
||||||
|
if (bytes === 0) return '0 B'
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
size,
|
||||||
|
formatted: formatSize(size)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取目录大小失败:', error)
|
||||||
|
return { size: 0, formatted: '0 B' }
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -14,18 +14,21 @@ ipcMain.handle('music-cache:get-info', async () => {
|
|||||||
// 清空缓存
|
// 清空缓存
|
||||||
ipcMain.handle('music-cache:clear', async () => {
|
ipcMain.handle('music-cache:clear', async () => {
|
||||||
try {
|
try {
|
||||||
|
console.log('收到清空缓存请求')
|
||||||
await musicCacheService.clearCache()
|
await musicCacheService.clearCache()
|
||||||
|
console.log('缓存清空完成')
|
||||||
return { success: true, message: '缓存已清空' }
|
return { success: true, message: '缓存已清空' }
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('清空缓存失败:', error)
|
console.error('清空缓存失败:', error)
|
||||||
return { success: false, message: '清空缓存失败' }
|
return { success: false, message: `清空缓存失败: ${error.message}` }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取缓存大小
|
// 获取缓存大小
|
||||||
ipcMain.handle('music-cache:get-size', async () => {
|
ipcMain.handle('music-cache:get-size', async () => {
|
||||||
try {
|
try {
|
||||||
return await musicCacheService.getCacheSize()
|
const info = await musicCacheService.getCacheInfo()
|
||||||
|
return info.size
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取缓存大小失败:', error)
|
console.error('获取缓存大小失败:', error)
|
||||||
return 0
|
return 0
|
||||||
|
|||||||
@@ -217,15 +217,22 @@ ipcMain.handle('get-app-version', () => {
|
|||||||
aiEvents(mainWindow)
|
aiEvents(mainWindow)
|
||||||
import './events/musicCache'
|
import './events/musicCache'
|
||||||
import './events/songList'
|
import './events/songList'
|
||||||
|
import './events/directorySettings'
|
||||||
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
||||||
|
|
||||||
// This method will be called when Electron has finished
|
// This method will be called when Electron has finished
|
||||||
// initialization and is ready to create browser windows.
|
// initialization and is ready to create browser windows.
|
||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows - 确保与 electron-builder.yml 中的 appId 一致
|
||||||
|
electronApp.setAppUserModelId('com.cerumusic.app')
|
||||||
|
|
||||||
electronApp.setAppUserModelId('com.cerulean.music')
|
// 在 Windows 上设置应用程序名称,帮助 SMTC 识别
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
app.setAppUserModelId('com.cerumusic.app')
|
||||||
|
// 设置应用程序名称
|
||||||
|
app.setName('澜音')
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// 初始化插件系统
|
// 初始化插件系统
|
||||||
|
|||||||
@@ -3,18 +3,43 @@ import * as path from 'path'
|
|||||||
import * as fs from 'fs/promises'
|
import * as fs from 'fs/promises'
|
||||||
import * as crypto from 'crypto'
|
import * as crypto from 'crypto'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { CONFIG_NAME } from '../../events/directorySettings'
|
||||||
|
|
||||||
export class MusicCacheService {
|
export class MusicCacheService {
|
||||||
private cacheDir: string
|
|
||||||
private cacheIndex: Map<string, string> = new Map()
|
private cacheIndex: Map<string, string> = new Map()
|
||||||
private indexFilePath: string
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cacheDir = path.join(app.getPath('userData'), 'music-cache')
|
|
||||||
this.indexFilePath = path.join(this.cacheDir, 'cache-index.json')
|
|
||||||
this.initCache()
|
this.initCache()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getCacheDirectory(): string {
|
||||||
|
try {
|
||||||
|
// 尝试从配置文件读取自定义缓存目录
|
||||||
|
const configPath = path.join(app.getPath('userData'), CONFIG_NAME)
|
||||||
|
const configData = require('fs').readFileSync(configPath, 'utf-8')
|
||||||
|
const config = JSON.parse(configData)
|
||||||
|
|
||||||
|
if (config.cacheDir && typeof config.cacheDir === 'string') {
|
||||||
|
return config.cacheDir
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 配置文件不存在或读取失败,使用默认目录
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认缓存目录
|
||||||
|
return path.join(app.getPath('userData'), 'music-cache')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态获取缓存目录
|
||||||
|
public get cacheDir(): string {
|
||||||
|
return this.getCacheDirectory()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态获取索引文件路径
|
||||||
|
public get indexFilePath(): string {
|
||||||
|
return path.join(this.cacheDir, 'cache-index.json')
|
||||||
|
}
|
||||||
|
|
||||||
private async initCache() {
|
private async initCache() {
|
||||||
try {
|
try {
|
||||||
// 确保缓存目录存在
|
// 确保缓存目录存在
|
||||||
@@ -130,43 +155,117 @@ export class MusicCacheService {
|
|||||||
|
|
||||||
async clearCache(): Promise<void> {
|
async clearCache(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// 删除所有缓存文件
|
console.log('开始清空缓存目录:', this.cacheDir)
|
||||||
|
|
||||||
|
// 先重新加载缓存索引,确保获取最新的文件列表
|
||||||
|
await this.loadCacheIndex()
|
||||||
|
|
||||||
|
// 删除索引中记录的所有缓存文件
|
||||||
|
let deletedFromIndex = 0
|
||||||
for (const filePath of this.cacheIndex.values()) {
|
for (const filePath of this.cacheIndex.values()) {
|
||||||
try {
|
try {
|
||||||
await fs.unlink(filePath)
|
await fs.unlink(filePath)
|
||||||
} catch (error) {
|
deletedFromIndex++
|
||||||
// 忽略文件不存在的错误
|
console.log('删除缓存文件:', filePath)
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn('删除文件失败:', filePath, error.message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 删除缓存目录中的所有其他文件(包括可能遗漏的文件)
|
||||||
|
let deletedFromDir = 0
|
||||||
|
try {
|
||||||
|
const files = await fs.readdir(this.cacheDir)
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(this.cacheDir, file)
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(filePath)
|
||||||
|
if (stats.isFile() && file !== 'cache-index.json') {
|
||||||
|
await fs.unlink(filePath)
|
||||||
|
deletedFromDir++
|
||||||
|
console.log('删除目录文件:', filePath)
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn('删除目录文件失败:', filePath, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn('读取缓存目录失败:', error.message)
|
||||||
|
}
|
||||||
|
|
||||||
// 清空缓存索引
|
// 清空缓存索引
|
||||||
this.cacheIndex.clear()
|
this.cacheIndex.clear()
|
||||||
await this.saveCacheIndex()
|
await this.saveCacheIndex()
|
||||||
|
|
||||||
console.log('音乐缓存已清空')
|
console.log(
|
||||||
|
`音乐缓存已清空 - 从索引删除: ${deletedFromIndex}个文件, 从目录删除: ${deletedFromDir}个文件`
|
||||||
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清空缓存失败:', error)
|
console.error('清空缓存失败:', error)
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCacheSize(): Promise<number> {
|
getDirectorySize = async (dirPath: string): Promise<number> => {
|
||||||
let totalSize = 0
|
let totalSize = 0
|
||||||
|
|
||||||
for (const filePath of this.cacheIndex.values()) {
|
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(filePath)
|
const items = await fs.readdir(dirPath)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(dirPath, item)
|
||||||
|
const stats = await fs.stat(itemPath)
|
||||||
|
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
totalSize += await this.getDirectorySize(itemPath)
|
||||||
|
} else {
|
||||||
totalSize += stats.size
|
totalSize += stats.size
|
||||||
} catch (error) {
|
|
||||||
// 文件不存在,忽略
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
// 忽略无法访问的文件/目录
|
||||||
|
}
|
||||||
|
|
||||||
return totalSize
|
return totalSize
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCacheInfo(): Promise<{ count: number; size: number; sizeFormatted: string }> {
|
async getCacheInfo(): Promise<{ count: number; size: number; sizeFormatted: string }> {
|
||||||
const size = await this.getCacheSize()
|
// 重新加载缓存索引以确保数据准确
|
||||||
const count = this.cacheIndex.size
|
await this.loadCacheIndex()
|
||||||
|
|
||||||
|
// 统计实际的缓存文件数量和大小
|
||||||
|
let actualCount = 0
|
||||||
|
let totalSize = 0
|
||||||
|
|
||||||
|
try {
|
||||||
|
const items = await fs.readdir(this.cacheDir)
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const itemPath = path.join(this.cacheDir, item)
|
||||||
|
try {
|
||||||
|
const stats = await fs.stat(itemPath)
|
||||||
|
|
||||||
|
if (stats.isFile() && item !== 'cache-index.json') {
|
||||||
|
// 检查是否是音频文件
|
||||||
|
const ext = path.extname(item).toLowerCase()
|
||||||
|
const audioExts = ['.mp3', '.flac', '.wav', '.aac', '.ogg', '.m4a', '.wma']
|
||||||
|
|
||||||
|
if (audioExts.includes(ext)) {
|
||||||
|
actualCount++
|
||||||
|
totalSize += stats.size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// 忽略无法访问的文件
|
||||||
|
console.warn('无法访问文件:', itemPath, error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.warn('读取缓存目录失败:', error.message)
|
||||||
|
// 如果无法读取目录,使用索引数据作为备选
|
||||||
|
totalSize = await this.getDirectorySize(this.cacheDir)
|
||||||
|
actualCount = this.cacheIndex.size
|
||||||
|
}
|
||||||
|
|
||||||
const formatSize = (bytes: number): string => {
|
const formatSize = (bytes: number): string => {
|
||||||
if (bytes === 0) return '0 B'
|
if (bytes === 0) return '0 B'
|
||||||
@@ -176,10 +275,12 @@ export class MusicCacheService {
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`缓存信息 - 文件数量: ${actualCount}, 总大小: ${totalSize} bytes`)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
count,
|
count: actualCount,
|
||||||
size,
|
size: totalSize,
|
||||||
sizeFormatted: formatSize(size)
|
sizeFormatted: formatSize(totalSize)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from './type'
|
} from './type'
|
||||||
import pluginService from '../plugin/index'
|
import pluginService from '../plugin/index'
|
||||||
import musicSdk from '../../utils/musicSdk/index'
|
import musicSdk from '../../utils/musicSdk/index'
|
||||||
import { getAppDirPath } from '../../utils/path'
|
|
||||||
import { musicCacheService } from '../musicCache'
|
import { musicCacheService } from '../musicCache'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
@@ -19,6 +18,8 @@ import fsPromise from 'fs/promises'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { pipeline } from 'node:stream/promises'
|
import { pipeline } from 'node:stream/promises'
|
||||||
import { fileURLToPath } from 'url'
|
import { fileURLToPath } from 'url'
|
||||||
|
import { app } from 'electron'
|
||||||
|
import { CONFIG_NAME } from '../../events/directorySettings'
|
||||||
|
|
||||||
const fileLock: Record<string, boolean> = {}
|
const fileLock: Record<string, boolean> = {}
|
||||||
|
|
||||||
@@ -89,6 +90,24 @@ function main(source: string) {
|
|||||||
const url = await this.getMusicUrl({ pluginId, songInfo, quality })
|
const url = await this.getMusicUrl({ pluginId, songInfo, quality })
|
||||||
if (typeof url === 'object') throw new Error('无法获取歌曲链接')
|
if (typeof url === 'object') throw new Error('无法获取歌曲链接')
|
||||||
|
|
||||||
|
// 获取自定义下载目录
|
||||||
|
const getDownloadDirectory = (): string => {
|
||||||
|
try {
|
||||||
|
const configPath = path.join(app.getPath('userData'), CONFIG_NAME)
|
||||||
|
const configData = fs.readFileSync(configPath, 'utf-8')
|
||||||
|
const config = JSON.parse(configData)
|
||||||
|
|
||||||
|
if (config.downloadDir && typeof config.downloadDir === 'string') {
|
||||||
|
return config.downloadDir
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 配置文件不存在或读取失败,使用默认目录
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认下载目录
|
||||||
|
return path.join(app.getPath('music'), 'CeruMusic/songs')
|
||||||
|
}
|
||||||
|
|
||||||
// 从URL中提取文件扩展名,如果没有则默认为mp3
|
// 从URL中提取文件扩展名,如果没有则默认为mp3
|
||||||
const getFileExtension = (url: string): string => {
|
const getFileExtension = (url: string): string => {
|
||||||
try {
|
try {
|
||||||
@@ -110,11 +129,10 @@ function main(source: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fileExtension = getFileExtension(url)
|
const fileExtension = getFileExtension(url)
|
||||||
|
const downloadDir = getDownloadDirectory()
|
||||||
const songPath = path.join(
|
const songPath = path.join(
|
||||||
getAppDirPath('music'),
|
downloadDir,
|
||||||
'CeruMusic',
|
`${songInfo.name}-${songInfo.singer}-${songInfo.source}.${fileExtension}`
|
||||||
'songs',
|
|
||||||
`${songInfo.name}-${songInfo.singer}-${source}.${fileExtension}`
|
|
||||||
.replace(/[/\\:*?"<>|]/g, '')
|
.replace(/[/\\:*?"<>|]/g, '')
|
||||||
.replace(/^\.+/, '')
|
.replace(/^\.+/, '')
|
||||||
.replace(/\.+$/, '')
|
.replace(/\.+$/, '')
|
||||||
|
|||||||
@@ -1,175 +0,0 @@
|
|||||||
import { Tray, Menu, BrowserWindow } from 'electron'
|
|
||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
|
||||||
import path from 'node:path'
|
|
||||||
// 使用传入的 tray 对象
|
|
||||||
export default function useWindow(
|
|
||||||
createWindow: { (): void; (): void },
|
|
||||||
ipcMain: Electron.IpcMain,
|
|
||||||
app: Electron.App,
|
|
||||||
mainWindow: BrowserWindow | null,
|
|
||||||
isQuitting: { value: boolean },
|
|
||||||
trayObj: { value: Tray | null }
|
|
||||||
) {
|
|
||||||
function createTray(): void {
|
|
||||||
// 创建系统托盘
|
|
||||||
const trayIconPath = path.join(__dirname, '../../resources/logo.png')
|
|
||||||
trayObj.value = new Tray(trayIconPath)
|
|
||||||
|
|
||||||
// 创建托盘菜单
|
|
||||||
const contextMenu = Menu.buildFromTemplate([
|
|
||||||
{
|
|
||||||
label: '显示窗口',
|
|
||||||
click: () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '播放/暂停',
|
|
||||||
click: () => {
|
|
||||||
// 这里可以添加播放控制逻辑
|
|
||||||
mainWindow?.webContents.send('music-control')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
label: '退出',
|
|
||||||
click: () => {
|
|
||||||
isQuitting.value = true
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
trayObj.value.setContextMenu(contextMenu)
|
|
||||||
trayObj.value.setToolTip('Ceru Music')
|
|
||||||
|
|
||||||
// 双击托盘图标显示窗口
|
|
||||||
trayObj.value.on('click', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
if (mainWindow.isVisible()) {
|
|
||||||
mainWindow.hide()
|
|
||||||
} else {
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
|
||||||
// Set app user model id for windows
|
|
||||||
// electronApp.setAppUserModelId('com.cerulean.music')
|
|
||||||
// Default open or close DevTools by F12 in development
|
|
||||||
// and ignore CommandOrControl + R in production.
|
|
||||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
|
||||||
app.on('browser-window-created', (_, window) => {
|
|
||||||
optimizer.watchWindowShortcuts(window)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 窗口控制 IPC 处理
|
|
||||||
ipcMain.on('window-minimize', () => {
|
|
||||||
console.log('收到 window-minimize 事件')
|
|
||||||
if (mainWindow) {
|
|
||||||
console.log('正在最小化窗口...')
|
|
||||||
mainWindow.minimize()
|
|
||||||
} else {
|
|
||||||
console.log('mainWindow 不存在')
|
|
||||||
const window = BrowserWindow.getFocusedWindow()
|
|
||||||
if (window) {
|
|
||||||
console.log('使用 getFocusedWindow 最小化窗口...')
|
|
||||||
window.minimize()
|
|
||||||
} else {
|
|
||||||
console.log('没有找到可用的窗口')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-maximize', () => {
|
|
||||||
console.log('收到 window-maximize 事件')
|
|
||||||
if (mainWindow) {
|
|
||||||
console.log('正在最大化/还原窗口...')
|
|
||||||
if (mainWindow.isMaximized()) {
|
|
||||||
mainWindow.unmaximize()
|
|
||||||
} else {
|
|
||||||
mainWindow.maximize()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('mainWindow 不存在')
|
|
||||||
const window = BrowserWindow.getFocusedWindow()
|
|
||||||
if (window) {
|
|
||||||
console.log('使用 getFocusedWindow 最大化/还原窗口...')
|
|
||||||
if (window.isMaximized()) {
|
|
||||||
window.unmaximize()
|
|
||||||
} else {
|
|
||||||
window.maximize()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('没有找到可用的窗口')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
ipcMain.on('window-close', () => {
|
|
||||||
console.log('收到 window-close 事件')
|
|
||||||
if (mainWindow) {
|
|
||||||
console.log('正在关闭窗口...')
|
|
||||||
mainWindow.close()
|
|
||||||
} else {
|
|
||||||
console.log('mainWindow 不存在')
|
|
||||||
const window = BrowserWindow.getFocusedWindow()
|
|
||||||
if (window) {
|
|
||||||
console.log('使用 getFocusedWindow 关闭窗口...')
|
|
||||||
window.close()
|
|
||||||
} else {
|
|
||||||
console.log('没有找到可用的窗口')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Mini 模式 IPC 处理 - 最小化到系统托盘
|
|
||||||
ipcMain.on('window-mini-mode', (_, isMini) => {
|
|
||||||
console.log('收到 window-mini-mode 事件,isMini:', isMini)
|
|
||||||
if (mainWindow) {
|
|
||||||
if (isMini) {
|
|
||||||
// 进入 Mini 模式:隐藏窗口到系统托盘
|
|
||||||
console.log('正在隐藏窗口...')
|
|
||||||
mainWindow.hide()
|
|
||||||
// 显示托盘通知(可选)
|
|
||||||
if (trayObj.value) {
|
|
||||||
console.log('显示托盘通知...')
|
|
||||||
trayObj.value.displayBalloon({
|
|
||||||
title: '澜音 Music',
|
|
||||||
content: '已最小化到系统托盘啦,点击托盘图标可重新打开~'
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('托盘对象不存在!trayObj.value:', trayObj.value)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 退出 Mini 模式:显示窗口
|
|
||||||
mainWindow.show()
|
|
||||||
mainWindow.focus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 全屏模式 IPC 处理
|
|
||||||
ipcMain.on('window-toggle-fullscreen', () => {
|
|
||||||
if (mainWindow) {
|
|
||||||
const isFullScreen = mainWindow.isFullScreen()
|
|
||||||
mainWindow.setFullScreen(!isFullScreen)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
createWindow()
|
|
||||||
createTray()
|
|
||||||
|
|
||||||
app.on('activate', function () {
|
|
||||||
// On macOS it's common to re-create a window in the app when the
|
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
42
src/preload/index.d.ts
vendored
42
src/preload/index.d.ts
vendored
@@ -79,6 +79,48 @@ interface CustomAPI {
|
|||||||
start: () => undefined
|
start: () => undefined
|
||||||
stop: () => undefined
|
stop: () => undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 目录设置API
|
||||||
|
directorySettings: {
|
||||||
|
getDirectories: () => Promise<{
|
||||||
|
cacheDir: string
|
||||||
|
downloadDir: string
|
||||||
|
}>
|
||||||
|
selectCacheDir: () => Promise<{
|
||||||
|
success: boolean
|
||||||
|
path?: string
|
||||||
|
message?: string
|
||||||
|
}>
|
||||||
|
selectDownloadDir: () => Promise<{
|
||||||
|
success: boolean
|
||||||
|
path?: string
|
||||||
|
message?: string
|
||||||
|
}>
|
||||||
|
saveDirectories: (directories: {
|
||||||
|
cacheDir: string
|
||||||
|
downloadDir: string
|
||||||
|
}) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
message: string
|
||||||
|
}>
|
||||||
|
resetDirectories: () => Promise<{
|
||||||
|
success: boolean
|
||||||
|
directories?: {
|
||||||
|
cacheDir: string
|
||||||
|
downloadDir: string
|
||||||
|
}
|
||||||
|
message?: string
|
||||||
|
}>
|
||||||
|
openDirectory: (dirPath: string) => Promise<{
|
||||||
|
success: boolean
|
||||||
|
message?: string
|
||||||
|
}>
|
||||||
|
getDirectorySize: (dirPath: string) => Promise<{
|
||||||
|
size: number
|
||||||
|
formatted: string
|
||||||
|
}>
|
||||||
|
}
|
||||||
|
|
||||||
// 用户配置API
|
// 用户配置API
|
||||||
getUserConfig: () => Promise<any>
|
getUserConfig: () => Promise<any>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,20 @@ const api = {
|
|||||||
stop: () => {
|
stop: () => {
|
||||||
ipcRenderer.send('stopPing')
|
ipcRenderer.send('stopPing')
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 目录设置相关
|
||||||
|
directorySettings: {
|
||||||
|
getDirectories: () => ipcRenderer.invoke('directory-settings:get-directories'),
|
||||||
|
selectCacheDir: () => ipcRenderer.invoke('directory-settings:select-cache-dir'),
|
||||||
|
selectDownloadDir: () => ipcRenderer.invoke('directory-settings:select-download-dir'),
|
||||||
|
saveDirectories: (directories: any) =>
|
||||||
|
ipcRenderer.invoke('directory-settings:save-directories', directories),
|
||||||
|
resetDirectories: () => ipcRenderer.invoke('directory-settings:reset-directories'),
|
||||||
|
openDirectory: (dirPath: string) =>
|
||||||
|
ipcRenderer.invoke('directory-settings:open-directory', dirPath),
|
||||||
|
getDirectorySize: (dirPath: string) =>
|
||||||
|
ipcRenderer.invoke('directory-settings:get-directory-size', dirPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
4
src/renderer/auto-imports.d.ts
vendored
4
src/renderer/auto-imports.d.ts
vendored
@@ -5,4 +5,6 @@
|
|||||||
// Generated by unplugin-auto-import
|
// Generated by unplugin-auto-import
|
||||||
// biome-ignore lint: disable
|
// biome-ignore lint: disable
|
||||||
export {}
|
export {}
|
||||||
declare global {}
|
declare global {
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
5
src/renderer/components.d.ts
vendored
5
src/renderer/components.d.ts
vendored
@@ -10,6 +10,7 @@ declare module 'vue' {
|
|||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
|
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.vue')['default']
|
||||||
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
AudioVisualizer: typeof import('./src/components/Play/AudioVisualizer.vue')['default']
|
||||||
|
DirectorySettings: typeof import('./src/components/Settings/DirectorySettings.vue')['default']
|
||||||
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
||||||
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
||||||
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
|
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
|
||||||
@@ -30,6 +31,7 @@ declare module 'vue' {
|
|||||||
TCard: typeof import('tdesign-vue-next')['Card']
|
TCard: typeof import('tdesign-vue-next')['Card']
|
||||||
TContent: typeof import('tdesign-vue-next')['Content']
|
TContent: typeof import('tdesign-vue-next')['Content']
|
||||||
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
||||||
|
TDivider: typeof import('tdesign-vue-next')['Divider']
|
||||||
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
|
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
|
||||||
TForm: typeof import('tdesign-vue-next')['Form']
|
TForm: typeof import('tdesign-vue-next')['Form']
|
||||||
TFormItem: typeof import('tdesign-vue-next')['FormItem']
|
TFormItem: typeof import('tdesign-vue-next')['FormItem']
|
||||||
@@ -40,8 +42,11 @@ declare module 'vue' {
|
|||||||
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
||||||
TLayout: typeof import('tdesign-vue-next')['Layout']
|
TLayout: typeof import('tdesign-vue-next')['Layout']
|
||||||
TLoading: typeof import('tdesign-vue-next')['Loading']
|
TLoading: typeof import('tdesign-vue-next')['Loading']
|
||||||
|
TRadioButton: typeof import('tdesign-vue-next')['RadioButton']
|
||||||
|
TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
|
||||||
TSlider: typeof import('tdesign-vue-next')['Slider']
|
TSlider: typeof import('tdesign-vue-next')['Slider']
|
||||||
TSwitch: typeof import('tdesign-vue-next')['Switch']
|
TSwitch: typeof import('tdesign-vue-next')['Switch']
|
||||||
|
TTag: typeof import('tdesign-vue-next')['Tag']
|
||||||
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
||||||
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
|
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
|
||||||
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
|
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'lyricfont';
|
font-family: 'lyricfont';
|
||||||
src: url('./lyricfont.ttf');
|
src: url('./lyricfont.ttf');
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
|
|||||||
@@ -493,6 +493,8 @@ const lightMainColor = computed(() => {
|
|||||||
perspective: 1000px;
|
perspective: 1000px;
|
||||||
|
|
||||||
.pointer {
|
.pointer {
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: calc(var(--cd-width-auto) / 3.5);
|
width: calc(var(--cd-width-auto) / 3.5);
|
||||||
left: calc(50% - 1.8vh);
|
left: calc(50% - 1.8vh);
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import {
|
|||||||
destroyPlaylistEventListeners,
|
destroyPlaylistEventListeners,
|
||||||
getSongRealUrl
|
getSongRealUrl
|
||||||
} from '@renderer/utils/playlistManager'
|
} from '@renderer/utils/playlistManager'
|
||||||
import mediaSessionController from '@renderer/utils/useAmtc'
|
import mediaSessionController from '@renderer/utils/useSmtc'
|
||||||
import defaultCoverImg from '/default-cover.png'
|
import defaultCoverImg from '/default-cover.png'
|
||||||
|
|
||||||
const controlAudio = ControlAudioStore()
|
const controlAudio = ControlAudioStore()
|
||||||
@@ -163,9 +163,10 @@ const playSong = async (song: SongList) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 更新歌曲信息并触发主题色更新
|
// 更新歌曲信息并触发主题色更新
|
||||||
songInfo.value = {
|
songInfo.value.name = song.name
|
||||||
...song
|
songInfo.value.singer = song.singer
|
||||||
}
|
songInfo.value.albumName = song.albumName
|
||||||
|
songInfo.value.img = song.img
|
||||||
|
|
||||||
// 更新媒体会话元数据
|
// 更新媒体会话元数据
|
||||||
mediaSessionController.updateMetadata({
|
mediaSessionController.updateMetadata({
|
||||||
@@ -176,7 +177,6 @@ const playSong = async (song: SongList) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 确保主题色更新
|
// 确保主题色更新
|
||||||
await setColor()
|
|
||||||
|
|
||||||
let urlToPlay = ''
|
let urlToPlay = ''
|
||||||
|
|
||||||
@@ -209,7 +209,10 @@ const playSong = async (song: SongList) => {
|
|||||||
|
|
||||||
// 等待音频准备就绪
|
// 等待音频准备就绪
|
||||||
await waitForAudioReady()
|
await waitForAudioReady()
|
||||||
|
await setColor()
|
||||||
|
songInfo.value = {
|
||||||
|
...song
|
||||||
|
}
|
||||||
// // 短暂延迟确保音频状态稳定
|
// // 短暂延迟确保音频状态稳定
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 100))
|
// await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
|||||||
342
src/renderer/src/components/Settings/DirectorySettings.vue
Normal file
342
src/renderer/src/components/Settings/DirectorySettings.vue
Normal file
@@ -0,0 +1,342 @@
|
|||||||
|
<template>
|
||||||
|
<div class="directory-settings">
|
||||||
|
<t-card title="存储目录配置" hover-shadow>
|
||||||
|
<template #actions>
|
||||||
|
<t-button theme="default" size="small" @click="resetDirectories"> 重置为默认 </t-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="directory-section">
|
||||||
|
<h4>缓存目录</h4>
|
||||||
|
<p class="directory-description">用于存储歌曲缓存文件,提高播放速度</p>
|
||||||
|
|
||||||
|
<div class="directory-item">
|
||||||
|
<div class="directory-info">
|
||||||
|
<div class="directory-path">
|
||||||
|
<t-input
|
||||||
|
v-model="directories.cacheDir"
|
||||||
|
readonly
|
||||||
|
placeholder="缓存目录路径"
|
||||||
|
class="path-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="directory-size">
|
||||||
|
<t-tag theme="primary" variant="light">
|
||||||
|
{{ cacheDirSize.formatted }}
|
||||||
|
</t-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directory-actions">
|
||||||
|
<t-button theme="default" @click="selectCacheDir"> 选择目录 </t-button>
|
||||||
|
<t-button theme="default" variant="outline" @click="openCacheDir"> 打开目录 </t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<t-divider />
|
||||||
|
|
||||||
|
<div class="directory-section">
|
||||||
|
<h4>下载目录</h4>
|
||||||
|
<p class="directory-description">用于存储下载的音乐文件</p>
|
||||||
|
|
||||||
|
<div class="directory-item">
|
||||||
|
<div class="directory-info">
|
||||||
|
<div class="directory-path">
|
||||||
|
<t-input
|
||||||
|
v-model="directories.downloadDir"
|
||||||
|
readonly
|
||||||
|
placeholder="下载目录路径"
|
||||||
|
class="path-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="directory-size">
|
||||||
|
<t-tag theme="success" variant="light">
|
||||||
|
{{ downloadDirSize.formatted }}
|
||||||
|
</t-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="directory-actions">
|
||||||
|
<t-button theme="default" @click="selectDownloadDir"> 选择目录 </t-button>
|
||||||
|
<t-button theme="default" variant="outline" @click="openDownloadDir">
|
||||||
|
打开目录
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="save-section">
|
||||||
|
<t-button theme="primary" size="large" :loading="isSaving" @click="saveDirectories">
|
||||||
|
保存设置
|
||||||
|
</t-button>
|
||||||
|
</div>
|
||||||
|
</t-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, toRaw } from 'vue'
|
||||||
|
import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'directory-changed': []
|
||||||
|
'cache-cleared': []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const directories = ref({
|
||||||
|
cacheDir: '',
|
||||||
|
downloadDir: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const cacheDirSize = ref({ size: 0, formatted: '0 B' })
|
||||||
|
const downloadDirSize = ref({ size: 0, formatted: '0 B' })
|
||||||
|
const isSaving = ref(false)
|
||||||
|
|
||||||
|
// 加载目录配置
|
||||||
|
const loadDirectories = async () => {
|
||||||
|
try {
|
||||||
|
const dirs = await window.api.directorySettings.getDirectories()
|
||||||
|
directories.value = dirs
|
||||||
|
|
||||||
|
// 获取目录大小
|
||||||
|
await Promise.all([updateCacheDirSize(), updateDownloadDirSize()])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载目录配置失败:', error)
|
||||||
|
MessagePlugin.error('加载目录配置失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新缓存目录大小
|
||||||
|
const updateCacheDirSize = async () => {
|
||||||
|
try {
|
||||||
|
const sizeInfo = await window.api.directorySettings.getDirectorySize(directories.value.cacheDir)
|
||||||
|
cacheDirSize.value = sizeInfo
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取缓存目录大小失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新下载目录大小
|
||||||
|
const updateDownloadDirSize = async () => {
|
||||||
|
try {
|
||||||
|
const sizeInfo = await window.api.directorySettings.getDirectorySize(
|
||||||
|
directories.value.downloadDir
|
||||||
|
)
|
||||||
|
downloadDirSize.value = sizeInfo
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取下载目录大小失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择缓存目录
|
||||||
|
const selectCacheDir = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.directorySettings.selectCacheDir()
|
||||||
|
|
||||||
|
if (result.success && result.path) {
|
||||||
|
directories.value.cacheDir = result.path
|
||||||
|
await updateCacheDirSize()
|
||||||
|
MessagePlugin.success('缓存目录已选择,记得保存奥')
|
||||||
|
} else if (result.message !== '用户取消选择') {
|
||||||
|
MessagePlugin.error(result.message || '选择目录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择缓存目录失败:', error)
|
||||||
|
MessagePlugin.error('选择目录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择下载目录
|
||||||
|
const selectDownloadDir = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.directorySettings.selectDownloadDir()
|
||||||
|
|
||||||
|
if (result.success && result.path) {
|
||||||
|
directories.value.downloadDir = result.path
|
||||||
|
await updateDownloadDirSize()
|
||||||
|
MessagePlugin.success('下载目录已选择,记得保存奥')
|
||||||
|
} else if (result.message !== '用户取消选择') {
|
||||||
|
MessagePlugin.error(result.message || '选择目录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择下载目录失败:', error)
|
||||||
|
MessagePlugin.error('选择目录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开缓存目录
|
||||||
|
const openCacheDir = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.directorySettings.openDirectory(directories.value.cacheDir)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
MessagePlugin.error(result.message || '打开目录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开缓存目录失败:', error)
|
||||||
|
MessagePlugin.error('打开目录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开下载目录
|
||||||
|
const openDownloadDir = async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.directorySettings.openDirectory(directories.value.downloadDir)
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
MessagePlugin.error(result.message || '打开目录失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开下载目录失败:', error)
|
||||||
|
MessagePlugin.error('打开目录失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存目录设置
|
||||||
|
const saveDirectories = async () => {
|
||||||
|
isSaving.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await window.api.directorySettings.saveDirectories(toRaw(directories.value))
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
MessagePlugin.success('目录设置已保存')
|
||||||
|
emit('directory-changed')
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(result.message || '保存设置失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存目录设置失败:', error)
|
||||||
|
MessagePlugin.error('保存设置失败')
|
||||||
|
} finally {
|
||||||
|
isSaving.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置为默认目录
|
||||||
|
const resetDirectories = async () => {
|
||||||
|
const confirm = DialogPlugin.confirm({
|
||||||
|
header: '重置目录设置',
|
||||||
|
body: '确定要重置为默认目录吗?这将清除当前的自定义目录设置。',
|
||||||
|
confirmBtn: '确定重置',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
onConfirm: async () => {
|
||||||
|
try {
|
||||||
|
const result = await window.api.directorySettings.resetDirectories()
|
||||||
|
|
||||||
|
if (result.success && result.directories) {
|
||||||
|
directories.value = result.directories
|
||||||
|
await Promise.all([updateCacheDirSize(), updateDownloadDirSize()])
|
||||||
|
MessagePlugin.success('已重置为默认目录')
|
||||||
|
emit('directory-changed')
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(result.message || '重置失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('重置目录设置失败:', error)
|
||||||
|
MessagePlugin.error('重置失败')
|
||||||
|
}
|
||||||
|
confirm.hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新目录大小(供父组件调用)
|
||||||
|
const refreshDirectorySizes = async () => {
|
||||||
|
console.log('刷新目录大小')
|
||||||
|
await Promise.all([updateCacheDirSize(), updateDownloadDirSize()])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
refreshDirectorySizes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件挂载时加载配置
|
||||||
|
onMounted(() => {
|
||||||
|
loadDirectories()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.directory-settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--td-text-color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-description {
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.directory-path {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.path-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-size {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid var(--td-border-level-1-color);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cache-management {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式设计 */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.directory-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.directory-actions {
|
||||||
|
justify-content: stretch;
|
||||||
|
|
||||||
|
button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,22 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="music-cache">
|
<div class="music-cache">
|
||||||
<t-card hover-shadow :loading="cacheInfo ? false : true" title="本地歌曲缓存配置">
|
<t-card hover-shadow :loading="!cacheInfo || cacheInfo.clearing" title="本地歌曲缓存配置">
|
||||||
<template #actions> 已有歌曲缓存大小:{{ cacheInfo.sizeFormatted }} </template>
|
<template #actions>
|
||||||
|
已有歌曲缓存大小:{{ cacheInfo?.sizeFormatted || '0 B' }}
|
||||||
|
<span v-if="cacheInfo?.count > 0">({{ cacheInfo.count }} 个文件)</span>
|
||||||
|
</template>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<t-button size="large" @click="clearCache"> 清除本地缓存 </t-button>
|
<t-button
|
||||||
|
size="large"
|
||||||
|
:loading="cacheInfo?.clearing"
|
||||||
|
:disabled="!cacheInfo?.count || cacheInfo?.count === 0"
|
||||||
|
@click="clearCache"
|
||||||
|
>
|
||||||
|
{{ cacheInfo?.clearing ? '正在清除...' : '清除本地缓存' }}
|
||||||
|
</t-button>
|
||||||
|
<div v-if="!cacheInfo?.count || cacheInfo?.count === 0" class="no-cache-tip">
|
||||||
|
暂无缓存文件
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</t-card>
|
</t-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { DialogPlugin } from 'tdesign-vue-next'
|
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next'
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'cache-cleared': []
|
||||||
|
}>()
|
||||||
|
|
||||||
const cacheInfo: any = ref({})
|
const cacheInfo: any = ref({})
|
||||||
|
|
||||||
|
const loadCacheInfo = async (forceRefresh = false) => {
|
||||||
|
try {
|
||||||
|
console.log('正在获取缓存信息...', forceRefresh ? '(强制刷新)' : '')
|
||||||
|
const res = await window.api.musicCache.getInfo()
|
||||||
|
console.log('获取到缓存信息:', res)
|
||||||
|
cacheInfo.value = res
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取缓存信息失败:', error)
|
||||||
|
MessagePlugin.error('获取缓存信息失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
|
loadCacheInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
const clearCache = () => {
|
const clearCache = () => {
|
||||||
const confirm = DialogPlugin.confirm({
|
const confirm = DialogPlugin.confirm({
|
||||||
header: '确认清除缓存吗',
|
header: '确认清除缓存吗',
|
||||||
@@ -29,12 +61,79 @@ const clearCache = () => {
|
|||||||
},
|
},
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
confirm.hide()
|
confirm.hide()
|
||||||
cacheInfo.value = {}
|
|
||||||
await window.api.musicCache.clear()
|
try {
|
||||||
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
|
// 显示加载状态
|
||||||
|
cacheInfo.value = { ...cacheInfo.value, clearing: true }
|
||||||
|
|
||||||
|
// 执行清除操作
|
||||||
|
const result = await window.api.musicCache.clear()
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
console.log('缓存清除成功,开始更新界面')
|
||||||
|
MessagePlugin.success(result.message || '缓存清除成功')
|
||||||
|
|
||||||
|
// 发射缓存清除事件
|
||||||
|
emit('cache-cleared')
|
||||||
|
|
||||||
|
// 立即重置缓存信息显示
|
||||||
|
cacheInfo.value = {
|
||||||
|
count: 0,
|
||||||
|
size: 0,
|
||||||
|
sizeFormatted: '0 B',
|
||||||
|
clearing: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 多次尝试重新加载,确保获取到最新状态
|
||||||
|
let retryCount = 0
|
||||||
|
const maxRetries = 3
|
||||||
|
|
||||||
|
const reloadWithRetry = async () => {
|
||||||
|
retryCount++
|
||||||
|
console.log(`第${retryCount}次尝试重新加载缓存信息`)
|
||||||
|
|
||||||
|
await loadCacheInfo(true)
|
||||||
|
|
||||||
|
// 如果还有缓存文件且重试次数未达上限,继续重试
|
||||||
|
if (cacheInfo.value.count > 0 && retryCount < maxRetries) {
|
||||||
|
console.log(`仍有${cacheInfo.value.count}个缓存文件,1秒后重试`)
|
||||||
|
setTimeout(reloadWithRetry, 1000)
|
||||||
|
} else {
|
||||||
|
console.log('缓存信息更新完成:', cacheInfo.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟一下再开始重新加载
|
||||||
|
setTimeout(reloadWithRetry, 300)
|
||||||
|
} else {
|
||||||
|
MessagePlugin.error(result.message || '缓存清除失败')
|
||||||
|
// 清除加载状态
|
||||||
|
if (cacheInfo.value.clearing) {
|
||||||
|
delete cacheInfo.value.clearing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('清除缓存失败:', error)
|
||||||
|
MessagePlugin.error('清除缓存失败,请重试')
|
||||||
|
// 清除加载状态
|
||||||
|
if (cacheInfo.value.clearing) {
|
||||||
|
delete cacheInfo.value.clearing
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 刷新缓存信息(供父组件调用)
|
||||||
|
const refreshCacheInfo = async () => {
|
||||||
|
console.log('刷新缓存信息')
|
||||||
|
await loadCacheInfo(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露方法给父组件
|
||||||
|
defineExpose({
|
||||||
|
refreshCacheInfo
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -42,8 +141,14 @@ const clearCache = () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
.card-body {
|
.card-body {
|
||||||
padding: 10px;
|
padding: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
|
.no-cache-tip {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: var(--td-text-color-placeholder);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { ref } from 'vue'
|
|||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
showFloatBall: boolean
|
showFloatBall: boolean
|
||||||
|
directories?: {
|
||||||
|
cacheDir: string
|
||||||
|
downloadDir: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
|
|||||||
@@ -39,12 +39,21 @@ class MediaSessionController {
|
|||||||
if (!this.isSupported) return
|
if (!this.isSupported) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
// 确保元数据完整性,避免空值导致SMTC显示异常
|
||||||
title: metadata.title,
|
const safeMetadata = {
|
||||||
artist: metadata.artist,
|
title: metadata.title || '未知歌曲',
|
||||||
album: metadata.album,
|
artist: metadata.artist || '未知艺术家',
|
||||||
artwork: this.generateArtworkSizes(metadata.artworkUrl)
|
album: metadata.album || '未知专辑',
|
||||||
})
|
artwork: metadata.artworkUrl ? this.generateArtworkSizes(metadata.artworkUrl) : []
|
||||||
|
}
|
||||||
|
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata(safeMetadata)
|
||||||
|
|
||||||
|
// 强制更新播放状态,确保SMTC正确识别
|
||||||
|
if (this.audioElement) {
|
||||||
|
const currentState = this.audioElement.paused ? 'paused' : 'playing'
|
||||||
|
navigator.mediaSession.playbackState = currentState
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to update media session metadata:', error)
|
console.warn('Failed to update media session metadata:', error)
|
||||||
}
|
}
|
||||||
@@ -77,9 +86,19 @@ class MediaSessionController {
|
|||||||
this.audioElement = audioElement
|
this.audioElement = audioElement
|
||||||
this.callbacks = callbacks
|
this.callbacks = callbacks
|
||||||
|
|
||||||
// 只设置媒体会话动作处理器,不自动监听音频事件
|
// 设置媒体会话动作处理器
|
||||||
// 让应用层手动控制播放状态更新,避免循环调用
|
|
||||||
this.setupMediaSessionActionHandlers()
|
this.setupMediaSessionActionHandlers()
|
||||||
|
|
||||||
|
// 初始化时设置默认的播放状态
|
||||||
|
navigator.mediaSession.playbackState = 'none'
|
||||||
|
|
||||||
|
// 设置默认元数据,确保SMTC能够识别应用
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: '澜音',
|
||||||
|
artist: 'CeruMusic',
|
||||||
|
album: '音乐播放器',
|
||||||
|
artwork: []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from 'tdesign-icons-vue-next'
|
} from 'tdesign-icons-vue-next'
|
||||||
import fonts from '@renderer/assets/icon_font/icons'
|
import fonts from '@renderer/assets/icon_font/icons'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import DirectorySettings from '@renderer/components/Settings/DirectorySettings.vue'
|
||||||
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
|
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
|
||||||
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
|
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
|
||||||
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
|
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
|
||||||
@@ -28,6 +29,26 @@ const activeCategory = ref<string>('appearance')
|
|||||||
// 应用版本号
|
// 应用版本号
|
||||||
const appVersion = ref('1.0.0')
|
const appVersion = ref('1.0.0')
|
||||||
|
|
||||||
|
// 组件引用
|
||||||
|
const musicCacheRef = ref()
|
||||||
|
const directorySettingsRef = ref()
|
||||||
|
|
||||||
|
// 处理目录更改事件
|
||||||
|
const handleDirectoryChanged = () => {
|
||||||
|
console.log('目录已更改,刷新缓存信息')
|
||||||
|
if (musicCacheRef.value?.refreshCacheInfo) {
|
||||||
|
musicCacheRef.value.refreshCacheInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理缓存清除事件
|
||||||
|
const handleCacheCleared = () => {
|
||||||
|
console.log('缓存已清除,刷新目录大小')
|
||||||
|
if (directorySettingsRef.value?.refreshDirectorySizes) {
|
||||||
|
directorySettingsRef.value.refreshDirectorySizes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 获取应用版本号
|
// 获取应用版本号
|
||||||
const getAppVersion = async () => {
|
const getAppVersion = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -542,9 +563,13 @@ const openLink = (url: string) => {
|
|||||||
|
|
||||||
<!-- 存储管理 -->
|
<!-- 存储管理 -->
|
||||||
<div v-else-if="activeCategory === 'storage'" key="storage" class="settings-section">
|
<div v-else-if="activeCategory === 'storage'" key="storage" class="settings-section">
|
||||||
<div class="setting-group">
|
<DirectorySettings
|
||||||
<h3>音乐缓存管理</h3>
|
ref="directorySettingsRef"
|
||||||
<MusicCache />
|
@directory-changed="handleDirectoryChanged"
|
||||||
|
@cache-cleared="handleCacheCleared"
|
||||||
|
/>
|
||||||
|
<div style="margin-top: 20px">
|
||||||
|
<MusicCache ref="musicCacheRef" @cache-cleared="handleCacheCleared" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
5
src/types/global.d.ts
vendored
5
src/types/global.d.ts
vendored
@@ -20,6 +20,11 @@ declare global {
|
|||||||
removeMessageListener: () => void
|
removeMessageListener: () => void
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
electron: {
|
||||||
|
ipcRenderer: {
|
||||||
|
invoke: (channel: string, ...args: any[]) => Promise<any>
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user