refactor: 重构音乐和歌词缓存逻辑 可配置缓存目录

This commit is contained in:
alger
2026-03-06 19:56:01 +08:00
parent b02ca859de
commit a62e6d256e
38 changed files with 1808 additions and 94 deletions
+2 -3
View File
@@ -29,6 +29,7 @@ export default {
retry: 'Retry', retry: 'Retry',
reset: 'Reset', reset: 'Reset',
loadFailed: 'Load Failed', loadFailed: 'Load Failed',
noData: 'No data',
back: 'Back', back: 'Back',
copySuccess: 'Copied to clipboard', copySuccess: 'Copied to clipboard',
copyFailed: 'Copy failed', copyFailed: 'Copy failed',
@@ -36,9 +37,7 @@ export default {
required: 'This field is required', required: 'This field is required',
invalidInput: 'Invalid input', invalidInput: 'Invalid input',
selectRequired: 'Please select an option', selectRequired: 'Please select an option',
numberRange: 'Please enter a number between {min} and {max}', numberRange: 'Please enter a number between {min} and {max}'
ipAddress: 'Please enter a valid IP address',
portNumber: 'Please enter a valid port number (1-65535)'
}, },
viewMore: 'View More', viewMore: 'View More',
noMore: 'No more', noMore: 'No more',
+3 -4
View File
@@ -16,7 +16,6 @@ export default {
progress: { progress: {
total: 'Total Progress: {progress}%' total: 'Total Progress: {progress}%'
}, },
items: 'items',
status: { status: {
downloading: 'Downloading', downloading: 'Downloading',
completed: 'Completed', completed: 'Completed',
@@ -42,12 +41,12 @@ export default {
'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.', 'Are you sure you want to clear all download records? This will not delete the actual music files, but will clear all records.',
confirm: 'Clear', confirm: 'Clear',
cancel: 'Cancel', cancel: 'Cancel',
success: 'Download records cleared' success: 'Download records cleared',
failed: 'Failed to clear download records'
}, },
message: { message: {
downloadComplete: '{filename} download completed', downloadComplete: '{filename} download completed',
downloadFailed: '{filename} download failed: {error}', downloadFailed: '{filename} download failed: {error}'
alreadyDownloading: '{filename} is already downloading'
}, },
loading: 'Loading...', loading: 'Loading...',
playStarted: 'Play started: {name}', playStarted: 'Play started: {name}',
-4
View File
@@ -2,12 +2,8 @@ export default {
title: 'Favorites', title: 'Favorites',
count: 'Total {count}', count: 'Total {count}',
batchDownload: 'Batch Download', batchDownload: 'Batch Download',
selectAll: 'All',
download: 'Download ({count})', download: 'Download ({count})',
cancel: 'Cancel',
emptyTip: 'No favorite songs yet', emptyTip: 'No favorite songs yet',
viewMore: 'View More',
noMore: 'No more',
downloadSuccess: 'Download completed', downloadSuccess: 'Download completed',
downloadFailed: 'Download failed', downloadFailed: 'Download failed',
downloading: 'Downloading, please wait...', downloading: 'Downloading, please wait...',
-1
View File
@@ -23,7 +23,6 @@ export default {
merging: 'Merging records...', merging: 'Merging records...',
noDescription: 'No description', noDescription: 'No description',
noData: 'No records', noData: 'No records',
newKey: 'New translation',
heatmap: { heatmap: {
title: 'Play Heatmap', title: 'Play Heatmap',
loading: 'Loading data...', loading: 'Loading data...',
+1 -4
View File
@@ -131,10 +131,7 @@ export default {
timerEnded: 'Sleep timer ended', timerEnded: 'Sleep timer ended',
playbackStopped: 'Music playback stopped', playbackStopped: 'Music playback stopped',
minutesRemaining: '{minutes} min remaining', minutesRemaining: '{minutes} min remaining',
songsRemaining: '{count} songs remaining', songsRemaining: '{count} songs remaining'
activeTime: 'Timer Active',
activeSongs: 'Counting Songs',
activeEnd: 'End After List'
}, },
playList: { playList: {
clearAll: 'Clear Playlist', clearAll: 'Clear Playlist',
+4
View File
@@ -12,6 +12,10 @@ export default {
subscribe: 'Subscribe', subscribe: 'Subscribe',
subscribed: 'Subscribed', subscribed: 'Subscribed',
unsubscribe: 'Unsubscribe', unsubscribe: 'Unsubscribe',
unsubscribed: 'Unsubscribed',
subscribeSuccess: 'Subscribed successfully',
unsubscribeFailed: 'Failed to unsubscribe',
subscribeFailed: 'Failed to subscribe',
radioDetail: 'Radio Detail', radioDetail: 'Radio Detail',
programList: 'Episodes', programList: 'Episodes',
playProgram: 'Play', playProgram: 'Play',
+38 -1
View File
@@ -196,6 +196,36 @@ export default {
system: { system: {
cache: 'Cache Management', cache: 'Cache Management',
cacheDesc: 'Clear cache', cacheDesc: 'Clear cache',
diskCache: 'Disk Cache',
diskCacheDesc: 'Cache played music and lyrics on local disk to speed up repeated playback',
cacheDirectory: 'Cache Directory',
cacheDirectoryDesc: 'Custom directory for music and lyric cache files',
selectDirectory: 'Select Directory',
openDirectory: 'Open Directory',
cacheMaxSize: 'Cache Size Limit',
cacheMaxSizeDesc: 'Older cache items are cleaned automatically when limit is reached',
cleanupPolicy: 'Cleanup Policy',
cleanupPolicyDesc: 'Auto cleanup rule when cache reaches the size limit',
cleanupPolicyOptions: {
lru: 'Least Recently Used',
fifo: 'First In, First Out'
},
cacheStatus: 'Cache Status',
cacheStatusDesc: 'Used {used} / Limit {limit}',
cacheStatusDetail: 'Music {musicCount}, Lyrics {lyricCount}',
manageDiskCache: 'Manual Disk Cache Cleanup',
manageDiskCacheDesc: 'Clean cache by category',
clearMusicCache: 'Clear Music Cache',
clearLyricCache: 'Clear Lyric Cache',
clearAllCache: 'Clear All Cache',
switchDirectoryMigrateTitle: 'Existing Cache Detected',
switchDirectoryMigrateContent: 'Do you want to migrate old cache files to the new directory?',
switchDirectoryMigrateConfirm: 'Migrate',
switchDirectoryDestroyTitle: 'Destroy Old Cache',
switchDirectoryDestroyContent:
'If you do not migrate, do you want to destroy old cache files in the previous directory?',
switchDirectoryDestroyConfirm: 'Destroy',
switchDirectoryKeepOld: 'Keep Old Cache',
cacheClearTitle: 'Select cache types to clear:', cacheClearTitle: 'Select cache types to clear:',
cacheTypes: { cacheTypes: {
history: { history: {
@@ -230,7 +260,14 @@ export default {
restart: 'Restart', restart: 'Restart',
restartDesc: 'Restart application', restartDesc: 'Restart application',
messages: { messages: {
clearSuccess: 'Cache cleared successfully, some settings will take effect after restart' clearSuccess: 'Cache cleared successfully, some settings will take effect after restart',
diskCacheClearSuccess: 'Disk cache cleaned',
diskCacheClearFailed: 'Failed to clean disk cache',
diskCacheStatsLoadFailed: 'Failed to load cache status',
switchDirectorySuccess: 'Cache directory switched, old cache is kept',
switchDirectoryFailed: 'Failed to switch cache directory',
switchDirectoryMigrated: 'Cache directory switched, migrated {count} cache files',
switchDirectoryDestroyed: 'Cache directory switched, destroyed {count} old cache files'
} }
}, },
about: { about: {
+1
View File
@@ -29,6 +29,7 @@ export default {
retry: '再試行', retry: '再試行',
reset: 'リセット', reset: 'リセット',
loadFailed: '読み込みに失敗しました', loadFailed: '読み込みに失敗しました',
noData: 'データがありません',
back: '戻る', back: '戻る',
copySuccess: 'クリップボードにコピーしました', copySuccess: 'クリップボードにコピーしました',
copyFailed: 'コピーに失敗しました', copyFailed: 'コピーに失敗しました',
+2 -1
View File
@@ -41,7 +41,8 @@ export default {
'すべてのダウンロード記録をクリアしますか?この操作はダウンロード済みの音楽ファイルを削除しませんが、すべての記録をクリアします。', 'すべてのダウンロード記録をクリアしますか?この操作はダウンロード済みの音楽ファイルを削除しませんが、すべての記録をクリアします。',
confirm: 'クリア確認', confirm: 'クリア確認',
cancel: 'キャンセル', cancel: 'キャンセル',
success: 'ダウンロード記録をクリアしました' success: 'ダウンロード記録をクリアしました',
failed: 'ダウンロード記録のクリアに失敗しました'
}, },
message: { message: {
downloadComplete: '{filename}のダウンロードが完了しました', downloadComplete: '{filename}のダウンロードが完了しました',
+4
View File
@@ -12,6 +12,10 @@ export default {
subscribe: '購読', subscribe: '購読',
subscribed: '購読中', subscribed: '購読中',
unsubscribe: '購読解除', unsubscribe: '購読解除',
unsubscribed: '購読を解除しました',
subscribeSuccess: '購読しました',
unsubscribeFailed: '購読解除に失敗しました',
subscribeFailed: '購読に失敗しました',
radioDetail: 'ラジオ詳細', radioDetail: 'ラジオ詳細',
programList: 'エピソード一覧', programList: 'エピソード一覧',
playProgram: '再生', playProgram: '再生',
+38 -1
View File
@@ -195,6 +195,35 @@ export default {
system: { system: {
cache: 'キャッシュ管理', cache: 'キャッシュ管理',
cacheDesc: 'キャッシュをクリア', cacheDesc: 'キャッシュをクリア',
diskCache: 'ディスクキャッシュ',
diskCacheDesc: '再生した音楽と歌詞をローカルディスクへ保存し、再生速度を向上します',
cacheDirectory: 'キャッシュディレクトリ',
cacheDirectoryDesc: '音楽・歌詞キャッシュの保存先を指定',
selectDirectory: 'ディレクトリ選択',
openDirectory: 'ディレクトリを開く',
cacheMaxSize: 'キャッシュ上限',
cacheMaxSizeDesc: '上限に達すると古いキャッシュを自動削除します',
cleanupPolicy: 'クリーンアップポリシー',
cleanupPolicyDesc: 'キャッシュ上限到達時の自動削除ルール',
cleanupPolicyOptions: {
lru: '最近未使用優先',
fifo: '先入れ先出し'
},
cacheStatus: 'キャッシュ状態',
cacheStatusDesc: '使用量 {used} / 上限 {limit}',
cacheStatusDetail: '音楽 {musicCount} 曲、歌詞 {lyricCount} 曲',
manageDiskCache: '手動キャッシュクリア',
manageDiskCacheDesc: '種類ごとにキャッシュを削除',
clearMusicCache: '音楽キャッシュを削除',
clearLyricCache: '歌詞キャッシュを削除',
clearAllCache: 'すべて削除',
switchDirectoryMigrateTitle: '既存キャッシュを検出',
switchDirectoryMigrateContent: '旧ディレクトリのキャッシュを新ディレクトリへ移行しますか?',
switchDirectoryMigrateConfirm: '移行する',
switchDirectoryDestroyTitle: '旧キャッシュを削除',
switchDirectoryDestroyContent: '移行しない場合、旧ディレクトリのキャッシュを削除しますか?',
switchDirectoryDestroyConfirm: '削除する',
switchDirectoryKeepOld: '旧キャッシュを保持',
cacheClearTitle: 'クリアするキャッシュタイプを選択してください:', cacheClearTitle: 'クリアするキャッシュタイプを選択してください:',
cacheTypes: { cacheTypes: {
history: { history: {
@@ -229,7 +258,15 @@ export default {
restart: '再起動', restart: '再起動',
restartDesc: 'アプリを再起動', restartDesc: 'アプリを再起動',
messages: { messages: {
clearSuccess: 'クリア成功。一部の設定は再起動後に有効になります' clearSuccess: 'クリア成功。一部の設定は再起動後に有効になります',
diskCacheClearSuccess: 'ディスクキャッシュを削除しました',
diskCacheClearFailed: 'ディスクキャッシュの削除に失敗しました',
diskCacheStatsLoadFailed: 'キャッシュ状態の取得に失敗しました',
switchDirectorySuccess: 'キャッシュディレクトリを切り替えました(旧キャッシュは保持)',
switchDirectoryFailed: 'キャッシュディレクトリの切り替えに失敗しました',
switchDirectoryMigrated: 'キャッシュディレクトリを切り替え、{count} 件を移行しました',
switchDirectoryDestroyed:
'キャッシュディレクトリを切り替え、旧キャッシュ {count} 件を削除しました'
} }
}, },
about: { about: {
+1
View File
@@ -29,6 +29,7 @@ export default {
retry: '다시 시도', retry: '다시 시도',
reset: '재설정', reset: '재설정',
loadFailed: '로드 실패', loadFailed: '로드 실패',
noData: '데이터 없음',
back: '뒤로', back: '뒤로',
copySuccess: '클립보드에 복사됨', copySuccess: '클립보드에 복사됨',
copyFailed: '복사 실패', copyFailed: '복사 실패',
+2 -1
View File
@@ -41,7 +41,8 @@ export default {
'모든 다운로드 기록을 지우시겠습니까? 이 작업은 다운로드된 음악 파일을 삭제하지 않지만 모든 기록을 지웁니다.', '모든 다운로드 기록을 지우시겠습니까? 이 작업은 다운로드된 음악 파일을 삭제하지 않지만 모든 기록을 지웁니다.',
confirm: '지우기 확인', confirm: '지우기 확인',
cancel: '취소', cancel: '취소',
success: '다운로드 기록이 지워졌습니다' success: '다운로드 기록이 지워졌습니다',
failed: '다운로드 기록 삭제에 실패했습니다'
}, },
message: { message: {
downloadComplete: '{filename} 다운로드 완료', downloadComplete: '{filename} 다운로드 완료',
+4
View File
@@ -12,6 +12,10 @@ export default {
subscribe: '구독', subscribe: '구독',
subscribed: '구독 중', subscribed: '구독 중',
unsubscribe: '구독 취소', unsubscribe: '구독 취소',
unsubscribed: '구독이 취소되었습니다',
subscribeSuccess: '구독되었습니다',
unsubscribeFailed: '구독 취소에 실패했습니다',
subscribeFailed: '구독에 실패했습니다',
radioDetail: '라디오 상세', radioDetail: '라디오 상세',
programList: '에피소드 목록', programList: '에피소드 목록',
playProgram: '재생', playProgram: '재생',
+38 -1
View File
@@ -196,6 +196,36 @@ export default {
system: { system: {
cache: '캐시 관리', cache: '캐시 관리',
cacheDesc: '캐시 지우기', cacheDesc: '캐시 지우기',
diskCache: '디스크 캐시',
diskCacheDesc: '재생한 음악과 가사를 로컬 디스크에 캐시하여 재생 속도를 높입니다',
cacheDirectory: '캐시 디렉터리',
cacheDirectoryDesc: '음악 및 가사 캐시 저장 경로를 사용자 지정',
selectDirectory: '디렉터리 선택',
openDirectory: '디렉터리 열기',
cacheMaxSize: '캐시 용량 제한',
cacheMaxSizeDesc: '용량 제한 도달 시 오래된 캐시를 자동 정리합니다',
cleanupPolicy: '정리 정책',
cleanupPolicyDesc: '캐시 용량 제한 도달 시 적용할 자동 정리 규칙',
cleanupPolicyOptions: {
lru: '최근 사용 안 함 우선',
fifo: '선입선출'
},
cacheStatus: '캐시 상태',
cacheStatusDesc: '사용량 {used} / 제한 {limit}',
cacheStatusDetail: '음악 {musicCount}곡, 가사 {lyricCount}곡',
manageDiskCache: '수동 디스크 캐시 정리',
manageDiskCacheDesc: '캐시 유형별로 정리',
clearMusicCache: '음악 캐시 정리',
clearLyricCache: '가사 캐시 정리',
clearAllCache: '전체 캐시 정리',
switchDirectoryMigrateTitle: '기존 캐시가 감지되었습니다',
switchDirectoryMigrateContent: '기존 캐시를 새 디렉터리로 마이그레이션할까요?',
switchDirectoryMigrateConfirm: '마이그레이션',
switchDirectoryDestroyTitle: '기존 캐시 삭제',
switchDirectoryDestroyContent:
'마이그레이션하지 않을 경우, 이전 디렉터리의 캐시 파일을 삭제할까요?',
switchDirectoryDestroyConfirm: '삭제',
switchDirectoryKeepOld: '기존 캐시 유지',
cacheClearTitle: '지울 캐시 유형을 선택하세요:', cacheClearTitle: '지울 캐시 유형을 선택하세요:',
cacheTypes: { cacheTypes: {
history: { history: {
@@ -230,7 +260,14 @@ export default {
restart: '재시작', restart: '재시작',
restartDesc: '앱 재시작', restartDesc: '앱 재시작',
messages: { messages: {
clearSuccess: '지우기 성공, 일부 설정은 재시작 후 적용됩니다' clearSuccess: '지우기 성공, 일부 설정은 재시작 후 적용됩니다',
diskCacheClearSuccess: '디스크 캐시를 정리했습니다',
diskCacheClearFailed: '디스크 캐시 정리에 실패했습니다',
diskCacheStatsLoadFailed: '캐시 상태를 불러오지 못했습니다',
switchDirectorySuccess: '캐시 디렉터리가 변경되었습니다. 기존 캐시는 유지됩니다',
switchDirectoryFailed: '캐시 디렉터리 변경에 실패했습니다',
switchDirectoryMigrated: '캐시 디렉터리를 변경하고 {count}개 파일을 마이그레이션했습니다',
switchDirectoryDestroyed: '캐시 디렉터리를 변경하고 기존 캐시 {count}개 파일을 삭제했습니다'
} }
}, },
about: { about: {
+1
View File
@@ -29,6 +29,7 @@ export default {
retry: '重试', retry: '重试',
reset: '重置', reset: '重置',
loadFailed: '加载失败', loadFailed: '加载失败',
noData: '暂无数据',
back: '返回', back: '返回',
copySuccess: '已复制到剪贴板', copySuccess: '已复制到剪贴板',
copyFailed: '复制失败', copyFailed: '复制失败',
+2 -1
View File
@@ -40,7 +40,8 @@ export default {
message: '确定要清空所有下载记录吗?此操作不会删除已下载的音乐文件,但将清空所有记录。', message: '确定要清空所有下载记录吗?此操作不会删除已下载的音乐文件,但将清空所有记录。',
confirm: '确定清空', confirm: '确定清空',
cancel: '取消', cancel: '取消',
success: '下载记录已清空' success: '下载记录已清空',
failed: '清空下载记录失败'
}, },
message: { message: {
downloadComplete: '{filename} 下载完成', downloadComplete: '{filename} 下载完成',
+4
View File
@@ -12,6 +12,10 @@ export default {
subscribe: '订阅', subscribe: '订阅',
subscribed: '已订阅', subscribed: '已订阅',
unsubscribe: '取消订阅', unsubscribe: '取消订阅',
unsubscribed: '已取消订阅',
subscribeSuccess: '订阅成功',
unsubscribeFailed: '取消订阅失败',
subscribeFailed: '订阅失败',
radioDetail: '电台详情', radioDetail: '电台详情',
programList: '节目列表', programList: '节目列表',
playProgram: '播放节目', playProgram: '播放节目',
+37 -1
View File
@@ -193,6 +193,35 @@ export default {
system: { system: {
cache: '缓存管理', cache: '缓存管理',
cacheDesc: '清除缓存', cacheDesc: '清除缓存',
diskCache: '磁盘缓存',
diskCacheDesc: '将播放过的音乐与歌词缓存到本地磁盘,提升二次播放速度',
cacheDirectory: '缓存目录',
cacheDirectoryDesc: '自定义音乐与歌词缓存保存目录',
selectDirectory: '选择目录',
openDirectory: '打开目录',
cacheMaxSize: '缓存上限',
cacheMaxSizeDesc: '达到上限后将自动清理最旧缓存',
cleanupPolicy: '清理策略',
cleanupPolicyDesc: '达到缓存上限时的自动清理规则',
cleanupPolicyOptions: {
lru: '最近最少使用',
fifo: '先进先出'
},
cacheStatus: '缓存状态',
cacheStatusDesc: '已用 {used} / 上限 {limit}',
cacheStatusDetail: '音乐 {musicCount} 首,歌词 {lyricCount} 首',
manageDiskCache: '手动清理磁盘缓存',
manageDiskCacheDesc: '按缓存类型进行清理',
clearMusicCache: '清理音乐缓存',
clearLyricCache: '清理歌词缓存',
clearAllCache: '清理全部缓存',
switchDirectoryMigrateTitle: '检测到已有缓存',
switchDirectoryMigrateContent: '是否将旧目录缓存迁移到新目录?',
switchDirectoryMigrateConfirm: '迁移',
switchDirectoryDestroyTitle: '是否销毁旧缓存',
switchDirectoryDestroyContent: '不迁移时,是否销毁旧目录缓存文件?',
switchDirectoryDestroyConfirm: '销毁',
switchDirectoryKeepOld: '保留旧缓存',
cacheClearTitle: '请选择要清除的缓存类型:', cacheClearTitle: '请选择要清除的缓存类型:',
cacheTypes: { cacheTypes: {
history: { history: {
@@ -227,7 +256,14 @@ export default {
restart: '重启', restart: '重启',
restartDesc: '重启应用', restartDesc: '重启应用',
messages: { messages: {
clearSuccess: '清除成功,部分设置在重启后生效' clearSuccess: '清除成功,部分设置在重启后生效',
diskCacheClearSuccess: '磁盘缓存已清理',
diskCacheClearFailed: '清理磁盘缓存失败',
diskCacheStatsLoadFailed: '读取缓存状态失败',
switchDirectorySuccess: '缓存目录已切换,旧缓存已保留',
switchDirectoryFailed: '缓存目录切换失败',
switchDirectoryMigrated: '缓存目录已切换,已迁移 {count} 个缓存文件',
switchDirectoryDestroyed: '缓存目录已切换,已销毁 {count} 个旧缓存文件'
} }
}, },
about: { about: {
+1
View File
@@ -29,6 +29,7 @@ export default {
retry: '重試', retry: '重試',
reset: '重設', reset: '重設',
loadFailed: '載入失敗', loadFailed: '載入失敗',
noData: '暫無資料',
back: '返回', back: '返回',
copySuccess: '已複製到剪貼簿', copySuccess: '已複製到剪貼簿',
copyFailed: '複製失敗', copyFailed: '複製失敗',
+2 -1
View File
@@ -40,7 +40,8 @@ export default {
message: '確定要清空所有下載記錄嗎?此操作不會刪除已下載的音樂檔案,但將清空所有記錄。', message: '確定要清空所有下載記錄嗎?此操作不會刪除已下載的音樂檔案,但將清空所有記錄。',
confirm: '確定清空', confirm: '確定清空',
cancel: '取消', cancel: '取消',
success: '下載記錄已清空' success: '下載記錄已清空',
failed: '清空下載記錄失敗'
}, },
message: { message: {
downloadComplete: '{filename} 下載完成', downloadComplete: '{filename} 下載完成',
+4
View File
@@ -12,6 +12,10 @@ export default {
subscribe: '訂閱', subscribe: '訂閱',
subscribed: '已訂閱', subscribed: '已訂閱',
unsubscribe: '取消訂閱', unsubscribe: '取消訂閱',
unsubscribed: '已取消訂閱',
subscribeSuccess: '訂閱成功',
unsubscribeFailed: '取消訂閱失敗',
subscribeFailed: '訂閱失敗',
radioDetail: '電台詳情', radioDetail: '電台詳情',
programList: '節目列表', programList: '節目列表',
playProgram: '播放節目', playProgram: '播放節目',
+37 -2
View File
@@ -25,7 +25,6 @@ export default {
tokenSet: '已設定', tokenSet: '已設定',
tokenNotSet: '未設定', tokenNotSet: '未設定',
setToken: '設定Cookie', setToken: '設定Cookie',
setCookie: '設定Cookie',
modifyToken: '修改Cookie', modifyToken: '修改Cookie',
clearToken: '清除Cookie', clearToken: '清除Cookie',
font: '字體設定', font: '字體設定',
@@ -190,6 +189,35 @@ export default {
system: { system: {
cache: '快取管理', cache: '快取管理',
cacheDesc: '清除快取', cacheDesc: '清除快取',
diskCache: '磁碟快取',
diskCacheDesc: '將播放過的音樂與歌詞快取到本機磁碟,加速二次播放',
cacheDirectory: '快取目錄',
cacheDirectoryDesc: '自訂音樂與歌詞快取儲存位置',
selectDirectory: '選擇目錄',
openDirectory: '開啟目錄',
cacheMaxSize: '快取上限',
cacheMaxSizeDesc: '達到上限時會自動清理較舊快取',
cleanupPolicy: '清理策略',
cleanupPolicyDesc: '快取達到上限時的自動清理規則',
cleanupPolicyOptions: {
lru: '最近最少使用',
fifo: '先進先出'
},
cacheStatus: '快取狀態',
cacheStatusDesc: '已用 {used} / 上限 {limit}',
cacheStatusDetail: '音樂 {musicCount} 首,歌詞 {lyricCount} 首',
manageDiskCache: '手動清理磁碟快取',
manageDiskCacheDesc: '依快取類型進行清理',
clearMusicCache: '清理音樂快取',
clearLyricCache: '清理歌詞快取',
clearAllCache: '清理全部快取',
switchDirectoryMigrateTitle: '偵測到既有快取',
switchDirectoryMigrateContent: '是否將舊目錄快取搬移到新目錄?',
switchDirectoryMigrateConfirm: '搬移',
switchDirectoryDestroyTitle: '是否刪除舊快取',
switchDirectoryDestroyContent: '不搬移時,是否刪除舊目錄的快取檔案?',
switchDirectoryDestroyConfirm: '刪除',
switchDirectoryKeepOld: '保留舊快取',
cacheClearTitle: '請選擇要清除的快取類型:', cacheClearTitle: '請選擇要清除的快取類型:',
cacheTypes: { cacheTypes: {
history: { history: {
@@ -224,7 +252,14 @@ export default {
restart: '重新啟動', restart: '重新啟動',
restartDesc: '重新啟動應用程式', restartDesc: '重新啟動應用程式',
messages: { messages: {
clearSuccess: '清除成功,部分設定在重啟後生效' clearSuccess: '清除成功,部分設定在重啟後生效',
diskCacheClearSuccess: '磁碟快取已清理',
diskCacheClearFailed: '清理磁碟快取失敗',
diskCacheStatsLoadFailed: '讀取快取狀態失敗',
switchDirectorySuccess: '快取目錄已切換,舊快取已保留',
switchDirectoryFailed: '快取目錄切換失敗',
switchDirectoryMigrated: '快取目錄已切換,已搬移 {count} 個快取檔案',
switchDirectoryDestroyed: '快取目錄已切換,已刪除 {count} 個舊快取檔案'
} }
}, },
about: { about: {
+1044 -42
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -1,5 +1,6 @@
import { app, ipcMain } from 'electron'; import { app, ipcMain } from 'electron';
import Store from 'electron-store'; import Store from 'electron-store';
import * as path from 'path';
import { createDefaultShortcuts, type ShortcutsConfig } from '../../shared/shortcuts'; import { createDefaultShortcuts, type ShortcutsConfig } from '../../shared/shortcuts';
import set from '../set.json'; import set from '../set.json';
@@ -26,6 +27,11 @@ type SetConfig = {
language: string; language: string;
showTopAction: boolean; showTopAction: boolean;
enableGpuAcceleration: boolean; enableGpuAcceleration: boolean;
downloadPath: string;
enableDiskCache: boolean;
diskCacheDir: string;
diskCacheMaxSizeMB: number;
diskCacheCleanupPolicy: 'lru' | 'fifo';
}; };
interface StoreType { interface StoreType {
set: SetConfig; set: SetConfig;
@@ -47,6 +53,17 @@ export function initializeConfig() {
}); });
store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads')); store.get('set.downloadPath') || store.set('set.downloadPath', app.getPath('downloads'));
store.get('set.diskCacheDir') ||
store.set('set.diskCacheDir', path.join(app.getPath('userData'), 'cache'));
if (store.get('set.diskCacheMaxSizeMB') === undefined) {
store.set('set.diskCacheMaxSizeMB', 4096);
}
if (!store.get('set.diskCacheCleanupPolicy')) {
store.set('set.diskCacheCleanupPolicy', 'lru');
}
if (store.get('set.enableDiskCache') === undefined) {
store.set('set.enableDiskCache', true);
}
// 定义ipcRenderer监听事件 // 定义ipcRenderer监听事件
ipcMain.on('set-store-value', (_, key, value) => { ipcMain.on('set-store-value', (_, key, value) => {
+5 -1
View File
@@ -34,5 +34,9 @@
"customApiPluginName": "", "customApiPluginName": "",
"lxMusicScripts": [], "lxMusicScripts": [],
"activeLxMusicApiId": null, "activeLxMusicApiId": null,
"enableGpuAcceleration": true "enableGpuAcceleration": true,
"enableDiskCache": true,
"diskCacheDir": "",
"diskCacheMaxSizeMB": 4096,
"diskCacheCleanupPolicy": "lru"
} }
+1 -1
View File
@@ -55,7 +55,7 @@ const getDescription = () => {
} }
if (props.item.size !== undefined) { if (props.item.size !== undefined) {
parts.push(t('user.album.songCount', { count: props.item.size })); parts.push(t('common.songCount', { count: props.item.size }));
} }
return parts.join(' · ') || t('history.noDescription'); return parts.join(' · ') || t('history.noDescription');
@@ -33,7 +33,7 @@
<span <span
class="px-3 py-1 text-xs font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 rounded-full" class="px-3 py-1 text-xs font-medium text-white bg-gradient-to-r from-green-500 to-emerald-600 rounded-full"
> >
{{ t('comp.update.newVersion') }} {{ t('comp.update.title') }}
</span> </span>
</div> </div>
<h2 class="text-2xl font-bold text-gray-900 dark:text-white truncate"> <h2 class="text-2xl font-bold text-gray-900 dark:text-white truncate">
@@ -65,7 +65,7 @@
@click="handleLater" @click="handleLater"
class="flex-1 py-4 px-4 rounded-2xl text-base font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 active:scale-[0.98] transition-all duration-200" class="flex-1 py-4 px-4 rounded-2xl text-base font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 active:scale-[0.98] transition-all duration-200"
> >
{{ t('comp.update.later') }} {{ t('comp.update.noThanks') }}
</button> </button>
<button <button
@click="handleUpdate" @click="handleUpdate"
@@ -73,7 +73,7 @@
> >
<span class="flex items-center justify-center gap-2"> <span class="flex items-center justify-center gap-2">
<i class="ri-download-2-line text-lg"></i> <i class="ri-download-2-line text-lg"></i>
{{ t('comp.update.updateNow') }} {{ t('comp.update.nowUpdate') }}
</span> </span>
</button> </button>
</div> </div>
@@ -213,9 +213,9 @@ let timerInterval: number | null = null;
const hasTimerActive = computed(() => playerStore.hasSleepTimerActive); const hasTimerActive = computed(() => playerStore.hasSleepTimerActive);
const timerStatusText = computed(() => { const timerStatusText = computed(() => {
if (sleepTimer.value.type === 'time') return t('player.sleepTimer.activeTime'); if (sleepTimer.value.type === 'time') return t('player.sleepTimer.timeMode');
if (sleepTimer.value.type === 'songs') return t('player.sleepTimer.activeSongs'); if (sleepTimer.value.type === 'songs') return t('player.sleepTimer.songsMode');
if (sleepTimer.value.type === 'end') return t('player.sleepTimer.activeEnd'); if (sleepTimer.value.type === 'end') return t('player.sleepTimer.afterPlaylist');
return ''; return '';
}); });
@@ -396,7 +396,7 @@ const toggleSource = (sourceKey: string) => {
if (index > -1) { if (index > -1) {
// 至少保留一个音源 // 至少保留一个音源
if (selectedSources.value.length <= 1) { if (selectedSources.value.length <= 1) {
message.warning(t('settings.playback.musicSourcesMinWarning')); message.warning(t('settings.playback.musicSourcesWarning'));
return; return;
} }
selectedSources.value.splice(index, 1); selectedSources.value.splice(index, 1);
+79 -9
View File
@@ -6,12 +6,57 @@ import { getMusicLrc, getMusicUrl, getParsingMusicUrl } from '@/api/music';
import { playbackRequestManager } from '@/services/playbackRequestManager'; import { playbackRequestManager } from '@/services/playbackRequestManager';
import { SongSourceConfigManager } from '@/services/SongSourceConfigManager'; import { SongSourceConfigManager } from '@/services/SongSourceConfigManager';
import type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music'; import type { ILyric, ILyricText, IWordData, SongResult } from '@/types/music';
import { getImgUrl } from '@/utils'; import { getImgUrl, isElectron } from '@/utils';
import { getImageLinearBackground } from '@/utils/linearColor'; import { getImageLinearBackground } from '@/utils/linearColor';
import { parseLyrics as parseYrcLyrics } from '@/utils/yrcParser'; import { parseLyrics as parseYrcLyrics } from '@/utils/yrcParser';
const { message } = createDiscreteApi(['message']); const { message } = createDiscreteApi(['message']);
type DiskCacheResolveResult = {
url?: string;
cached?: boolean;
queued?: boolean;
};
const getSongArtistText = (songData: SongResult): string => {
if (songData?.ar?.length) {
return songData.ar.map((artist) => artist.name).join(' / ');
}
if (songData?.song?.artists?.length) {
return songData.song.artists.map((artist) => artist.name).join(' / ');
}
return '';
};
const resolveCachedPlaybackUrl = async (
url: string | null | undefined,
songData: SongResult
): Promise<string | null | undefined> => {
if (!url || !isElectron || !/^https?:\/\//i.test(url)) {
return url;
}
try {
const result = (await window.electron.ipcRenderer.invoke('resolve-cached-music-url', {
songId: Number(songData.id),
source: songData.source,
url,
title: songData.name,
artist: getSongArtistText(songData)
})) as DiskCacheResolveResult;
if (result?.url) {
return result.url;
}
} catch (error) {
console.warn('解析缓存播放地址失败,回退到在线地址:', error);
}
return url;
};
/** /**
* 获取歌曲播放URL(独立函数) * 获取歌曲播放URL(独立函数)
*/ */
@@ -35,7 +80,8 @@ export const getSongUrl = async (
} }
if (songData.playMusicUrl) { if (songData.playMusicUrl) {
return songData.playMusicUrl; if (isDownloaded) return songData.playMusicUrl;
return await resolveCachedPlaybackUrl(songData.playMusicUrl, songData);
} }
// ==================== 自定义API最优先 ==================== // ==================== 自定义API最优先 ====================
@@ -70,7 +116,7 @@ export const getSongUrl = async (
) { ) {
console.log('自定义API解析成功!'); console.log('自定义API解析成功!');
if (isDownloaded) return customResult.data.data as any; if (isDownloaded) return customResult.data.data as any;
return customResult.data.data.url; return await resolveCachedPlaybackUrl(customResult.data.data.url, songData);
} else { } else {
console.log('自定义API解析失败,将使用默认降级流程...'); console.log('自定义API解析失败,将使用默认降级流程...');
message.warning(i18n.global.t('player.reparse.customApiFailed')); message.warning(i18n.global.t('player.reparse.customApiFailed'));
@@ -98,7 +144,7 @@ export const getSongUrl = async (
} }
if (res && res.data && res.data.data && res.data.data.url) { if (res && res.data && res.data.data && res.data.data.url) {
return res.data.data.url; return await resolveCachedPlaybackUrl(res.data.data.url, songData);
} }
console.warn('自定义音源解析失败,使用默认音源'); console.warn('自定义音源解析失败,使用默认音源');
} catch (error) { } catch (error) {
@@ -133,12 +179,13 @@ export const getSongUrl = async (
throw new Error('Request cancelled'); throw new Error('Request cancelled');
} }
if (isDownloaded) return res?.data?.data as any; if (isDownloaded) return res?.data?.data as any;
return res?.data?.data?.url || null; const parsedUrl = res?.data?.data?.url || null;
return await resolveCachedPlaybackUrl(parsedUrl, songData);
} }
console.log('官方API解析成功!'); console.log('官方API解析成功!');
if (isDownloaded) return songDetail as any; if (isDownloaded) return songDetail as any;
return songDetail.url; return await resolveCachedPlaybackUrl(songDetail.url, songData);
} }
console.log('官方API返回数据结构异常,进入内置备用解析...'); console.log('官方API返回数据结构异常,进入内置备用解析...');
@@ -149,7 +196,8 @@ export const getSongUrl = async (
throw new Error('Request cancelled'); throw new Error('Request cancelled');
} }
if (isDownloaded) return res?.data?.data as any; if (isDownloaded) return res?.data?.data as any;
return res?.data?.data?.url || null; const parsedUrl = res?.data?.data?.url || null;
return await resolveCachedPlaybackUrl(parsedUrl, songData);
} catch (error) { } catch (error) {
if ((error as Error).message === 'Request cancelled') { if ((error as Error).message === 'Request cancelled') {
throw error; throw error;
@@ -157,7 +205,8 @@ export const getSongUrl = async (
console.error('官方API请求失败,进入内置备用解析流程:', error); console.error('官方API请求失败,进入内置备用解析流程:', error);
const res = await getParsingMusicUrl(numericId, cloneDeep(songData)); const res = await getParsingMusicUrl(numericId, cloneDeep(songData));
if (isDownloaded) return res?.data?.data as any; if (isDownloaded) return res?.data?.data as any;
return res?.data?.data?.url || null; const parsedUrl = res?.data?.data?.url || null;
return await resolveCachedPlaybackUrl(parsedUrl, songData);
} }
}; };
@@ -218,7 +267,28 @@ const parseLyrics = (lyricsString: string): { lyrics: ILyricText[]; times: numbe
export const loadLrc = async (id: string | number): Promise<ILyric> => { export const loadLrc = async (id: string | number): Promise<ILyric> => {
try { try {
const numericId = typeof id === 'string' ? parseInt(id, 10) : id; const numericId = typeof id === 'string' ? parseInt(id, 10) : id;
const { data } = await getMusicLrc(numericId); let lyricData: any;
if (isElectron) {
try {
lyricData = await window.electron.ipcRenderer.invoke('get-cached-lyric', numericId);
} catch (error) {
console.warn('读取磁盘歌词缓存失败:', error);
}
}
if (!lyricData) {
const { data } = await getMusicLrc(numericId);
lyricData = data;
if (isElectron && lyricData) {
void window.electron.ipcRenderer
.invoke('cache-lyric', numericId, lyricData)
.catch((error) => console.warn('写入磁盘歌词缓存失败:', error));
}
}
const data = lyricData ?? {};
const { lyrics, times } = parseLyrics(data?.yrc?.lyric || data?.lrc?.lyric); const { lyrics, times } = parseLyrics(data?.yrc?.lyric || data?.lrc?.lyric);
// 检查是否有逐字歌词 // 检查是否有逐字歌词
@@ -16,7 +16,7 @@
@click="playAll" @click="playAll"
> >
<i class="iconfont icon-playfill text-sm" /> <i class="iconfont icon-playfill text-sm" />
<span>{{ t('musicList.playAll') }}</span> <span>{{ t('comp.musicList.playAll') }}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -67,7 +67,7 @@
<!-- 无结果 --> <!-- 无结果 -->
<div v-else-if="!loading" class="empty-state"> <div v-else-if="!loading" class="empty-state">
<i class="ri-search-line"></i> <i class="ri-search-line"></i>
<span>{{ t('search.noResult') }}</span> <span>{{ t('comp.musicList.noSearchResults') }}</span>
</div> </div>
</div> </div>
</div> </div>
+1 -1
View File
@@ -342,7 +342,7 @@ const categoryList = computed(() => {
}); });
const currentCategoryName = computed(() => { const currentCategoryName = computed(() => {
if (currentCategoryId.value === -1) return t('podcast.recommend'); if (currentCategoryId.value === -1) return t('podcast.recommended');
return categories.value.find((c) => c.id === currentCategoryId.value)?.name || ''; return categories.value.find((c) => c.id === currentCategoryId.value)?.name || '';
}); });
+1 -1
View File
@@ -207,7 +207,7 @@
class="flex flex-col items-center justify-center py-20 text-neutral-400" class="flex flex-col items-center justify-center py-20 text-neutral-400"
> >
<i class="ri-search-line text-6xl mb-4 opacity-20"></i> <i class="ri-search-line text-6xl mb-4 opacity-20"></i>
<p>{{ t('search.noResults') }}</p> <p>{{ t('comp.musicList.noSearchResults') }}</p>
</div> </div>
<!-- Loading More / Footer --> <!-- Loading More / Footer -->
+423 -2
View File
@@ -473,6 +473,119 @@
<!-- 系统管理 --> <!-- 系统管理 -->
<div v-show="currentSection === 'system'" class="animate-fade-in"> <div v-show="currentSection === 'system'" class="animate-fade-in">
<setting-section v-if="isElectron" :title="t('settings.sections.system')"> <setting-section v-if="isElectron" :title="t('settings.sections.system')">
<!-- 磁盘缓存开关 -->
<setting-item
:title="t('settings.system.diskCache')"
:description="t('settings.system.diskCacheDesc')"
>
<n-switch v-model:value="setData.enableDiskCache">
<template #checked>{{ t('common.on') }}</template>
<template #unchecked>{{ t('common.off') }}</template>
</n-switch>
</setting-item>
<!-- 缓存目录 -->
<setting-item
:title="t('settings.system.cacheDirectory')"
:description="
setData.diskCacheDir ||
diskCacheStats.directory ||
t('settings.system.cacheDirectoryDesc')
"
>
<template #action>
<div class="flex items-center gap-2 max-md:flex-wrap">
<n-button size="small" @click="selectCacheDirectory">
{{ t('settings.system.selectDirectory') }}
</n-button>
<n-button size="small" @click="openCacheDirectory">
{{ t('settings.system.openDirectory') }}
</n-button>
</div>
</template>
</setting-item>
<!-- 缓存上限 -->
<setting-item
:title="t('settings.system.cacheMaxSize')"
:description="t('settings.system.cacheMaxSizeDesc')"
>
<template #action>
<div class="flex items-center gap-2">
<n-input-number
v-model:value="setData.diskCacheMaxSizeMB"
:min="256"
:max="102400"
:step="256"
class="max-md:w-32"
/>
<span class="text-xs text-neutral-500">MB</span>
</div>
</template>
</setting-item>
<!-- 清理策略 -->
<setting-item
:title="t('settings.system.cleanupPolicy')"
:description="t('settings.system.cleanupPolicyDesc')"
>
<n-select
v-model:value="setData.diskCacheCleanupPolicy"
:options="cleanupPolicyOptions"
class="w-40"
/>
</setting-item>
<!-- 缓存状态 -->
<setting-item
:title="t('settings.system.cacheStatus')"
:description="
t('settings.system.cacheStatusDesc', {
used: formatBytes(diskCacheStats.totalSizeBytes),
limit: `${setData.diskCacheMaxSizeMB || diskCacheStats.maxSizeMB || 0} MB`
})
"
>
<template #action>
<div class="flex items-center gap-3 max-md:flex-wrap">
<div class="w-40 max-md:w-32">
<n-progress type="line" :percentage="diskCacheUsagePercent" />
</div>
<span class="text-xs text-neutral-500">
{{
t('settings.system.cacheStatusDetail', {
musicCount: diskCacheStats.musicFiles,
lyricCount: diskCacheStats.lyricFiles
})
}}
</span>
<n-button size="small" @click="refreshDiskCacheStats()">{{
t('common.refresh')
}}</n-button>
</div>
</template>
</setting-item>
<!-- 手动清理磁盘缓存 -->
<setting-item
:title="t('settings.system.manageDiskCache')"
:description="t('settings.system.manageDiskCacheDesc')"
>
<template #action>
<div class="flex items-center gap-2 max-md:flex-wrap">
<n-button size="small" @click="clearDiskCacheByScope('music')">
{{ t('settings.system.clearMusicCache') }}
</n-button>
<n-button size="small" @click="clearDiskCacheByScope('lyrics')">
{{ t('settings.system.clearLyricCache') }}
</n-button>
<n-button type="error" size="small" @click="clearDiskCacheByScope('all')">
{{ t('settings.system.clearAllCache') }}
</n-button>
</div>
</template>
</setting-item>
<!-- 清除缓存 --> <!-- 清除缓存 -->
<setting-item <setting-item
:title="t('settings.system.cache')" :title="t('settings.system.cache')"
@@ -567,7 +680,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useDebounceFn } from '@vueuse/core'; import { useDebounceFn } from '@vueuse/core';
import { useMessage } from 'naive-ui'; import { useDialog, useMessage } from 'naive-ui';
import { computed, h, onMounted, onUnmounted, ref, watch } from 'vue'; import { computed, h, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -611,11 +724,40 @@ const fontPreviews = [
{ key: 'korean' } { key: 'korean' }
]; ];
type DiskCacheScope = 'all' | 'music' | 'lyrics';
type DiskCacheCleanupPolicy = 'lru' | 'fifo';
type CacheSwitchAction = 'migrate' | 'destroy' | 'keep';
type DiskCacheConfig = {
enabled: boolean;
directory: string;
maxSizeMB: number;
cleanupPolicy: DiskCacheCleanupPolicy;
};
type DiskCacheStats = DiskCacheConfig & {
totalSizeBytes: number;
musicSizeBytes: number;
lyricSizeBytes: number;
totalFiles: number;
musicFiles: number;
lyricFiles: number;
usage: number;
};
type SwitchCacheDirectoryResult = {
success: boolean;
config: DiskCacheConfig;
migratedFiles: number;
destroyedFiles: number;
};
// ==================== 平台和Store ==================== // ==================== 平台和Store ====================
const platform = window.electron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web'; const platform = window.electron ? window.electron.ipcRenderer.sendSync('get-platform') : 'web';
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const userStore = useUserStore(); const userStore = useUserStore();
const message = useMessage(); const message = useMessage();
const dialog = useDialog();
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@@ -764,6 +906,268 @@ const openDownloadPath = () => {
openDirectory(setData.value.downloadPath, message); openDirectory(setData.value.downloadPath, message);
}; };
// ==================== 磁盘缓存设置 ====================
const diskCacheStats = ref<DiskCacheStats>({
enabled: true,
directory: '',
maxSizeMB: 4096,
cleanupPolicy: 'lru',
totalSizeBytes: 0,
musicSizeBytes: 0,
lyricSizeBytes: 0,
totalFiles: 0,
musicFiles: 0,
lyricFiles: 0,
usage: 0
});
const applyingDiskCacheConfig = ref(false);
const switchingCacheDirectory = ref(false);
const cleanupPolicyOptions = computed(() => [
{ label: t('settings.system.cleanupPolicyOptions.lru'), value: 'lru' },
{ label: t('settings.system.cleanupPolicyOptions.fifo'), value: 'fifo' }
]);
const diskCacheUsagePercent = computed(() =>
Math.min(100, Math.max(0, Math.round((diskCacheStats.value.usage || 0) * 100)))
);
const formatBytes = (bytes: number) => {
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B';
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let unitIndex = 0;
while (value >= 1024 && unitIndex < units.length - 1) {
value /= 1024;
unitIndex++;
}
return `${value.toFixed(value >= 10 ? 0 : 1)} ${units[unitIndex]}`;
};
const readDiskCacheConfigFromUI = (): DiskCacheConfig => {
const cleanupPolicy: DiskCacheCleanupPolicy =
setData.value.diskCacheCleanupPolicy === 'fifo' ? 'fifo' : 'lru';
const maxSizeMB = Math.max(256, Math.floor(Number(setData.value.diskCacheMaxSizeMB || 4096)));
return {
enabled: setData.value.enableDiskCache !== false,
directory: String(setData.value.diskCacheDir || ''),
maxSizeMB,
cleanupPolicy
};
};
const refreshDiskCacheStats = async (silent: boolean = true) => {
if (!window.electron) return;
try {
const stats = (await window.electron.ipcRenderer.invoke(
'get-disk-cache-stats'
)) as DiskCacheStats;
if (stats) {
diskCacheStats.value = stats;
}
} catch (error) {
console.error('读取磁盘缓存统计失败:', error);
if (!silent) {
message.error(t('settings.system.messages.diskCacheStatsLoadFailed'));
}
}
};
const loadDiskCacheConfig = async () => {
if (!window.electron) return;
try {
const config = (await window.electron.ipcRenderer.invoke(
'get-disk-cache-config'
)) as DiskCacheConfig;
if (config) {
setData.value = {
...setData.value,
enableDiskCache: config.enabled,
diskCacheDir: config.directory,
diskCacheMaxSizeMB: config.maxSizeMB,
diskCacheCleanupPolicy: config.cleanupPolicy
};
}
} catch (error) {
console.error('读取磁盘缓存配置失败:', error);
}
};
const applyDiskCacheConfig = async () => {
if (!window.electron || applyingDiskCacheConfig.value) return;
applyingDiskCacheConfig.value = true;
try {
const config = readDiskCacheConfigFromUI();
const updated = (await window.electron.ipcRenderer.invoke(
'set-disk-cache-config',
config
)) as DiskCacheConfig;
if (updated) {
setData.value = {
...setData.value,
enableDiskCache: updated.enabled,
diskCacheDir: updated.directory,
diskCacheMaxSizeMB: updated.maxSizeMB,
diskCacheCleanupPolicy: updated.cleanupPolicy
};
}
await refreshDiskCacheStats();
} catch (error) {
console.error('更新磁盘缓存配置失败:', error);
} finally {
applyingDiskCacheConfig.value = false;
}
};
const applyDiskCacheConfigDebounced = useDebounceFn(() => {
void applyDiskCacheConfig();
}, 500);
watch(
() => [
setData.value.enableDiskCache,
setData.value.diskCacheDir,
setData.value.diskCacheMaxSizeMB,
setData.value.diskCacheCleanupPolicy
],
() => {
if (!window.electron || applyingDiskCacheConfig.value || switchingCacheDirectory.value) return;
applyDiskCacheConfigDebounced();
}
);
const askCacheSwitchMigrate = (): Promise<boolean> => {
return new Promise((resolve) => {
let resolved = false;
const finish = (value: boolean) => {
if (resolved) return;
resolved = true;
resolve(value);
};
dialog.warning({
title: t('settings.system.switchDirectoryMigrateTitle'),
content: t('settings.system.switchDirectoryMigrateContent'),
positiveText: t('settings.system.switchDirectoryMigrateConfirm'),
negativeText: t('settings.system.switchDirectoryKeepOld'),
onPositiveClick: () => finish(true),
onNegativeClick: () => finish(false),
onClose: () => finish(false)
});
});
};
const askCacheSwitchDestroy = (): Promise<boolean> => {
return new Promise((resolve) => {
let resolved = false;
const finish = (value: boolean) => {
if (resolved) return;
resolved = true;
resolve(value);
};
dialog.warning({
title: t('settings.system.switchDirectoryDestroyTitle'),
content: t('settings.system.switchDirectoryDestroyContent'),
positiveText: t('settings.system.switchDirectoryDestroyConfirm'),
negativeText: t('settings.system.switchDirectoryKeepOld'),
onPositiveClick: () => finish(true),
onNegativeClick: () => finish(false),
onClose: () => finish(false)
});
});
};
const selectCacheDirectory = async () => {
if (!window.electron) return;
const selectedPath = await selectDirectory(message);
if (!selectedPath) return;
const currentDirectory = setData.value.diskCacheDir || diskCacheStats.value.directory;
if (currentDirectory && selectedPath === currentDirectory) {
return;
}
let action: CacheSwitchAction = 'keep';
if (currentDirectory && diskCacheStats.value.totalFiles > 0) {
const shouldMigrate = await askCacheSwitchMigrate();
if (shouldMigrate) {
action = 'migrate';
} else {
const shouldDestroy = await askCacheSwitchDestroy();
action = shouldDestroy ? 'destroy' : 'keep';
}
}
switchingCacheDirectory.value = true;
try {
const result = (await window.electron.ipcRenderer.invoke('switch-disk-cache-directory', {
directory: selectedPath,
action
})) as SwitchCacheDirectoryResult;
if (!result?.success) {
message.error(t('settings.system.messages.switchDirectoryFailed'));
return;
}
setData.value = {
...setData.value,
enableDiskCache: result.config.enabled,
diskCacheDir: result.config.directory,
diskCacheMaxSizeMB: result.config.maxSizeMB,
diskCacheCleanupPolicy: result.config.cleanupPolicy
};
await refreshDiskCacheStats();
if (action === 'migrate') {
message.success(
t('settings.system.messages.switchDirectoryMigrated', { count: result.migratedFiles })
);
return;
}
if (action === 'destroy') {
message.success(
t('settings.system.messages.switchDirectoryDestroyed', { count: result.destroyedFiles })
);
return;
}
message.success(t('settings.system.messages.switchDirectorySuccess'));
} catch (error) {
console.error('切换缓存目录失败:', error);
message.error(t('settings.system.messages.switchDirectoryFailed'));
} finally {
switchingCacheDirectory.value = false;
}
};
const openCacheDirectory = () => {
const targetPath = setData.value.diskCacheDir || diskCacheStats.value.directory;
openDirectory(targetPath, message);
};
const clearDiskCacheByScope = async (scope: DiskCacheScope) => {
if (!window.electron) return;
try {
const success = await window.electron.ipcRenderer.invoke('clear-disk-cache', scope);
if (success) {
await refreshDiskCacheStats();
message.success(t('settings.system.messages.diskCacheClearSuccess'));
return;
}
message.error(t('settings.system.messages.diskCacheClearFailed'));
} catch (error) {
console.error('手动清理磁盘缓存失败:', error);
message.error(t('settings.system.messages.diskCacheClearFailed'));
}
};
// ==================== 代理设置 ==================== // ==================== 代理设置 ====================
const showProxyModal = ref(false); const showProxyModal = ref(false);
const proxyForm = ref({ protocol: 'http', host: '127.0.0.1', port: 7890 }); const proxyForm = ref({ protocol: 'http', host: '127.0.0.1', port: 7890 });
@@ -883,6 +1287,7 @@ const clearCache = async (selectedCacheTypes: string[]) => {
case 'resources': case 'resources':
if (window.electron) { if (window.electron) {
window.electron.ipcRenderer.send('clear-audio-cache'); window.electron.ipcRenderer.send('clear-audio-cache');
await window.electron.ipcRenderer.invoke('clear-disk-cache', 'music');
} }
localStorage.removeItem('lyricCache'); localStorage.removeItem('lyricCache');
localStorage.removeItem('musicUrlCache'); localStorage.removeItem('musicUrlCache');
@@ -897,11 +1302,15 @@ const clearCache = async (selectedCacheTypes: string[]) => {
} }
break; break;
case 'lyrics': case 'lyrics':
window.api.invoke('clear-lyrics-cache'); if (window.electron) {
await window.electron.ipcRenderer.invoke('clear-disk-cache', 'lyrics');
}
await window.api.invoke('clear-lyrics-cache');
break; break;
} }
}); });
await Promise.all(clearTasks); await Promise.all(clearTasks);
await refreshDiskCacheStats();
message.success(t('settings.system.messages.clearSuccess')); message.success(t('settings.system.messages.clearSuccess'));
}; };
@@ -997,6 +1406,18 @@ onMounted(async () => {
if (setData.value.enableRealIP === undefined) { if (setData.value.enableRealIP === undefined) {
setData.value = { ...setData.value, enableRealIP: false }; setData.value = { ...setData.value, enableRealIP: false };
} }
if (setData.value.enableDiskCache === undefined) {
setData.value = { ...setData.value, enableDiskCache: true };
}
if (!setData.value.diskCacheMaxSizeMB) {
setData.value = { ...setData.value, diskCacheMaxSizeMB: 4096 };
}
if (!['lru', 'fifo'].includes(setData.value.diskCacheCleanupPolicy)) {
setData.value = { ...setData.value, diskCacheCleanupPolicy: 'lru' };
}
await loadDiskCacheConfig();
await refreshDiskCacheStats();
if (window.electron) { if (window.electron) {
window.electron.ipcRenderer.on('gpu-acceleration-updated', (_, enabled: boolean) => { window.electron.ipcRenderer.on('gpu-acceleration-updated', (_, enabled: boolean) => {
+1 -1
View File
@@ -204,7 +204,7 @@ const loadFollowerList = async () => {
hasMoreFollowers.value = newFollowers.length >= followerLimit.value; hasMoreFollowers.value = newFollowers.length >= followerLimit.value;
} catch (error) { } catch (error) {
console.error('加载粉丝列表失败:', error); console.error('加载粉丝列表失败:', error);
message.error(t('user.follower.loadFailed')); message.error(t('common.loadFailed'));
} finally { } finally {
followerListLoading.value = false; followerListLoading.value = false;
} }
+1 -1
View File
@@ -206,7 +206,7 @@ const loadFollowList = async () => {
hasMoreFollows.value = newFollows.length >= followLimit.value; hasMoreFollows.value = newFollows.length >= followLimit.value;
} catch (error) { } catch (error) {
console.error('加载关注列表失败:', error); console.error('加载关注列表失败:', error);
message.error(t('user.follow.loadFailed')); message.error(t('common.loadFailed'));
} finally { } finally {
followListLoading.value = false; followListLoading.value = false;
} }