mirror of
https://github.com/atdunbg/Nekosonic-Music.git
synced 2026-06-22 00:58:51 +08:00
- 新增 src/api.ts,按职责分为 MusicApi/AudioApi/DeviceApi/DownloadApi/AppApi 五个命名空间 - 替换 15 个文件中所有 invoke 调用为 API 层方法 - 后端接口变更只需修改 api.ts 一处,便于后期迭代维护
135 lines
3.6 KiB
TypeScript
135 lines
3.6 KiB
TypeScript
import { reactive, watch } from 'vue';
|
|
import { listen } from '@tauri-apps/api/event';
|
|
import { useSettingsStore } from '../stores/settings';
|
|
import { showToast } from '../composables/useToast';
|
|
import { getCoverUrl, type Song } from '../utils/song';
|
|
import { DownloadApi } from '../api';
|
|
|
|
interface DownloadTask {
|
|
id: number;
|
|
name: string;
|
|
progress: number;
|
|
}
|
|
|
|
const downloadingIds = reactive<Set<number>>(new Set());
|
|
const tasks = reactive<DownloadTask[]>([]);
|
|
const localSongIds = reactive<Set<number>>(new Set());
|
|
|
|
let listenerSetup = false;
|
|
let storeSetup = false;
|
|
|
|
async function setupDownloadListener() {
|
|
if (listenerSetup) return;
|
|
listenerSetup = true;
|
|
await listen<{ id: number; progress: number; name: string }>('download-progress', (event) => {
|
|
const { id, progress, name } = event.payload;
|
|
if (progress >= 100) {
|
|
const idx = tasks.findIndex(t => t.id === id);
|
|
if (idx >= 0) {
|
|
tasks.splice(idx, 1);
|
|
downloadingIds.delete(id);
|
|
showToast(`${name} 下载完成`, 'success');
|
|
}
|
|
} else {
|
|
const task = tasks.find(t => t.id === id);
|
|
if (task) {
|
|
task.progress = progress;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async function refreshLocalIds() {
|
|
try {
|
|
const settings = useSettingsStore();
|
|
const list: { id: number }[] = await DownloadApi.listLocalSongs(settings.downloadPath || null);
|
|
localSongIds.clear();
|
|
for (const s of list) {
|
|
localSongIds.add(s.id);
|
|
}
|
|
} catch { /* 忽略 */ }
|
|
}
|
|
|
|
function ensureStoreSetup() {
|
|
if (storeSetup) return;
|
|
storeSetup = true;
|
|
const settings = useSettingsStore();
|
|
refreshLocalIds();
|
|
watch(() => settings.downloadPath, () => {
|
|
refreshLocalIds();
|
|
});
|
|
}
|
|
|
|
function isDownloaded(songId: number): boolean {
|
|
return localSongIds.has(songId);
|
|
}
|
|
|
|
function isDownloading(songId: number): boolean {
|
|
return downloadingIds.has(songId);
|
|
}
|
|
|
|
function getDownloadProgress(songId: number): number {
|
|
const task = tasks.find(t => t.id === songId);
|
|
return task?.progress ?? 0;
|
|
}
|
|
|
|
async function downloadSong(song: Song) {
|
|
if (downloadingIds.has(song.id)) return;
|
|
if (localSongIds.has(song.id)) {
|
|
showToast(`${song.name} 已下载`, 'info');
|
|
return;
|
|
}
|
|
|
|
const settings = useSettingsStore();
|
|
const artist = song.ar?.map(a => a.name).join(' / ') || '未知';
|
|
const albumName = song.al?.name || null;
|
|
const durationVal = song.dt || null;
|
|
const coverUrl = getCoverUrl(song) || null;
|
|
|
|
downloadingIds.add(song.id);
|
|
tasks.push({ id: song.id, name: song.name, progress: 0 });
|
|
|
|
try {
|
|
await DownloadApi.downloadSong({
|
|
id: song.id,
|
|
name: song.name,
|
|
artist,
|
|
album: albumName,
|
|
duration: durationVal,
|
|
coverUrl,
|
|
level: settings.audioQuality,
|
|
downloadPath: settings.downloadPath || null,
|
|
});
|
|
localSongIds.add(song.id);
|
|
} catch (e: any) {
|
|
downloadingIds.delete(song.id);
|
|
const idx = tasks.findIndex(t => t.id === song.id);
|
|
if (idx >= 0) tasks.splice(idx, 1);
|
|
if (e === '文件已存在') {
|
|
localSongIds.add(song.id);
|
|
showToast(`${song.name} 已下载`, 'info');
|
|
} else if (e === 'VIP歌曲无法下载') {
|
|
showToast(`${song.name} 为 VIP 歌曲,无法下载`, 'error');
|
|
} else if (typeof e === 'string' && e.includes('VIP')) {
|
|
showToast(`${song.name} 需要 VIP 权限才能下载`, 'error');
|
|
} else {
|
|
showToast(`下载失败: ${e}`, 'error');
|
|
}
|
|
}
|
|
}
|
|
|
|
export function useDownload() {
|
|
setupDownloadListener();
|
|
ensureStoreSetup();
|
|
return {
|
|
downloadingIds,
|
|
tasks,
|
|
localSongIds,
|
|
isDownloaded,
|
|
isDownloading,
|
|
getDownloadProgress,
|
|
downloadSong,
|
|
refreshLocalIds,
|
|
};
|
|
}
|