mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-07-08 02:17:30 +08:00
feat(local-music): 支持从本地列表移除单曲并防止扫描失败误删(#713)
- 右键菜单新增'从本地列表移除'(本地歌曲自动切换文案,仅移除条目不删文件,5 语言文案)
- localMusic store 新增 removeEntry action
- 扫描失败的文件夹不再参与'已删除清理',避免移动盘/网络盘暂时不可用时整夹歌曲被误删
- 注:'刷新不清理已删除歌曲'主症状已由 c28368f 修复,本次补齐评论区诉求与防护
Closes #713
This commit is contained in:
@@ -9,5 +9,7 @@ export default {
|
||||
emptyState: 'No local music found. Please select a folder to scan.',
|
||||
fileNotFound: 'File not found or has been moved',
|
||||
rescan: 'Rescan',
|
||||
songCount: '{count} songs'
|
||||
songCount: '{count} songs',
|
||||
removeFromLibrary: 'Remove from Library',
|
||||
removedFromLibrary: 'Removed from library (file not deleted)'
|
||||
};
|
||||
|
||||
@@ -9,5 +9,7 @@ export default {
|
||||
emptyState: 'ローカル音楽がありません。フォルダを選択してスキャンしてください。',
|
||||
fileNotFound: 'ファイルが見つからないか、移動されました',
|
||||
rescan: '再スキャン',
|
||||
songCount: '{count} 曲'
|
||||
songCount: '{count} 曲',
|
||||
removeFromLibrary: 'ライブラリから削除',
|
||||
removedFromLibrary: 'ライブラリから削除しました(ファイルは削除されません)'
|
||||
};
|
||||
|
||||
@@ -9,5 +9,7 @@ export default {
|
||||
emptyState: '로컬 음악이 없습니다. 폴더를 선택하여 스캔하세요.',
|
||||
fileNotFound: '파일을 찾을 수 없거나 이동되었습니다',
|
||||
rescan: '다시 스캔',
|
||||
songCount: '{count}곡'
|
||||
songCount: '{count}곡',
|
||||
removeFromLibrary: '라이브러리에서 제거',
|
||||
removedFromLibrary: '라이브러리에서 제거했습니다 (파일은 삭제되지 않음)'
|
||||
};
|
||||
|
||||
@@ -9,5 +9,7 @@ export default {
|
||||
emptyState: '暂无本地音乐,请先选择文件夹进行扫描',
|
||||
fileNotFound: '文件不存在或已被移动',
|
||||
rescan: '重新扫描',
|
||||
songCount: '{count} 首歌曲'
|
||||
songCount: '{count} 首歌曲',
|
||||
removeFromLibrary: '从本地列表移除',
|
||||
removedFromLibrary: '已从本地列表移除(不删除文件)'
|
||||
};
|
||||
|
||||
@@ -9,5 +9,7 @@ export default {
|
||||
emptyState: '暫無本地音樂,請先選擇資料夾進行掃描',
|
||||
fileNotFound: '檔案不存在或已被移動',
|
||||
rescan: '重新掃描',
|
||||
songCount: '{count} 首歌曲'
|
||||
songCount: '{count} 首歌曲',
|
||||
removeFromLibrary: '從本機清單移除',
|
||||
removedFromLibrary: '已從本機清單移除(不刪除檔案)'
|
||||
};
|
||||
|
||||
@@ -59,6 +59,12 @@ const userStore = useUserStore();
|
||||
// 后者不具备响应性,登录/登出后菜单禁用状态不会刷新(#706)
|
||||
const hasRealAuth = computed(() => !!userStore.user && userStore.loginType !== 'uid');
|
||||
|
||||
// 本地歌曲:移除菜单项显示"从本地列表移除"而非"从歌单中删除"(#713)
|
||||
const isLocalSong = computed(
|
||||
() =>
|
||||
typeof props.item.playMusicUrl === 'string' && props.item.playMusicUrl.startsWith('local://')
|
||||
);
|
||||
|
||||
// 渲染歌曲预览
|
||||
const renderSongPreview = () => {
|
||||
return h(
|
||||
@@ -198,7 +204,9 @@ const dropdownOptions = computed<MenuOption[]>(() => {
|
||||
key: 'd2'
|
||||
},
|
||||
{
|
||||
label: t('songItem.menu.removeFromPlaylist'),
|
||||
label: isLocalSong.value
|
||||
? t('localMusic.removeFromLibrary')
|
||||
: t('songItem.menu.removeFromPlaylist'),
|
||||
key: 'remove',
|
||||
icon: () => h('i', { class: 'iconfont ri-delete-bin-line' })
|
||||
}
|
||||
|
||||
@@ -127,6 +127,9 @@ export const useLocalMusicStore = defineStore(
|
||||
|
||||
// 磁盘上实际存在的文件路径集合(扫描时收集)
|
||||
const diskFilePaths = new Set<string>();
|
||||
// 扫描失败的文件夹:其下的缓存条目不参与"已删除清理",
|
||||
// 避免移动盘/网络盘暂时不可用时整个文件夹的歌曲被误删(#713)
|
||||
const failedFolders: string[] = [];
|
||||
|
||||
// 遍历每个文件夹进行扫描
|
||||
for (const folderPath of folderPaths.value) {
|
||||
@@ -138,6 +141,7 @@ export const useLocalMusicStore = defineStore(
|
||||
if ((result as any).error) {
|
||||
console.error(`扫描文件夹失败: ${folderPath}`, (result as any).error);
|
||||
message.error(`扫描失败: ${(result as any).error}`);
|
||||
failedFolders.push(folderPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -178,12 +182,23 @@ export const useLocalMusicStore = defineStore(
|
||||
} catch (error) {
|
||||
console.error(`扫描文件夹出错: ${folderPath}`, error);
|
||||
message.error(`扫描文件夹出错: ${folderPath}`);
|
||||
failedFolders.push(folderPath);
|
||||
}
|
||||
}
|
||||
|
||||
/** 判断文件路径是否位于某个扫描失败的文件夹下 */
|
||||
const isUnderFailedFolder = (filePath: string): boolean =>
|
||||
failedFolders.some((folder) => {
|
||||
if (!filePath.startsWith(folder)) return false;
|
||||
if (folder.endsWith('/') || folder.endsWith('\\')) return true;
|
||||
const next = filePath.charAt(folder.length);
|
||||
return next === '/' || next === '\\';
|
||||
});
|
||||
|
||||
// 4. 清理已删除文件:从 IndexedDB 移除磁盘上不存在的条目
|
||||
// (扫描失败的文件夹跳过清理,其文件未被枚举并不代表已删除)
|
||||
for (const [filePath, entry] of cachedMap) {
|
||||
if (!diskFilePaths.has(filePath)) {
|
||||
if (!diskFilePaths.has(filePath) && !isUnderFailedFolder(filePath)) {
|
||||
await localDB.deleteData(LOCAL_MUSIC_STORE, entry.id);
|
||||
}
|
||||
}
|
||||
@@ -213,6 +228,19 @@ export const useLocalMusicStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地列表移除单个条目(仅软件层面移除,不删除磁盘文件)(#713)
|
||||
* @param id 条目 ID(generateId 生成的 hex 字符串)
|
||||
*/
|
||||
async function removeEntry(id: string): Promise<void> {
|
||||
const localDB = await getDB();
|
||||
await localDB.deleteData(LOCAL_MUSIC_STORE, id);
|
||||
const index = musicList.value.findIndex((entry) => entry.id === id);
|
||||
if (index !== -1) {
|
||||
musicList.value.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存:检查文件存在性,移除已不存在的文件条目
|
||||
*/
|
||||
@@ -271,6 +299,7 @@ export const useLocalMusicStore = defineStore(
|
||||
removeFolder,
|
||||
scanFolders,
|
||||
loadFromCache,
|
||||
removeEntry,
|
||||
clearCache
|
||||
};
|
||||
},
|
||||
|
||||
@@ -143,7 +143,9 @@
|
||||
:key="item.id"
|
||||
:index="index"
|
||||
:item="item"
|
||||
:can-remove="true"
|
||||
@play="handlePlaySong"
|
||||
@remove-song="handleRemoveSong"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -283,6 +285,20 @@ async function handlePlaySong(_song: SongResult): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地列表移除单曲(仅软件层面移除,不删除磁盘文件)(#713)
|
||||
* @param id SongResult.id,即 LocalMusicEntry.id(hex 字符串)
|
||||
*/
|
||||
async function handleRemoveSong(id: number | string): Promise<void> {
|
||||
try {
|
||||
await localMusicStore.removeEntry(String(id));
|
||||
message.success(t('localMusic.removedFromLibrary'));
|
||||
} catch (error) {
|
||||
console.error('移除本地歌曲失败:', error);
|
||||
message.error(String(error));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放全部
|
||||
* 将完整列表转换为 SongResult[] 后设置为播放列表并从第一首开始播放
|
||||
|
||||
Reference in New Issue
Block a user