This commit is contained in:
ikun 2024-09-08 16:51:58 +08:00
parent 509baae636
commit d677963554
29 changed files with 1170 additions and 76 deletions

View File

@ -6,6 +6,13 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [1.6.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.5.0...v1.6.0) - 2024-08-24
### 新增
- 新增 我的列表-歌曲右击菜单-歌曲换源 功能,换源后下次再播放该列表的该歌曲时将优先尝试播放所选源的歌曲,该功能允许你手动指定来源以解决自动换源失败或者换源不准确的问题
- 新增 Scheme URL 调用支持调用传参格式与PC端一致详情看文档说明 https://lyswhut.github.io/lx-music-doc/mobile/scheme-url
## [1.5.0](https://github.com/lyswhut/lx-music-mobile/compare/v1.4.2...v1.5.0) - 2024-08-03
我们发布了关于 LX Music 项目发展调整与新项目计划的说明,

View File

@ -31,6 +31,12 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter android:label="@string/app_name">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="lxmusic" android:host="*" />
</intent-filter>
</activity>
<!-- Define a FileProvider for API24+ -->

View File

@ -1,16 +1,4 @@
我们发布了关于 LX Music Mod 项目发展调整与新项目计划的说明,
详情看: https://github.com/lyswhut/lx-music-desktop/issues/1912
### 优化
### 新增
- 首次使用的提示窗口可以点击背景或者返回键关闭(#577
- 新增重复歌曲列表可以方便移除我的列表中的重复歌曲此列表会列出目标列表里歌曲名相同的歌曲可在“我的列表”里的列表名菜单中使用该功能与PC端的区别是可以点击歌曲名多选删除
- 新增打开当前歌曲详情页菜单,可以在歌曲菜单中使用
### 修复
- 修复潜在桌面歌词导致的崩溃问题
### 其他
- 更新 React native 到 v0.73.9
- 更新 exoplayer 到 v1.4.0

File diff suppressed because one or more lines are too long

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

@ -0,0 +1,60 @@
import { Linking } from 'react-native'
import { errorDialog } from './utils'
import { handleMusicAction } from './musicAction'
import { handlePlayerAction, type PlayerAction } from './playerAction'
import { handleSonglistAction } from './songlistAction'
const handleLinkAction = async(link: string) => {
// console.log(link)
const [url, search] = link.split('?')
const [type, action, ...paths] = url.replace('lxmusic://', '').split('/')
const params: {
paths: string[]
data?: string
[key: string]: any
} = {
paths: [],
}
if (search) {
for (const param of search.split('&')) {
const [key, value] = param.split('=')
params[key] = value
}
if (params.data) params.data = JSON.parse(decodeURIComponent(params.data))
}
params.paths = paths.map(p => decodeURIComponent(p))
console.log(params)
switch (type) {
case 'music':
await handleMusicAction(action, params)
break
case 'songlist':
await handleSonglistAction(action, params)
break
case 'player':
await handlePlayerAction(action as PlayerAction)
break
// default: throw new Error('Unknown type: ' + type)
}
}
const runLinkAction = async(link: string) => {
if (!link.startsWith('lxmusic://')) return
try {
await handleLinkAction(link)
} catch (err: any) {
errorDialog(err.message)
// focusWindow()
}
}
export const initDeeplink = async() => {
Linking.addEventListener('url', ({ url }) => {
void runLinkAction(url)
console.log('deeplink', url)
})
const initialUrl = await Linking.getInitialURL()
if (initialUrl == null) return
console.log('deeplink', initialUrl)
void runLinkAction(initialUrl)
}

View File

@ -0,0 +1,214 @@
import { decodeName, toNewMusicInfo } from '@/utils'
import { dataVerify, qualityFilter } from './utils'
import playerState from '@/store/player/state'
import { LIST_IDS } from '@/config/constant'
import { playNext } from '@/core/player/player'
import { getOtherSource } from '@/core/music/utils'
import { addTempPlayList } from '@/core/player/tempPlayList'
// const handleSearchMusic = ({ paths, data: params }) => {
// let text
// let source
// if (params) {
// text = dataVerify([
// { key: 'keywords', types: ['string', 'number'], max: 128, required: true },
// ], params).keywords
// source = params.source
// } else {
// if (!paths.length) throw new Error('Keyword missing')
// if (paths.length > 1) {
// text = paths[1]
// source = paths[0]
// } else {
// text = paths[0]
// }
// if (text.length > 128) text = text.substring(0, 128)
// }
// if (isShowPlayerDetail.value) setShowPlayerDetail(false)
// const sourceList = [...sources, 'all']
// source = sourceList.includes(source) ? source : null
// setTimeout(() => {
// router.replace({
// path: '/search',
// query: {
// text,
// source,
// },
// })
// }, 500)
// }
const filterInfoByPlayMusic = musicInfo => {
switch (musicInfo.source) {
case 'kw':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
], musicInfo)
break
case 'kg':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: '_interval', types: ['number'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'hash', types: ['string'], required: true, max: 64 },
], musicInfo)
break
case 'tx':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'strMediaMid', types: ['string'], required: true, max: 64 },
{ key: 'albumMid', types: ['string'], max: 64 },
], musicInfo)
break
case 'wy':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
], musicInfo)
break
case 'mg':
musicInfo = dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], required: true, max: 200 },
{ key: 'source', types: ['string'], required: true },
{ key: 'songmid', types: ['string', 'number'], max: 64, required: true },
{ key: 'img', types: ['string'], max: 1024 },
{ key: 'albumId', types: ['string', 'number'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'types', types: ['object'], required: true },
{ key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 },
{ key: 'lrcUrl', types: ['string'], max: 1024 },
{ key: 'trcUrl', types: ['string'], max: 1024 },
{ key: 'mrcUrl', types: ['string'], max: 1024 },
], musicInfo)
break
default: throw new Error('Unknown source: ' + musicInfo.source)
}
musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types)
return musicInfo
}
const handlePlayMusic = ({ data: _musicInfo }) => {
_musicInfo = filterInfoByPlayMusic(_musicInfo)
let musicInfo = {
..._musicInfo,
singer: decodeName(_musicInfo.singer),
name: decodeName(_musicInfo.name),
albumName: decodeName(_musicInfo.albumName),
otherSource: null,
_types: {},
typeUrl: {},
}
for (const type of musicInfo.types) {
musicInfo._types[type.type] = { size: type.size }
}
musicInfo = toNewMusicInfo(musicInfo)
const isPlaying = !!playerState.playMusicInfo.musicInfo
addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])
if (isPlaying) playNext()
}
const verifyInfo = (info) => {
return dataVerify([
{ key: 'name', types: ['string'], required: true, max: 200 },
{ key: 'singer', types: ['string'], max: 200 },
{ key: 'albumName', types: ['string'], max: 64 },
{ key: 'interval', types: ['string'], max: 64 },
{ key: 'playLater', types: ['boolean'] },
], info)
}
const searchMusic = async(name, singer, albumName, interval) => {
return getOtherSource({
name,
singer,
interval,
meta: {
albumName,
},
source: 'local',
id: `sp_${name}_s${singer}_a${albumName}_i${interval ?? ''}`,
})
}
const handleSearchPlayMusic = async({ paths, data }) => {
// console.log(paths, data)
let info
if (paths.length) {
let name = paths[0].trim()
let singer = ''
if (name.includes('-')) [name, singer] = name.split('-').map(val => val.trim())
info = {
name,
singer,
}
} else info = data
info = verifyInfo(info)
if (!info.name) return
const musicList = await searchMusic(info.name, info.singer || '', info.albumName || '', info.interval || null)
if (musicList.length) {
console.log('find music:', musicList)
const musicInfo = musicList[0]
const isPlaying = !!playerState.playMusicInfo.musicInfo
if (info.playLater) {
addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo }])
} else {
addTempPlayList([{ listId: LIST_IDS.PLAY_LATER, musicInfo, isTop: true }])
if (isPlaying) playNext()
}
} else {
console.log('msuic not found:', info)
}
}
export const handleMusicAction = async(action, info) => {
switch (action) {
// case 'search':
// handleSearchMusic(info)
// break
case 'play':
handlePlayMusic(info)
break
case 'searchPlay':
await handleSearchPlayMusic(info)
break
// default: throw new Error('Unknown action: ' + action)
}
}

View File

@ -0,0 +1,44 @@
import { LIST_IDS } from '@/config/constant'
import { setTempList } from '@/core/list'
import { playList } from '@/core/player/player'
import { getListDetail, getListDetailAll } from '@/core/songlist'
import listState from '@/store/list/state'
const getListPlayIndex = (list: LX.Music.MusicInfoOnline[], index?: number) => {
if (index == null) {
index = 1
} else {
if (index < 1) index = 1
else if (index > list.length) index = list.length
}
return index - 1
}
const playSongListDetail = async(source: LX.OnlineSource, link: string, playIndex?: number) => {
// console.log(source, link, playIndex)
if (link == null) return
let isPlayingList = false
const id = decodeURIComponent(link)
const playListId = `${source}__${decodeURIComponent(link)}`
let list = (await getListDetail(id, source, 1)).list
if (playIndex == null || list.length > playIndex) {
isPlayingList = true
await setTempList(playListId, list)
await playList(LIST_IDS.TEMP, getListPlayIndex(list, playIndex))
}
list = await getListDetailAll(source, id)
if (isPlayingList) {
if (listState.tempListMeta.id == id) await setTempList(playListId, list)
} else {
await setTempList(playListId, list)
await playList(LIST_IDS.TEMP, getListPlayIndex(list, playIndex))
}
}
export const playSonglist = async(source: LX.OnlineSource, link: string, playIndex?: number) => {
try {
await playSongListDetail(source, link, playIndex)
} catch (err) {
console.error(err)
throw new Error('Get play list failed.')
}
}

View File

@ -0,0 +1,33 @@
import { collectMusic, dislikeMusic, pause, play, playNext, playPrev, togglePlay, uncollectMusic } from '@/core/player/player'
export type PlayerAction = 'play' | 'pause' | 'skipNext' | 'skipPrev' | 'togglePlay' | 'collect' | 'uncollect' | 'dislike'
export const handlePlayerAction = async(action: PlayerAction) => {
switch (action) {
case 'play':
play()
break
case 'pause':
void pause()
break
case 'skipNext':
void playNext()
break
case 'skipPrev':
void playPrev()
break
case 'togglePlay':
togglePlay()
break
case 'collect':
collectMusic()
break
case 'uncollect':
uncollectMusic()
break
case 'dislike':
void dislikeMusic()
break
// default: throw new Error('Unknown action: ' + (action as any ?? ''))
}
}

View File

@ -0,0 +1,95 @@
import { playSonglist } from './playSonglist'
import { dataVerify, sourceVerify } from './utils'
// const handleOpenSonglist = params => {
// if (params.id) {
// router[route.path == '/songList/detail' ? 'replace' : 'push']({
// path: '/songList/detail',
// query: {
// source: params.source,
// id: params.id,
// },
// })
// } else if (params.url) {
// router[route.path == '/songList/detail' ? 'replace' : 'push']({
// path: '/songList/detail',
// query: {
// source: params.source,
// id: params.url,
// },
// })
// }
// }
// const openSonglist = () => {
// return ({ paths, data }) => {
// let songlistInfo = {
// source: null,
// id: null,
// url: null,
// }
// if (data) {
// songlistInfo = data
// } else {
// songlistInfo.source = paths[0]
// songlistInfo.url = paths[1]
// }
// sourceVerify(songlistInfo.source)
// songlistInfo = dataVerify([
// { key: 'source', types: ['string'] },
// { key: 'id', types: ['string', 'number'], max: 64 },
// { key: 'url', types: ['string'], max: 500 },
// ], songlistInfo)
// if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')
// if (isShowPlayerDetail.value) setShowPlayerDetail(false)
// handleOpenSonglist(songlistInfo)
// focusWindow()
// }
// }
const handlePlaySonglist = async({ paths, data }) => {
let songlistInfo = {
source: null,
id: null,
url: null,
index: null,
}
if (data) {
songlistInfo = data
} else {
songlistInfo.source = paths[0]
songlistInfo.url = paths[1]
songlistInfo.index = paths[2]
if (songlistInfo.index != null) {
songlistInfo.index = parseInt(songlistInfo.index)
if (Number.isNaN(songlistInfo.index)) delete songlistInfo.index
}
}
sourceVerify(songlistInfo.source)
songlistInfo = dataVerify([
{ key: 'source', types: ['string'] },
{ key: 'id', types: ['string', 'number'], max: 64 },
{ key: 'url', types: ['string'], max: 500 },
{ key: 'index', types: ['number'], max: 1000000 },
], songlistInfo)
if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing')
await playSonglist(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index)
}
export const handleSonglistAction = async(action, info) => {
switch (action) {
// case 'open':
// handleOpenSonglist(info)
// break
case 'play':
await handlePlaySonglist(info)
break
// default: throw new Error('Unknown action: ' + action)
}
}

View File

@ -7,6 +7,7 @@ import initPlayer from './player'
import dataInit from './dataInit'
import initSync from './sync'
import initCommonState from './common'
import { initDeeplink } from './deeplink'
import { setApiSource } from '@/core/apiSource'
import commonActions from '@/store/common/action'
import settingState from '@/store/setting/state'
@ -22,6 +23,7 @@ const handlePushedHomeScreen = async() => {
if (isFirstPush) {
isFirstPush = false
void checkUpdate()
void initDeeplink()
}
} else {
if (isFirstPush) isFirstPush = false

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,19 +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

@ -25,6 +25,9 @@ import { getRandom } from '@/utils/common'
import { filterList } from './utils'
import BackgroundTimer from 'react-native-background-timer'
import { checkIgnoringBatteryOptimization, checkNotificationPermission, debounceBackgroundTimer } from '@/utils/tools'
import { LIST_IDS } from '@/config/constant'
import { addListMusics, removeListMusics } from '@/core/list'
import { addDislikeInfo } from '@/core/dislikeList'
// import { checkMusicFileAvailable } from '@renderer/utils/music'
@ -56,12 +59,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,14 +99,21 @@ 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 getMusicUrl({
musicInfo,
return (toggleMusicInfo ? getMusicUrl({
musicInfo: toggleMusicInfo,
isRefresh,
onToggleSource(mInfo) {
if (diffCurrentMusicInfo(musicInfo)) return
setStatusText(global.i18n.t('toggle_source_try'))
},
allowToggleSource: false,
}) : Promise.reject(new Error('not found'))).catch(async() => {
return getMusicUrl({
musicInfo,
isRefresh,
onToggleSource(mInfo) {
if (diffCurrentMusicInfo(musicInfo)) return
setStatusText(global.i18n.t('toggle_source_try'))
},
})
}).then(url => {
if (global.lx.isPlayedStop || diffCurrentMusicInfo(musicInfo)) return null
@ -122,7 +136,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 +489,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()
@ -509,3 +523,38 @@ export const togglePlay = () => {
play()
}
}
/**
*
*/
export const collectMusic = () => {
if (!playerState.playMusicInfo.musicInfo) return
void addListMusics(LIST_IDS.LOVE, [
'progress' in playerState.playMusicInfo.musicInfo
? playerState.playMusicInfo.musicInfo.metadata.musicInfo
: playerState.playMusicInfo.musicInfo,
], settingState.setting['list.addMusicLocationType'])
}
/**
*
*/
export const uncollectMusic = () => {
if (!playerState.playMusicInfo.musicInfo) return
void removeListMusics(LIST_IDS.LOVE, [
'progress' in playerState.playMusicInfo.musicInfo
? playerState.playMusicInfo.musicInfo.metadata.musicInfo.id
: playerState.playMusicInfo.musicInfo.id,
])
}
/**
*
*/
export const dislikeMusic = async() => {
if (!playerState.playMusicInfo.musicInfo) return
const minfo = 'progress' in playerState.playMusicInfo.musicInfo ? playerState.playMusicInfo.musicInfo.metadata.musicInfo : playerState.playMusicInfo.musicInfo
await addDislikeInfo([{ name: minfo.name, singer: minfo.singer }])
await playNext(true)
}

View File

@ -37,6 +37,7 @@
"date_format_hour": "{num} hours ago",
"date_format_minute": "{num} minutes ago",
"date_format_second": "{num} seconds ago",
"deep_link__handle_error_tip": "Call failed: {message}",
"delete": "Delete",
"dialog_cancel": "No",
"dialog_confirm": "OK",
@ -457,6 +458,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

@ -37,6 +37,7 @@
"date_format_hour": "{num}小时前",
"date_format_minute": "{num}分钟前",
"date_format_second": "{num}秒前",
"deep_link__handle_error_tip": "调用失败:{message}",
"delete": "删除",
"dialog_cancel": "我不",
"dialog_confirm": "好的",
@ -458,6 +459,7 @@
"timeout_exit_tip_max": "最多只能设置{num}分钟哦",
"timeout_exit_tip_off": "设置定时停止播放",
"timeout_exit_tip_on": "{time} 后停止播放",
"toggle_source": "歌曲换源",
"toggle_source_failed": "换源失败,请尝试手动在其他源搜索该歌曲播放",
"toggle_source_try": "尝试切换到其他源...",
"understand": "已了解 👌",

View File

@ -11,6 +11,7 @@ import ModalContent from './ModalContent'
import { exitApp } from '@/utils/nativeModules/utils'
import { updateSetting } from '@/core/common'
import { checkUpdate } from '@/core/version'
import { initDeeplink } from '@/core/init/deeplink'
const Content = () => {
const theme = useTheme()
@ -88,6 +89,7 @@ const Footer = ({ componentId }: { componentId: string }) => {
text: Buffer.from('e5a5bde79a8420284f4b29', 'hex').toString(),
onPress: () => {
void checkUpdate()
void initDeeplink()
},
}],
)

Binary file not shown.

File diff suppressed because one or more lines are too long

View File

@ -21,6 +21,8 @@ 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
onDownload: (selectInfo: SelectInfo) => void
@ -66,6 +68,8 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
{ action: 'download', label: '下载' },
{ 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: 'changePosition', label: t('change_position') },
{ action: 'dislike', disabled: hasDislike(musicInfo), label: t('dislike') },
@ -133,6 +137,14 @@ 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)
break
case 'dislike':
props.onDislikeMusic(selectInfo)
break

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

@ -15,6 +15,7 @@ import ListMusicSearch, { type ListMusicSearchType } from './ListMusicSearch'
import MusicPositionModal, { type MusicPositionModalType } from './MusicPositionModal'
import MusicDownloadModal, { type MusicDownloadModalType } from './MusicDownloadModal'
import MetadataEditModal, { type MetadataEditType, type MetadataEditProps } from '@/components/MetadataEditModal'
import MusicToggleModal, { type MusicToggleModalType } from './MusicToggleModal'
export default () => {
@ -30,6 +31,7 @@ export default () => {
const musicDownloadModalRef = useRef<MusicDownloadModalType>(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)
@ -165,11 +167,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,9 +10,8 @@ import playerState from '@/store/player/state'
import RNFetchBlob from 'rn-fetch-blob'
import type { SelectInfo } from './ListMenu'
import { type Metadata } from '@/components/MetadataEditModal'
import { getMusicUrl } from '@/core/music'
import log from '@/plugins/sync/log'
import { setStatusText } from '@/core/player/playStatus'
import musicSdk from '@/utils/musicSdk'
import { getListMusicSync } from '@/utils/listManage'
export const handlePlay = (listId: SelectInfo['listId'], index: SelectInfo['index']) => {
void playList(listId, index)
@ -168,3 +167,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

@ -49,10 +49,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('、')
@ -78,44 +92,39 @@ 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) {
item.name = trimStr(item.name)
item.sortedSinger = filterStr(String(sortSingle(item.singer)).toLowerCase())
item.lowerCaseName = filterStr(String(item.name ?? '').toLowerCase())
item.lowerCaseAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())
// console.log(lowerCaseName, item.lowerCaseName, item.source)
if (
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())
item.lowerCaseAlbumName = filterStr(String(item.albumName ?? '').toLowerCase())
// console.log(lowerCaseName, item.lowerCaseName, item.source)
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.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
) ||
(
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
(interval ? item.interval == interval : true)
) ||
(
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
)
) {
return item
}
if (!singer) {
if (item.lowerCaseName == lowerCaseName && (interval ? item.interval == interval : true)) return item
}
(
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
(interval ? item.interval == interval : true)
) ||
(
item.lowerCaseName == lowerCaseName && (lowerCaseAlbumName ? item.lowerCaseAlbumName == lowerCaseAlbumName : true) &&
(item.sortedSinger.includes(sortedSinger) || sortedSinger.includes(item.sortedSinger))
)
) {
return item
}
return null
}).catch(_ => null))
}
const result = (await Promise.all(tasks)).filter(s => s)
if (!singer) {
if (item.lowerCaseName == lowerCaseName && (interval ? item.interval == interval : true)) return item
}
}
return null
}).filter(s => s)
const newResult = []
if (result.length) {
newResult.push(...sortMusic(result, item => item.sortedSinger == sortedSinger && item.lowerCaseName == lowerCaseName && item.interval == interval))

View File

@ -549,7 +549,7 @@ export const cheatTip = async() => {
title: '提示',
message: `本项目是对LX Music的二次开发请勿将本项目用于商业用途否则后果自负。如有疑问请加入QQ群690309707。`,
btnText: '我知道了 (Close)',
bgClose: false,
bgClose: true,
}).then(() => {
void saveData(storageDataPrefix.cheatTip, true)
})

View File

@ -15,10 +15,10 @@ const abis = [
const address = [
[`https://raw.githubusercontent.com/${author.name}/${name}/master/publish/version.json`, 'direct'],
['https://registry.npmjs.org/lx-music-mobile-version-info/latest', 'npm'],
['https://registry.npmmirror.com/lx-music-mobile-version-info/latest', 'npm'],
[`https://cdn.jsdelivr.net/gh/${author.name}/${name}/publish/version.json`, 'direct'],
[`https://fastly.jsdelivr.net/gh/${author.name}/${name}/publish/version.json`, 'direct'],
[`https://gcore.jsdelivr.net/gh/${author.name}/${name}/publish/version.json`, 'direct'],
['https://registry.npmmirror.com/lx-music-mobile-version-info/latest', 'npm'],
['https://gitee.com/lyswhut/lx-music-mobile-versions/raw/master/version.json', 'direct'],
['http://cdn.stsky.cn/lx-music/mobile/version.json', 'direct'],
]