feat:缓存路径支持自定义

下载路径支持自定义;fix:播放页面唱针可以拖动问题
播放按钮加载中 因为自动下一曲 导致动画变形问题
SMTC 功能 系统显示未知应用问题
播放页歌词字体粗细偶现丢失问题
This commit is contained in:
sqj
2025-09-17 00:39:44 +08:00
parent 59d3b0c65c
commit 87f69fc782
35 changed files with 1998 additions and 28010 deletions

View File

@@ -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"
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +0,0 @@
{
"type": "module"
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -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
}

View File

@@ -1,7 +0,0 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}

View File

@@ -1,3 +0,0 @@
provider: generic
url: https://update.ceru.shiqianjiang.cn
updaterCacheDirName: ceru-music-updater

View File

@@ -32,7 +32,8 @@ export default defineConfig({
{ text: '音乐播放列表', link: '/guide/used/playList' },
]
},
{ text: '软件设计文档', link: '/guide/design' }
{ text: '软件设计文档', link: '/guide/design' },
{ text: '更新日志', link: '/guide/updateLog' }
]
},
{

View File

@@ -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
View File

@@ -0,0 +1,15 @@
# 澜音版本更新日志
## 日志
- 2025-9-17 **V1.3.1**
1. **设置功能页**
- 缓存路径支持自定义
- 下载路径支持自定义
2. **debug**
- 播放页面唱针可以拖动问题
- 播放按钮加载中 因为自动下一曲 导致动画变形问题
- **SMTC** 功能 系统显示**未知应用**问题
- 播放页歌词**字体粗细**偶现丢失问题

View File

@@ -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",

View 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' }
}
})

View File

@@ -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

View File

@@ -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 () => {
// 初始化插件系统

View File

@@ -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)
}
}
}

View File

@@ -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(/\.+$/, '')

View File

@@ -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()
})
})
}

View File

@@ -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>
}

View File

@@ -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)
}
}

View File

@@ -5,4 +5,6 @@
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {}
declare global {
}

View File

@@ -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']

View File

@@ -5,7 +5,6 @@
@font-face {
font-family: 'lyricfont';
src: url('./lyricfont.ttf');
font-weight: 500;
}
*,
*::before,

View File

@@ -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);

View File

@@ -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))

View 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>

View File

@@ -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>

View File

@@ -3,6 +3,10 @@ import { ref } from 'vue'
export interface SettingsState {
showFloatBall: boolean
directories?: {
cacheDir: string
downloadDir: string
}
}
export const useSettingsStore = defineStore('settings', () => {

View File

@@ -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: []
})
}
/**

View File

@@ -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>

View File

@@ -20,6 +20,11 @@ declare global {
removeMessageListener: () => void
}
}
electron: {
ipcRenderer: {
invoke: (channel: string, ...args: any[]) => Promise<any>
}
}
}
}

1464
yarn.lock

File diff suppressed because it is too large Load Diff