From fdca217a4ac7c3608ed9b5b67bf50f3d6dca3519 Mon Sep 17 00:00:00 2001 From: imsyy Date: Tue, 12 May 2026 00:39:57 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E3=80=8C?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E4=B8=8E=E7=BC=93=E5=AD=98=E3=80=8D=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E5=88=86=E7=B1=BB=EF=BC=8C=E6=94=AF=E6=8C=81=E8=87=AA?= =?UTF-8?q?=E5=AE=9A=E4=B9=89=E7=BC=93=E5=AD=98=E7=9B=AE=E5=BD=95=E4=B8=8E?= =?UTF-8?q?=E5=88=86=E7=B1=BB=E6=B8=85=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.d.ts | 1 + electron/main/apis/musicbrainz.ts | 10 +- electron/main/ipc/cache.ts | 190 ++++++++++++++ electron/main/ipc/index.ts | 2 + electron/main/ipc/theme.ts | 13 +- electron/main/services/engine.ts | 11 +- electron/main/services/scanner.ts | 4 +- electron/main/utils/config.ts | 19 +- electron/main/utils/protocol.ts | 6 +- electron/preload/index.d.ts | 8 + electron/preload/index.ts | 14 + shared/defaults/settings.ts | 6 + shared/types/settings.ts | 16 ++ src/components/list/SongList.vue | 245 ++++++++--------- .../settings/custom/CacheManager.vue | 248 ++++++++++++++++++ src/i18n/locales/en-US.json | 52 +++- src/i18n/locales/zh-CN.json | 52 +++- src/settings/categories/localCache.ts | 36 +++ src/settings/schema.ts | 2 + 19 files changed, 786 insertions(+), 149 deletions(-) create mode 100644 electron/main/ipc/cache.ts create mode 100644 src/components/settings/custom/CacheManager.vue create mode 100644 src/settings/categories/localCache.ts diff --git a/components.d.ts b/components.d.ts index 954a07e2..44dff720 100644 --- a/components.d.ts +++ b/components.d.ts @@ -17,6 +17,7 @@ declare module 'vue' { AutoCloseDialog: typeof import('./src/components/modals/AutoCloseDialog.vue')['default'] BackgroundImagePicker: typeof import('./src/components/settings/custom/BackgroundImagePicker.vue')['default'] BottomSpectrum: typeof import('./src/components/player/FullPlayer/BottomSpectrum.vue')['default'] + CacheManager: typeof import('./src/components/settings/custom/CacheManager.vue')['default'] ComboboxAnchor: typeof import('reka-ui')['ComboboxAnchor'] ComboboxContent: typeof import('reka-ui')['ComboboxContent'] ComboboxEmpty: typeof import('reka-ui')['ComboboxEmpty'] diff --git a/electron/main/apis/musicbrainz.ts b/electron/main/apis/musicbrainz.ts index 374d96b0..4953a0b8 100644 --- a/electron/main/apis/musicbrainz.ts +++ b/electron/main/apis/musicbrainz.ts @@ -1,7 +1,7 @@ import { net } from "electron"; import fs from "node:fs"; import path from "node:path"; -import { artistCacheDir } from "@main/utils/config"; +import { getArtistCacheDir } from "@main/utils/config"; import { toCacheUrl } from "@main/utils/protocol"; /** MusicBrainz User-Agent */ @@ -34,8 +34,8 @@ const getNotFoundFileName = (artistName: string): string => { /** 确保缓存目录存在 */ const ensureCacheDir = (): void => { - if (!fs.existsSync(artistCacheDir)) { - fs.mkdirSync(artistCacheDir, { recursive: true }); + if (!fs.existsSync(getArtistCacheDir())) { + fs.mkdirSync(getArtistCacheDir(), { recursive: true }); } }; @@ -92,13 +92,13 @@ const fetchArtistAvatarCore = async (artistName: string): Promise // 确保目录存在 ensureCacheDir(); const cacheFileName = getCacheFileName(name); - const cachePath = path.join(artistCacheDir, cacheFileName); + const cachePath = path.join(getArtistCacheDir(), cacheFileName); // 已缓存 if (fs.existsSync(cachePath)) { return toCacheUrl(cachePath) ?? null; } // 已标记为未找到(7 天过期) - const notFoundPath = path.join(artistCacheDir, getNotFoundFileName(name)); + const notFoundPath = path.join(getArtistCacheDir(), getNotFoundFileName(name)); if (fs.existsSync(notFoundPath)) { const stat = fs.statSync(notFoundPath); if (Date.now() - stat.mtimeMs < 7 * 24 * 60 * 60 * 1000) return null; diff --git a/electron/main/ipc/cache.ts b/electron/main/ipc/cache.ts new file mode 100644 index 00000000..5699e21e --- /dev/null +++ b/electron/main/ipc/cache.ts @@ -0,0 +1,190 @@ +import { dialog, ipcMain } from "electron"; +import path from "node:path"; +import fs from "node:fs/promises"; +import { existsSync, statSync, readdirSync } from "node:fs"; +import { store } from "@main/store"; +import { + defaultAppCacheDir, + getAppCacheDir, + getCoverCacheDir, + getArtistCacheDir, + getBackgroundsDir, +} from "@main/utils/config"; +import { syncCoverCacheDir } from "@main/services/engine"; +import { getDb } from "@main/database"; +import { clearLyricCache } from "@main/database/lyricCache"; +import { clearLyricTtmlCache } from "@main/database/lyricTtmlCache"; +import { clearLyricMatchCache } from "@main/database/lyricMatchCache"; +import { systemLog } from "@main/utils/logger"; + +/** 已知的缓存类别 */ +export type CacheCategory = + | "covers" + | "artists" + | "backgrounds" + | "lyric" + | "lyricTTML" + | "lyricMatch"; + +/** 单类别的占用情况 */ +export interface CacheStat { + id: CacheCategory; + /** 显示用路径或来源;DB 类条目展示表名 */ + path: string; + size: number; +} + +/** 递归累计目录占用 */ +const dirSize = (dir: string): number => { + if (!existsSync(dir)) return 0; + let total = 0; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + try { + if (entry.isDirectory()) { + total += dirSize(full); + } else if (entry.isFile()) { + total += statSync(full).size; + } + } catch {} + } + return total; +}; + +/** 清空目录但保留目录本身 */ +const clearDir = async (dir: string): Promise => { + if (!existsSync(dir)) return; + const entries = await fs.readdir(dir); + await Promise.all( + entries.map((name) => fs.rm(path.join(dir, name), { recursive: true, force: true })), + ); +}; + +/** 目录是否为空(不存在视为空) */ +const isDirEmpty = async (dir: string): Promise => { + if (!existsSync(dir)) return true; + const entries = await fs.readdir(dir); + return entries.length === 0; +}; + +/** sqlite 单表占用:取所有 TEXT/BLOB 字段 length 之和 */ +const tableSize = (table: string, columns: string[]): number => { + try { + const expr = columns.map((c) => `COALESCE(length(${c}), 0)`).join(" + "); + const row = getDb().prepare(`SELECT SUM(${expr}) AS total FROM ${table}`).get() as + | { total: number | null } + | undefined; + return row?.total ?? 0; + } catch { + return 0; + } +}; + +/** 类别 → 占用统计 / 路径展示 / 清空动作 */ +const categoryHandlers: Record< + CacheCategory, + { path: () => string; size: () => number; clear: () => void | Promise } +> = { + covers: { + path: getCoverCacheDir, + size: () => dirSize(getCoverCacheDir()), + clear: () => clearDir(getCoverCacheDir()), + }, + artists: { + path: getArtistCacheDir, + size: () => dirSize(getArtistCacheDir()), + clear: () => clearDir(getArtistCacheDir()), + }, + backgrounds: { + path: getBackgroundsDir, + size: () => dirSize(getBackgroundsDir()), + clear: () => clearDir(getBackgroundsDir()), + }, + lyric: { + path: () => "lyric_cache", + size: () => tableSize("lyric_cache", ["data"]), + clear: clearLyricCache, + }, + lyricTTML: { + path: () => "lyric_ttml_cache", + size: () => tableSize("lyric_ttml_cache", ["content"]), + clear: clearLyricTtmlCache, + }, + lyricMatch: { + path: () => "lyric_match_cache", + size: () => tableSize("lyric_match_cache", ["fingerprint", "platform_id", "extra"]), + clear: clearLyricMatchCache, + }, +}; + +/** 文件类目(受自定义缓存目录影响,切换时需要清空) */ +const fileCategories: CacheCategory[] = ["covers", "artists", "backgrounds"]; + +/** 注册缓存相关 IPC */ +export const registerCacheIpc = (): void => { + ipcMain.handle("cache:getStats", (): CacheStat[] => { + return (Object.keys(categoryHandlers) as CacheCategory[]).map((id) => ({ + id, + path: categoryHandlers[id].path(), + size: categoryHandlers[id].size(), + })); + }); + + ipcMain.handle("cache:clear", async (_event, id: CacheCategory): Promise => { + const handler = categoryHandlers[id]; + if (!handler) return; + try { + await handler.clear(); + systemLog.info(`[cache] cleared ${id}`); + } catch (err) { + systemLog.error(`[cache] clear ${id} failed`, err); + throw err; + } + }); + + ipcMain.handle("cache:clearAll", async (): Promise => { + await Promise.all( + (Object.keys(categoryHandlers) as CacheCategory[]).map((id) => categoryHandlers[id].clear()), + ); + systemLog.info(`[cache] cleared all`); + }); + + ipcMain.handle("cache:getDir", (): string => getAppCacheDir()); + + /** + * 切换缓存目录 + * - 用户必须选择空目录才允许切换 + * - 切换前清空旧目录下的文件类缓存(封面 / 头像 / 背景图) + * - sqlite 中的歌词等缓存路径独立,不参与本次清空 + */ + ipcMain.handle( + "cache:pickDir", + async (): Promise<{ ok: boolean; dir: string; reason?: "canceled" | "notEmpty" }> => { + const current = getAppCacheDir(); + const result = await dialog.showOpenDialog({ + title: "选择缓存目录", + properties: ["openDirectory", "createDirectory"], + }); + if (result.canceled || result.filePaths.length === 0) { + return { ok: false, dir: current, reason: "canceled" }; + } + const next = result.filePaths[0]; + if (!(await isDirEmpty(next))) { + return { ok: false, dir: current, reason: "notEmpty" }; + } + await Promise.all(fileCategories.map((id) => categoryHandlers[id].clear())); + store.set("cache.dir", next); + syncCoverCacheDir(); + systemLog.info(`[cache] dir switched to ${next}`); + return { ok: true, dir: next }; + }, + ); + + /** 还原默认缓存目录(同样清空旧的文件类缓存) */ + ipcMain.handle("cache:resetDir", async (): Promise => { + await Promise.all(fileCategories.map((id) => categoryHandlers[id].clear())); + store.set("cache.dir", null); + syncCoverCacheDir(); + return defaultAppCacheDir; + }); +}; diff --git a/electron/main/ipc/index.ts b/electron/main/ipc/index.ts index ea4f49d8..ba248ec8 100644 --- a/electron/main/ipc/index.ts +++ b/electron/main/ipc/index.ts @@ -10,6 +10,7 @@ import { registerLyricsIpc } from "./lyrics"; import { registerHotkeyIpc } from "./hotkey"; import { registerThemeIpc } from "./theme"; import { registerStreamingIpc } from "./streaming"; +import { registerCacheIpc } from "./cache"; /** 注册所有 IPC 处理 */ export const registerIpcHandlers = (): void => { @@ -25,4 +26,5 @@ export const registerIpcHandlers = (): void => { registerHotkeyIpc(); registerThemeIpc(); registerStreamingIpc(); + registerCacheIpc(); }; diff --git a/electron/main/ipc/theme.ts b/electron/main/ipc/theme.ts index b3549aeb..75053844 100644 --- a/electron/main/ipc/theme.ts +++ b/electron/main/ipc/theme.ts @@ -2,13 +2,10 @@ import { dialog, ipcMain } from "electron"; import path from "node:path"; import fs from "node:fs/promises"; import { createHash } from "node:crypto"; -import { appCacheDir } from "@main/utils/config"; +import { getBackgroundsDir } from "@main/utils/config"; import { toCacheUrl } from "@main/utils/protocol"; import { systemLog } from "@main/utils/logger"; -/** 背景图片缓存目录 */ -const bgDir = path.join(appCacheDir, "backgrounds"); - /** 允许的图片扩展名 */ const allowedExt = new Set([".jpg", ".jpeg", ".png", ".webp", ".bmp", ".gif"]); @@ -47,9 +44,9 @@ export const registerThemeIpc = (): void => { // 内容 hash 作为文件名 const hash = createHash("sha1").update(data).digest("hex").slice(0, 16); // 清空整个目录再写入新图 - await fs.rm(bgDir, { recursive: true, force: true }); - await fs.mkdir(bgDir, { recursive: true }); - const dest = path.join(bgDir, `${hash}${ext}`); + await fs.rm(getBackgroundsDir(), { recursive: true, force: true }); + await fs.mkdir(getBackgroundsDir(), { recursive: true }); + const dest = path.join(getBackgroundsDir(), `${hash}${ext}`); await fs.writeFile(dest, data); return toCacheUrl(dest) ?? null; } catch (err) { @@ -63,7 +60,7 @@ export const registerThemeIpc = (): void => { */ ipcMain.handle("theme:clearBackgroundImages", async (): Promise => { try { - await fs.rm(bgDir, { recursive: true, force: true }); + await fs.rm(getBackgroundsDir(), { recursive: true, force: true }); } catch (err) { systemLog.error("[theme] clearBackgroundImages failed", err); } diff --git a/electron/main/services/engine.ts b/electron/main/services/engine.ts index c2c2f9cc..ce2b5704 100644 --- a/electron/main/services/engine.ts +++ b/electron/main/services/engine.ts @@ -1,5 +1,5 @@ import { loadNativeModule } from "@main/utils/nativeLoader"; -import { coverCacheDir, isDev } from "@main/utils/config"; +import { getCoverCacheDir, isDev } from "@main/utils/config"; import { playerLog, nativeLogsDir } from "@main/utils/logger"; type AudioEngineModule = typeof import("@splayer/audio-engine"); @@ -36,7 +36,7 @@ export const getPlayer = (): PlayerInstance => { if (!playerInstance) { const mod = getEngine(); playerInstance = new mod.AudioPlayer(); - playerInstance.setCoverCacheDir(coverCacheDir); + playerInstance.setCoverCacheDir(getCoverCacheDir()); for (const cb of onCreatedCallbacks) { cb(playerInstance); } @@ -85,3 +85,10 @@ export const setPreampGain = (preampDb: number): void => { playerInstance.setPreampGain(preampDb); } }; + +/** 同步当前封面缓存目录到原生引擎(缓存路径切换时调用) */ +export const syncCoverCacheDir = (): void => { + if (playerInstance) { + playerInstance.setCoverCacheDir(getCoverCacheDir()); + } +}; diff --git a/electron/main/services/scanner.ts b/electron/main/services/scanner.ts index 4a9be8f4..3a1ea562 100644 --- a/electron/main/services/scanner.ts +++ b/electron/main/services/scanner.ts @@ -11,7 +11,7 @@ import { broadcast } from "@main/utils/broadcast"; import { toCacheUrl } from "@main/utils/protocol"; import { toMs } from "@main/utils/time"; import { parseArtists, parseAlbum } from "@main/utils/metadata"; -import { coverCacheDir } from "@main/utils/config"; +import { getCoverCacheDir } from "@main/utils/config"; import { libraryLog } from "@main/utils/logger"; let scanning = false; @@ -106,7 +106,7 @@ export const startScan = (dirs: string[], incremental = true): void => { } } }, - coverCacheDir, + getCoverCacheDir(), incrementalData, ); }; diff --git a/electron/main/utils/config.ts b/electron/main/utils/config.ts index 1e45848d..9f3b9db9 100644 --- a/electron/main/utils/config.ts +++ b/electron/main/utils/config.ts @@ -1,6 +1,7 @@ import { is } from "@electron-toolkit/utils"; import { app } from "electron"; import path from "node:path"; +import { store } from "@main/store"; /** * 是否为开发环境 @@ -24,11 +25,17 @@ export const appVersion = app.getVersion(); /** 应用名称 */ export const appName = app.getName(); -/** 应用自有缓存根目录(区别于 Electron 内置的 Cache/Code Cache) */ -export const appCacheDir = path.join(app.getPath("userData"), "app-cache"); +/** 默认缓存根目录(区别于 Electron 内置的 Cache/Code Cache) */ +export const defaultAppCacheDir = path.join(app.getPath("userData"), "app-cache"); -/** 封面缩略图缓存目录 */ -export const coverCacheDir = path.join(appCacheDir, "covers"); +/** 当前生效的缓存根目录:用户在设置中可选自定义路径,未配置时回退默认 */ +export const getAppCacheDir = (): string => store.get("cache.dir") || defaultAppCacheDir; -/** 歌手头像缓存目录 */ -export const artistCacheDir = path.join(appCacheDir, "artists"); +/** 当前生效的封面缩略图目录 */ +export const getCoverCacheDir = (): string => path.join(getAppCacheDir(), "covers"); + +/** 当前生效的歌手头像目录 */ +export const getArtistCacheDir = (): string => path.join(getAppCacheDir(), "artists"); + +/** 当前生效的背景图目录 */ +export const getBackgroundsDir = (): string => path.join(getAppCacheDir(), "backgrounds"); diff --git a/electron/main/utils/protocol.ts b/electron/main/utils/protocol.ts index 795459f1..51151824 100644 --- a/electron/main/utils/protocol.ts +++ b/electron/main/utils/protocol.ts @@ -1,6 +1,6 @@ import path from "node:path"; import { net, protocol, session } from "electron"; -import { appCacheDir } from "./config"; +import { getAppCacheDir } from "./config"; /** cache:// 协议方案名 */ const SCHEME = "cache"; @@ -26,7 +26,7 @@ export const registerCacheScheme = (): void => { /** cache:// 协议的处理函数 */ const cacheHandler = (request: Request): Response | Promise => { const relativePath = decodeURIComponent(request.url.slice(`${SCHEME}://`.length)); - const filePath = path.join(appCacheDir, relativePath); + const filePath = path.join(getAppCacheDir(), relativePath); return net.fetch(`file://${filePath.replace(/\\/g, "/")}`); }; @@ -53,6 +53,6 @@ export const handleCacheProtocolOnPartition = (partition: string): void => { */ export const toCacheUrl = (filePath: string | undefined | null): string | undefined => { if (!filePath) return undefined; - const relative = path.relative(appCacheDir, filePath).replace(/\\/g, "/"); + const relative = path.relative(getAppCacheDir(), filePath).replace(/\\/g, "/"); return `${SCHEME}://${relative}`; }; diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 3a460c19..4fd2e162 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -47,6 +47,14 @@ declare global { pickBackgroundImage: () => Promise; clearBackgroundImages: () => Promise; }; + cache: { + getStats: () => Promise<{ id: string; path: string; size: number }[]>; + clear: (id: string) => Promise; + clearAll: () => Promise; + getDir: () => Promise; + pickDir: () => Promise<{ ok: boolean; dir: string; reason?: "canceled" | "notEmpty" }>; + resetDir: () => Promise; + }; hotkey: HotkeyApi; streaming: StreamingApi; }; diff --git a/electron/preload/index.ts b/electron/preload/index.ts index ea42e78e..600bdfa6 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -307,6 +307,20 @@ const api = { // 清空已缓存的背景图 clearBackgroundImages: (): Promise => ipcRenderer.invoke("theme:clearBackgroundImages"), }, + cache: { + // 各类别占用统计 + getStats: () => ipcRenderer.invoke("cache:getStats"), + // 清除单个类别 + clear: (id: string) => ipcRenderer.invoke("cache:clear", id), + // 清空全部 + clearAll: () => ipcRenderer.invoke("cache:clearAll"), + // 获取当前缓存目录 + getDir: () => ipcRenderer.invoke("cache:getDir"), + // 选择新的缓存目录 + pickDir: () => ipcRenderer.invoke("cache:pickDir"), + // 还原默认缓存目录 + resetDir: () => ipcRenderer.invoke("cache:resetDir"), + }, streaming: { // 加载服务器配置(密码已解密) loadServers: () => ipcRenderer.invoke("streaming:loadServers"), diff --git a/shared/defaults/settings.ts b/shared/defaults/settings.ts index 52de4116..1066f748 100644 --- a/shared/defaults/settings.ts +++ b/shared/defaults/settings.ts @@ -88,6 +88,12 @@ export const defaultSystemConfig: SystemConfig = { enableOnlineTTMLLyric: false, amllDbServer: "https://amlldb.bikonoo.com/%p/%s.ttml", }, + localLyric: { + enableLocalTTMLOverride: false, + }, + cache: { + dir: null, + }, streaming: { enabled: true, }, diff --git a/shared/types/settings.ts b/shared/types/settings.ts index 8a670457..42199149 100644 --- a/shared/types/settings.ts +++ b/shared/types/settings.ts @@ -191,6 +191,18 @@ export interface OnlineLyricSettings { amllDbServer: string; } +/** 本地歌词配置 */ +export interface LocalLyricSettings { + /** 启用本地 TTML 覆盖:扫描到同目录的 .ttml 时优先于在线源 */ + enableLocalTTMLOverride: boolean; +} + +/** 缓存配置 */ +export interface CacheSettings { + /** 自定义缓存目录;null 使用默认 {userData}/app-cache */ + dir: string | null; +} + /** 主窗口几何 */ export interface MainWindowState { width: number; @@ -249,6 +261,10 @@ export interface SystemConfig { taskbarLyric: TaskbarLyricSettings; /** 在线歌词服务配置 */ lyric: OnlineLyricSettings; + /** 本地歌词配置 */ + localLyric: LocalLyricSettings; + /** 缓存配置 */ + cache: CacheSettings; /** 流媒体总开关 */ streaming: StreamingSettings; /** 系统配置 */ diff --git a/src/components/list/SongList.vue b/src/components/list/SongList.vue index 710f8789..100ab388 100644 --- a/src/components/list/SongList.vue +++ b/src/components/list/SongList.vue @@ -231,136 +231,139 @@ defineExpose({ diff --git a/src/components/settings/custom/CacheManager.vue b/src/components/settings/custom/CacheManager.vue new file mode 100644 index 00000000..f3b1e363 --- /dev/null +++ b/src/components/settings/custom/CacheManager.vue @@ -0,0 +1,248 @@ + + + diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index e1108954..48e00781 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -251,6 +251,7 @@ "hotkeys": "Hotkey Settings", "services": "Services", "mediaSource": "Media Sources", + "localCache": "Local & Cache", "plugins": "Plugin Manager" }, "section": { @@ -278,7 +279,9 @@ "systemConfig": "System", "reset": "Reset", "pluginsList": "Installed Plugins", - "streaming": "Streaming" + "streaming": "Streaming", + "localFiles": "Local Files", + "cache": "Cache Database" }, "language": { "label": "Language", @@ -878,6 +881,53 @@ "pluginManager": { "label": "Plugin Manager", "description": "Import, enable, or uninstall extension scripts" + }, + "enableLocalTTMLOverride": { + "label": "Local TTML Override", + "description": "Prefer a local .ttml file (when available) over online lyrics" + }, + "cacheManager": { + "label": "Cache Manager", + "description": "Inspect cache usage and clear it" + }, + "cacheDir": { + "label": "Cache Directory", + "open": "Open in file manager", + "change": "Change", + "reset": "Reset", + "hint": "Covers, artist avatars and backgrounds live under this folder. Switching wipes the old caches; the new location must be an empty folder.", + "changedHint": "Cache directory switched", + "notEmpty": "The selected folder isn't empty. Please pick an empty folder.", + "switchConfirmTitle": "Switch cache directory?", + "switchConfirmDesc": "Current cover / artist / background caches will be cleared before the switch, and the new location must be an empty folder. Database-backed lyric caches are unaffected.", + "switchConfirmOk": "Pick Folder", + "resetConfirmTitle": "Reset to default cache directory?", + "resetConfirmDesc": "Current file caches will be cleared before resetting." + }, + "cacheUsage": { + "label": "Usage by Category", + "total": "Total", + "refresh": "Refresh" + }, + "cacheCategory": { + "covers": "Cover Thumbnails", + "artists": "Artist Avatars", + "backgrounds": "Backgrounds", + "lyric": "Lyric Content Cache", + "lyricTTML": "TTML Lyric Cache", + "lyricMatch": "Lyric Match Cache" + }, + "cacheCategoryClear": { + "label": "Clear", + "confirmTitle": "Clear \"{name}\"?", + "confirmDesc": "All cached data in this category will be deleted. This cannot be undone." + }, + "cacheClearAll": { + "label": "Clear All", + "description": "Wipe cached data across all categories. This cannot be undone.", + "button": "Clear All", + "confirmTitle": "Clear all caches?", + "confirmDesc": "All cached data above will be deleted and regenerated on next use. This cannot be undone." } }, "collection": { diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json index 881fda07..3853f02c 100644 --- a/src/i18n/locales/zh-CN.json +++ b/src/i18n/locales/zh-CN.json @@ -251,6 +251,7 @@ "hotkeys": "快捷键配置", "services": "网络与服务", "mediaSource": "媒体源配置", + "localCache": "本地与缓存", "plugins": "插件管理" }, "section": { @@ -278,7 +279,9 @@ "systemConfig": "系统配置", "reset": "重置", "pluginsList": "已安装插件", - "streaming": "流媒体管理" + "streaming": "流媒体管理", + "localFiles": "本地文件", + "cache": "缓存数据库" }, "language": { "label": "界面语言", @@ -878,6 +881,53 @@ "pluginManager": { "label": "插件管理", "description": "导入、启用或卸载扩展脚本" + }, + "enableLocalTTMLOverride": { + "label": "本地 TTML 覆盖", + "description": "当本地存在 .ttml 歌词文件时,优先使用本地内容覆盖在线歌词" + }, + "cacheManager": { + "label": "缓存管理", + "description": "查看缓存占用并清理" + }, + "cacheDir": { + "label": "缓存目录", + "open": "在文件管理器中打开", + "change": "更改", + "reset": "还原默认", + "hint": "封面、歌手头像、背景图都会存到该目录下;切换会先清空旧缓存,新位置必须是空文件夹", + "changedHint": "缓存目录已切换", + "notEmpty": "所选文件夹不为空,请选择一个空文件夹", + "switchConfirmTitle": "切换缓存目录?", + "switchConfirmDesc": "切换前会清空当前的封面、歌手头像、背景图缓存,新位置必须是空文件夹。歌词等数据库缓存不受影响。", + "switchConfirmOk": "选择文件夹", + "resetConfirmTitle": "还原默认缓存目录?", + "resetConfirmDesc": "还原前同样会清空当前的文件类缓存" + }, + "cacheUsage": { + "label": "类型占用", + "total": "合计", + "refresh": "刷新" + }, + "cacheCategory": { + "covers": "封面缩略图", + "artists": "歌手头像", + "backgrounds": "背景图", + "lyric": "歌词内容缓存", + "lyricTTML": "TTML 歌词缓存", + "lyricMatch": "歌词匹配缓存" + }, + "cacheCategoryClear": { + "label": "清除", + "confirmTitle": "清除「{name}」?", + "confirmDesc": "此操作将删除该类别下的全部缓存数据,不可撤销" + }, + "cacheClearAll": { + "label": "一键清空", + "description": "清空所有类别的缓存数据,不可撤销", + "button": "清空全部", + "confirmTitle": "清空所有缓存?", + "confirmDesc": "将删除以上全部类别的缓存数据,下次使用时会重新生成,不可撤销" } }, "collection": { diff --git a/src/settings/categories/localCache.ts b/src/settings/categories/localCache.ts new file mode 100644 index 00000000..cd868e61 --- /dev/null +++ b/src/settings/categories/localCache.ts @@ -0,0 +1,36 @@ +import type { SettingCategory } from "@/types/settings-schema"; +import CacheManager from "@/components/settings/custom/CacheManager.vue"; +import IconLucideHardDrive from "~icons/lucide/hard-drive"; + +const localCacheCategory: SettingCategory = { + id: "localCache", + icon: IconLucideHardDrive, + sections: [ + { + id: "localFiles", + items: [ + { + key: "enableLocalTTMLOverride", + type: "switch", + binding: { store: "settings", path: "system.localLyric.enableLocalTTMLOverride" }, + defaultValue: false, + tag: { text: "Beta" }, + }, + ], + }, + { + id: "cache", + items: [ + { + key: "cacheManager", + type: "custom", + component: CacheManager, + fullWidth: true, + keywords: ["cacheDir.label", "cacheUsage.label", "cacheClearAll.label"], + }, + ], + }, + ], +}; + +export default localCacheCategory; diff --git a/src/settings/schema.ts b/src/settings/schema.ts index 919b38bb..a0342981 100644 --- a/src/settings/schema.ts +++ b/src/settings/schema.ts @@ -7,6 +7,7 @@ import externalLyricCategory from "./categories/externalLyric"; import hotkeysCategory from "./categories/hotkeys"; import servicesCategory from "./categories/services"; import mediaSourceCategory from "./categories/streaming"; +import localCacheCategory from "./categories/localCache"; import pluginsCategory from "./categories/plugins"; /** 设置项 schema:左侧分类(category)→ 区块(section)→ 设置项(item)三级结构。 @@ -20,5 +21,6 @@ export const settingsSchema: SettingCategory[] = [ hotkeysCategory, servicesCategory, mediaSourceCategory, + localCacheCategory, pluginsCategory, ]; From d7dfc778e8ed252dd7a58468edf305d275ddf0b3 Mon Sep 17 00:00:00 2001 From: imsyy Date: Tue, 12 May 2026 16:42:44 +0800 Subject: [PATCH 2/7] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E7=BC=93?= =?UTF-8?q?=E5=AD=98=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components.d.ts | 2 + electron/main/ipc/cache.ts | 45 ++-- electron/main/ipc/config.ts | 63 +++++- electron/main/ipc/system.ts | 8 +- electron/main/store/index.ts | 7 + electron/preload/index.d.ts | 7 +- electron/preload/index.ts | 14 +- shared/types/settings.ts | 8 + .../settings/custom/DbCacheManager.vue | 147 +++++++++++++ ...{CacheManager.vue => FileCacheManager.vue} | 70 ++---- .../settings/custom/StorageManager.vue | 204 ++++++++++++++---- src/composables/useCacheStats.ts | 41 ++++ src/i18n/locales/en-US.json | 65 ++++-- src/i18n/locales/zh-CN.json | 65 ++++-- src/settings/categories/general.ts | 4 +- src/settings/categories/localCache.ts | 21 +- 16 files changed, 627 insertions(+), 144 deletions(-) create mode 100644 src/components/settings/custom/DbCacheManager.vue rename src/components/settings/custom/{CacheManager.vue => FileCacheManager.vue} (80%) create mode 100644 src/composables/useCacheStats.ts diff --git a/components.d.ts b/components.d.ts index 44dff720..9eec2843 100644 --- a/components.d.ts +++ b/components.d.ts @@ -38,6 +38,7 @@ declare module 'vue' { ContextMenuSubTrigger: typeof import('reka-ui')['ContextMenuSubTrigger'] ContextMenuTrigger: typeof import('reka-ui')['ContextMenuTrigger'] CoverList: typeof import('./src/components/list/CoverList.vue')['default'] + DbCacheManager: typeof import('./src/components/settings/custom/DbCacheManager.vue')['default'] DeviceSelector: typeof import('./src/components/settings/custom/DeviceSelector.vue')['default'] DialogClose: typeof import('reka-ui')['DialogClose'] DialogContent: typeof import('reka-ui')['DialogContent'] @@ -57,6 +58,7 @@ declare module 'vue' { DropdownMenuTrigger: typeof import('reka-ui')['DropdownMenuTrigger'] EqualizerDialog: typeof import('./src/components/modals/EqualizerDialog.vue')['default'] ExcludeLyricsConfig: typeof import('./src/components/settings/custom/ExcludeLyricsConfig.vue')['default'] + FileCacheManager: typeof import('./src/components/settings/custom/FileCacheManager.vue')['default'] FontConfig: typeof import('./src/components/settings/custom/FontConfig.vue')['default'] FullPlayer: typeof import('./src/components/player/FullPlayer/index.vue')['default'] HotkeyConfig: typeof import('./src/components/settings/custom/HotkeyConfig.vue')['default'] diff --git a/electron/main/ipc/cache.ts b/electron/main/ipc/cache.ts index 5699e21e..27984561 100644 --- a/electron/main/ipc/cache.ts +++ b/electron/main/ipc/cache.ts @@ -26,10 +26,14 @@ export type CacheCategory = | "lyricTTML" | "lyricMatch"; +/** 缓存介质 */ +export type CacheKind = "file" | "db"; + /** 单类别的占用情况 */ export interface CacheStat { id: CacheCategory; - /** 显示用路径或来源;DB 类条目展示表名 */ + kind: CacheKind; + /** 显示用路径或来源 */ path: string; size: number; } @@ -80,51 +84,65 @@ const tableSize = (table: string, columns: string[]): number => { } }; -/** 类别 → 占用统计 / 路径展示 / 清空动作 */ +/** 类别 → 介质 / 占用统计 / 路径展示 / 清空动作 */ const categoryHandlers: Record< CacheCategory, - { path: () => string; size: () => number; clear: () => void | Promise } + { + kind: CacheKind; + path: () => string; + size: () => number; + clear: () => void | Promise; + } > = { covers: { + kind: "file", path: getCoverCacheDir, size: () => dirSize(getCoverCacheDir()), clear: () => clearDir(getCoverCacheDir()), }, artists: { + kind: "file", path: getArtistCacheDir, size: () => dirSize(getArtistCacheDir()), clear: () => clearDir(getArtistCacheDir()), }, backgrounds: { + kind: "file", path: getBackgroundsDir, size: () => dirSize(getBackgroundsDir()), clear: () => clearDir(getBackgroundsDir()), }, lyric: { + kind: "db", path: () => "lyric_cache", size: () => tableSize("lyric_cache", ["data"]), clear: clearLyricCache, }, lyricTTML: { + kind: "db", path: () => "lyric_ttml_cache", size: () => tableSize("lyric_ttml_cache", ["content"]), clear: clearLyricTtmlCache, }, lyricMatch: { + kind: "db", path: () => "lyric_match_cache", size: () => tableSize("lyric_match_cache", ["fingerprint", "platform_id", "extra"]), clear: clearLyricMatchCache, }, }; -/** 文件类目(受自定义缓存目录影响,切换时需要清空) */ -const fileCategories: CacheCategory[] = ["covers", "artists", "backgrounds"]; +const idsByKind = (kind: CacheKind): CacheCategory[] => + (Object.keys(categoryHandlers) as CacheCategory[]).filter( + (id) => categoryHandlers[id].kind === kind, + ); /** 注册缓存相关 IPC */ export const registerCacheIpc = (): void => { ipcMain.handle("cache:getStats", (): CacheStat[] => { return (Object.keys(categoryHandlers) as CacheCategory[]).map((id) => ({ id, + kind: categoryHandlers[id].kind, path: categoryHandlers[id].path(), size: categoryHandlers[id].size(), })); @@ -142,11 +160,11 @@ export const registerCacheIpc = (): void => { } }); - ipcMain.handle("cache:clearAll", async (): Promise => { - await Promise.all( - (Object.keys(categoryHandlers) as CacheCategory[]).map((id) => categoryHandlers[id].clear()), - ); - systemLog.info(`[cache] cleared all`); + /** 一键清空:按介质分桶,避免误删另一类 */ + ipcMain.handle("cache:clearAllByKind", async (_event, kind: CacheKind): Promise => { + const ids = idsByKind(kind); + await Promise.all(ids.map((id) => categoryHandlers[id].clear())); + systemLog.info(`[cache] cleared all (${kind})`); }); ipcMain.handle("cache:getDir", (): string => getAppCacheDir()); @@ -154,8 +172,7 @@ export const registerCacheIpc = (): void => { /** * 切换缓存目录 * - 用户必须选择空目录才允许切换 - * - 切换前清空旧目录下的文件类缓存(封面 / 头像 / 背景图) - * - sqlite 中的歌词等缓存路径独立,不参与本次清空 + * - 切换前清空旧目录下的文件类缓存 */ ipcMain.handle( "cache:pickDir", @@ -172,7 +189,7 @@ export const registerCacheIpc = (): void => { if (!(await isDirEmpty(next))) { return { ok: false, dir: current, reason: "notEmpty" }; } - await Promise.all(fileCategories.map((id) => categoryHandlers[id].clear())); + await Promise.all(idsByKind("file").map((id) => categoryHandlers[id].clear())); store.set("cache.dir", next); syncCoverCacheDir(); systemLog.info(`[cache] dir switched to ${next}`); @@ -182,7 +199,7 @@ export const registerCacheIpc = (): void => { /** 还原默认缓存目录(同样清空旧的文件类缓存) */ ipcMain.handle("cache:resetDir", async (): Promise => { - await Promise.all(fileCategories.map((id) => categoryHandlers[id].clear())); + await Promise.all(idsByKind("file").map((id) => categoryHandlers[id].clear())); store.set("cache.dir", null); syncCoverCacheDir(); return defaultAppCacheDir; diff --git a/electron/main/ipc/config.ts b/electron/main/ipc/config.ts index e784cdae..78484655 100644 --- a/electron/main/ipc/config.ts +++ b/electron/main/ipc/config.ts @@ -1,6 +1,8 @@ -import { ipcMain } from "electron"; +import fs from "node:fs/promises"; +import { dialog, ipcMain } from "electron"; import { store } from "@main/store"; import type { ConfigPath } from "@main/store/types"; +import { systemLog } from "@main/utils/logger"; import { enable as enableMedia, disable as disableMedia, @@ -94,4 +96,63 @@ export const registerConfigIpc = (): void => { }); ipcMain.handle("config:getAll", () => store.store); ipcMain.handle("config:reset", () => store.clear()); + + /** 替换整盘配置 */ + ipcMain.handle("config:replaceAll", (_event, payload: unknown) => { + store.replaceAll(payload); + }); + + /** 备份 */ + ipcMain.handle( + "config:exportToFile", + async ( + _event, + payload: unknown, + ): Promise<{ ok: boolean; reason?: "canceled" | "writeFailed" }> => { + const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + const result = await dialog.showSaveDialog({ + title: "导出设置备份", + defaultPath: `splayer-settings-${stamp}.json`, + filters: [{ name: "JSON", extensions: ["json"] }], + }); + if (result.canceled || !result.filePath) return { ok: false, reason: "canceled" }; + try { + await fs.writeFile(result.filePath, JSON.stringify(payload, null, 2), "utf-8"); + systemLog.info(`[config] settings exported to ${result.filePath}`); + return { ok: true }; + } catch (err) { + systemLog.error("[config] exportToFile failed", err); + return { ok: false, reason: "writeFailed" }; + } + }, + ); + + /** 恢复 */ + ipcMain.handle( + "config:importFromFile", + async (): Promise< + { ok: true; data: unknown } | { ok: false; reason: "canceled" | "readFailed" | "parseFailed" } + > => { + const result = await dialog.showOpenDialog({ + title: "选择设置备份文件", + filters: [{ name: "JSON", extensions: ["json"] }], + properties: ["openFile"], + }); + if (result.canceled || result.filePaths.length === 0) { + return { ok: false, reason: "canceled" }; + } + try { + const text = await fs.readFile(result.filePaths[0], "utf-8"); + try { + return { ok: true, data: JSON.parse(text) }; + } catch (err) { + systemLog.error("[config] importFromFile parse failed", err); + return { ok: false, reason: "parseFailed" }; + } + } catch (err) { + systemLog.error("[config] importFromFile read failed", err); + return { ok: false, reason: "readFailed" }; + } + }, + ); }; diff --git a/electron/main/ipc/system.ts b/electron/main/ipc/system.ts index 550a7957..9c82776f 100644 --- a/electron/main/ipc/system.ts +++ b/electron/main/ipc/system.ts @@ -1,4 +1,4 @@ -import { ipcMain, shell } from "electron"; +import { app, ipcMain, shell } from "electron"; import { getFonts } from "font-list"; import type { LocaleCode } from "@shared/types/settings"; import { setLocale } from "@main/utils/i18n"; @@ -58,6 +58,12 @@ export const registerSystemIpc = (): void => { return fontsCache; }); + // 重启应用 + ipcMain.handle("system:relaunch", () => { + app.relaunch(); + app.exit(0); + }); + // 把任意 http(s) URL 拉成字节回渲染层 // 用于 canvas 取色等需要绕过跨域 tainted 的场景;不限流媒体 ipcMain.handle("system:fetchRemoteBytes", async (_event, url: string) => { diff --git a/electron/main/store/index.ts b/electron/main/store/index.ts index 68ed733d..476b9776 100644 --- a/electron/main/store/index.ts +++ b/electron/main/store/index.ts @@ -123,4 +123,11 @@ export const store = { data = structuredClone(defaultSystemConfig); flush(data); }, + + /** 用导入的配置替换当前配置 */ + replaceAll(input: unknown): void { + const raw = (input && typeof input === "object" ? input : {}) as Record; + data = deepMerge(defaultSystemConfig, raw); + flushImmediate(data); + }, }; diff --git a/electron/preload/index.d.ts b/electron/preload/index.d.ts index 4fd2e162..d2f0f673 100644 --- a/electron/preload/index.d.ts +++ b/electron/preload/index.d.ts @@ -33,6 +33,7 @@ declare global { ) => () => void; listFonts: () => Promise; fetchRemoteBytes: (url: string) => Promise>; + relaunch: () => Promise; }; library: LibraryApi; window: WindowApi; @@ -48,9 +49,11 @@ declare global { clearBackgroundImages: () => Promise; }; cache: { - getStats: () => Promise<{ id: string; path: string; size: number }[]>; + getStats: () => Promise< + { id: string; kind: "file" | "db"; path: string; size: number }[] + >; clear: (id: string) => Promise; - clearAll: () => Promise; + clearAllByKind: (kind: "file" | "db") => Promise; getDir: () => Promise; pickDir: () => Promise<{ ok: boolean; dir: string; reason?: "canceled" | "notEmpty" }>; resetDir: () => Promise; diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 600bdfa6..c52c0988 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -20,6 +20,14 @@ const api = { set: (keyPath: string, value: unknown) => ipcRenderer.invoke("config:set", keyPath, value), getAll: () => ipcRenderer.invoke("config:getAll"), reset: () => ipcRenderer.invoke("config:reset"), + replaceAll: (config: unknown) => ipcRenderer.invoke("config:replaceAll", config), + exportToFile: ( + payload: unknown, + ): Promise<{ ok: boolean; reason?: "canceled" | "writeFailed" }> => + ipcRenderer.invoke("config:exportToFile", payload), + importFromFile: (): Promise< + { ok: true; data: unknown } | { ok: false; reason: "canceled" | "readFailed" | "parseFailed" } + > => ipcRenderer.invoke("config:importFromFile"), }, player: { // 加载音频(本地路径或网络地址) @@ -106,6 +114,8 @@ const api = { listFonts: () => ipcRenderer.invoke("system:listFonts"), // 拉远端字节回渲染层 fetchRemoteBytes: (url: string) => ipcRenderer.invoke("system:fetchRemoteBytes", url), + // 重启应用 + relaunch: () => ipcRenderer.invoke("system:relaunch"), }, library: { // 开始扫描(默认增量) @@ -312,8 +322,8 @@ const api = { getStats: () => ipcRenderer.invoke("cache:getStats"), // 清除单个类别 clear: (id: string) => ipcRenderer.invoke("cache:clear", id), - // 清空全部 - clearAll: () => ipcRenderer.invoke("cache:clearAll"), + // 按介质清空 + clearAllByKind: (kind: "file" | "db") => ipcRenderer.invoke("cache:clearAllByKind", kind), // 获取当前缓存目录 getDir: () => ipcRenderer.invoke("cache:getDir"), // 选择新的缓存目录 diff --git a/shared/types/settings.ts b/shared/types/settings.ts index 42199149..65e6fbaf 100644 --- a/shared/types/settings.ts +++ b/shared/types/settings.ts @@ -292,4 +292,12 @@ export interface ConfigApi { getAll: () => Promise; /** 重置为默认值 */ reset: () => Promise; + /** 整盘替换主进程配置 */ + replaceAll: (config: unknown) => Promise; + /** 写入用户选择的备份文件 */ + exportToFile: (payload: unknown) => Promise<{ ok: boolean; reason?: "canceled" | "writeFailed" }>; + /** 读取用户选择的备份文件 */ + importFromFile: () => Promise< + { ok: true; data: unknown } | { ok: false; reason: "canceled" | "readFailed" | "parseFailed" } + >; } diff --git a/src/components/settings/custom/DbCacheManager.vue b/src/components/settings/custom/DbCacheManager.vue new file mode 100644 index 00000000..f049af00 --- /dev/null +++ b/src/components/settings/custom/DbCacheManager.vue @@ -0,0 +1,147 @@ + + + diff --git a/src/components/settings/custom/CacheManager.vue b/src/components/settings/custom/FileCacheManager.vue similarity index 80% rename from src/components/settings/custom/CacheManager.vue rename to src/components/settings/custom/FileCacheManager.vue index f3b1e363..049ec2f6 100644 --- a/src/components/settings/custom/CacheManager.vue +++ b/src/components/settings/custom/FileCacheManager.vue @@ -10,55 +10,27 @@ import IconLucideRefreshCw from "~icons/lucide/refresh-cw"; import IconLucideImage from "~icons/lucide/image"; import IconLucideUserRound from "~icons/lucide/user-round"; import IconLucideImagePlus from "~icons/lucide/image-plus"; -import IconLucideMic2 from "~icons/lucide/mic-2"; -import IconLucideFileText from "~icons/lucide/file-text"; -import IconLucideSearch from "~icons/lucide/search"; import IconLucideDatabase from "~icons/lucide/database"; +import { useCacheStats } from "@/composables/useCacheStats"; defineOptions({ inheritAttrs: false }); const { t } = useI18n(); +const { stats, cacheDir, loading, clearingId, clearingKind, refresh, setCacheDir } = + useCacheStats(); -interface CacheStat { - id: string; - path: string; - size: number; -} - -/** 类别 → 图标 */ const iconMap: Record = { covers: IconLucideImage, artists: IconLucideUserRound, backgrounds: IconLucideImagePlus, - lyric: IconLucideMic2, - lyricTTML: IconLucideFileText, - lyricMatch: IconLucideSearch, }; -const stats = ref([]); -const cacheDir = ref(""); -const loading = ref(false); -const clearingId = ref(null); -const clearingAll = ref(false); - -/** 总占用 */ -const totalSize = computed(() => stats.value.reduce((sum, s) => sum + s.size, 0)); - -const refresh = async (): Promise => { - loading.value = true; - try { - const [list, dir] = await Promise.all([ - window.api.cache.getStats(), - window.api.cache.getDir(), - ]); - stats.value = list; - cacheDir.value = dir; - } finally { - loading.value = false; - } -}; +const fileStats = computed(() => stats.value.filter((stat) => stat.kind === "file")); +const totalSize = computed(() => fileStats.value.reduce((sum, stat) => sum + stat.size, 0)); -onMounted(refresh); +onMounted(() => { + if (stats.value.length === 0) void refresh(); +}); const handlePickDir = async (): Promise => { const confirmed = await dialog.confirm({ @@ -73,7 +45,7 @@ const handlePickDir = async (): Promise => { if (result.reason === "notEmpty") toast.error(t("settings.cacheDir.notEmpty")); return; } - cacheDir.value = result.dir; + setCacheDir(result.dir); await refresh(); toast.success(t("settings.cacheDir.changedHint")); }; @@ -85,7 +57,7 @@ const handleResetDir = async (): Promise => { type: "warning", }); if (!confirmed) return; - cacheDir.value = await window.api.cache.resetDir(); + setCacheDir(await window.api.cache.resetDir()); await refresh(); }; @@ -113,17 +85,17 @@ const requestClear = async (id: string): Promise => { const requestClearAll = async (): Promise => { const confirmed = await dialog.confirm({ - title: t("settings.cacheClearAll.confirmTitle"), - content: t("settings.cacheClearAll.confirmDesc"), + title: t("settings.fileClearAll.confirmTitle"), + content: t("settings.fileClearAll.confirmDesc"), type: "error", }); if (!confirmed) return; - clearingAll.value = true; + clearingKind.value = "file"; try { - await window.api.cache.clearAll(); + await window.api.cache.clearAllByKind("file"); await refresh(); } finally { - clearingAll.value = false; + clearingKind.value = null; } }; @@ -190,7 +162,7 @@ const requestClearAll = async (): Promise => {
-