mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-05-23 22:37:41 +08:00
新增 我的列表-歌曲右击菜单-歌曲换源 功能
This commit is contained in:
parent
7cba01934c
commit
2b3b02cf97
Binary file not shown.
@ -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
|
|
||||||
|
@ -20,6 +20,15 @@ export interface SourceSelectorType<S extends Sources> {
|
|||||||
setSourceList: (list: S, activeSource: S[number]) => void
|
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 Component = <S extends Sources>({ fontSize = 15, center, onSourceChange }: SourceSelectorProps<S>, ref: Ref<SourceSelectorType<S>>) => {
|
||||||
const sourceNameType = useSettingValue('common.sourceNameType')
|
const sourceNameType = useSettingValue('common.sourceNameType')
|
||||||
const [list, setList] = useState([] as unknown as S)
|
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(() => {
|
const sourceList_t = useSourceListI18n(list)
|
||||||
return list.map(s => ({ label: t(`source_${sourceNameType}_${s}`), action: s }))
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [list, sourceNameType, t])
|
|
||||||
|
|
||||||
type DorpDownMenuProps = _DorpDownMenuProps<typeof sourceList_t>
|
type DorpDownMenuProps = _DorpDownMenuProps<typeof sourceList_t>
|
||||||
|
|
||||||
|
@ -8,17 +8,18 @@ import {
|
|||||||
} from './online'
|
} from './online'
|
||||||
import { buildLyricInfo, getCachedLyricInfo } from './utils'
|
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
|
musicInfo: LX.Download.ListItem
|
||||||
isRefresh: boolean
|
isRefresh: boolean
|
||||||
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
||||||
|
allowToggleSource?: boolean
|
||||||
}): Promise<string> => {
|
}): Promise<string> => {
|
||||||
// if (!isRefresh) {
|
// if (!isRefresh) {
|
||||||
// const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
|
// const path = await getDownloadFilePath(musicInfo, appSetting['download.savePath'])
|
||||||
// if (path) return path
|
// 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 = () => {} }: {
|
export const getPicUrl = async({ musicInfo, isRefresh, listId, onToggleSource = () => {} }: {
|
||||||
|
@ -24,18 +24,20 @@ export const getMusicUrl = async({
|
|||||||
quality,
|
quality,
|
||||||
isRefresh = false,
|
isRefresh = false,
|
||||||
onToggleSource,
|
onToggleSource,
|
||||||
|
allowToggleSource,
|
||||||
}: {
|
}: {
|
||||||
musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
|
musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
|
||||||
isRefresh?: boolean
|
isRefresh?: boolean
|
||||||
quality?: LX.Quality
|
quality?: LX.Quality
|
||||||
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
||||||
|
allowToggleSource?: boolean
|
||||||
}): Promise<string> => {
|
}): Promise<string> => {
|
||||||
if ('progress' in musicInfo) {
|
if ('progress' in musicInfo) {
|
||||||
return getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource })
|
return getDownloadMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource })
|
||||||
} else if (musicInfo.source == 'local') {
|
} else if (musicInfo.source == 'local') {
|
||||||
return getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource })
|
return getLocalMusicUrl({ musicInfo, isRefresh, onToggleSource, allowToggleSource })
|
||||||
} else {
|
} else {
|
||||||
return getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource })
|
return getOnlineMusicUrl({ musicInfo, isRefresh, quality, onToggleSource, allowToggleSource })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,10 +66,11 @@ const getOtherSourceByLocal = async<T>(musicInfo: LX.Music.MusicInfoLocal, handl
|
|||||||
throw new Error('source not found')
|
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
|
musicInfo: LX.Music.MusicInfoLocal
|
||||||
isRefresh: boolean
|
isRefresh: boolean
|
||||||
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
onToggleSource?: (musicInfo?: LX.Music.MusicInfoOnline) => void
|
||||||
|
allowToggleSource?: boolean
|
||||||
}): Promise<string> => {
|
}): Promise<string> => {
|
||||||
if (!isRefresh) {
|
if (!isRefresh) {
|
||||||
const path = await getLocalFilePath(musicInfo)
|
const path = await getLocalFilePath(musicInfo)
|
||||||
@ -84,6 +85,8 @@ export const getMusicUrl = async({ musicInfo, isRefresh, onToggleSource = () =>
|
|||||||
})
|
})
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|
||||||
|
if (!allowToggleSource) throw new Error('failed')
|
||||||
|
|
||||||
onToggleSource()
|
onToggleSource()
|
||||||
return getOtherSourceByLocal(musicInfo, async(otherSource) => {
|
return getOtherSourceByLocal(musicInfo, async(otherSource) => {
|
||||||
return getOnlineOtherSourceMusicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {
|
return getOnlineOtherSourceMusicUrl({ musicInfos: [...otherSource], onToggleSource, isRefresh }).then(({ url, quality: targetQuality, musicInfo: targetMusicInfo, isFromCache }) => {
|
||||||
|
@ -56,12 +56,16 @@ const createDelayNextTimeout = (delay: number) => {
|
|||||||
const { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000)
|
const { addDelayNextTimeout, clearDelayNextTimeout } = createDelayNextTimeout(5000)
|
||||||
const { addDelayNextTimeout: addLoadTimeout, clearDelayNextTimeout: clearLoadTimeout } = createDelayNextTimeout(100000)
|
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 => {
|
const diffCurrentMusicInfo = (curMusicInfo: LX.Music.MusicInfo | LX.Download.ListItem): boolean => {
|
||||||
// return curMusicInfo !== playerState.playMusicInfo.musicInfo || playerState.isPlay
|
// 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
|
let cancelDelayRetry: (() => void) | null = null
|
||||||
@ -92,14 +96,21 @@ const getMusicPlayUrl = async(musicInfo: LX.Music.MusicInfo | LX.Download.ListIt
|
|||||||
addLoadTimeout()
|
addLoadTimeout()
|
||||||
|
|
||||||
// const type = getPlayType(settingState.setting['player.isPlayHighQuality'], musicInfo)
|
// const type = getPlayType(settingState.setting['player.isPlayHighQuality'], musicInfo)
|
||||||
|
let toggleMusicInfo = ('progress' in musicInfo ? musicInfo.metadata.musicInfo : musicInfo).meta.toggleMusicInfo
|
||||||
|
|
||||||
return getMusicUrl({
|
return (toggleMusicInfo ? getMusicUrl({
|
||||||
musicInfo,
|
musicInfo: toggleMusicInfo,
|
||||||
isRefresh,
|
isRefresh,
|
||||||
onToggleSource(mInfo) {
|
allowToggleSource: false,
|
||||||
if (diffCurrentMusicInfo(musicInfo)) return
|
}) : Promise.reject(new Error('not found'))).catch(async() => {
|
||||||
setStatusText(global.i18n.t('toggle_source_try'))
|
return getMusicUrl({
|
||||||
},
|
musicInfo,
|
||||||
|
isRefresh,
|
||||||
|
onToggleSource(mInfo) {
|
||||||
|
if (diffCurrentMusicInfo(musicInfo)) return
|
||||||
|
setStatusText(global.i18n.t('toggle_source_try'))
|
||||||
|
},
|
||||||
|
})
|
||||||
}).then(url => {
|
}).then(url => {
|
||||||
if (global.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
|
if (global.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
|
||||||
|
|
||||||
@ -122,7 +133,7 @@ export const setMusicUrl = (musicInfo: LX.Music.MusicInfo | LX.Download.ListItem
|
|||||||
// addLoadTimeout()
|
// addLoadTimeout()
|
||||||
if (!diffCurrentMusicInfo(musicInfo)) return
|
if (!diffCurrentMusicInfo(musicInfo)) return
|
||||||
if (cancelDelayRetry) cancelDelayRetry()
|
if (cancelDelayRetry) cancelDelayRetry()
|
||||||
global.lx.gettingUrlId = musicInfo.id
|
global.lx.gettingUrlId = createGettingUrlId(musicInfo)
|
||||||
void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {
|
void getMusicPlayUrl(musicInfo, isRefresh).then((url) => {
|
||||||
if (!url) return
|
if (!url) return
|
||||||
setResource(musicInfo, url, playerState.progress.nowPlayTime)
|
setResource(musicInfo, url, playerState.progress.nowPlayTime)
|
||||||
@ -475,7 +486,7 @@ export const playPrev = async(isAutoToggle = false): Promise<void> => {
|
|||||||
export const play = () => {
|
export const play = () => {
|
||||||
if (playerState.playMusicInfo.musicInfo == null) return
|
if (playerState.playMusicInfo.musicInfo == null) return
|
||||||
if (isEmpty()) {
|
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
|
return
|
||||||
}
|
}
|
||||||
void setPlay()
|
void setPlay()
|
||||||
|
@ -455,6 +455,7 @@
|
|||||||
"timeout_exit_tip_max": "You can only set up to {num} minutes",
|
"timeout_exit_tip_max": "You can only set up to {num} minutes",
|
||||||
"timeout_exit_tip_off": "Set timer to stop playing",
|
"timeout_exit_tip_off": "Set timer to stop playing",
|
||||||
"timeout_exit_tip_on": "Stop playing after {time}",
|
"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_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...",
|
"toggle_source_try": "Try switching to another source...",
|
||||||
"understand": "Already understood 👌",
|
"understand": "Already understood 👌",
|
||||||
|
@ -455,6 +455,7 @@
|
|||||||
"timeout_exit_tip_max": "最多只能设置{num}分钟哦",
|
"timeout_exit_tip_max": "最多只能设置{num}分钟哦",
|
||||||
"timeout_exit_tip_off": "设置定时停止播放",
|
"timeout_exit_tip_off": "设置定时停止播放",
|
||||||
"timeout_exit_tip_on": "{time} 后停止播放",
|
"timeout_exit_tip_on": "{time} 后停止播放",
|
||||||
|
"toggle_source": "歌曲换源",
|
||||||
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
|
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
|
||||||
"toggle_source_try": "尝试切换到其他源...",
|
"toggle_source_try": "尝试切换到其他源...",
|
||||||
"understand": "已了解 👌",
|
"understand": "已了解 👌",
|
||||||
|
Binary file not shown.
File diff suppressed because one or more lines are too long
@ -21,6 +21,7 @@ export interface ListMenuProps {
|
|||||||
onEditMetadata: (selectInfo: SelectInfo) => void
|
onEditMetadata: (selectInfo: SelectInfo) => void
|
||||||
onCopyName: (selectInfo: SelectInfo) => void
|
onCopyName: (selectInfo: SelectInfo) => void
|
||||||
onChangePosition: (selectInfo: SelectInfo) => void
|
onChangePosition: (selectInfo: SelectInfo) => void
|
||||||
|
onToggleSource: (selectInfo: SelectInfo) => void
|
||||||
onMusicSourceDetail: (selectInfo: SelectInfo) => void
|
onMusicSourceDetail: (selectInfo: SelectInfo) => void
|
||||||
onDislikeMusic: (selectInfo: SelectInfo) => void
|
onDislikeMusic: (selectInfo: SelectInfo) => void
|
||||||
onRemove: (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: 'add', label: t('add_to') },
|
||||||
{ action: 'move', label: t('move_to') },
|
{ action: 'move', label: t('move_to') },
|
||||||
{ action: 'changePosition', label: t('change_position') },
|
{ action: 'changePosition', label: t('change_position') },
|
||||||
|
{ action: 'toggleSource', label: t('toggle_source') },
|
||||||
{ action: 'copyName', label: t('copy_name') },
|
{ action: 'copyName', label: t('copy_name') },
|
||||||
{ action: 'musicSourceDetail', disabled: musicInfo.source == 'local', label: t('music_source_detail') },
|
{ action: 'musicSourceDetail', disabled: musicInfo.source == 'local', label: t('music_source_detail') },
|
||||||
// { action: 'musicSearch', label: t('music_search') },
|
// { action: 'musicSearch', label: t('music_search') },
|
||||||
@ -124,6 +126,10 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
|
|||||||
props.onChangePosition(selectInfo)
|
props.onChangePosition(selectInfo)
|
||||||
// setVIsibleMusicPosition(true)
|
// setVIsibleMusicPosition(true)
|
||||||
break
|
break
|
||||||
|
case 'toggleSource':
|
||||||
|
props.onToggleSource(selectInfo)
|
||||||
|
// setVIsibleMusicPosition(true)
|
||||||
|
break
|
||||||
case 'musicSourceDetail':
|
case 'musicSourceDetail':
|
||||||
props.onMusicSourceDetail(selectInfo)
|
props.onMusicSourceDetail(selectInfo)
|
||||||
// setVIsibleMusicPosition(true)
|
// setVIsibleMusicPosition(true)
|
||||||
|
529
src/screens/Home/Views/Mylist/MusicList/MusicToggleModal.tsx
Normal file
529
src/screens/Home/Views/Mylist/MusicList/MusicToggleModal.tsx
Normal 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',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -14,6 +14,7 @@ import ListSearchBar, { type ListSearchBarType } from './ListSearchBar'
|
|||||||
import ListMusicSearch, { type ListMusicSearchType } from './ListMusicSearch'
|
import ListMusicSearch, { type ListMusicSearchType } from './ListMusicSearch'
|
||||||
import MusicPositionModal, { type MusicPositionModalType } from './MusicPositionModal'
|
import MusicPositionModal, { type MusicPositionModalType } from './MusicPositionModal'
|
||||||
import MetadataEditModal, { type MetadataEditType, type MetadataEditProps } from '@/components/MetadataEditModal'
|
import MetadataEditModal, { type MetadataEditType, type MetadataEditProps } from '@/components/MetadataEditModal'
|
||||||
|
import MusicToggleModal, { type MusicToggleModalType } from './MusicToggleModal'
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@ -28,6 +29,7 @@ export default () => {
|
|||||||
const musicPositionModalRef = useRef<MusicPositionModalType>(null)
|
const musicPositionModalRef = useRef<MusicPositionModalType>(null)
|
||||||
const metadataEditTypeRef = useRef<MetadataEditType>(null)
|
const metadataEditTypeRef = useRef<MetadataEditType>(null)
|
||||||
const listMenuRef = useRef<ListMenuType>(null)
|
const listMenuRef = useRef<ListMenuType>(null)
|
||||||
|
const musicToggleModalRef = useRef<MusicToggleModalType>(null)
|
||||||
const layoutHeightRef = useRef<number>(0)
|
const layoutHeightRef = useRef<number>(0)
|
||||||
const isShowMultipleModeBar = useRef(false)
|
const isShowMultipleModeBar = useRef(false)
|
||||||
const isShowSearchBarModeBar = useRef(false)
|
const isShowSearchBarModeBar = useRef(false)
|
||||||
@ -161,11 +163,13 @@ export default () => {
|
|||||||
onMove={handleMoveMusic}
|
onMove={handleMoveMusic}
|
||||||
onEditMetadata={handleEditMetadata}
|
onEditMetadata={handleEditMetadata}
|
||||||
onChangePosition={info => musicPositionModalRef.current?.show(info)}
|
onChangePosition={info => musicPositionModalRef.current?.show(info)}
|
||||||
|
onToggleSource={info => musicToggleModalRef.current?.show(info)}
|
||||||
/>
|
/>
|
||||||
<MetadataEditModal
|
<MetadataEditModal
|
||||||
ref={metadataEditTypeRef}
|
ref={metadataEditTypeRef}
|
||||||
onUpdate={handleUpdateMetadata}
|
onUpdate={handleUpdateMetadata}
|
||||||
/>
|
/>
|
||||||
|
<MusicToggleModal ref={musicToggleModalRef} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import playerState from '@/store/player/state'
|
|||||||
import type { SelectInfo } from './ListMenu'
|
import type { SelectInfo } from './ListMenu'
|
||||||
import { type Metadata } from '@/components/MetadataEditModal'
|
import { type Metadata } from '@/components/MetadataEditModal'
|
||||||
import musicSdk from '@/utils/musicSdk'
|
import musicSdk from '@/utils/musicSdk'
|
||||||
|
import { getListMusicSync } from '@/utils/listManage'
|
||||||
|
|
||||||
export const handlePlay = (listId: SelectInfo['listId'], index: SelectInfo['index']) => {
|
export const handlePlay = (listId: SelectInfo['listId'], index: SelectInfo['index']) => {
|
||||||
void playList(listId, index)
|
void playList(listId, index)
|
||||||
@ -111,3 +112,28 @@ export const handleDislikeMusic = async(musicInfo: SelectInfo['musicInfo']) => {
|
|||||||
void playNext(true)
|
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
|
||||||
|
}
|
||||||
|
1
src/types/music.d.ts
vendored
1
src/types/music.d.ts
vendored
@ -22,6 +22,7 @@ declare namespace LX {
|
|||||||
songId: string | number // 歌曲ID,mg源为copyrightId,local为文件路径
|
songId: string | number // 歌曲ID,mg源为copyrightId,local为文件路径
|
||||||
albumName: string // 歌曲专辑名称
|
albumName: string // 歌曲专辑名称
|
||||||
picUrl?: string | null // 歌曲图片链接
|
picUrl?: string | null // 歌曲图片链接
|
||||||
|
toggleMusicInfo?: MusicInfoOnline | null
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MusicInfoMeta_online extends MusicInfoMetaBase {
|
interface MusicInfoMeta_online extends MusicInfoMetaBase {
|
||||||
|
@ -57,10 +57,24 @@ export const init = () => {
|
|||||||
return Promise.all(tasks)
|
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) => {
|
export const findMusic = async(musicInfo) => {
|
||||||
const { name, singer, albumName, interval, source: s } = musicInfo
|
const { name, singer, albumName, interval, source: s } = musicInfo
|
||||||
|
|
||||||
const tasks = []
|
const lists = await searchMusic({ name, singer, source: s, limit: 25 })
|
||||||
|
|
||||||
const singersRxp = /、|&|;|;|\/|,|,|\|/
|
const singersRxp = /、|&|;|;|\/|,|,|\|/
|
||||||
const sortSingle = singer => singersRxp.test(singer)
|
const sortSingle = singer => singersRxp.test(singer)
|
||||||
? singer.split(singersRxp).sort((a, b) => a.localeCompare(b)).join('、')
|
? singer.split(singersRxp).sort((a, b) => a.localeCompare(b)).join('、')
|
||||||
@ -86,44 +100,39 @@ export const findMusic = async(musicInfo) => {
|
|||||||
const sortedSinger = filterStr(String(sortSingle(singer)).toLowerCase())
|
const sortedSinger = filterStr(String(sortSingle(singer)).toLowerCase())
|
||||||
const lowerCaseName = filterStr(String(musicName).toLowerCase())
|
const lowerCaseName = filterStr(String(musicName).toLowerCase())
|
||||||
const lowerCaseAlbumName = filterStr(String(albumName).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 => {
|
const result = lists.map(source => {
|
||||||
for (const item of res.list) {
|
for (const item of source.list) {
|
||||||
item.name = trimStr(item.name)
|
item.name = trimStr(item.name)
|
||||||
item.sortedSinger = filterStr(String(sortSingle(item.singer)).toLowerCase())
|
item.sortedSinger = filterStr(String(sortSingle(item.singer)).toLowerCase())
|
||||||
item.lowerCaseName = filterStr(String(item.name ?? '').toLowerCase())
|
item.lowerCaseName = filterStr(String(item.name ?? '').toLowerCase())
|
||||||
item.lowerCaseAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())
|
item.lowerCaseAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())
|
||||||
// console.log(lowerCaseName, item.lowerCaseName, item.source)
|
// console.log(lowerCaseName, item.lowerCaseName, item.source)
|
||||||
if (
|
if (
|
||||||
|
(
|
||||||
|
item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName
|
||||||
|
) ||
|
||||||
(
|
(
|
||||||
item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName
|
(interval ? item.interval == interval : true) && item.lowerCaseName == lowerCaseName &&
|
||||||
|
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
|
||||||
) ||
|
) ||
|
||||||
(
|
(
|
||||||
(interval ? item.interval == interval : true) && item.lowerCaseName == lowerCaseName &&
|
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
|
||||||
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
|
(interval ? item.interval == interval : true)
|
||||||
) ||
|
) ||
|
||||||
(
|
(
|
||||||
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
|
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
|
||||||
(interval ? item.interval == interval : true)
|
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
|
||||||
) ||
|
)
|
||||||
(
|
) {
|
||||||
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
|
return item
|
||||||
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
if (!singer) {
|
|
||||||
if (item.lowerCaseName == lowerCaseName && (interval ? item.interval == interval : true)) return item
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null
|
if (!singer) {
|
||||||
}).catch(_ => null))
|
if (item.lowerCaseName == lowerCaseName && (interval ? item.interval == interval : true)) return item
|
||||||
}
|
}
|
||||||
const result = (await Promise.all(tasks)).filter(s => s)
|
}
|
||||||
|
return null
|
||||||
|
}).filter(s => s)
|
||||||
const newResult = []
|
const newResult = []
|
||||||
if (result.length) {
|
if (result.length) {
|
||||||
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName && item.interval == interval))
|
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName && item.interval == interval))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user