新增 我的列表-歌曲右击菜单-歌曲换源 功能

This commit is contained in:
lyswhut 2024-08-17 20:31:52 +08:00
parent 7cba01934c
commit 2b3b02cf97
17 changed files with 656 additions and 69 deletions

View File

@ -1,16 +1,3 @@
我们发布了关于 LX Music 项目发展调整与新项目计划的说明,
详情看: https://github.com/lyswhut/lx-music-desktop/issues/1912
### 新增
- 新增重复歌曲列表可以方便移除我的列表中的重复歌曲此列表会列出目标列表里歌曲名相同的歌曲可在“我的列表”里的列表名菜单中使用该功能与PC端的区别是可以点击歌曲名多选删除
- 新增打开当前歌曲详情页菜单,可以在歌曲菜单中使用
### 修复
- 修复潜在桌面歌词导致的崩溃问题
### 其他
- 更新 React native 到 v0.73.9
- 更新 exoplayer 到 v1.4.0
- 新增 我的列表-歌曲右击菜单-歌曲换源 功能,换源后下次再播放该列表的该歌曲时将优先尝试播放所选源的歌曲,该功能允许你手动指定来源以解决自动换源失败或者换源不准确的问题

View File

@ -20,6 +20,15 @@ export interface SourceSelectorType<S extends Sources> {
setSourceList: (list: S, activeSource: S[number]) => void
}
export const useSourceListI18n = (list: Sources) => {
const sourceNameType = useSettingValue('common.sourceNameType')
const t = useI18n()
return useMemo(() => {
return list.map(s => ({ label: t(`source_${sourceNameType}_${s}`), action: s }))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list, sourceNameType, t])
}
const Component = <S extends Sources>({ fontSize = 15, center, onSourceChange }: SourceSelectorProps<S>, ref: Ref<SourceSelectorType<S>>) => {
const sourceNameType = useSettingValue('common.sourceNameType')
const [list, setList] = useState([] as unknown as S)
@ -33,10 +42,7 @@ const Component = <S extends Sources>({ fontSize = 15, center, onSourceChange }:
},
}), [])
const sourceList_t = useMemo(() => {
return list.map(s => ({ label: t(`source_${sourceNameType}_${s}`), action: s }))
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [list, sourceNameType, t])
const sourceList_t = useSourceListI18n(list)
type DorpDownMenuProps = _DorpDownMenuProps<typeof sourceList_t>

View File

@ -8,17 +8,18 @@ import {
} from './online'
import { buildLyricInfo, getCachedLyricInfo } from './utils'
export const getMusicUrl = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {
export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
musicInfo: LX.Download.ListItem
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
allowToggleSource?: boolean
}): Promise<string> => {
// if (!isRefresh) {
// const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
// if (path) return path
// }
return getOnlineMusicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource })
return getOnlineMusicUrl({ musicInfo: musicInfo.metadata.musicInfo, isRefresh, onToggleSource, allowToggleSource })
}
export const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource = () => {} }: {

View File

@ -24,18 +24,20 @@ export const getMusicUrl = async({
quality,
isRefresh = false,
onToggleSource,
allowToggleSource,
}: {
musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
isRefresh?: boolean
quality?: LX.Quality
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
allowToggleSource?: boolean
}): Promise<string> => {
if ('progress' in musicInfo) {
return getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource })
return getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource })
} else if (musicInfo.source == 'local') {
return getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource })
return getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource })
} else {
return getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource })
return getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource, allowToggleSource })
}
}

View File

@ -66,10 +66,11 @@ const getOtherSourceByLocal = async<T>(musicInfo: LX.Music.MusicInfoLocal, handl
throw new Error('source not found')
}
export const getMusicUrl = async({ musicInfo, isRefresh, onToggleSource = () => {} }: {
export const getMusicUrl = async({ musicInfo, isRefresh, allowToggleSource = true, onToggleSource = () => {} }: {
musicInfo: LX.Music.MusicInfoLocal
isRefresh: boolean
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
allowToggleSource?: boolean
}): Promise<string> => {
if (!isRefresh) {
const path = await getLocalFilePath(musicInfo)
@ -84,6 +85,8 @@ export const getMusicUrl = async({ musicInfo, isRefresh, onToggleSource = () =>
})
} catch {}
if (!allowToggleSource) throw new Error('failed')
onToggleSource()
return getOtherSourceByLocal(musicInfo, async(otherSource) => {
return getOnlineOtherSourceMusicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {

View File

@ -56,12 +56,16 @@ const createDelayNextTimeout = (delay: number) => {
const { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000)
const { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTimeout } = createDelayNextTimeout(100000)
const createGettingUrlId = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem) => {
const tInfo = 'progress' in musicInfo ? musicInfo.metadata.musicInfo.meta.toggleMusicInfo : musicInfo.meta.toggleMusicInfo
return `${musicInfo.id}_${tInfo?.id ?? ''}`
}
/**
*
*/
const diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {
// return curMusicInfo !== playerState.playMusicInfo.musicInfo || playerState.isPlay
return curMusicInfo.id != global.lx.gettingUrlId || curMusicInfo.id != playerState.playMusicInfo.musicInfo?.id || playerState.isPlay
return createGettingUrlId(curMusicInfo) != global.lx.gettingUrlId || curMusicInfo.id != playerState.playMusicInfo.musicInfo?.id || playerState.isPlay
}
let cancelDelayRetry: (() => void) | null = null
@ -92,7 +96,13 @@ const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListIt
addLoadTimeout()
// const type = getPlayType(settingState.setting['player.isPlayHighQuality'], musicInfo)
let toggleMusicInfo = ('progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo).meta.toggleMusicInfo
return (toggleMusicInfo ? getMusicUrl({
musicInfo: toggleMusicInfo,
isRefresh,
allowToggleSource: false,
}) : Promise.reject(new Error('not found'))).catch(async() => {
return getMusicUrl({
musicInfo,
isRefresh,
@ -100,6 +110,7 @@ const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListIt
if (diffCurrentMusicInfo(musicInfo)) return
setStatusText(global.i18n.t('toggle_source_try'))
},
})
}).then(url => {
if (global.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
@ -122,7 +133,7 @@ export const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
// addLoadTimeout()
if (!diffCurrentMusicInfo(musicInfo)) return
if (cancelDelayRetry) cancelDelayRetry()
global.lx.gettingUrlId = musicInfo.id
global.lx.gettingUrlId = createGettingUrlId(musicInfo)
void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {
if (!url) return
setResource(musicInfo, url, playerState.progress.nowPlayTime)
@ -475,7 +486,7 @@ export const playPrev = async(isAutoToggle = false): Promise<void> => {
export const play = () => {
if (playerState.playMusicInfo.musicInfo == null) return
if (isEmpty()) {
if (playerState.playMusicInfo.musicInfo.id != global.lx.gettingUrlId) setMusicUrl(playerState.playMusicInfo.musicInfo)
if (createGettingUrlId(playerState.playMusicInfo.musicInfo) != global.lx.gettingUrlId) setMusicUrl(playerState.playMusicInfo.musicInfo)
return
}
void setPlay()

View File

@ -455,6 +455,7 @@
"timeout_exit_tip_max": "You can only set up to {num} minutes",
"timeout_exit_tip_off": "Set timer to stop playing",
"timeout_exit_tip_on": "Stop playing after {time}",
"toggle_source": "Source change",
"toggle_source_failed": "Failed to change the source, please try to manually search for the song in other sources to play",
"toggle_source_try": "Try switching to another source...",
"understand": "Already understood 👌",

View File

@ -455,6 +455,7 @@
"timeout_exit_tip_max": "最多只能设置{num}分钟哦",
"timeout_exit_tip_off": "设置定时停止播放",
"timeout_exit_tip_on": "{time} 后停止播放",
"toggle_source": "歌曲换源",
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
"toggle_source_try": "尝试切换到其他源...",
"understand": "已了解 👌",

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,7 @@ export interface ListMenuProps {
onEditMetadata: (selectInfo: SelectInfo) => void
onCopyName: (selectInfo: SelectInfo) => void
onChangePosition: (selectInfo: SelectInfo) => void
onToggleSource: (selectInfo: SelectInfo) => void
onMusicSourceDetail: (selectInfo: SelectInfo) => void
onDislikeMusic: (selectInfo: SelectInfo) => void
onRemove: (selectInfo: SelectInfo) => void
@ -67,6 +68,7 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
{ action: 'add', label: t('add_to') },
{ action: 'move', label: t('move_to') },
{ action: 'changePosition', label: t('change_position') },
{ action: 'toggleSource', label: t('toggle_source') },
{ action: 'copyName', label: t('copy_name') },
{ action: 'musicSourceDetail', disabled: musicInfo.source == 'local', label: t('music_source_detail') },
// { action: 'musicSearch', label: t('music_search') },
@ -124,6 +126,10 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
props.onChangePosition(selectInfo)
// setVIsibleMusicPosition(true)
break
case 'toggleSource':
props.onToggleSource(selectInfo)
// setVIsibleMusicPosition(true)
break
case 'musicSourceDetail':
props.onMusicSourceDetail(selectInfo)
// setVIsibleMusicPosition(true)

View File

@ -0,0 +1,529 @@
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, ScrollView, TouchableOpacity, View, type FlatListProps as _FlatListProps } from 'react-native'
import { scaleSizeH } from '@/utils/pixelRatio'
import { useTheme } from '@/store/theme/hook'
import { Icon } from '@/components/common/Icon'
import { useHorizontalMode, useUnmounted } from '@/utils/hooks'
import { useI18n } from '@/lang'
import Button from '@/components/common/Button'
import { useSourceListI18n } from '@/components/SourceSelector'
import { searchMusic } from '@/utils/musicSdk'
import { toNewMusicInfo } from '@/utils'
import { handleShowMusicSourceDetail, handleToggleSource } from './listAction'
import { BorderRadius, BorderWidths } from '@/theme'
type FlatListProps = _FlatListProps<LX.Music.MusicInfoOnline>
const ITEM_HEIGHT = scaleSizeH(56)
const Tabs = <T extends LX.OnlineSource>({ list, source, onChangeSource }: {
list: T[]
source: T | ''
onChangeSource: (source: T) => void
}) => {
const list_t = useSourceListI18n(list)
const theme = useTheme()
const scrollViewRef = useRef<ScrollView>(null)
return (
<ScrollView ref={scrollViewRef} style={styles.tabContainer} keyboardShouldPersistTaps={'always'} horizontal>
{
list_t.map(s => (
<TouchableOpacity
style={{ ...styles.tabButton, borderBottomColor: source == s.action ? theme['c-primary-background-active'] : 'transparent' }}
onPress={() => {
onChangeSource(s.action as T)
}}
key={s.action}
>
<Text style={styles.tabButtonText} color={source == s.action ? theme['c-primary-font-active'] : theme['c-font']}>{s.label}</Text>
</TouchableOpacity>
))
}
</ScrollView>
)
}
const Empty = ({ loading, error, onReload }: { loading: boolean, error: boolean, onReload: () => void }) => {
const theme = useTheme()
const t = useI18n()
const label = loading
? t('list_loading')
: error
? t('list_error')
: t('no_item')
return (
<View style={styles.noitem}>
{
error ? (
<Text onPress={onReload} color={theme['c-font-label']}>{label}</Text>
) : (
<Text color={theme['c-font-label']}>{label}</Text>
)
}
</View>
)
}
const ListItem = memo(({ info, onToggleSource, onOpenDetail }: {
info: LX.Music.MusicInfoOnline
onToggleSource: (info: LX.Music.MusicInfoOnline) => void
onOpenDetail: (info: LX.Music.MusicInfoOnline) => 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.name}</Text>
<View style={styles.listItemAlbum}>
<Text color={theme['c-font']} size={12} numberOfLines={1}>
{info.singer}
{
info.meta.albumName ? (
<Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.meta.albumName})</Text>
) : null
}
</Text>
</View>
</View>
<View style={styles.listItemLabel}>
{/* <Text style={styles.listItemLabelText} size={13} color={theme['c-300']}>{ info.source }</Text> */}
<Text style={styles.listItemLabelText} size={13} color={theme['c-300']}>{info.interval}</Text>
</View>
<View style={styles.listItemBtns}>
<Button style={styles.listItemBtn} onPress={() => { onOpenDetail(info) }}>
<Icon name="share" style={{ color: theme['c-button-font'] }} size={18} />
</Button>
<Button style={styles.listItemBtn} onPress={() => { onToggleSource(info) }}>
<Icon name="play" style={{ color: theme['c-button-font'] }} size={18} />
</Button>
</View>
</View>
)
}, (prevProps, nextProps) => {
return prevProps.info === nextProps.info
})
const List = ({ source, lists, onToggleSource }: {
source: LX.OnlineSource | ''
lists: Partial<Record<LX.OnlineSource, LX.Music.MusicInfoOnline[]>>
onToggleSource: (info?: LX.Music.MusicInfoOnline | null) => void
}) => {
const [list, setList] = useState<LX.Music.MusicInfoOnline[]>([])
const isFirstRef = useRef(true)
useEffect(() => {
if (isFirstRef.current) {
setList(lists[source as LX.OnlineSource] ?? [])
isFirstRef.current = false
return
}
requestAnimationFrame(() => {
setList(lists[source as LX.OnlineSource] ?? [])
})
}, [lists, source])
const openDetail = useCallback((musicInfo: LX.Music.MusicInfoOnline) => {
void handleShowMusicSourceDetail(musicInfo)
}, [])
const renderItem = useCallback(({ item }: { item: LX.Music.MusicInfoOnline, index: number }) => {
return <ListItem info={item} onToggleSource={onToggleSource} onOpenDetail={openDetail} />
}, [onToggleSource, openDetail])
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 (
<FlatList
style={styles.list}
maxToRenderPerBatch={4}
windowSize={8}
removeClippedSubviews={true}
initialNumToRender={12}
data={list}
renderItem={renderItem}
keyExtractor={getkey}
getItemLayout={getItemLayout}
/>
)
}
const SourceDetail = ({ info, onToggleSource }: { info: LX.Music.MusicInfo, onToggleSource: (info?: LX.Music.MusicInfoOnline | null) => void }) => {
const theme = useTheme()
const isHorizontalMode = useHorizontalMode()
const cleanToggle = useCallback(() => {
onToggleSource(null)
}, [onToggleSource])
const toggleSource = info.meta.toggleMusicInfo
return isHorizontalMode ? (
<View style={styles.detailContainer}>
<View style={styles.detailContainerX}>
<View style={styles.detailInfo}>
<View style={styles.detailInfoName}>
<Text style={styles.detailInfoNameText} color={theme['c-font']} size={13} numberOfLines={2}>
{info.name}
</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.source}</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.interval}</Text>
</View>
<View style={styles.listItemAlbum}>
<Text color={theme['c-font']} size={12} numberOfLines={1}>
{info.singer}
{
info.meta.albumName ? (
<Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.meta.albumName})</Text>
) : null
}
</Text>
</View>
</View>
{
toggleSource ? (
<>
<Text></Text>
<View style={styles.detailInfo}>
<View style={styles.detailInfoName}>
<Text style={styles.detailInfoNameText} color={theme['c-font']} size={13} numberOfLines={2}>
{toggleSource.name}
</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.source}</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.interval}</Text>
</View>
<View style={styles.listItemAlbum}>
<Text color={theme['c-font']} size={12} numberOfLines={1}>
{toggleSource.singer}
{
toggleSource.meta.albumName ? (
<Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({toggleSource.meta.albumName})</Text>
) : null
}
</Text>
</View>
</View>
</>
) : null
}
</View>
<Button
onPress={cleanToggle}
style={{ ...styles.button, backgroundColor: theme['c-button-background'] }}
disabled={!toggleSource}
>
<Text color={theme['c-button-font']}></Text>
</Button>
</View>
) : (
<View style={styles.detailContainer}>
<View style={styles.detailContainerY}>
<View style={styles.detailInfo}>
<View style={styles.detailInfoName}>
<Text style={styles.detailInfoNameText} color={theme['c-font']} size={14} numberOfLines={2}>
{info.name}
</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.source}</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{info.interval}</Text>
</View>
<View style={styles.listItemAlbum}>
<Text color={theme['c-font']} size={12} numberOfLines={1}>
{info.singer}
{
info.meta.albumName ? (
<Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({info.meta.albumName})</Text>
) : null
}
</Text>
</View>
</View>
{
toggleSource ? (
<>
<Text></Text>
<View style={styles.detailInfo}>
<View style={styles.detailInfoName}>
<Text style={styles.detailInfoNameText} color={theme['c-font']} size={14} numberOfLines={2}>
{toggleSource.name}
</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.source}</Text>
<Text style={styles.detailInfoLabelText} size={12} color={theme['c-primary']}>{toggleSource.interval}</Text>
</View>
<View style={styles.listItemAlbum}>
<Text color={theme['c-font']} size={12} numberOfLines={1}>
{toggleSource.singer}
{
toggleSource.meta.albumName ? (
<Text color={theme['c-font-label']} size={12} numberOfLines={1}> ({toggleSource.meta.albumName})</Text>
) : null
}
</Text>
</View>
</View>
</>
) : null
}
</View>
<Button
onPress={cleanToggle}
style={{ ...styles.button, backgroundColor: theme['c-button-background'] }}
disabled={!toggleSource}
>
<Text color={theme['c-button-font']}></Text>
</Button>
</View>
)
}
interface ModalType {
show: (info: SelectInfo) => void
}
const initInfo = {}
const Modal = forwardRef<ModalType, {}>((props, ref) => {
const [info, setInfo] = useState<SelectInfo>(initInfo as SelectInfo)
const [sourceInfo, setSourceInfo] = useState<{
sourceInfo: LX.OnlineSource[]
lists: Partial<Record<LX.OnlineSource, LX.Music.MusicInfoOnline[]>>
loading: boolean
error: boolean
}>({ sourceInfo: [], lists: {}, loading: false, error: false })
const [source, setSource] = useState<LX.OnlineSource | ''>('')
const dialogRef = useRef<DialogType>(null)
const isUnmountedRef = useUnmounted()
const loadData = useCallback((selectInfo: SelectInfo = info) => {
setSourceInfo({ sourceInfo: [], lists: {}, loading: true, error: false })
searchMusic({
name: selectInfo.musicInfo.name,
singer: selectInfo.musicInfo.singer,
source: '',
}).then((result: Array<{ source: LX.OnlineSource, list: LX.Music.MusicInfoOnline[] }>) => {
if (isUnmountedRef.current) return
const tags: LX.OnlineSource[] = []
const lists: Partial<Record<LX.OnlineSource, LX.Music.MusicInfoOnline[]>> = {}
for (const s of result) {
tags.push(s.source)
lists[s.source] = s.list.map(s => toNewMusicInfo(s) as LX.Music.MusicInfoOnline)
}
setSourceInfo({ sourceInfo: tags, lists, loading: false, error: false })
if (tags.length) setSource(tags[0])
}).catch(() => {
if (isUnmountedRef.current) return
setSourceInfo({ ...sourceInfo, error: true })
})
}, [info, isUnmountedRef, sourceInfo])
useImperativeHandle(ref, () => ({
show(info) {
setInfo(info)
setSource('')
loadData(info)
requestAnimationFrame(() => {
dialogRef.current?.setVisible(true)
})
},
}))
const toggleSource = useCallback((musicInfo?: LX.Music.MusicInfoOnline | null) => {
const newInfo = handleToggleSource(info.listId, info.musicInfo, musicInfo)
if (newInfo) {
setInfo({ ...info, musicInfo: newInfo })
} else dialogRef.current?.setVisible(false)
}, [info])
return (
<Dialog ref={dialogRef}>
<View style={styles.container}>
{
sourceInfo.sourceInfo.length
? (<>
<Tabs
list={sourceInfo.sourceInfo}
source={source}
onChangeSource={setSource}
/>
<List
source={source}
lists={sourceInfo.lists}
onToggleSource={toggleSource}
/>
</>)
: <Empty loading={sourceInfo.loading} error={sourceInfo.error} onReload={loadData} />
}
<SourceDetail info={info.musicInfo} onToggleSource={toggleSource} />
</View>
</Dialog>
)
})
export interface SelectInfo {
musicInfo: LX.Music.MusicInfo
listId: string
}
export interface MusicToggleModalType {
show: (listInfo: SelectInfo) => void
}
export default forwardRef<MusicToggleModalType, {}>((props, ref) => {
const musicAddModalRef = useRef<ModalType>(null)
const [visible, setVisible] = useState(false)
useImperativeHandle(ref, () => ({
show(musicInfo) {
if (visible) musicAddModalRef.current?.show(musicInfo)
else {
setVisible(true)
requestAnimationFrame(() => {
musicAddModalRef.current?.show(musicInfo)
})
}
},
}))
return (
visible
? <Modal ref={musicAddModalRef} />
: null
)
})
const styles = createStyle({
container: {
flexGrow: 1,
flexShrink: 1,
width: 600,
maxWidth: '100%',
},
tabContainer: {
flexGrow: 0,
flexShrink: 0,
// paddingLeft: 5,
// paddingRight: 5,
paddingVertical: 6,
},
tabButton: {
// height: 38,
// lineHeight: 38,
justifyContent: 'center',
paddingHorizontal: 6,
// width: 80,
// backgroundColor: 'rgba(0,0,0,0.1)',
borderBottomWidth: BorderWidths.normal3,
},
tabButtonText: {
// height: 38,
// lineHeight: 38,
textAlign: 'center',
paddingHorizontal: 2,
paddingVertical: 5,
},
list: {
flexGrow: 1,
flexShrink: 1,
},
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)',
paddingLeft: 15,
paddingRight: 5,
},
listItemAlbum: {
flexDirection: 'row',
marginTop: 3,
},
listItemLabel: {
flex: 0,
},
listItemLabelText: {
paddingHorizontal: 5,
},
listItemBtns: {
flex: 0,
flexDirection: 'row',
gap: 5,
paddingHorizontal: 8,
},
listItemBtn: {
padding: 8,
},
detailContainer: {
flexDirection: 'row',
gap: 5,
alignItems: 'center',
paddingHorizontal: 10,
paddingVertical: 10,
},
detailContainerY: {
flexDirection: 'column',
flexGrow: 1,
flexShrink: 1,
gap: 5,
},
detailContainerX: {
flexDirection: 'row',
flexGrow: 1,
flexShrink: 1,
gap: 5,
alignItems: 'center',
},
detailInfo: {
flexGrow: 0,
flexShrink: 1,
flexDirection: 'column',
// width: '50%',
justifyContent: 'center',
},
detailInfoName: {
gap: 5,
flexDirection: 'row',
flexGrow: 0,
flexShrink: 1,
// backgroundColor: 'rgba(0,0,0,0.2)',
},
detailInfoNameText: {
// backgroundColor: 'rgba(0,0,0,0.2)',
flexShrink: 1,
flexGrow: 0,
},
detailInfoLabelText: {
// backgroundColor: 'rgba(0,0,0,0.2)',
},
noitem: {
flexGrow: 1,
flexShrink: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
borderRadius: BorderRadius.normal,
paddingHorizontal: 10,
paddingVertical: 8,
alignItems: 'center',
},
})

View File

@ -14,6 +14,7 @@ import ListSearchBar, { type ListSearchBarType } from './ListSearchBar'
import ListMusicSearch, { type ListMusicSearchType } from './ListMusicSearch'
import MusicPositionModal, { type MusicPositionModalType } from './MusicPositionModal'
import MetadataEditModal, { type MetadataEditType, type MetadataEditProps } from '@/components/MetadataEditModal'
import MusicToggleModal, { type MusicToggleModalType } from './MusicToggleModal'
export default () => {
@ -28,6 +29,7 @@ export default () => {
const musicPositionModalRef = useRef<MusicPositionModalType>(null)
const metadataEditTypeRef = useRef<MetadataEditType>(null)
const listMenuRef = useRef<ListMenuType>(null)
const musicToggleModalRef = useRef<MusicToggleModalType>(null)
const layoutHeightRef = useRef<number>(0)
const isShowMultipleModeBar = useRef(false)
const isShowSearchBarModeBar = useRef(false)
@ -161,11 +163,13 @@ export default () => {
onMove={handleMoveMusic}
onEditMetadata={handleEditMetadata}
onChangePosition={info => musicPositionModalRef.current?.show(info)}
onToggleSource={info => musicToggleModalRef.current?.show(info)}
/>
<MetadataEditModal
ref={metadataEditTypeRef}
onUpdate={handleUpdateMetadata}
/>
<MusicToggleModal ref={musicToggleModalRef} />
</View>
)
}

View File

@ -10,6 +10,7 @@ import playerState from '@/store/player/state'
import type { SelectInfo } from './ListMenu'
import { type Metadata } from '@/components/MetadataEditModal'
import musicSdk from '@/utils/musicSdk'
import { getListMusicSync } from '@/utils/listManage'
export const handlePlay = (listId: SelectInfo['listId'], index: SelectInfo['index']) => {
void playList(listId, index)
@ -111,3 +112,28 @@ export const handleDislikeMusic = async(musicInfo: SelectInfo['musicInfo']) => {
void playNext(true)
}
}
export const handleToggleSource = (listId: string, musicInfo: LX.Music.MusicInfo, toggleMusicInfo?: LX.Music.MusicInfoOnline | null) => {
const list = getListMusicSync(listId)
const idx = list.findIndex(m => m.id == musicInfo.id)
if (idx < 0) return null
musicInfo.meta.toggleMusicInfo = toggleMusicInfo
const newInfo = {
...musicInfo,
meta: {
...musicInfo.meta,
toggleMusicInfo,
},
}
void updateListMusics([
{
id: listId,
musicInfo: newInfo as LX.Music.MusicInfo,
},
])
if (!!toggleMusicInfo || (playerState.playMusicInfo.listId == listId && playerState.playMusicInfo.musicInfo?.id == musicInfo.id)) {
void playList(listId, idx)
}
return newInfo as LX.Music.MusicInfo
}

View File

@ -22,6 +22,7 @@ declare namespace LX {
songId: string | number // 歌曲IDmg源为copyrightIdlocal为文件路径
albumName: string // 歌曲专辑名称
picUrl?: string | null // 歌曲图片链接
toggleMusicInfo?: MusicInfoOnline | null
}
interface MusicInfoMeta_online extends MusicInfoMetaBase {

View File

@ -57,10 +57,24 @@ export const init = () => {
return Promise.all(tasks)
}
export const searchMusic = async({ name, singer, source: s, limit = 25 }) => {
const trimStr = str => typeof str == 'string' ? str.trim() : str
const musicName = trimStr(name)
const tasks = []
const excludeSource = ['xm']
for (const source of sources.sources) {
if (!sources[source.id].musicSearch || source.id == s || excludeSource.includes(source.id)) continue
tasks.push(sources[source.id].musicSearch.search(`${musicName} ${singer || ''}`.trim(), 1, limit).catch(_ => null))
}
return (await Promise.all(tasks)).filter(s => s)
}
export const findMusic = async(musicInfo) => {
const { name, singer, albumName, interval, source: s } = musicInfo
const tasks = []
const lists = await searchMusic({ name, singer, source: s, limit: 25 })
const singersRxp = /、|&|;||\/|,||\|/
const sortSingle = singer => singersRxp.test(singer)
? singer.split(singersRxp).sort((a, b) => a.localeCompare(b)).join('、')
@ -86,12 +100,9 @@ export const findMusic = async(musicInfo) => {
const sortedSinger = filterStr(String(sortSingle(singer)).toLowerCase())
const lowerCaseName = filterStr(String(musicName).toLowerCase())
const lowerCaseAlbumName = filterStr(String(albumName).toLowerCase())
const excludeSource = ['xm']
for (const source of sources.sources) {
if (!sources[source.id].musicSearch || source.id == s || excludeSource.includes(source.id)) continue
tasks.push(sources[source.id].musicSearch.search(`${musicName} ${singer || ''}`.trim(), 1, 25).then(res => {
for (const item of res.list) {
const result = lists.map(source => {
for (const item of source.list) {
item.name = trimStr(item.name)
item.sortedSinger = filterStr(String(sortSingle(item.singer)).toLowerCase())
item.lowerCaseName = filterStr(String(item.name ?? '').toLowerCase())
@ -121,9 +132,7 @@ export const findMusic = async(musicInfo) => {
}
}
return null
}).catch(_ => null))
}
const result = (await Promise.all(tasks)).filter(s => s)
}).filter(s => s)
const newResult = []
if (result.length) {
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName && item.interval == interval))