mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-03 17:12:10 +08:00
新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在“我的列表”里的列表名菜单中使用
This commit is contained in:
parent
a081b64a9c
commit
163daa1d66
@ -1,6 +1,6 @@
|
|||||||
我们发布了关于 LX Music 项目发展调整与新项目计划的说明,
|
我们发布了关于 LX Music 项目发展调整与新项目计划的说明,
|
||||||
详情看: https://github.com/lyswhut/lx-music-desktop/issues/1912
|
详情看: https://github.com/lyswhut/lx-music-desktop/issues/1912
|
||||||
|
|
||||||
### 修复
|
### 新增
|
||||||
|
|
||||||
- 修复数据存储管理在移除数据时可能出现移除失败的问题
|
- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在“我的列表”里的列表名菜单中使用
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
"list_update_error": "{name} failed to update",
|
"list_update_error": "{name} failed to update",
|
||||||
"list_update_success": "{name} updated successfully",
|
"list_update_success": "{name} updated successfully",
|
||||||
"list_updating": "updating",
|
"list_updating": "updating",
|
||||||
|
"lists__duplicate": "repeat song",
|
||||||
"lists_dislike_music_add_tip": "Added",
|
"lists_dislike_music_add_tip": "Added",
|
||||||
"lists_dislike_music_tip": "Do you really dislike {name}?",
|
"lists_dislike_music_tip": "Do you really dislike {name}?",
|
||||||
"load_failed": "Ah, loading failed 😥",
|
"load_failed": "Ah, loading failed 😥",
|
||||||
|
@ -115,6 +115,7 @@
|
|||||||
"list_update_error": "{name} 更新失败",
|
"list_update_error": "{name} 更新失败",
|
||||||
"list_update_success": "{name} 更新成功",
|
"list_update_success": "{name} 更新成功",
|
||||||
"list_updating": "更新中",
|
"list_updating": "更新中",
|
||||||
|
"lists__duplicate": "重复歌曲",
|
||||||
"lists_dislike_music_add_tip": "已添加",
|
"lists_dislike_music_add_tip": "已添加",
|
||||||
"lists_dislike_music_tip": "你真的不喜欢 {name} 吗?",
|
"lists_dislike_music_tip": "你真的不喜欢 {name} 吗?",
|
||||||
"load_failed": "啊 加载失败了 😥",
|
"load_failed": "啊 加载失败了 😥",
|
||||||
|
257
src/screens/Home/Views/Mylist/MyList/DuplicateMusic.tsx
Normal file
257
src/screens/Home/Views/Mylist/MyList/DuplicateMusic.tsx
Normal file
@ -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<DuplicateMusicItem>
|
||||||
|
const ITEM_HEIGHT = scaleSizeH(56)
|
||||||
|
|
||||||
|
const Title = ({ title }: {
|
||||||
|
title: string
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Text style={styles.title} size={16}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Empty = () => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.noitem}>
|
||||||
|
<Text color={theme['c-font-label']}>{t('no_item')}</Text>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListItem = memo(({ info, index, onRemove, onPlay }: {
|
||||||
|
info: DuplicateMusicItem
|
||||||
|
index: number
|
||||||
|
onPlay: (info: DuplicateMusicItem) => void
|
||||||
|
onRemove: (idx: number) => void
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ ...styles.listItem, height: ITEM_HEIGHT }} onStartShouldSetResponder={() => true}>
|
||||||
|
<View style={styles.listItemLabel}>
|
||||||
|
<Text style={styles.sn} size={13} color={theme['c-300']}>{info.index + 1}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.listItemInfo}>
|
||||||
|
<Text color={theme['c-font']} size={14} numberOfLines={1}>{info.musicInfo.name}</Text>
|
||||||
|
<View style={styles.listItemAlbum}>
|
||||||
|
<Text color={theme['c-font']} size={12} numberOfLines={1}>
|
||||||
|
{info.musicInfo.singer}
|
||||||
|
{
|
||||||
|
info.musicInfo.meta.albumName ? (
|
||||||
|
<Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.musicInfo.meta.albumName})</Text>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.listItemLabel}>
|
||||||
|
<Text style={styles.sn} size={13} color={theme['c-300']}>{ info.musicInfo.source }</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.listItemLabel}>
|
||||||
|
<Text style={styles.sn} size={13} color={theme['c-300']}>{info.musicInfo.interval}</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.listItemBtns}>
|
||||||
|
<Button style={styles.listItemBtn} onPress={() => { onPlay(info) }}>
|
||||||
|
<Icon name="play-outline" style={{ color: theme['c-button-font'] }} size={18} />
|
||||||
|
</Button>
|
||||||
|
<Button style={styles.listItemBtn} onPress={() => { onRemove(index) }}>
|
||||||
|
<Icon name="remove" style={{ color: theme['c-button-font'] }} size={18} />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const List = ({ listId }: { listId: string }) => {
|
||||||
|
const [list, setList] = useState<DuplicateMusicItem[]>([])
|
||||||
|
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 <ListItem info={item} index={index} onPlay={handlePlay} onRemove={handleRemove} />
|
||||||
|
}, [handlePlay, handleRemove])
|
||||||
|
const getkey = useCallback<NonNullable<FlatListProps['keyExtractor']>>(item => item.id, [])
|
||||||
|
const getItemLayout = useCallback<NonNullable<FlatListProps['getItemLayout']>>((data, index) => {
|
||||||
|
return { length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
list.length ? (
|
||||||
|
<FlatList
|
||||||
|
style={styles.list}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
keyboardShouldPersistTaps={'always'}
|
||||||
|
data={list}
|
||||||
|
renderItem={renderItem}
|
||||||
|
keyExtractor={getkey}
|
||||||
|
getItemLayout={getItemLayout}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Empty />
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModalType {
|
||||||
|
show: (info: LX.List.MyListInfo) => void
|
||||||
|
}
|
||||||
|
const initInfo = {}
|
||||||
|
|
||||||
|
const Modal = forwardRef<ModalType, {}>((props, ref) => {
|
||||||
|
const [info, setInfo] = useState<LX.List.MyListInfo>(initInfo as LX.List.MyListInfo)
|
||||||
|
const dialogRef = useRef<DialogType>(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 (
|
||||||
|
<Dialog ref={dialogRef} onHide={handleHide}>
|
||||||
|
{
|
||||||
|
info.name
|
||||||
|
? (<>
|
||||||
|
<Title title={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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -22,6 +22,7 @@ export interface ListMenuProps {
|
|||||||
onNew: (position: number) => void
|
onNew: (position: number) => void
|
||||||
onRename: (listInfo: LX.List.UserListInfo) => void
|
onRename: (listInfo: LX.List.UserListInfo) => void
|
||||||
onSort: (listInfo: LX.List.MyListInfo) => void
|
onSort: (listInfo: LX.List.MyListInfo) => void
|
||||||
|
onDuplicateMusic: (listInfo: LX.List.MyListInfo) => void
|
||||||
onImport: (listInfo: LX.List.MyListInfo, index: number) => void
|
onImport: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||||
onExport: (listInfo: LX.List.MyListInfo, index: number) => void
|
onExport: (listInfo: LX.List.MyListInfo, index: number) => void
|
||||||
onSync: (listInfo: LX.List.UserListInfo) => void
|
onSync: (listInfo: LX.List.UserListInfo) => void
|
||||||
@ -40,6 +41,7 @@ export default forwardRef<ListMenuType, ListMenuProps>(({
|
|||||||
onNew,
|
onNew,
|
||||||
onRename,
|
onRename,
|
||||||
onSort,
|
onSort,
|
||||||
|
onDuplicateMusic,
|
||||||
onImport,
|
onImport,
|
||||||
onExport,
|
onExport,
|
||||||
onSync,
|
onSync,
|
||||||
@ -88,6 +90,7 @@ export default forwardRef<ListMenuType, ListMenuProps>(({
|
|||||||
{ action: 'new', label: t('list_create') },
|
{ action: 'new', label: t('list_create') },
|
||||||
{ action: 'rename', disabled: !rename, label: t('list_rename') },
|
{ action: 'rename', disabled: !rename, label: t('list_rename') },
|
||||||
{ action: 'sort', label: t('list_sort') },
|
{ 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: 'local_file', disabled: !local_file, label: t('list_select_local_file') },
|
||||||
{ action: 'sync', disabled: !sync || !local_file, label: t('list_sync') },
|
{ action: 'sync', disabled: !sync || !local_file, label: t('list_sync') },
|
||||||
{ action: 'import', label: t('list_import') },
|
{ action: 'import', label: t('list_import') },
|
||||||
@ -109,6 +112,9 @@ export default forwardRef<ListMenuType, ListMenuProps>(({
|
|||||||
case 'sort':
|
case 'sort':
|
||||||
onSort(selectInfo.listInfo)
|
onSort(selectInfo.listInfo)
|
||||||
break
|
break
|
||||||
|
case 'duplicateMusic':
|
||||||
|
onDuplicateMusic(selectInfo.listInfo)
|
||||||
|
break
|
||||||
case 'import':
|
case 'import':
|
||||||
onImport(selectInfo.listInfo, selectInfo.index)
|
onImport(selectInfo.listInfo, selectInfo.index)
|
||||||
break
|
break
|
||||||
|
@ -6,6 +6,7 @@ import List from './List'
|
|||||||
import ListImportExport, { type ListImportExportType } from './ListImportExport'
|
import ListImportExport, { type ListImportExportType } from './ListImportExport'
|
||||||
import { handleRemove, handleSync } from './listAction'
|
import { handleRemove, handleSync } from './listAction'
|
||||||
import ListMusicSort, { type ListMusicSortType } from './ListMusicSort'
|
import ListMusicSort, { type ListMusicSortType } from './ListMusicSort'
|
||||||
|
import DuplicateMusic, { type DuplicateMusicType } from './DuplicateMusic'
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@ -13,6 +14,7 @@ export default () => {
|
|||||||
const listMenuRef = useRef<ListMenuType>(null)
|
const listMenuRef = useRef<ListMenuType>(null)
|
||||||
const listNameEditRef = useRef<ListNameEditType>(null)
|
const listNameEditRef = useRef<ListNameEditType>(null)
|
||||||
const listMusicSortRef = useRef<ListMusicSortType>(null)
|
const listMusicSortRef = useRef<ListMusicSortType>(null)
|
||||||
|
const duplicateMusicRef = useRef<DuplicateMusicType>(null)
|
||||||
const listImportExportRef = useRef<ListImportExportType>(null)
|
const listImportExportRef = useRef<ListImportExportType>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,12 +40,14 @@ export default () => {
|
|||||||
<List onShowMenu={(info, position) => listMenuRef.current?.show(info, position)} />
|
<List onShowMenu={(info, position) => listMenuRef.current?.show(info, position)} />
|
||||||
<ListNameEdit ref={listNameEditRef} />
|
<ListNameEdit ref={listNameEditRef} />
|
||||||
<ListMusicSort ref={listMusicSortRef} />
|
<ListMusicSort ref={listMusicSortRef} />
|
||||||
|
<DuplicateMusic ref={duplicateMusicRef} />
|
||||||
<ListImportExport ref={listImportExportRef} />
|
<ListImportExport ref={listImportExportRef} />
|
||||||
<ListMenu
|
<ListMenu
|
||||||
ref={listMenuRef}
|
ref={listMenuRef}
|
||||||
onNew={index => listNameEditRef.current?.showCreate(index)}
|
onNew={index => listNameEditRef.current?.showCreate(index)}
|
||||||
onRename={info => listNameEditRef.current?.show(info)}
|
onRename={info => listNameEditRef.current?.show(info)}
|
||||||
onSort={info => listMusicSortRef.current?.show(info)}
|
onSort={info => listMusicSortRef.current?.show(info)}
|
||||||
|
onDuplicateMusic={info => duplicateMusicRef.current?.show(info)}
|
||||||
onImport={(info, position) => listImportExportRef.current?.import(info, position)}
|
onImport={(info, position) => listImportExportRef.current?.import(info, position)}
|
||||||
onExport={(info, position) => listImportExportRef.current?.export(info, position)}
|
onExport={(info, position) => listImportExportRef.current?.export(info, position)}
|
||||||
onRemove={info => { handleRemove(info) }}
|
onRemove={info => { handleRemove(info) }}
|
||||||
|
@ -87,3 +87,55 @@ export const sortListMusicInfo = (list: LX.Music.MusicInfo[], sortType: 'up' | '
|
|||||||
}
|
}
|
||||||
return list
|
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()
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user