mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-07-30 03:07: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.',
|
emptyState: 'No local music found. Please select a folder to scan.',
|
||||||
fileNotFound: 'File not found or has been moved',
|
fileNotFound: 'File not found or has been moved',
|
||||||
rescan: 'Rescan',
|
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: 'ローカル音楽がありません。フォルダを選択してスキャンしてください。',
|
emptyState: 'ローカル音楽がありません。フォルダを選択してスキャンしてください。',
|
||||||
fileNotFound: 'ファイルが見つからないか、移動されました',
|
fileNotFound: 'ファイルが見つからないか、移動されました',
|
||||||
rescan: '再スキャン',
|
rescan: '再スキャン',
|
||||||
songCount: '{count} 曲'
|
songCount: '{count} 曲',
|
||||||
|
removeFromLibrary: 'ライブラリから削除',
|
||||||
|
removedFromLibrary: 'ライブラリから削除しました(ファイルは削除されません)'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ export default {
|
|||||||
emptyState: '로컬 음악이 없습니다. 폴더를 선택하여 스캔하세요.',
|
emptyState: '로컬 음악이 없습니다. 폴더를 선택하여 스캔하세요.',
|
||||||
fileNotFound: '파일을 찾을 수 없거나 이동되었습니다',
|
fileNotFound: '파일을 찾을 수 없거나 이동되었습니다',
|
||||||
rescan: '다시 스캔',
|
rescan: '다시 스캔',
|
||||||
songCount: '{count}곡'
|
songCount: '{count}곡',
|
||||||
|
removeFromLibrary: '라이브러리에서 제거',
|
||||||
|
removedFromLibrary: '라이브러리에서 제거했습니다 (파일은 삭제되지 않음)'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ export default {
|
|||||||
emptyState: '暂无本地音乐,请先选择文件夹进行扫描',
|
emptyState: '暂无本地音乐,请先选择文件夹进行扫描',
|
||||||
fileNotFound: '文件不存在或已被移动',
|
fileNotFound: '文件不存在或已被移动',
|
||||||
rescan: '重新扫描',
|
rescan: '重新扫描',
|
||||||
songCount: '{count} 首歌曲'
|
songCount: '{count} 首歌曲',
|
||||||
|
removeFromLibrary: '从本地列表移除',
|
||||||
|
removedFromLibrary: '已从本地列表移除(不删除文件)'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,5 +9,7 @@ export default {
|
|||||||
emptyState: '暫無本地音樂,請先選擇資料夾進行掃描',
|
emptyState: '暫無本地音樂,請先選擇資料夾進行掃描',
|
||||||
fileNotFound: '檔案不存在或已被移動',
|
fileNotFound: '檔案不存在或已被移動',
|
||||||
rescan: '重新掃描',
|
rescan: '重新掃描',
|
||||||
songCount: '{count} 首歌曲'
|
songCount: '{count} 首歌曲',
|
||||||
|
removeFromLibrary: '從本機清單移除',
|
||||||
|
removedFromLibrary: '已從本機清單移除(不刪除檔案)'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ const userStore = useUserStore();
|
|||||||
// 后者不具备响应性,登录/登出后菜单禁用状态不会刷新(#706)
|
// 后者不具备响应性,登录/登出后菜单禁用状态不会刷新(#706)
|
||||||
const hasRealAuth = computed(() => !!userStore.user && userStore.loginType !== 'uid');
|
const hasRealAuth = computed(() => !!userStore.user && userStore.loginType !== 'uid');
|
||||||
|
|
||||||
|
// 本地歌曲:移除菜单项显示"从本地列表移除"而非"从歌单中删除"(#713)
|
||||||
|
const isLocalSong = computed(
|
||||||
|
() =>
|
||||||
|
typeof props.item.playMusicUrl === 'string' && props.item.playMusicUrl.startsWith('local://')
|
||||||
|
);
|
||||||
|
|
||||||
// 渲染歌曲预览
|
// 渲染歌曲预览
|
||||||
const renderSongPreview = () => {
|
const renderSongPreview = () => {
|
||||||
return h(
|
return h(
|
||||||
@@ -198,7 +204,9 @@ const dropdownOptions = computed<MenuOption[]>(() => {
|
|||||||
key: 'd2'
|
key: 'd2'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t('songItem.menu.removeFromPlaylist'),
|
label: isLocalSong.value
|
||||||
|
? t('localMusic.removeFromLibrary')
|
||||||
|
: t('songItem.menu.removeFromPlaylist'),
|
||||||
key: 'remove',
|
key: 'remove',
|
||||||
icon: () => h('i', { class: 'iconfont ri-delete-bin-line' })
|
icon: () => h('i', { class: 'iconfont ri-delete-bin-line' })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,9 @@ export const useLocalMusicStore = defineStore(
|
|||||||
|
|
||||||
// 磁盘上实际存在的文件路径集合(扫描时收集)
|
// 磁盘上实际存在的文件路径集合(扫描时收集)
|
||||||
const diskFilePaths = new Set<string>();
|
const diskFilePaths = new Set<string>();
|
||||||
|
// 扫描失败的文件夹:其下的缓存条目不参与"已删除清理",
|
||||||
|
// 避免移动盘/网络盘暂时不可用时整个文件夹的歌曲被误删(#713)
|
||||||
|
const failedFolders: string[] = [];
|
||||||
|
|
||||||
// 遍历每个文件夹进行扫描
|
// 遍历每个文件夹进行扫描
|
||||||
for (const folderPath of folderPaths.value) {
|
for (const folderPath of folderPaths.value) {
|
||||||
@@ -138,6 +141,7 @@ export const useLocalMusicStore = defineStore(
|
|||||||
if ((result as any).error) {
|
if ((result as any).error) {
|
||||||
console.error(`扫描文件夹失败: ${folderPath}`, (result as any).error);
|
console.error(`扫描文件夹失败: ${folderPath}`, (result as any).error);
|
||||||
message.error(`扫描失败: ${(result as any).error}`);
|
message.error(`扫描失败: ${(result as any).error}`);
|
||||||
|
failedFolders.push(folderPath);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +182,23 @@ export const useLocalMusicStore = defineStore(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`扫描文件夹出错: ${folderPath}`, error);
|
console.error(`扫描文件夹出错: ${folderPath}`, error);
|
||||||
message.error(`扫描文件夹出错: ${folderPath}`);
|
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 移除磁盘上不存在的条目
|
// 4. 清理已删除文件:从 IndexedDB 移除磁盘上不存在的条目
|
||||||
|
// (扫描失败的文件夹跳过清理,其文件未被枚举并不代表已删除)
|
||||||
for (const [filePath, entry] of cachedMap) {
|
for (const [filePath, entry] of cachedMap) {
|
||||||
if (!diskFilePaths.has(filePath)) {
|
if (!diskFilePaths.has(filePath) && !isUnderFailedFolder(filePath)) {
|
||||||
await localDB.deleteData(LOCAL_MUSIC_STORE, entry.id);
|
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,
|
removeFolder,
|
||||||
scanFolders,
|
scanFolders,
|
||||||
loadFromCache,
|
loadFromCache,
|
||||||
|
removeEntry,
|
||||||
clearCache
|
clearCache
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -143,7 +143,9 @@
|
|||||||
:key="item.id"
|
:key="item.id"
|
||||||
:index="index"
|
:index="index"
|
||||||
:item="item"
|
:item="item"
|
||||||
|
:can-remove="true"
|
||||||
@play="handlePlaySong"
|
@play="handlePlaySong"
|
||||||
|
@remove-song="handleRemoveSong"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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[] 后设置为播放列表并从第一首开始播放
|
* 将完整列表转换为 SongResult[] 后设置为播放列表并从第一首开始播放
|
||||||
|
|||||||
Reference in New Issue
Block a user