diff --git a/publish/changeLog.md b/publish/changeLog.md index ce94c43..2348ce0 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -1,6 +1,6 @@ 我们发布了关于 LX Music 项目发展调整与新项目计划的说明, 详情看: https://github.com/lyswhut/lx-music-desktop/issues/1912 -### 修复 +### 新增 -- 修复数据存储管理在移除数据时可能出现移除失败的问题 +- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在“我的列表”里的列表名菜单中使用 diff --git a/src/lang/en_us.json b/src/lang/en_us.json index ad92c22..300065c 100644 --- a/src/lang/en_us.json +++ b/src/lang/en_us.json @@ -115,6 +115,7 @@ "list_update_error": "{name} failed to update", "list_update_success": "{name} updated successfully", "list_updating": "updating", + "lists__duplicate": "repeat song", "lists_dislike_music_add_tip": "Added", "lists_dislike_music_tip": "Do you really dislike {name}?", "load_failed": "Ah, loading failed 😥", diff --git a/src/lang/zh_cn.json b/src/lang/zh_cn.json index d1c6309..d50d5f1 100644 --- a/src/lang/zh_cn.json +++ b/src/lang/zh_cn.json @@ -115,6 +115,7 @@ "list_update_error": "{name} 更新失败", "list_update_success": "{name} 更新成功", "list_updating": "更新中", + "lists__duplicate": "重复歌曲", "lists_dislike_music_add_tip": "已添加", "lists_dislike_music_tip": "你真的不喜欢 {name} 吗?", "load_failed": "啊 加载失败了 😥", diff --git a/src/screens/Home/Views/Mylist/MyList/DuplicateMusic.tsx b/src/screens/Home/Views/Mylist/MyList/DuplicateMusic.tsx new file mode 100644 index 0000000..272f54d --- /dev/null +++ b/src/screens/Home/Views/Mylist/MyList/DuplicateMusic.tsx @@ -0,0 +1,257 @@ +import { useRef, useImperativeHandle, forwardRef, useState, useCallback, memo, useEffect } from 'react' +import Text from '@/components/common/Text' +import { createStyle } from '@/utils/tools' +import Dialog, { type DialogType } from '@/components/common/Dialog' +import { FlatList, View, type FlatListProps as _FlatListProps } from 'react-native' +import { scaleSizeH } from '@/utils/pixelRatio' +import { useTheme } from '@/store/theme/hook' +import { type DuplicateMusicItem, filterDuplicateMusic } from './utils' +import { getListMusics, removeListMusics } from '@/core/list' +import Button from '@/components/common/Button' +import { Icon } from '@/components/common/Icon' +import { useUnmounted } from '@/utils/hooks' +import { playList } from '@/core/player/player' +import { useI18n } from '@/lang' + +type FlatListProps = _FlatListProps +const ITEM_HEIGHT = scaleSizeH(56) + +const Title = ({ title }: { + title: string +}) => { + return ( + + {title} + + ) +} + +const Empty = () => { + const theme = useTheme() + const t = useI18n() + + return ( + + {t('no_item')} + + ) +} + +const ListItem = memo(({ info, index, onRemove, onPlay }: { + info: DuplicateMusicItem + index: number + onPlay: (info: DuplicateMusicItem) => void + onRemove: (idx: number) => void +}) => { + const theme = useTheme() + + return ( + true}> + + {info.index + 1} + + + {info.musicInfo.name} + + + {info.musicInfo.singer} + { + info.musicInfo.meta.albumName ? ( + ({info.musicInfo.meta.albumName}) + ) : null + } + + + + + { info.musicInfo.source } + + + {info.musicInfo.interval} + + + + + + + ) +}) + +const List = ({ listId }: { listId: string }) => { + const [list, setList] = useState([]) + const isUnmountedRef = useUnmounted() + + const handleFilterList = useCallback(() => { + if (isUnmountedRef.current) return + void getListMusics(listId).then((list) => { + if (isUnmountedRef.current) return + void filterDuplicateMusic(list).then((l) => { + if (isUnmountedRef.current) return + setList(l) + }) + }) + }, [isUnmountedRef, listId]) + const handlePlay = useCallback((info: DuplicateMusicItem) => { + const { index: musicInfoIndex } = info + void playList(listId, musicInfoIndex) + }, [listId]) + const handleRemove = useCallback((index: number) => { + setList(list => { + const { musicInfo: targetMusicInfo } = list.splice(index, 1)[0] + void removeListMusics(listId, [targetMusicInfo.id]).then(() => { + handleFilterList() + }) + return [...list] + }) + }, [handleFilterList, listId]) + + useEffect(handleFilterList, [handleFilterList]) + + const renderItem = useCallback(({ item, index }: { item: DuplicateMusicItem, index: number }) => { + return + }, [handlePlay, handleRemove]) + const getkey = useCallback>(item => item.id, []) + const getItemLayout = useCallback>((data, index) => { + return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index } + }, []) + + return ( + list.length ? ( + + ) : ( + + ) + ) +} + +export interface ModalType { + show: (info: LX.List.MyListInfo) => void +} +const initInfo = {} + +const Modal = forwardRef((props, ref) => { + const [info, setInfo] = useState(initInfo as LX.List.MyListInfo) + const dialogRef = useRef(null) + useImperativeHandle(ref, () => ({ + show(info) { + setInfo(info) + + requestAnimationFrame(() => { + dialogRef.current?.setVisible(true) + }) + }, + })) + const handleHide = () => { + requestAnimationFrame(() => { + const ninfo = { ...info, name: '' } + setInfo(ninfo as LX.List.MyListInfo) + }) + } + + return ( + + { + info.name + ? (<> + + <List listId={info.id} /> + </>) + : null + } + </Dialog> + ) +}) + +export interface DuplicateMusicType { + show: (info: LX.List.MyListInfo) => void +} + +export default forwardRef<DuplicateMusicType, {}>((props, ref) => { + const musicAddModalRef = useRef<ModalType>(null) + const [visible, setVisible] = useState(false) + + useImperativeHandle(ref, () => ({ + show(listInfo) { + if (visible) musicAddModalRef.current?.show(listInfo) + else { + setVisible(true) + requestAnimationFrame(() => { + musicAddModalRef.current?.show(listInfo) + }) + } + }, + })) + + return ( + visible + ? <Modal ref={musicAddModalRef} /> + : null + ) +}) + + +const styles = createStyle({ + container: { + // flexGrow: 1, + }, + title: { + textAlign: 'center', + paddingVertical: 15, + // backgroundColor: 'rgba(0,0,0,0.2)', + }, + list: { + flexGrow: 0, + }, + listItem: { + flexDirection: 'row', + flexWrap: 'nowrap', + alignItems: 'center', + }, + sn: { + width: 38, + // fontSize: 12, + textAlign: 'center', + // backgroundColor: 'rgba(0,0,0,0.2)', + paddingLeft: 3, + paddingRight: 3, + }, + listItemInfo: { + flexGrow: 1, + flexShrink: 1, + // backgroundColor: 'rgba(0,0,0,0.2)', + }, + listItemAlbum: { + flexDirection: 'row', + marginTop: 3, + }, + listItemLabel: { + flex: 0, + }, + listItemBtns: { + flex: 0, + flexDirection: 'row', + gap: 5, + paddingHorizontal: 8, + }, + listItemBtn: { + padding: 5, + }, + noitem: { + paddingVertical: 35, + alignItems: 'center', + }, +}) + + diff --git a/src/screens/Home/Views/Mylist/MyList/ListMenu.tsx b/src/screens/Home/Views/Mylist/MyList/ListMenu.tsx index d00e37f..afbf309 100644 --- a/src/screens/Home/Views/Mylist/MyList/ListMenu.tsx +++ b/src/screens/Home/Views/Mylist/MyList/ListMenu.tsx @@ -22,6 +22,7 @@ export interface ListMenuProps { onNew: (position: number) => void onRename: (listInfo: LX.List.UserListInfo) => void onSort: (listInfo: LX.List.MyListInfo) => void + onDuplicateMusic: (listInfo: LX.List.MyListInfo) => void onImport: (listInfo: LX.List.MyListInfo, index: number) => void onExport: (listInfo: LX.List.MyListInfo, index: number) => void onSync: (listInfo: LX.List.UserListInfo) => void @@ -40,6 +41,7 @@ export default forwardRef<ListMenuType, ListMenuProps>(({ onNew, onRename, onSort, + onDuplicateMusic, onImport, onExport, onSync, @@ -88,6 +90,7 @@ export default forwardRef<ListMenuType, ListMenuProps>(({ { action: 'new', label: t('list_create') }, { action: 'rename', disabled: !rename, label: t('list_rename') }, { action: 'sort', label: t('list_sort') }, + { action: 'duplicateMusic', label: t('lists__duplicate') }, { action: 'local_file', disabled: !local_file, label: t('list_select_local_file') }, { action: 'sync', disabled: !sync || !local_file, label: t('list_sync') }, { action: 'import', label: t('list_import') }, @@ -109,6 +112,9 @@ export default forwardRef<ListMenuType, ListMenuProps>(({ case 'sort': onSort(selectInfo.listInfo) break + case 'duplicateMusic': + onDuplicateMusic(selectInfo.listInfo) + break case 'import': onImport(selectInfo.listInfo, selectInfo.index) break diff --git a/src/screens/Home/Views/Mylist/MyList/index.tsx b/src/screens/Home/Views/Mylist/MyList/index.tsx index 367cf4b..93fc752 100644 --- a/src/screens/Home/Views/Mylist/MyList/index.tsx +++ b/src/screens/Home/Views/Mylist/MyList/index.tsx @@ -6,6 +6,7 @@ import List from './List' import ListImportExport, { type ListImportExportType } from './ListImportExport' import { handleRemove, handleSync } from './listAction' import ListMusicSort, { type ListMusicSortType } from './ListMusicSort' +import DuplicateMusic, { type DuplicateMusicType } from './DuplicateMusic' export default () => { @@ -13,6 +14,7 @@ export default () => { const listMenuRef = useRef<ListMenuType>(null) const listNameEditRef = useRef<ListNameEditType>(null) const listMusicSortRef = useRef<ListMusicSortType>(null) + const duplicateMusicRef = useRef<DuplicateMusicType>(null) const listImportExportRef = useRef<ListImportExportType>(null) useEffect(() => { @@ -38,12 +40,14 @@ export default () => { <List onShowMenu={(info, position) => listMenuRef.current?.show(info, position)} /> <ListNameEdit ref={listNameEditRef} /> <ListMusicSort ref={listMusicSortRef} /> + <DuplicateMusic ref={duplicateMusicRef} /> <ListImportExport ref={listImportExportRef} /> <ListMenu ref={listMenuRef} onNew={index => listNameEditRef.current?.showCreate(index)} onRename={info => listNameEditRef.current?.show(info)} onSort={info => listMusicSortRef.current?.show(info)} + onDuplicateMusic={info => duplicateMusicRef.current?.show(info)} onImport={(info, position) => listImportExportRef.current?.import(info, position)} onExport={(info, position) => listImportExportRef.current?.export(info, position)} onRemove={info => { handleRemove(info) }} diff --git a/src/screens/Home/Views/Mylist/MyList/utils.ts b/src/screens/Home/Views/Mylist/MyList/utils.ts index f647c31..a645936 100644 --- a/src/screens/Home/Views/Mylist/MyList/utils.ts +++ b/src/screens/Home/Views/Mylist/MyList/utils.ts @@ -87,3 +87,55 @@ export const sortListMusicInfo = (list: LX.Music.MusicInfo[], sortType: 'up' | ' } return list } + + +const variantRxp = /(\(|().+(\)|))/g +const variantRxp2 = /\s|'|\.|,|,|&|"|、|\(|\)|(|)|`|~|-|<|>|\||\/|\]|\[/g +export interface DuplicateMusicItem { + id: string + index: number + musicInfo: LX.Music.MusicInfo +} +/** + * 过滤列表内重复的歌曲 + * @param list 歌曲列表 + * @param isFilterVariant 是否过滤 Live Explicit 等歌曲名 + * @returns + */ +export const filterDuplicateMusic = async(list: LX.Music.MusicInfo[], isFilterVariant: boolean = true) => { + const listMap = new Map<string, DuplicateMusicItem[]>() + const duplicateList = new Set<string>() + const handleFilter = (name: string, index: number, musicInfo: LX.Music.MusicInfo) => { + if (listMap.has(name)) { + const targetMusicInfo = listMap.get(name) + targetMusicInfo!.push({ + id: musicInfo.id, + index, + musicInfo, + }) + duplicateList.add(name) + } else { + listMap.set(name, [{ + id: musicInfo.id, + index, + musicInfo, + }]) + } + } + if (isFilterVariant) { + list.forEach((musicInfo, index) => { + let musicInfoName = musicInfo.name.toLowerCase().replace(variantRxp, '').replace(variantRxp2, '') + musicInfoName ||= musicInfo.name.toLowerCase().replace(/\s+/g, '') + handleFilter(musicInfoName, index, musicInfo) + }) + } else { + list.forEach((musicInfo, index) => { + const musicInfoName = musicInfo.name.toLowerCase().trim() + handleFilter(musicInfoName, index, musicInfo) + }) + } + // console.log(duplicateList) + const duplicateNames = Array.from(duplicateList) + duplicateNames.sort((a, b) => a.localeCompare(b)) + return duplicateNames.map(name => listMap.get(name)!).flat() +}