mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 03:15:07 +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/design' }
|
||||
{ text: '软件设计文档', link: '/guide/design' },
|
||||
{ text: '更新日志', link: '/guide/updateLog' }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -163,20 +163,20 @@ html.dark #app {
|
||||
--check-line: 1;
|
||||
|
||||
/* 自动编号格式设置 无需自动编号可全部注释掉或部分注释掉*/
|
||||
--autonum-h1: counter(h1) ". ";
|
||||
--autonum-h2: counter(h1) "." counter(h2) ". ";
|
||||
--autonum-h3: counter(h1) "." counter(h2) "." counter(h3) ". ";
|
||||
--autonum-h4: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
|
||||
--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-h1: counter(h1) ". ";
|
||||
// --autonum-h2: counter(h1) "." counter(h2) ". ";
|
||||
// --autonum-h3: counter(h1) "." counter(h2) "." counter(h3) ". ";
|
||||
// --autonum-h4: counter(h1) "." counter(h2) "." counter(h3) "." counter(h4) ". ";
|
||||
// --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) ". ";
|
||||
|
||||
/* 下面是文章内Toc目录自动编号,与上面一样即可 */
|
||||
--autonum-h1toc: counter(h1toc) ". ";
|
||||
--autonum-h2toc: counter(h1toc) "." counter(h2toc) ". ";
|
||||
--autonum-h3toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) ". ";
|
||||
--autonum-h4toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) ". ";
|
||||
--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-h1toc: counter(h1toc) ". ";
|
||||
// --autonum-h2toc: counter(h1toc) "." counter(h2toc) ". ";
|
||||
// --autonum-h3toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) ". ";
|
||||
// --autonum-h4toc: counter(h1toc) "." counter(h2toc) "." counter(h3toc) "." counter(h4toc) ". ";
|
||||
// --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) ". ";
|
||||
|
||||
/* 主题颜色 */
|
||||
|
||||
|
||||
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",
|
||||
"version": "1.3.0",
|
||||
"version": "1.3.1",
|
||||
"description": "一款简洁优雅的音乐播放器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "sqj,wldss,star",
|
||||
@@ -45,6 +45,7 @@
|
||||
"@pixi/filter-color-matrix": "^7.4.3",
|
||||
"@pixi/sprite": "^7.4.3",
|
||||
"@types/needle": "^3.3.0",
|
||||
"NeteaseCloudMusicApi": "^4.27.0",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.11.0",
|
||||
"color-extraction": "^1.0.8",
|
||||
@@ -61,7 +62,6 @@
|
||||
"marked": "^16.1.2",
|
||||
"mitt": "^3.0.1",
|
||||
"needle": "^3.3.1",
|
||||
"NeteaseCloudMusicApi": "^4.27.0",
|
||||
"node-fetch": "2",
|
||||
"pinia": "^3.0.3",
|
||||
"tdesign-vue-next": "^1.15.2",
|
||||
@@ -80,7 +80,7 @@
|
||||
"@types/node": "^22.16.5",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"electron": "^37.3.1",
|
||||
"electron": "^38.1.0",
|
||||
"electron-builder": "^25.1.8",
|
||||
"electron-icon-builder": "^2.0.1",
|
||||
"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 () => {
|
||||
try {
|
||||
console.log('收到清空缓存请求')
|
||||
await musicCacheService.clearCache()
|
||||
console.log('缓存清空完成')
|
||||
return { success: true, message: '缓存已清空' }
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error('清空缓存失败:', error)
|
||||
return { success: false, message: '清空缓存失败' }
|
||||
return { success: false, message: `清空缓存失败: ${error.message}` }
|
||||
}
|
||||
})
|
||||
|
||||
// 获取缓存大小
|
||||
ipcMain.handle('music-cache:get-size', async () => {
|
||||
try {
|
||||
return await musicCacheService.getCacheSize()
|
||||
const info = await musicCacheService.getCacheInfo()
|
||||
return info.size
|
||||
} catch (error) {
|
||||
console.error('获取缓存大小失败:', error)
|
||||
return 0
|
||||
|
||||
@@ -217,15 +217,22 @@ ipcMain.handle('get-app-version', () => {
|
||||
aiEvents(mainWindow)
|
||||
import './events/musicCache'
|
||||
import './events/songList'
|
||||
import './events/directorySettings'
|
||||
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
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 () => {
|
||||
// 初始化插件系统
|
||||
|
||||
@@ -3,18 +3,43 @@ import * as path from 'path'
|
||||
import * as fs from 'fs/promises'
|
||||
import * as crypto from 'crypto'
|
||||
import axios from 'axios'
|
||||
import { CONFIG_NAME } from '../../events/directorySettings'
|
||||
|
||||
export class MusicCacheService {
|
||||
private cacheDir: string
|
||||
private cacheIndex: Map<string, string> = new Map()
|
||||
private indexFilePath: string
|
||||
|
||||
constructor() {
|
||||
this.cacheDir = path.join(app.getPath('userData'), 'music-cache')
|
||||
this.indexFilePath = path.join(this.cacheDir, 'cache-index.json')
|
||||
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() {
|
||||
try {
|
||||
// 确保缓存目录存在
|
||||
@@ -130,43 +155,117 @@ export class MusicCacheService {
|
||||
|
||||
async clearCache(): Promise<void> {
|
||||
try {
|
||||
// 删除所有缓存文件
|
||||
console.log('开始清空缓存目录:', this.cacheDir)
|
||||
|
||||
// 先重新加载缓存索引,确保获取最新的文件列表
|
||||
await this.loadCacheIndex()
|
||||
|
||||
// 删除索引中记录的所有缓存文件
|
||||
let deletedFromIndex = 0
|
||||
for (const filePath of this.cacheIndex.values()) {
|
||||
try {
|
||||
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()
|
||||
await this.saveCacheIndex()
|
||||
|
||||
console.log('音乐缓存已清空')
|
||||
console.log(
|
||||
`音乐缓存已清空 - 从索引删除: ${deletedFromIndex}个文件, 从目录删除: ${deletedFromDir}个文件`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('清空缓存失败:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async getCacheSize(): Promise<number> {
|
||||
getDirectorySize = async (dirPath: string): Promise<number> => {
|
||||
let totalSize = 0
|
||||
|
||||
for (const filePath of this.cacheIndex.values()) {
|
||||
try {
|
||||
const stats = await fs.stat(filePath)
|
||||
totalSize += stats.size
|
||||
} catch (error) {
|
||||
// 文件不存在,忽略
|
||||
try {
|
||||
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
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 忽略无法访问的文件/目录
|
||||
}
|
||||
|
||||
return totalSize
|
||||
}
|
||||
|
||||
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 => {
|
||||
if (bytes === 0) return '0 B'
|
||||
@@ -176,10 +275,12 @@ export class MusicCacheService {
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
console.log(`缓存信息 - 文件数量: ${actualCount}, 总大小: ${totalSize} bytes`)
|
||||
|
||||
return {
|
||||
count,
|
||||
size,
|
||||
sizeFormatted: formatSize(size)
|
||||
count: actualCount,
|
||||
size: totalSize,
|
||||
sizeFormatted: formatSize(totalSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from './type'
|
||||
import pluginService from '../plugin/index'
|
||||
import musicSdk from '../../utils/musicSdk/index'
|
||||
import { getAppDirPath } from '../../utils/path'
|
||||
import { musicCacheService } from '../musicCache'
|
||||
import path from 'node:path'
|
||||
import fs from 'fs'
|
||||
@@ -19,6 +18,8 @@ import fsPromise from 'fs/promises'
|
||||
import axios from 'axios'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { app } from 'electron'
|
||||
import { CONFIG_NAME } from '../../events/directorySettings'
|
||||
|
||||
const fileLock: Record<string, boolean> = {}
|
||||
|
||||
@@ -89,6 +90,24 @@ function main(source: string) {
|
||||
const url = await this.getMusicUrl({ pluginId, songInfo, quality })
|
||||
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
|
||||
const getFileExtension = (url: string): string => {
|
||||
try {
|
||||
@@ -110,11 +129,10 @@ function main(source: string) {
|
||||
}
|
||||
|
||||
const fileExtension = getFileExtension(url)
|
||||
const downloadDir = getDownloadDirectory()
|
||||
const songPath = path.join(
|
||||
getAppDirPath('music'),
|
||||
'CeruMusic',
|
||||
'songs',
|
||||
`${songInfo.name}-${songInfo.singer}-${source}.${fileExtension}`
|
||||
downloadDir,
|
||||
`${songInfo.name}-${songInfo.singer}-${songInfo.source}.${fileExtension}`
|
||||
.replace(/[/\\:*?"<>|]/g, '')
|
||||
.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
|
||||
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
|
||||
getUserConfig: () => Promise<any>
|
||||
}
|
||||
|
||||
@@ -162,6 +162,20 @@ const api = {
|
||||
stop: () => {
|
||||
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
|
||||
// biome-ignore lint: disable
|
||||
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 {
|
||||
AIFloatBallSettings: typeof import('./src/components/Settings/AIFloatBallSettings.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']
|
||||
FullPlay: typeof import('./src/components/Play/FullPlay.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']
|
||||
TContent: typeof import('tdesign-vue-next')['Content']
|
||||
TDialog: typeof import('tdesign-vue-next')['Dialog']
|
||||
TDivider: typeof import('tdesign-vue-next')['Divider']
|
||||
TDropdown: typeof import('tdesign-vue-next')['Dropdown']
|
||||
TForm: typeof import('tdesign-vue-next')['Form']
|
||||
TFormItem: typeof import('tdesign-vue-next')['FormItem']
|
||||
@@ -40,8 +42,11 @@ declare module 'vue' {
|
||||
TitleBarControls: typeof import('./src/components/TitleBarControls.vue')['default']
|
||||
TLayout: typeof import('tdesign-vue-next')['Layout']
|
||||
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']
|
||||
TSwitch: typeof import('tdesign-vue-next')['Switch']
|
||||
TTag: typeof import('tdesign-vue-next')['Tag']
|
||||
TTextarea: typeof import('tdesign-vue-next')['Textarea']
|
||||
TTooltip: typeof import('tdesign-vue-next')['Tooltip']
|
||||
UpdateExample: typeof import('./src/components/UpdateExample.vue')['default']
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
@font-face {
|
||||
font-family: 'lyricfont';
|
||||
src: url('./lyricfont.ttf');
|
||||
font-weight: 500;
|
||||
}
|
||||
*,
|
||||
*::before,
|
||||
|
||||
@@ -493,6 +493,8 @@ const lightMainColor = computed(() => {
|
||||
perspective: 1000px;
|
||||
|
||||
.pointer {
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
position: absolute;
|
||||
width: calc(var(--cd-width-auto) / 3.5);
|
||||
left: calc(50% - 1.8vh);
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
destroyPlaylistEventListeners,
|
||||
getSongRealUrl
|
||||
} from '@renderer/utils/playlistManager'
|
||||
import mediaSessionController from '@renderer/utils/useAmtc'
|
||||
import mediaSessionController from '@renderer/utils/useSmtc'
|
||||
import defaultCoverImg from '/default-cover.png'
|
||||
|
||||
const controlAudio = ControlAudioStore()
|
||||
@@ -163,9 +163,10 @@ const playSong = async (song: SongList) => {
|
||||
}
|
||||
|
||||
// 更新歌曲信息并触发主题色更新
|
||||
songInfo.value = {
|
||||
...song
|
||||
}
|
||||
songInfo.value.name = song.name
|
||||
songInfo.value.singer = song.singer
|
||||
songInfo.value.albumName = song.albumName
|
||||
songInfo.value.img = song.img
|
||||
|
||||
// 更新媒体会话元数据
|
||||
mediaSessionController.updateMetadata({
|
||||
@@ -176,7 +177,6 @@ const playSong = async (song: SongList) => {
|
||||
})
|
||||
|
||||
// 确保主题色更新
|
||||
await setColor()
|
||||
|
||||
let urlToPlay = ''
|
||||
|
||||
@@ -209,7 +209,10 @@ const playSong = async (song: SongList) => {
|
||||
|
||||
// 等待音频准备就绪
|
||||
await waitForAudioReady()
|
||||
|
||||
await setColor()
|
||||
songInfo.value = {
|
||||
...song
|
||||
}
|
||||
// // 短暂延迟确保音频状态稳定
|
||||
// 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>
|
||||
<div class="music-cache">
|
||||
<t-card hover-shadow :loading="cacheInfo ? false : true" title="本地歌曲缓存配置">
|
||||
<template #actions> 已有歌曲缓存大小:{{ cacheInfo.sizeFormatted }} </template>
|
||||
<t-card hover-shadow :loading="!cacheInfo || cacheInfo.clearing" title="本地歌曲缓存配置">
|
||||
<template #actions>
|
||||
已有歌曲缓存大小:{{ cacheInfo?.sizeFormatted || '0 B' }}
|
||||
<span v-if="cacheInfo?.count > 0">({{ cacheInfo.count }} 个文件)</span>
|
||||
</template>
|
||||
<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>
|
||||
</t-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DialogPlugin } from 'tdesign-vue-next'
|
||||
import { DialogPlugin, MessagePlugin } from 'tdesign-vue-next'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits<{
|
||||
'cache-cleared': []
|
||||
}>()
|
||||
|
||||
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(() => {
|
||||
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
|
||||
loadCacheInfo()
|
||||
})
|
||||
|
||||
const clearCache = () => {
|
||||
const confirm = DialogPlugin.confirm({
|
||||
header: '确认清除缓存吗',
|
||||
@@ -29,12 +61,79 @@ const clearCache = () => {
|
||||
},
|
||||
onConfirm: async () => {
|
||||
confirm.hide()
|
||||
cacheInfo.value = {}
|
||||
await window.api.musicCache.clear()
|
||||
window.api.musicCache.getInfo().then((res) => (cacheInfo.value = res))
|
||||
|
||||
try {
|
||||
// 显示加载状态
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -42,8 +141,14 @@ const clearCache = () => {
|
||||
width: 100%;
|
||||
|
||||
.card-body {
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
|
||||
.no-cache-tip {
|
||||
margin-top: 10px;
|
||||
color: var(--td-text-color-placeholder);
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,10 @@ import { ref } from 'vue'
|
||||
|
||||
export interface SettingsState {
|
||||
showFloatBall: boolean
|
||||
directories?: {
|
||||
cacheDir: string
|
||||
downloadDir: string
|
||||
}
|
||||
}
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
|
||||
@@ -39,12 +39,21 @@ class MediaSessionController {
|
||||
if (!this.isSupported) return
|
||||
|
||||
try {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({
|
||||
title: metadata.title,
|
||||
artist: metadata.artist,
|
||||
album: metadata.album,
|
||||
artwork: this.generateArtworkSizes(metadata.artworkUrl)
|
||||
})
|
||||
// 确保元数据完整性,避免空值导致SMTC显示异常
|
||||
const safeMetadata = {
|
||||
title: metadata.title || '未知歌曲',
|
||||
artist: metadata.artist || '未知艺术家',
|
||||
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) {
|
||||
console.warn('Failed to update media session metadata:', error)
|
||||
}
|
||||
@@ -77,9 +86,19 @@ class MediaSessionController {
|
||||
this.audioElement = audioElement
|
||||
this.callbacks = callbacks
|
||||
|
||||
// 只设置媒体会话动作处理器,不自动监听音频事件
|
||||
// 让应用层手动控制播放状态更新,避免循环调用
|
||||
// 设置媒体会话动作处理器
|
||||
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'
|
||||
import fonts from '@renderer/assets/icon_font/icons'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DirectorySettings from '@renderer/components/Settings/DirectorySettings.vue'
|
||||
import MusicCache from '@renderer/components/Settings/MusicCache.vue'
|
||||
import AIFloatBallSettings from '@renderer/components/Settings/AIFloatBallSettings.vue'
|
||||
import ThemeSelector from '@renderer/components/ThemeSelector.vue'
|
||||
@@ -28,6 +29,26 @@ const activeCategory = ref<string>('appearance')
|
||||
// 应用版本号
|
||||
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 () => {
|
||||
try {
|
||||
@@ -542,9 +563,13 @@ const openLink = (url: string) => {
|
||||
|
||||
<!-- 存储管理 -->
|
||||
<div v-else-if="activeCategory === 'storage'" key="storage" class="settings-section">
|
||||
<div class="setting-group">
|
||||
<h3>音乐缓存管理</h3>
|
||||
<MusicCache />
|
||||
<DirectorySettings
|
||||
ref="directorySettingsRef"
|
||||
@directory-changed="handleDirectoryChanged"
|
||||
@cache-cleared="handleCacheCleared"
|
||||
/>
|
||||
<div style="margin-top: 20px">
|
||||
<MusicCache ref="musicCacheRef" @cache-cleared="handleCacheCleared" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
5
src/types/global.d.ts
vendored
5
src/types/global.d.ts
vendored
@@ -20,6 +20,11 @@ declare global {
|
||||
removeMessageListener: () => void
|
||||
}
|
||||
}
|
||||
electron: {
|
||||
ipcRenderer: {
|
||||
invoke: (channel: string, ...args: any[]) => Promise<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user