mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-18 11:37:31 +08:00
feat: 添加下载设置功能,支持自定义文件名格式和下载路径配置
- 新增下载设置抽屉,允许用户设置下载路径和文件名格式 - 支持多种文件名格式预设和自定义格式 - 实现下载项的显示名称格式化 - 优化下载管理逻辑,避免重复通知
This commit is contained in:
@@ -3,17 +3,20 @@ export default {
|
|||||||
localMusic: 'Local Music',
|
localMusic: 'Local Music',
|
||||||
count: '{count} songs in total',
|
count: '{count} songs in total',
|
||||||
clearAll: 'Clear All',
|
clearAll: 'Clear All',
|
||||||
|
settings: 'Settings',
|
||||||
tabs: {
|
tabs: {
|
||||||
downloading: 'Downloading',
|
downloading: 'Downloading',
|
||||||
downloaded: 'Downloaded'
|
downloaded: 'Downloaded'
|
||||||
},
|
},
|
||||||
empty: {
|
empty: {
|
||||||
noTasks: 'No download tasks',
|
noTasks: 'No download tasks',
|
||||||
noDownloaded: 'No downloaded songs'
|
noDownloaded: 'No downloaded songs',
|
||||||
|
noDownloadedHint: 'Download your favorite songs to listen offline'
|
||||||
},
|
},
|
||||||
progress: {
|
progress: {
|
||||||
total: 'Total Progress: {progress}%'
|
total: 'Total Progress: {progress}%'
|
||||||
},
|
},
|
||||||
|
items: 'items',
|
||||||
status: {
|
status: {
|
||||||
downloading: 'Downloading',
|
downloading: 'Downloading',
|
||||||
completed: 'Completed',
|
completed: 'Completed',
|
||||||
@@ -43,7 +46,8 @@ export default {
|
|||||||
},
|
},
|
||||||
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}',
|
||||||
@@ -51,5 +55,37 @@ export default {
|
|||||||
path: {
|
path: {
|
||||||
copied: 'Path copied to clipboard',
|
copied: 'Path copied to clipboard',
|
||||||
copyFailed: 'Failed to copy path'
|
copyFailed: 'Failed to copy path'
|
||||||
|
},
|
||||||
|
settingsPanel: {
|
||||||
|
title: 'Download Settings',
|
||||||
|
path: 'Download Location',
|
||||||
|
pathDesc: 'Set where your music files will be saved',
|
||||||
|
pathPlaceholder: 'Please select download path',
|
||||||
|
noPathSelected: 'Please select download path first',
|
||||||
|
select: 'Select Folder',
|
||||||
|
open: 'Open Folder',
|
||||||
|
fileFormat: 'Filename Format',
|
||||||
|
fileFormatDesc: 'Set how downloaded music files will be named',
|
||||||
|
customFormat: 'Custom Format',
|
||||||
|
separator: 'Separator',
|
||||||
|
separators: {
|
||||||
|
dash: 'Space-dash-space',
|
||||||
|
underscore: 'Underscore',
|
||||||
|
space: 'Space'
|
||||||
|
},
|
||||||
|
dragToArrange: 'Sort or use arrow buttons to arrange:',
|
||||||
|
formatVariables: 'Available variables',
|
||||||
|
preview: 'Preview:',
|
||||||
|
saveSuccess: 'Download settings saved',
|
||||||
|
presets: {
|
||||||
|
songArtist: 'Song - Artist',
|
||||||
|
artistSong: 'Artist - Song',
|
||||||
|
songOnly: 'Song only'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
songName: 'Song name',
|
||||||
|
artistName: 'Artist name',
|
||||||
|
albumName: 'Album name'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
localMusic: '本地音乐',
|
localMusic: '本地音乐',
|
||||||
count: '共 {count} 首歌曲',
|
count: '共 {count} 首歌曲',
|
||||||
clearAll: '清空记录',
|
clearAll: '清空记录',
|
||||||
|
settings: '设置',
|
||||||
tabs: {
|
tabs: {
|
||||||
downloading: '下载中',
|
downloading: '下载中',
|
||||||
downloaded: '已下载'
|
downloaded: '已下载'
|
||||||
@@ -50,5 +51,37 @@ export default {
|
|||||||
path: {
|
path: {
|
||||||
copied: '路径已复制到剪贴板',
|
copied: '路径已复制到剪贴板',
|
||||||
copyFailed: '复制路径失败'
|
copyFailed: '复制路径失败'
|
||||||
|
},
|
||||||
|
settingsPanel: {
|
||||||
|
title: '下载设置',
|
||||||
|
path: '下载位置',
|
||||||
|
pathDesc: '设置音乐文件下载保存的位置',
|
||||||
|
pathPlaceholder: '请选择下载路径',
|
||||||
|
noPathSelected: '请先选择下载路径',
|
||||||
|
select: '选择文件夹',
|
||||||
|
open: '打开文件夹',
|
||||||
|
fileFormat: '文件名格式',
|
||||||
|
fileFormatDesc: '设置下载音乐时的文件命名格式',
|
||||||
|
customFormat: '自定义格式',
|
||||||
|
separator: '分隔符',
|
||||||
|
separators: {
|
||||||
|
dash: '空格-空格',
|
||||||
|
underscore: '下划线',
|
||||||
|
space: '空格'
|
||||||
|
},
|
||||||
|
dragToArrange: '拖动排序或使用箭头按钮调整顺序:',
|
||||||
|
formatVariables: '可用变量',
|
||||||
|
preview: '预览效果:',
|
||||||
|
saveSuccess: '下载设置已保存',
|
||||||
|
presets: {
|
||||||
|
songArtist: '歌曲名 - 歌手名',
|
||||||
|
artistSong: '歌手名 - 歌曲名',
|
||||||
|
songOnly: '仅歌曲名'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
songName: '歌曲名',
|
||||||
|
artistName: '歌手名',
|
||||||
|
albumName: '专辑名'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ const audioCacheStore = new Store({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 保存已发送通知的文件,避免重复通知
|
||||||
|
const sentNotifications = new Map();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 初始化文件管理相关的IPC监听
|
* 初始化文件管理相关的IPC监听
|
||||||
*/
|
*/
|
||||||
@@ -123,6 +126,23 @@ export function initializeFileManager() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取默认下载路径
|
||||||
|
ipcMain.handle('get-downloads-path', () => {
|
||||||
|
return app.getPath('downloads');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取存储的配置值
|
||||||
|
ipcMain.handle('get-store-value', (_, key) => {
|
||||||
|
const store = new Store();
|
||||||
|
return store.get(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置存储的配置值
|
||||||
|
ipcMain.on('set-store-value', (_, key, value) => {
|
||||||
|
const store = new Store();
|
||||||
|
store.set(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
// 下载音乐处理
|
// 下载音乐处理
|
||||||
ipcMain.on('download-music', handleDownloadRequest);
|
ipcMain.on('download-music', handleDownloadRequest);
|
||||||
|
|
||||||
@@ -359,8 +379,27 @@ async function downloadMusic(
|
|||||||
(configStore.get('set.downloadPath') as string) || app.getPath('downloads');
|
(configStore.get('set.downloadPath') as string) || app.getPath('downloads');
|
||||||
const apiPort = configStore.get('set.musicApiPort') || 30488;
|
const apiPort = configStore.get('set.musicApiPort') || 30488;
|
||||||
|
|
||||||
|
// 获取文件名格式设置
|
||||||
|
const nameFormat =
|
||||||
|
(configStore.get('set.downloadNameFormat') as string) || '{songName} - {artistName}';
|
||||||
|
|
||||||
|
// 根据格式创建文件名
|
||||||
|
let formattedFilename = filename;
|
||||||
|
if (songInfo) {
|
||||||
|
// 准备替换变量
|
||||||
|
const artistName = songInfo.ar?.map((a: any) => a.name).join('/') || '未知艺术家';
|
||||||
|
const songName = songInfo.name || filename;
|
||||||
|
const albumName = songInfo.al?.name || '未知专辑';
|
||||||
|
|
||||||
|
// 应用自定义格式
|
||||||
|
formattedFilename = nameFormat
|
||||||
|
.replace(/\{songName\}/g, songName)
|
||||||
|
.replace(/\{artistName\}/g, artistName)
|
||||||
|
.replace(/\{albumName\}/g, albumName);
|
||||||
|
}
|
||||||
|
|
||||||
// 清理文件名中的非法字符
|
// 清理文件名中的非法字符
|
||||||
const sanitizedFilename = sanitizeFilename(filename);
|
const sanitizedFilename = sanitizeFilename(formattedFilename);
|
||||||
|
|
||||||
// 创建临时文件路径 (在系统临时目录中创建)
|
// 创建临时文件路径 (在系统临时目录中创建)
|
||||||
const tempDir = path.join(os.tmpdir(), 'AlgerMusicPlayerTemp');
|
const tempDir = path.join(os.tmpdir(), 'AlgerMusicPlayerTemp');
|
||||||
@@ -635,27 +674,38 @@ async function downloadMusic(
|
|||||||
history.unshift(newSongInfo);
|
history.unshift(newSongInfo);
|
||||||
downloadStore.set('history', history);
|
downloadStore.set('history', history);
|
||||||
|
|
||||||
// 发送桌面通知
|
// 避免重复发送通知
|
||||||
try {
|
const notificationId = `download-${finalFilePath}`;
|
||||||
const artistNames =
|
if (!sentNotifications.has(notificationId)) {
|
||||||
(songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/') ||
|
sentNotifications.set(notificationId, true);
|
||||||
'未知艺术家';
|
|
||||||
const notification = new Notification({
|
|
||||||
title: '下载完成',
|
|
||||||
body: `${songInfo?.name || filename} - ${artistNames}`,
|
|
||||||
silent: false
|
|
||||||
});
|
|
||||||
|
|
||||||
notification.on('click', () => {
|
// 发送桌面通知
|
||||||
shell.showItemInFolder(finalFilePath);
|
try {
|
||||||
});
|
const artistNames =
|
||||||
|
(songInfo?.ar || songInfo?.song?.artists)?.map((a: any) => a.name).join('/') ||
|
||||||
|
'未知艺术家';
|
||||||
|
const notification = new Notification({
|
||||||
|
title: '下载完成',
|
||||||
|
body: `${songInfo?.name || filename} - ${artistNames}`,
|
||||||
|
silent: false
|
||||||
|
});
|
||||||
|
|
||||||
notification.show();
|
notification.on('click', () => {
|
||||||
} catch (notifyError) {
|
shell.showItemInFolder(finalFilePath);
|
||||||
console.error('发送通知失败:', notifyError);
|
});
|
||||||
|
|
||||||
|
notification.show();
|
||||||
|
|
||||||
|
// 60秒后清理通知记录,释放内存
|
||||||
|
setTimeout(() => {
|
||||||
|
sentNotifications.delete(notificationId);
|
||||||
|
}, 60000);
|
||||||
|
} catch (notifyError) {
|
||||||
|
console.error('发送通知失败:', notifyError);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送下载完成事件
|
// 发送下载完成事件,确保只发送一次
|
||||||
event.reply('music-download-complete', {
|
event.reply('music-download-complete', {
|
||||||
success: true,
|
success: true,
|
||||||
path: finalFilePath,
|
path: finalFilePath,
|
||||||
|
|||||||
@@ -6,11 +6,145 @@ import { useMessage } from 'naive-ui';
|
|||||||
import { getSongUrl } from '@/store/modules/player';
|
import { getSongUrl } from '@/store/modules/player';
|
||||||
import type { SongResult } from '@/type/music';
|
import type { SongResult } from '@/type/music';
|
||||||
|
|
||||||
|
// 全局下载管理(闭包模式)
|
||||||
|
const createDownloadManager = () => {
|
||||||
|
// 正在下载的文件集合
|
||||||
|
const activeDownloads = new Set<string>();
|
||||||
|
|
||||||
|
// 已经发送了通知的文件集合(避免重复通知)
|
||||||
|
const notifiedDownloads = new Set<string>();
|
||||||
|
|
||||||
|
// 事件监听器是否已初始化
|
||||||
|
let isInitialized = false;
|
||||||
|
|
||||||
|
// 监听器引用(用于清理)
|
||||||
|
let completeListener: ((event: any, data: any) => void) | null = null;
|
||||||
|
let errorListener: ((event: any, data: any) => void) | null = null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 添加下载
|
||||||
|
addDownload: (filename: string) => {
|
||||||
|
activeDownloads.add(filename);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 移除下载
|
||||||
|
removeDownload: (filename: string) => {
|
||||||
|
activeDownloads.delete(filename);
|
||||||
|
// 延迟清理通知记录
|
||||||
|
setTimeout(() => {
|
||||||
|
notifiedDownloads.delete(filename);
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 标记文件已通知
|
||||||
|
markNotified: (filename: string) => {
|
||||||
|
notifiedDownloads.add(filename);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查文件是否已通知
|
||||||
|
isNotified: (filename: string) => {
|
||||||
|
return notifiedDownloads.has(filename);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理所有下载
|
||||||
|
clearDownloads: () => {
|
||||||
|
activeDownloads.clear();
|
||||||
|
notifiedDownloads.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
initEventListeners: (message: any, t: any) => {
|
||||||
|
if (isInitialized) return;
|
||||||
|
|
||||||
|
// 移除可能存在的旧监听器
|
||||||
|
if (completeListener) {
|
||||||
|
window.electron.ipcRenderer.removeListener('music-download-complete', completeListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorListener) {
|
||||||
|
window.electron.ipcRenderer.removeListener('music-download-error', errorListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的监听器
|
||||||
|
completeListener = (_event, data) => {
|
||||||
|
if (!data.filename || !activeDownloads.has(data.filename)) return;
|
||||||
|
|
||||||
|
// 如果该文件已经通知过,则跳过
|
||||||
|
if (notifiedDownloads.has(data.filename)) return;
|
||||||
|
|
||||||
|
// 标记为已通知
|
||||||
|
notifiedDownloads.add(data.filename);
|
||||||
|
|
||||||
|
// 从活动下载移除
|
||||||
|
activeDownloads.delete(data.filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
errorListener = (_event, data) => {
|
||||||
|
if (!data.filename || !activeDownloads.has(data.filename)) return;
|
||||||
|
|
||||||
|
// 如果该文件已经通知过,则跳过
|
||||||
|
if (notifiedDownloads.has(data.filename)) return;
|
||||||
|
|
||||||
|
// 标记为已通知
|
||||||
|
notifiedDownloads.add(data.filename);
|
||||||
|
|
||||||
|
// 显示失败通知
|
||||||
|
message.error(t('songItem.message.downloadFailed', {
|
||||||
|
filename: data.filename,
|
||||||
|
error: data.error || '未知错误'
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 从活动下载移除
|
||||||
|
activeDownloads.delete(data.filename);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加监听器
|
||||||
|
window.electron.ipcRenderer.on('music-download-complete', completeListener);
|
||||||
|
window.electron.ipcRenderer.on('music-download-error', errorListener);
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理事件监听器
|
||||||
|
cleanupEventListeners: () => {
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
|
if (completeListener) {
|
||||||
|
window.electron.ipcRenderer.removeListener('music-download-complete', completeListener);
|
||||||
|
completeListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorListener) {
|
||||||
|
window.electron.ipcRenderer.removeListener('music-download-error', errorListener);
|
||||||
|
errorListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
isInitialized = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取活跃下载数量
|
||||||
|
getActiveDownloadCount: () => {
|
||||||
|
return activeDownloads.size;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查是否有特定文件正在下载
|
||||||
|
hasDownload: (filename: string) => {
|
||||||
|
return activeDownloads.has(filename);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建单例下载管理器
|
||||||
|
const downloadManager = createDownloadManager();
|
||||||
|
|
||||||
export const useDownload = () => {
|
export const useDownload = () => {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const isDownloading = ref(false);
|
const isDownloading = ref(false);
|
||||||
|
|
||||||
|
// 初始化事件监听器
|
||||||
|
downloadManager.initEventListeners(message, t);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 下载单首音乐
|
* 下载单首音乐
|
||||||
* @param song 歌曲信息
|
* @param song 歌曲信息
|
||||||
@@ -34,8 +168,18 @@ export const useDownload = () => {
|
|||||||
const artistNames = (song.ar || song.song?.artists)?.map((a) => a.name).join(',');
|
const artistNames = (song.ar || song.song?.artists)?.map((a) => a.name).join(',');
|
||||||
const filename = `${song.name} - ${artistNames}`;
|
const filename = `${song.name} - ${artistNames}`;
|
||||||
|
|
||||||
|
// 检查是否已在下载
|
||||||
|
if (downloadManager.hasDownload(filename)) {
|
||||||
|
isDownloading.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到活动下载集合
|
||||||
|
downloadManager.addDownload(filename);
|
||||||
|
|
||||||
const songData = cloneDeep(song);
|
const songData = cloneDeep(song);
|
||||||
songData.ar = songData.ar || songData.song?.artists;
|
songData.ar = songData.ar || songData.song?.artists;
|
||||||
|
|
||||||
// 发送下载请求
|
// 发送下载请求
|
||||||
window.electron.ipcRenderer.send('download-music', {
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
url: typeof musicUrl === 'string' ? musicUrl : musicUrl.url,
|
url: typeof musicUrl === 'string' ? musicUrl : musicUrl.url,
|
||||||
@@ -43,39 +187,16 @@ export const useDownload = () => {
|
|||||||
songInfo: {
|
songInfo: {
|
||||||
...songData,
|
...songData,
|
||||||
downloadTime: Date.now()
|
downloadTime: Date.now()
|
||||||
}
|
},
|
||||||
|
type: musicUrl.type
|
||||||
});
|
});
|
||||||
|
|
||||||
message.success(t('songItem.message.downloadQueued'));
|
message.success(t('songItem.message.downloadQueued'));
|
||||||
|
|
||||||
// 监听下载完成事件
|
// 简化的监听逻辑,基本通知由全局监听器处理
|
||||||
const handleDownloadComplete = (_, result) => {
|
setTimeout(() => {
|
||||||
if (result.filename === filename) {
|
isDownloading.value = false;
|
||||||
isDownloading.value = false;
|
}, 2000);
|
||||||
removeListeners();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 监听下载错误事件
|
|
||||||
const handleDownloadError = (_, result) => {
|
|
||||||
if (result.filename === filename) {
|
|
||||||
isDownloading.value = false;
|
|
||||||
removeListeners();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 移除监听器函数
|
|
||||||
const removeListeners = () => {
|
|
||||||
window.electron.ipcRenderer.removeListener('music-download-complete', handleDownloadComplete);
|
|
||||||
window.electron.ipcRenderer.removeListener('music-download-error', handleDownloadError);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加事件监听器
|
|
||||||
window.electron.ipcRenderer.once('music-download-complete', handleDownloadComplete);
|
|
||||||
window.electron.ipcRenderer.once('music-download-error', handleDownloadError);
|
|
||||||
|
|
||||||
// 30秒后自动清理监听器(以防下载过程中出现未知错误)
|
|
||||||
setTimeout(removeListeners, 30000);
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Download error:', error);
|
console.error('Download error:', error);
|
||||||
isDownloading.value = false;
|
isDownloading.value = false;
|
||||||
@@ -103,27 +224,17 @@ export const useDownload = () => {
|
|||||||
isDownloading.value = true;
|
isDownloading.value = true;
|
||||||
message.success(t('favorite.downloading'));
|
message.success(t('favorite.downloading'));
|
||||||
|
|
||||||
// 移除旧的监听器
|
|
||||||
window.electron.ipcRenderer.removeAllListeners('music-download-complete');
|
|
||||||
|
|
||||||
let successCount = 0;
|
let successCount = 0;
|
||||||
let failCount = 0;
|
let failCount = 0;
|
||||||
|
const totalCount = songs.length;
|
||||||
|
|
||||||
// 添加新的监听器
|
// 下载进度追踪
|
||||||
window.electron.ipcRenderer.on('music-download-complete', (_, result) => {
|
const trackProgress = () => {
|
||||||
if (result.success) {
|
if (successCount + failCount === totalCount) {
|
||||||
successCount++;
|
|
||||||
} else {
|
|
||||||
failCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 当所有下载完成时
|
|
||||||
if (successCount + failCount === songs.length) {
|
|
||||||
isDownloading.value = false;
|
isDownloading.value = false;
|
||||||
message.success(t('favorite.downloadSuccess'));
|
message.success(t('favorite.downloadSuccess'));
|
||||||
window.electron.ipcRenderer.removeAllListeners('music-download-complete');
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// 并行获取所有歌曲的下载链接
|
// 并行获取所有歌曲的下载链接
|
||||||
const downloadUrls = await Promise.all(
|
const downloadUrls = await Promise.all(
|
||||||
@@ -133,6 +244,7 @@ export const useDownload = () => {
|
|||||||
return { song, ...data };
|
return { song, ...data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`获取歌曲 ${song.name} 下载链接失败:`, error);
|
console.error(`获取歌曲 ${song.name} 下载链接失败:`, error);
|
||||||
|
failCount++;
|
||||||
return { song, url: null };
|
return { song, url: null };
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -142,21 +254,41 @@ export const useDownload = () => {
|
|||||||
downloadUrls.forEach(({ song, url, type }) => {
|
downloadUrls.forEach(({ song, url, type }) => {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
failCount++;
|
failCount++;
|
||||||
|
trackProgress();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const songData = cloneDeep(song);
|
const songData = cloneDeep(song);
|
||||||
|
const filename = `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`;
|
||||||
|
|
||||||
|
// 检查是否已在下载
|
||||||
|
if (downloadManager.hasDownload(filename)) {
|
||||||
|
failCount++;
|
||||||
|
trackProgress();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加到活动下载集合
|
||||||
|
downloadManager.addDownload(filename);
|
||||||
|
|
||||||
const songInfo = {
|
const songInfo = {
|
||||||
...songData,
|
...songData,
|
||||||
ar: songData.ar || songData.song?.artists,
|
ar: songData.ar || songData.song?.artists,
|
||||||
downloadTime: Date.now()
|
downloadTime: Date.now()
|
||||||
};
|
};
|
||||||
|
|
||||||
window.electron.ipcRenderer.send('download-music', {
|
window.electron.ipcRenderer.send('download-music', {
|
||||||
url,
|
url,
|
||||||
filename: `${song.name} - ${(song.ar || song.song?.artists)?.map((a) => a.name).join(',')}`,
|
filename,
|
||||||
songInfo,
|
songInfo,
|
||||||
type
|
type
|
||||||
});
|
});
|
||||||
|
|
||||||
|
successCount++;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 所有下载开始后,检查进度
|
||||||
|
trackProgress();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载失败:', error);
|
console.error('下载失败:', error);
|
||||||
isDownloading.value = false;
|
isDownloading.value = false;
|
||||||
|
|||||||
@@ -2,20 +2,26 @@
|
|||||||
<div class="download-page">
|
<div class="download-page">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1 class="page-title">{{ t('download.title') }}</h1>
|
<h1 class="page-title">{{ t('download.title') }}</h1>
|
||||||
<div class="segment-control">
|
<div class="flex items-center gap-3">
|
||||||
<div
|
<n-button size="small" @click="showSettingsDrawer = true">
|
||||||
class="segment-item"
|
<template #icon><i class="iconfont ri-settings-3-line"></i></template>
|
||||||
:class="{ 'active': tabName === 'downloading' }"
|
{{ t('download.settings') }}
|
||||||
@click="tabName = 'downloading'"
|
</n-button>
|
||||||
>
|
<div class="segment-control">
|
||||||
{{ t('download.tabs.downloading') }}
|
<div
|
||||||
</div>
|
class="segment-item"
|
||||||
<div
|
:class="{ 'active': tabName === 'downloading' }"
|
||||||
class="segment-item"
|
@click="tabName = 'downloading'"
|
||||||
:class="{ 'active': tabName === 'downloaded' }"
|
>
|
||||||
@click="tabName = 'downloaded'"
|
{{ t('download.tabs.downloading') }}
|
||||||
>
|
</div>
|
||||||
{{ t('download.tabs.downloaded') }}
|
<div
|
||||||
|
class="segment-item"
|
||||||
|
:class="{ 'active': tabName === 'downloaded' }"
|
||||||
|
@click="tabName = 'downloaded'"
|
||||||
|
>
|
||||||
|
{{ t('download.tabs.downloaded') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,8 +126,8 @@
|
|||||||
<img :src="getImgUrl(item.picUrl, '200y200')" alt="Cover" />
|
<img :src="getImgUrl(item.picUrl, '200y200')" alt="Cover" />
|
||||||
</div>
|
</div>
|
||||||
<div class="item-info flex items-center gap-4 w-full">
|
<div class="item-info flex items-center gap-4 w-full">
|
||||||
<div class="item-name min-w-[160px] max-w-[160px] truncate" :title="item.filename">
|
<div class="item-name min-w-[160px] max-w-[160px] truncate" :title="item.displayName || item.filename">
|
||||||
{{ item.filename }}
|
{{ item.displayName || item.filename }}
|
||||||
</div>
|
</div>
|
||||||
<div class="item-artist min-w-[120px] max-w-[120px] flex items-center gap-1 truncate">
|
<div class="item-artist min-w-[120px] max-w-[120px] flex items-center gap-1 truncate">
|
||||||
<i class="iconfont ri-user-line"></i>
|
<i class="iconfont ri-user-line"></i>
|
||||||
@@ -166,7 +172,7 @@
|
|||||||
<span>{{ t('download.delete.title') }}</span>
|
<span>{{ t('download.delete.title') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
{{ t('download.delete.message', { filename: itemToDelete?.filename }) }}
|
{{ t('download.delete.message', { filename: itemToDelete?.displayName || itemToDelete?.filename }) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button class="modal-btn cancel" @click="showDeleteConfirm = false">
|
<button class="modal-btn cancel" @click="showDeleteConfirm = false">
|
||||||
@@ -199,6 +205,164 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 下载设置抽屉 -->
|
||||||
|
<n-drawer v-model:show="showSettingsDrawer" :width="380" placement="right" :z-index="999999999">
|
||||||
|
<n-drawer-content :native-scrollbar="false">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-lg font-bold">{{ t('download.settingsPanel.title') }}</div>
|
||||||
|
<n-button type="primary" @click="saveDownloadSettings">
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="download-settings">
|
||||||
|
<!-- 下载路径设置 -->
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-title">{{ t('download.settingsPanel.path') }}</div>
|
||||||
|
<div class="setting-desc">{{ t('download.settingsPanel.pathDesc') }}</div>
|
||||||
|
<div class="flex flex-col gap-2 mt-2">
|
||||||
|
<n-input
|
||||||
|
v-model:value="downloadSettings.path"
|
||||||
|
:placeholder="t('download.settingsPanel.pathPlaceholder')"
|
||||||
|
readonly
|
||||||
|
class="flex-1"
|
||||||
|
/>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-button class="flex-1" @click="selectDownloadPath">{{ t('download.settingsPanel.select') }}</n-button>
|
||||||
|
<n-button class="flex-1" @click="openDownloadPath">
|
||||||
|
{{ t('download.settingsPanel.open') }}
|
||||||
|
<i class="iconfont ri-folder-open-line"></i>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件名格式设置 -->
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="setting-title">{{ t('download.settingsPanel.fileFormat') }}</div>
|
||||||
|
<div class="setting-desc">{{ t('download.settingsPanel.fileFormatDesc') }}</div>
|
||||||
|
|
||||||
|
<!-- 预设模板 -->
|
||||||
|
<div class="flex gap-2 my-2">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="downloadSettings.nameFormat === '{songName} - {artistName}' ? 'primary' : 'default'"
|
||||||
|
@click="downloadSettings.nameFormat = '{songName} - {artistName}'"
|
||||||
|
>
|
||||||
|
{{ t('download.settingsPanel.presets.songArtist') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="downloadSettings.nameFormat === '{artistName} - {songName}' ? 'primary' : 'default'"
|
||||||
|
@click="downloadSettings.nameFormat = '{artistName} - {songName}'"
|
||||||
|
>
|
||||||
|
{{ t('download.settingsPanel.presets.artistSong') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="downloadSettings.nameFormat === '{songName}' ? 'primary' : 'default'"
|
||||||
|
@click="downloadSettings.nameFormat = '{songName}'"
|
||||||
|
>
|
||||||
|
{{ t('download.settingsPanel.presets.songOnly') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分隔符设置 -->
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="text-sm text-gray-500 mb-2">{{ t('download.settingsPanel.separator') || '分隔符' }}</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="downloadSettings.separator === ' - ' ? 'primary' : 'default'"
|
||||||
|
@click="downloadSettings.separator = ' - '"
|
||||||
|
>
|
||||||
|
{{ t('download.settingsPanel.separators.dash') || '空格-空格' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="downloadSettings.separator === '_' ? 'primary' : 'default'"
|
||||||
|
@click="downloadSettings.separator = '_'"
|
||||||
|
>
|
||||||
|
{{ t('download.settingsPanel.separators.underscore') || '下划线' }}
|
||||||
|
</n-button>
|
||||||
|
<n-button
|
||||||
|
size="small"
|
||||||
|
:type="downloadSettings.separator === ' ' ? 'primary' : 'default'"
|
||||||
|
@click="downloadSettings.separator = ' '"
|
||||||
|
>
|
||||||
|
{{ t('download.settingsPanel.separators.space') || '空格' }}
|
||||||
|
</n-button>
|
||||||
|
<n-input
|
||||||
|
v-model:value="downloadSettings.separator"
|
||||||
|
size="small"
|
||||||
|
style="width: 100px"
|
||||||
|
placeholder="自定义"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 组件排序 -->
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="text-sm text-gray-500 mb-2">{{ t('download.settingsPanel.dragToArrange') }}</div>
|
||||||
|
<div class="format-components">
|
||||||
|
<div v-for="(component, index) in formatComponents" :key="component.id" class="format-item">
|
||||||
|
<div class="flex items-center justify-between w-full">
|
||||||
|
<span>{{ t(`download.settingsPanel.components.${component.type}`) }}</span>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<n-button quaternary circle size="small" @click="handleMoveUp(index)" :disabled="index === 0">
|
||||||
|
<template #icon><i class="iconfont ri-arrow-up-s-line"></i></template>
|
||||||
|
</n-button>
|
||||||
|
<n-button quaternary circle size="small" @click="handleMoveDown(index)" :disabled="index === formatComponents.length - 1">
|
||||||
|
<template #icon><i class="iconfont ri-arrow-down-s-line"></i></template>
|
||||||
|
</n-button>
|
||||||
|
<n-button quaternary circle size="small" @click="removeFormatComponent(index)" :disabled="formatComponents.length <= 1">
|
||||||
|
<template #icon><i class="iconfont ri-close-line"></i></template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex gap-2">
|
||||||
|
<n-button size="small" @click="addFormatComponent('songName')" :disabled="formatComponents.some(c => c.type === 'songName')">
|
||||||
|
+{{ t('download.settingsPanel.components.songName') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" @click="addFormatComponent('artistName')" :disabled="formatComponents.some(c => c.type === 'artistName')">
|
||||||
|
+{{ t('download.settingsPanel.components.artistName') }}
|
||||||
|
</n-button>
|
||||||
|
<n-button size="small" @click="addFormatComponent('albumName')" :disabled="formatComponents.some(c => c.type === 'albumName')">
|
||||||
|
+{{ t('download.settingsPanel.components.albumName') }}
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自定义格式 -->
|
||||||
|
<div class="my-3">
|
||||||
|
<div class="text-sm text-gray-500 mb-2">{{ t('download.settingsPanel.customFormat') }}</div>
|
||||||
|
<n-input
|
||||||
|
v-model:value="downloadSettings.nameFormat"
|
||||||
|
placeholder="{artistName} - {songName} - {albumName}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2 text-xs text-amber-500">
|
||||||
|
<i class="iconfont ri-information-line"></i>
|
||||||
|
{{ t('download.settingsPanel.formatVariables') }}:<br>
|
||||||
|
{songName}, {artistName}, {albumName}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 预览 -->
|
||||||
|
<div class="format-preview mt-3 bg-gray-100 dark:bg-dark-300 p-2 rounded">
|
||||||
|
<div class="text-xs text-gray-500 mb-1">{{ t('download.settingsPanel.preview') }}</div>
|
||||||
|
<div class="preview-content">{{ formatNamePreview }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</n-drawer-content>
|
||||||
|
</n-drawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@@ -233,6 +397,7 @@ interface DownloadedItem {
|
|||||||
id: number;
|
id: number;
|
||||||
picUrl: string;
|
picUrl: string;
|
||||||
ar: { name: string }[];
|
ar: { name: string }[];
|
||||||
|
displayName?: string;
|
||||||
}
|
}
|
||||||
const tabName = ref('downloading');
|
const tabName = ref('downloading');
|
||||||
|
|
||||||
@@ -330,14 +495,14 @@ const handlePlayMusic = async (item: DownloadedItem) => {
|
|||||||
const fileExists = await window.electron.ipcRenderer.invoke('check-file-exists', item.path);
|
const fileExists = await window.electron.ipcRenderer.invoke('check-file-exists', item.path);
|
||||||
|
|
||||||
if (!fileExists) {
|
if (!fileExists) {
|
||||||
message.error(t('download.delete.fileNotFound', { name: item.filename }));
|
message.error(t('download.delete.fileNotFound', { name: item.displayName || item.filename }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 转换下载项为播放所需的歌曲对象
|
// 转换下载项为播放所需的歌曲对象
|
||||||
const song: SongResult = {
|
const song: SongResult = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
name: item.filename,
|
name: item.displayName || item.filename,
|
||||||
ar: item.ar?.map(a => ({
|
ar: item.ar?.map(a => ({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
@@ -373,10 +538,10 @@ const handlePlayMusic = async (item: DownloadedItem) => {
|
|||||||
playerStore.setPlayMusic(true);
|
playerStore.setPlayMusic(true);
|
||||||
playerStore.setIsPlay(true);
|
playerStore.setIsPlay(true);
|
||||||
|
|
||||||
message.success(t('download.playStarted', { name: item.filename }));
|
message.success(t('download.playStarted', { name: item.displayName || item.filename }));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('播放音乐失败:', error);
|
console.error('播放音乐失败:', error);
|
||||||
message.error(t('download.playFailed', { name: item.filename }));
|
message.error(t('download.playFailed', { name: item.displayName || item.filename }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -439,6 +604,25 @@ const clearDownloadRecords = async () => {
|
|||||||
// 添加加载状态
|
// 添加加载状态
|
||||||
const isLoadingDownloaded = ref(false);
|
const isLoadingDownloaded = ref(false);
|
||||||
|
|
||||||
|
// 格式化歌曲名称,应用用户设置的格式
|
||||||
|
const formatSongName = (songInfo) => {
|
||||||
|
if (!songInfo) return '';
|
||||||
|
|
||||||
|
// 获取格式设置
|
||||||
|
const nameFormat = downloadSettings.value.nameFormat || '{songName} - {artistName}';
|
||||||
|
|
||||||
|
// 准备替换变量
|
||||||
|
const artistName = songInfo.ar?.map((a) => a.name).join('/') || '未知艺术家';
|
||||||
|
const songName = songInfo.name || songInfo.filename || '未知歌曲';
|
||||||
|
const albumName = songInfo.al?.name || '未知专辑';
|
||||||
|
|
||||||
|
// 应用自定义格式
|
||||||
|
return nameFormat
|
||||||
|
.replace(/\{songName\}/g, songName)
|
||||||
|
.replace(/\{artistName\}/g, artistName)
|
||||||
|
.replace(/\{albumName\}/g, albumName);
|
||||||
|
};
|
||||||
|
|
||||||
// 获取已下载音乐列表
|
// 获取已下载音乐列表
|
||||||
const refreshDownloadedList = async () => {
|
const refreshDownloadedList = async () => {
|
||||||
if (isLoadingDownloaded.value) return; // 防止重复加载
|
if (isLoadingDownloaded.value) return; // 防止重复加载
|
||||||
@@ -455,8 +639,14 @@ const refreshDownloadedList = async () => {
|
|||||||
|
|
||||||
const songIds = list.filter(item => item.id).map(item => item.id);
|
const songIds = list.filter(item => item.id).map(item => item.id);
|
||||||
if (songIds.length === 0) {
|
if (songIds.length === 0) {
|
||||||
downloadedList.value = list;
|
// 处理显示格式化文件名
|
||||||
localStorage.setItem('downloadedList', JSON.stringify(list));
|
const updatedList = list.map(item => ({
|
||||||
|
...item,
|
||||||
|
displayName: formatSongName(item) || item.filename
|
||||||
|
}));
|
||||||
|
|
||||||
|
downloadedList.value = updatedList;
|
||||||
|
localStorage.setItem('downloadedList', JSON.stringify(updatedList));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,18 +657,32 @@ const refreshDownloadedList = async () => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const updatedList = list.map(item => ({
|
const updatedList = list.map(item => {
|
||||||
...item,
|
const songDetail = songDetails[item.id];
|
||||||
picUrl: songDetails[item.id]?.al?.picUrl || item.picUrl || '/images/default_cover.png',
|
const updatedItem = {
|
||||||
ar: songDetails[item.id]?.ar || item.ar || [{ name: t('download.localMusic') }]
|
...item,
|
||||||
}));
|
picUrl: songDetail?.al?.picUrl || item.picUrl || '/images/default_cover.png',
|
||||||
|
ar: songDetail?.ar || item.ar || [{ name: t('download.localMusic') }],
|
||||||
|
name: songDetail?.name || item.name || item.filename
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加格式化的显示名称
|
||||||
|
updatedItem.displayName = formatSongName(updatedItem) || updatedItem.filename;
|
||||||
|
return updatedItem;
|
||||||
|
});
|
||||||
|
|
||||||
downloadedList.value = updatedList;
|
downloadedList.value = updatedList;
|
||||||
localStorage.setItem('downloadedList', JSON.stringify(updatedList));
|
localStorage.setItem('downloadedList', JSON.stringify(updatedList));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get music details:', error);
|
console.error('Failed to get music details:', error);
|
||||||
downloadedList.value = list;
|
// 处理显示格式化文件名
|
||||||
localStorage.setItem('downloadedList', JSON.stringify(list));
|
const updatedList = list.map(item => ({
|
||||||
|
...item,
|
||||||
|
displayName: formatSongName(item) || item.filename
|
||||||
|
}));
|
||||||
|
|
||||||
|
downloadedList.value = updatedList;
|
||||||
|
localStorage.setItem('downloadedList', JSON.stringify(updatedList));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to get downloaded music list:', error);
|
console.error('Failed to get downloaded music list:', error);
|
||||||
@@ -498,10 +702,13 @@ watch(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// 监听下载进度
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
refreshDownloadedList();
|
refreshDownloadedList();
|
||||||
|
|
||||||
|
// 记录已处理的下载项,避免重复触发事件
|
||||||
|
const processedDownloads = new Set<string>();
|
||||||
|
|
||||||
// 监听下载进度
|
// 监听下载进度
|
||||||
window.electron.ipcRenderer.on('music-download-progress', (_, data) => {
|
window.electron.ipcRenderer.on('music-download-progress', (_, data) => {
|
||||||
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
const existingItem = downloadList.value.find((item) => item.filename === data.filename);
|
||||||
@@ -517,7 +724,8 @@ onMounted(() => {
|
|||||||
songInfo: data.songInfo || existingItem.songInfo
|
songInfo: data.songInfo || existingItem.songInfo
|
||||||
});
|
});
|
||||||
|
|
||||||
// 如果下载完成,从列表中移除
|
// 如果下载完成,从列表中移除,但不触发完成通知
|
||||||
|
// 通知由 music-download-complete 事件处理
|
||||||
if (data.status === 'completed') {
|
if (data.status === 'completed') {
|
||||||
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
downloadList.value = downloadList.value.filter((item) => item.filename !== data.filename);
|
||||||
}
|
}
|
||||||
@@ -531,12 +739,31 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 监听下载完成
|
// 监听下载完成
|
||||||
window.electron.ipcRenderer.on('music-download-complete', async (_, data) => {
|
window.electron.ipcRenderer.on('music-download-complete', async (_, data) => {
|
||||||
|
// 如果已经处理过此文件的完成事件,则跳过
|
||||||
|
if (processedDownloads.has(data.filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记为已处理
|
||||||
|
processedDownloads.add(data.filename);
|
||||||
|
|
||||||
|
// 下载成功处理
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
// 从下载列表中移除
|
||||||
downloadList.value = downloadList.value.filter(item => item.filename !== data.filename);
|
downloadList.value = downloadList.value.filter(item => item.filename !== data.filename);
|
||||||
|
|
||||||
// 延迟刷新已下载列表,避免文件系统未完全写入
|
// 延迟刷新已下载列表,避免文件系统未完全写入
|
||||||
setTimeout(() => refreshDownloadedList(), 500);
|
setTimeout(() => refreshDownloadedList(), 500);
|
||||||
|
|
||||||
|
// 只在下载页面显示一次下载成功通知
|
||||||
message.success(t('download.message.downloadComplete', { filename: data.filename }));
|
message.success(t('download.message.downloadComplete', { filename: data.filename }));
|
||||||
|
|
||||||
|
// 避免通知过多占用内存,设置一个超时来清理已处理的标记
|
||||||
|
setTimeout(() => {
|
||||||
|
processedDownloads.delete(data.filename);
|
||||||
|
}, 10000); // 10秒后清除
|
||||||
} else {
|
} else {
|
||||||
|
// 下载失败处理
|
||||||
const existingItem = downloadList.value.find(item => item.filename === data.filename);
|
const existingItem = downloadList.value.find(item => item.filename === data.filename);
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
Object.assign(existingItem, {
|
Object.assign(existingItem, {
|
||||||
@@ -546,8 +773,10 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
downloadList.value = downloadList.value.filter(item => item.filename !== data.filename);
|
downloadList.value = downloadList.value.filter(item => item.filename !== data.filename);
|
||||||
|
processedDownloads.delete(data.filename);
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
message.error(t('download.message.downloadFailed', { filename: data.filename, error: data.error }));
|
message.error(t('download.message.downloadFailed', { filename: data.filename, error: data.error }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -568,6 +797,178 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 下载设置
|
||||||
|
const showSettingsDrawer = ref(false);
|
||||||
|
const downloadSettings = ref({
|
||||||
|
path: '',
|
||||||
|
nameFormat: '{songName} - {artistName}',
|
||||||
|
separator: ' - '
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式组件(用于拖拽排序)
|
||||||
|
const formatComponents = ref([
|
||||||
|
{ id: 1, type: 'songName' },
|
||||||
|
{ id: 2, type: 'artistName' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 处理组件排序
|
||||||
|
const handleMoveUp = (index: number) => {
|
||||||
|
if (index > 0) {
|
||||||
|
const temp = formatComponents.value.splice(index, 1)[0];
|
||||||
|
formatComponents.value.splice(index - 1, 0, temp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMoveDown = (index: number) => {
|
||||||
|
if (index < formatComponents.value.length - 1) {
|
||||||
|
const temp = formatComponents.value.splice(index, 1)[0];
|
||||||
|
formatComponents.value.splice(index + 1, 0, temp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加新的格式组件
|
||||||
|
const addFormatComponent = (type: string) => {
|
||||||
|
if (!formatComponents.value.some(item => item.type === type)) {
|
||||||
|
formatComponents.value.push({
|
||||||
|
id: Date.now(),
|
||||||
|
type
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 删除格式组件
|
||||||
|
const removeFormatComponent = (index: number) => {
|
||||||
|
formatComponents.value.splice(index, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听组件变化更新格式
|
||||||
|
watch(formatComponents, (newComponents) => {
|
||||||
|
let format = '';
|
||||||
|
newComponents.forEach((component, index) => {
|
||||||
|
format += `{${component.type}}`;
|
||||||
|
if (index < newComponents.length - 1) {
|
||||||
|
format += downloadSettings.value.separator;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
downloadSettings.value.nameFormat = format;
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// 监听分隔符变化更新格式
|
||||||
|
watch(() => downloadSettings.value.separator, (newSeparator) => {
|
||||||
|
if (formatComponents.value.length > 1) {
|
||||||
|
// 重新构建格式字符串
|
||||||
|
let format = '';
|
||||||
|
formatComponents.value.forEach((component, index) => {
|
||||||
|
format += `{${component.type}}`;
|
||||||
|
if (index < formatComponents.value.length - 1) {
|
||||||
|
format += newSeparator;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
downloadSettings.value.nameFormat = format;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 格式名称预览
|
||||||
|
const formatNamePreview = computed(() => {
|
||||||
|
const format = downloadSettings.value.nameFormat;
|
||||||
|
return format
|
||||||
|
.replace(/\{songName\}/g, '莫失莫忘')
|
||||||
|
.replace(/\{artistName\}/g, '香蜜沉沉烬如霜')
|
||||||
|
.replace(/\{albumName\}/g, '电视剧原声带');
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选择下载路径
|
||||||
|
const selectDownloadPath = async () => {
|
||||||
|
const result = await window.electron.ipcRenderer.invoke('select-directory');
|
||||||
|
if (result && !result.canceled && result.filePaths.length > 0) {
|
||||||
|
downloadSettings.value.path = result.filePaths[0];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开下载路径
|
||||||
|
const openDownloadPath = () => {
|
||||||
|
if (downloadSettings.value.path) {
|
||||||
|
window.electron.ipcRenderer.send('open-directory', downloadSettings.value.path);
|
||||||
|
} else {
|
||||||
|
message.warning(t('download.settingsPanel.noPathSelected'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 保存下载设置
|
||||||
|
const saveDownloadSettings = () => {
|
||||||
|
// 保存到配置
|
||||||
|
window.electron.ipcRenderer.send('set-store-value', 'set.downloadPath', downloadSettings.value.path);
|
||||||
|
window.electron.ipcRenderer.send('set-store-value', 'set.downloadNameFormat', downloadSettings.value.nameFormat);
|
||||||
|
window.electron.ipcRenderer.send('set-store-value', 'set.downloadSeparator', downloadSettings.value.separator);
|
||||||
|
|
||||||
|
// 如果是在已下载页面,刷新列表以更新显示
|
||||||
|
if (tabName.value === 'downloaded') {
|
||||||
|
refreshDownloadedList();
|
||||||
|
}
|
||||||
|
|
||||||
|
message.success(t('download.settingsPanel.saveSuccess'));
|
||||||
|
showSettingsDrawer.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化下载设置
|
||||||
|
const initDownloadSettings = async () => {
|
||||||
|
// 获取当前配置
|
||||||
|
const path = await window.electron.ipcRenderer.invoke('get-store-value', 'set.downloadPath');
|
||||||
|
const nameFormat = await window.electron.ipcRenderer.invoke('get-store-value', 'set.downloadNameFormat');
|
||||||
|
const separator = await window.electron.ipcRenderer.invoke('get-store-value', 'set.downloadSeparator');
|
||||||
|
|
||||||
|
downloadSettings.value = {
|
||||||
|
path: path || await window.electron.ipcRenderer.invoke('get-downloads-path'),
|
||||||
|
nameFormat: nameFormat || '{songName} - {artistName}',
|
||||||
|
separator: separator || ' - '
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化排序组件
|
||||||
|
updateFormatComponents();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据格式更新组件
|
||||||
|
const updateFormatComponents = () => {
|
||||||
|
// 提取格式中的变量
|
||||||
|
const format = downloadSettings.value.nameFormat;
|
||||||
|
const matches = Array.from(format.matchAll(/\{(\w+)\}/g));
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
formatComponents.value = [
|
||||||
|
{ id: 1, type: 'songName' },
|
||||||
|
{ id: 2, type: 'artistName' }
|
||||||
|
];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
formatComponents.value = matches.map((match, index) => ({
|
||||||
|
id: index + 1,
|
||||||
|
type: match[1]
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听格式变化更新组件
|
||||||
|
watch(() => downloadSettings.value.nameFormat, updateFormatComponents);
|
||||||
|
|
||||||
|
// 监听命名格式变化,更新已下载文件的显示名称
|
||||||
|
watch(() => downloadSettings.value.nameFormat, () => {
|
||||||
|
if (downloadedList.value.length > 0) {
|
||||||
|
// 更新所有已下载项的显示名称
|
||||||
|
downloadedList.value = downloadedList.value.map(item => ({
|
||||||
|
...item,
|
||||||
|
displayName: formatSongName(item) || item.filename
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 保存到本地存储
|
||||||
|
localStorage.setItem('downloadedList', JSON.stringify(downloadedList.value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
onMounted(() => {
|
||||||
|
initDownloadSettings();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -992,6 +1393,37 @@ onMounted(() => {
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.download-settings {
|
||||||
|
@apply flex flex-col gap-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
@apply bg-white dark:bg-dark-200 p-3 rounded-lg shadow-sm;
|
||||||
|
@apply border border-gray-200/60 dark:border-gray-700/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-title {
|
||||||
|
@apply text-base font-medium;
|
||||||
|
@apply text-gray-800 dark:text-gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-desc {
|
||||||
|
@apply text-sm text-gray-500 dark:text-gray-400 mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-components {
|
||||||
|
@apply flex flex-col gap-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-item {
|
||||||
|
@apply flex items-center px-3 py-2 bg-gray-100 dark:bg-dark-300 rounded;
|
||||||
|
@apply border border-gray-200 dark:border-gray-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.format-preview {
|
||||||
|
@apply text-sm;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user