mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-04 04:52:10 +08:00
新增歌曲标签编辑功能
This commit is contained in:
parent
f1d8c9509d
commit
c0d990c7ca
@ -58,7 +58,7 @@
|
|||||||
"react-native-exception-handler": "^2.10.10",
|
"react-native-exception-handler": "^2.10.10",
|
||||||
"react-native-fast-image": "^8.6.3",
|
"react-native-fast-image": "^8.6.3",
|
||||||
"react-native-fs": "^2.20.0",
|
"react-native-fs": "^2.20.0",
|
||||||
"react-native-local-media-metadata": "github:lyswhut/react-native-local-media-metadata#8c6525e20f5787dbcd2962e83b635787341e5be5",
|
"react-native-local-media-metadata": "github:lyswhut/react-native-local-media-metadata#c2b993093ddd141b4b88673baba7cb4d7b565d04",
|
||||||
"react-native-navigation": "^7.37.2",
|
"react-native-navigation": "^7.37.2",
|
||||||
"react-native-pager-view": "^6.2.3",
|
"react-native-pager-view": "^6.2.3",
|
||||||
"react-native-quick-base64": "^2.0.8",
|
"react-native-quick-base64": "^2.0.8",
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
- 新增自定义源(实验性功能),调用方式与PC端一致,但需要注意的是,移动端自定义源的环境与PC端不同,某些环境API不可用,详情看自定义说明文档
|
- 新增自定义源(实验性功能),调用方式与PC端一致,但需要注意的是,移动端自定义源的环境与PC端不同,某些环境API不可用,详情看自定义说明文档
|
||||||
- 新增长按收藏列表名自动跳转列表顶部的功能
|
- 新增长按收藏列表名自动跳转列表顶部的功能
|
||||||
- 新增实验性的添加本地歌曲到我的收藏支持,与PC端类似,在我的收藏的列表菜单中选择歌曲目录,将添加所选目录下的所有歌曲,目前支持mp3/flac/ogg/wav格式
|
- 新增实验性的添加本地歌曲到我的收藏支持,与PC端类似,在我的收藏的列表菜单中选择歌曲目录,将添加所选目录下的所有歌曲,目前支持mp3/flac/ogg/wav格式
|
||||||
|
- 新增歌曲标签编辑功能,允许编辑本地源且文件歌曲存在的歌曲标签信息
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
|
47
src/components/MetadataEditModal/InputItem.tsx
Normal file
47
src/components/MetadataEditModal/InputItem.tsx
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import { StyleSheet, View } from 'react-native'
|
||||||
|
import type { InputProps } from '@/components/common/Input'
|
||||||
|
import Input from '@/components/common/Input'
|
||||||
|
import { useTheme } from '@/store/theme/hook'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
|
||||||
|
|
||||||
|
export interface InputItemProps extends InputProps {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
onChanged: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(({ value, label, onChanged, ...props }: InputItemProps) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
return (
|
||||||
|
<View style={styles.container} onStartShouldSetResponder={() => true}>
|
||||||
|
<Text style={styles.label} size={14}>{label}</Text>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChanged}
|
||||||
|
style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
// paddingLeft: 25,
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
// borderRadius: 4,
|
||||||
|
// paddingTop: 3,
|
||||||
|
// paddingBottom: 3,
|
||||||
|
// maxWidth: 300,
|
||||||
|
},
|
||||||
|
})
|
128
src/components/MetadataEditModal/MetadataForm.tsx
Normal file
128
src/components/MetadataEditModal/MetadataForm.tsx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import { useImperativeHandle, forwardRef, useState, useCallback } from 'react'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
import { createStyle } from '@/utils/tools'
|
||||||
|
import InputItem from './InputItem'
|
||||||
|
import { useI18n } from '@/lang'
|
||||||
|
import TextAreaItem from './TextAreaItem'
|
||||||
|
import PicItem from './PicItem'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
import { useTheme } from '@/store/theme/hook'
|
||||||
|
|
||||||
|
export interface Metadata {
|
||||||
|
name: string // 歌曲名
|
||||||
|
singer: string // 艺术家名
|
||||||
|
albumName: string // 歌曲专辑名称
|
||||||
|
pic: string
|
||||||
|
lyric: string
|
||||||
|
}
|
||||||
|
export const defaultData = {
|
||||||
|
name: '',
|
||||||
|
singer: '',
|
||||||
|
albumName: '',
|
||||||
|
pic: '',
|
||||||
|
lyric: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataFormType {
|
||||||
|
setForm: (path: string, metadata: Metadata) => void
|
||||||
|
getForm: () => Metadata
|
||||||
|
}
|
||||||
|
export default forwardRef<MetadataFormType, {}>((props, ref) => {
|
||||||
|
const t = useI18n()
|
||||||
|
const [path, setPath] = useState('')
|
||||||
|
const [data, setData] = useState({ ...defaultData })
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
setForm(path, data) {
|
||||||
|
setPath(path)
|
||||||
|
setData(data)
|
||||||
|
},
|
||||||
|
getForm() {
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
name: data.name.trim(),
|
||||||
|
singer: data.singer.trim(),
|
||||||
|
albumName: data.albumName.trim(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleUpdateName = useCallback((name: string) => {
|
||||||
|
if (name.length > 150) name = name.substring(0, 150)
|
||||||
|
setData(data => {
|
||||||
|
return { ...data, name }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const handleUpdateSinger = useCallback((singer: string) => {
|
||||||
|
if (singer.length > 150) singer = singer.substring(0, 150)
|
||||||
|
setData(data => {
|
||||||
|
return { ...data, singer }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const handleUpdateAlbumName = useCallback((albumName: string) => {
|
||||||
|
if (albumName.length > 150) albumName = albumName.substring(0, 150)
|
||||||
|
setData(data => {
|
||||||
|
return { ...data, albumName }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const handleUpdatePic = useCallback((path: string) => {
|
||||||
|
setData(data => {
|
||||||
|
return { ...data, pic: path }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
const handleUpdateLyric = useCallback((lyric: string) => {
|
||||||
|
setData(data => {
|
||||||
|
return { ...data, lyric }
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<View>
|
||||||
|
<Text size={14}>{global.i18n.t('metadata_edit_modal_file_path')}</Text>
|
||||||
|
<Text size={14} selectable color={theme['c-primary-font']} style={styles.pathText}>{path}</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<InputItem
|
||||||
|
value={data.name}
|
||||||
|
label={t('metadata_edit_modal_form_name')}
|
||||||
|
onChanged={handleUpdateName}
|
||||||
|
keyboardType="name-phone-pad" />
|
||||||
|
<InputItem
|
||||||
|
value={data.singer}
|
||||||
|
label={t('metadata_edit_modal_form_singer')}
|
||||||
|
onChanged={handleUpdateSinger}
|
||||||
|
keyboardType="name-phone-pad" />
|
||||||
|
<InputItem
|
||||||
|
value={data.albumName}
|
||||||
|
label={t('metadata_edit_modal_form_album_name')}
|
||||||
|
onChanged={handleUpdateAlbumName}
|
||||||
|
keyboardType="name-phone-pad" />
|
||||||
|
<PicItem
|
||||||
|
value={data.pic}
|
||||||
|
label={t('metadata_edit_modal_form_pic')}
|
||||||
|
onChanged={handleUpdatePic} />
|
||||||
|
<TextAreaItem
|
||||||
|
value={data.lyric}
|
||||||
|
label={t('metadata_edit_modal_form_lyric')}
|
||||||
|
onChanged={handleUpdateLyric}
|
||||||
|
keyboardType="default" />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = createStyle({
|
||||||
|
container: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: 360,
|
||||||
|
maxWidth: '100%',
|
||||||
|
},
|
||||||
|
pathText: {
|
||||||
|
marginBottom: 10,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
86
src/components/MetadataEditModal/PicItem.tsx
Normal file
86
src/components/MetadataEditModal/PicItem.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { memo, useCallback, useRef } from 'react'
|
||||||
|
|
||||||
|
import { TouchableOpacity, View } from 'react-native'
|
||||||
|
import { useTheme } from '@/store/theme/hook'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
import { createStyle } from '@/utils/tools'
|
||||||
|
import Image from '@/components/common/Image'
|
||||||
|
import FileSelect, { type FileSelectType } from '@/components/common/FileSelect'
|
||||||
|
import { BorderWidths } from '@/theme'
|
||||||
|
|
||||||
|
|
||||||
|
export interface PicItemProps {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
onChanged: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(({ value, label, onChanged }: PicItemProps) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
const fileSelectRef = useRef<FileSelectType>(null)
|
||||||
|
const handleRemoveFile = useCallback(() => {
|
||||||
|
onChanged('')
|
||||||
|
}, [onChanged])
|
||||||
|
const handleShowSelectFile = useCallback(() => {
|
||||||
|
fileSelectRef.current?.show({
|
||||||
|
title: global.i18n.t('metadata_edit_modal_form_select_pic_title'),
|
||||||
|
dirOnly: false,
|
||||||
|
filter: /jpg|jpeg|png/,
|
||||||
|
}, (path) => {
|
||||||
|
onChanged(path)
|
||||||
|
})
|
||||||
|
}, [onChanged])
|
||||||
|
return (
|
||||||
|
<View style={styles.container} onStartShouldSetResponder={() => true}>
|
||||||
|
<View style={styles.header}>
|
||||||
|
<Text style={styles.label} size={14}>{label}</Text>
|
||||||
|
<View style={styles.btns}>
|
||||||
|
<TouchableOpacity onPress={handleRemoveFile}>
|
||||||
|
<Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_remove_pic')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={handleShowSelectFile}>
|
||||||
|
<Text size={13} color={theme['c-button-font']}>{global.i18n.t('metadata_edit_modal_form_select_pic')}</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.picContent}>
|
||||||
|
<Image
|
||||||
|
url={value}
|
||||||
|
style={{ ...styles.pic, borderColor: theme['c-border-background'] }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<FileSelect ref={fileSelectRef} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = createStyle({
|
||||||
|
container: {
|
||||||
|
// paddingLeft: 25,
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 30,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
btns: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: 15,
|
||||||
|
},
|
||||||
|
picContent: {
|
||||||
|
// backgroundColor: 'rgba(0,0,0,0.2)',
|
||||||
|
marginTop: 5,
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
pic: {
|
||||||
|
width: 180,
|
||||||
|
height: 180,
|
||||||
|
borderWidth: BorderWidths.normal,
|
||||||
|
borderStyle: 'dashed',
|
||||||
|
},
|
||||||
|
})
|
53
src/components/MetadataEditModal/TextAreaItem.tsx
Normal file
53
src/components/MetadataEditModal/TextAreaItem.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { memo } from 'react'
|
||||||
|
|
||||||
|
import { View } from 'react-native'
|
||||||
|
import type { InputProps } from '@/components/common/Input'
|
||||||
|
import Input from '@/components/common/Input'
|
||||||
|
import { useTheme } from '@/store/theme/hook'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
import { createStyle } from '@/utils/tools'
|
||||||
|
|
||||||
|
|
||||||
|
export interface TextAreaItemProps extends InputProps {
|
||||||
|
value: string
|
||||||
|
label: string
|
||||||
|
onChanged: (text: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(({ value, label, onChanged, ...props }: TextAreaItemProps) => {
|
||||||
|
const theme = useTheme()
|
||||||
|
return (
|
||||||
|
<View style={styles.container} onStartShouldSetResponder={() => true}>
|
||||||
|
<Text style={styles.label} size={14}>{label}</Text>
|
||||||
|
<Input
|
||||||
|
value={value}
|
||||||
|
onChangeText={onChanged}
|
||||||
|
numberOfLines={6}
|
||||||
|
scrollEnabled={false}
|
||||||
|
textAlignVertical='top'
|
||||||
|
multiline
|
||||||
|
style={{ ...styles.textarea, backgroundColor: theme['c-primary-input-background'] }}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const styles = createStyle({
|
||||||
|
container: {
|
||||||
|
// paddingLeft: 25,
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
marginBottom: 2,
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
paddingTop: 3,
|
||||||
|
paddingBottom: 3,
|
||||||
|
height: 'auto',
|
||||||
|
// height: 300,
|
||||||
|
// maxWidth: 300,
|
||||||
|
},
|
||||||
|
})
|
137
src/components/MetadataEditModal/index.tsx
Normal file
137
src/components/MetadataEditModal/index.tsx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
import { useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||||
|
import ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'
|
||||||
|
import Text from '@/components/common/Text'
|
||||||
|
import { View } from 'react-native'
|
||||||
|
import { createStyle, toast } from '@/utils/tools'
|
||||||
|
import {
|
||||||
|
readMetadata,
|
||||||
|
readPic,
|
||||||
|
readLyric,
|
||||||
|
writeMetadata,
|
||||||
|
writePic,
|
||||||
|
writeLyric,
|
||||||
|
} from '@/utils/localMediaMetadata'
|
||||||
|
import { useUnmounted } from '@/utils/hooks'
|
||||||
|
import MetadataForm, { defaultData, type Metadata, type MetadataFormType } from './MetadataForm'
|
||||||
|
|
||||||
|
export type {
|
||||||
|
Metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MetadataEditType {
|
||||||
|
show: (filePath: string) => void
|
||||||
|
}
|
||||||
|
export interface MetadataEditProps {
|
||||||
|
onUpdate: (info: Metadata) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default forwardRef<MetadataEditType, MetadataEditProps>((props, ref) => {
|
||||||
|
const alertRef = useRef<ConfirmAlertType>(null)
|
||||||
|
const metadataFormRef = useRef<MetadataFormType>(null)
|
||||||
|
const filePath = useRef<string>('')
|
||||||
|
const metadata = useRef<Metadata>({ ...defaultData })
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const [processing, setProcessing] = useState(false)
|
||||||
|
const isUnmounted = useUnmounted()
|
||||||
|
|
||||||
|
const handleShow = (filePath: string) => {
|
||||||
|
alertRef.current?.setVisible(true)
|
||||||
|
void Promise.all([
|
||||||
|
readMetadata(filePath),
|
||||||
|
readPic(filePath),
|
||||||
|
readLyric(filePath),
|
||||||
|
]).then(([_metadata, pic, lyric]) => {
|
||||||
|
if (!_metadata) return
|
||||||
|
if (isUnmounted) return
|
||||||
|
metadata.current = {
|
||||||
|
name: _metadata.name,
|
||||||
|
singer: _metadata.singer,
|
||||||
|
albumName: _metadata.albumName,
|
||||||
|
pic,
|
||||||
|
lyric,
|
||||||
|
}
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
metadataFormRef.current?.setForm(filePath, metadata.current)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
show(path) {
|
||||||
|
filePath.current = path
|
||||||
|
if (visible) handleShow(path)
|
||||||
|
else {
|
||||||
|
setVisible(true)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
handleShow(path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
const handleUpdate = async() => {
|
||||||
|
if (!metadataFormRef.current) return
|
||||||
|
let _metadata = metadataFormRef.current.getForm()
|
||||||
|
if (!_metadata.name) {
|
||||||
|
toast(global.i18n.t('metadata_edit_modal_tip'), 'long')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setProcessing(true)
|
||||||
|
let isUpdated = false
|
||||||
|
if (
|
||||||
|
_metadata.name != metadata.current.name ||
|
||||||
|
_metadata.singer != metadata.current.singer ||
|
||||||
|
_metadata.albumName != metadata.current.albumName
|
||||||
|
) {
|
||||||
|
isUpdated ||= true
|
||||||
|
await writeMetadata(filePath.current, {
|
||||||
|
name: _metadata.name,
|
||||||
|
singer: _metadata.singer,
|
||||||
|
albumName: _metadata.albumName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (_metadata.pic != metadata.current.pic) {
|
||||||
|
isUpdated ||= true
|
||||||
|
await writePic(filePath.current, _metadata.pic)
|
||||||
|
}
|
||||||
|
if (_metadata.lyric != metadata.current.lyric) {
|
||||||
|
isUpdated ||= true
|
||||||
|
await writeLyric(filePath.current, _metadata.lyric)
|
||||||
|
}
|
||||||
|
setProcessing(false)
|
||||||
|
if (isUpdated) toast(global.i18n.t('metadata_edit_modal_success'), 'long')
|
||||||
|
alertRef.current?.setVisible(false)
|
||||||
|
props.onUpdate(_metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
visible
|
||||||
|
? <ConfirmAlert
|
||||||
|
ref={alertRef}
|
||||||
|
onConfirm={handleUpdate}
|
||||||
|
confirmText={processing ? global.i18n.t('metadata_edit_modal_processing') : global.i18n.t('metadata_edit_modal_confirm')}
|
||||||
|
disabledConfirm={processing}
|
||||||
|
>
|
||||||
|
<View style={styles.renameContent} onStartShouldSetResponder={() => true}>
|
||||||
|
<Text style={styles.title}>{global.i18n.t('metadata_edit_modal_title')}</Text>
|
||||||
|
<MetadataForm ref={metadataFormRef} />
|
||||||
|
</View>
|
||||||
|
</ConfirmAlert>
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const styles = createStyle({
|
||||||
|
renameContent: {
|
||||||
|
flexGrow: 1,
|
||||||
|
flexShrink: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
marginBottom: 20,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
|
@ -240,7 +240,7 @@ const styles = createStyle({
|
|||||||
inputContent: {
|
inputContent: {
|
||||||
marginTop: 8,
|
marginTop: 8,
|
||||||
flex: 1,
|
flex: 1,
|
||||||
flexDirection: 'row',
|
// flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
|
@ -8,7 +8,7 @@ import ConfirmAlert, { type ConfirmAlertType } from '@/components/common/Confirm
|
|||||||
import { checkStoragePermissions, requestStoragePermission, toast } from '@/utils/tools'
|
import { checkStoragePermissions, requestStoragePermission, toast } from '@/utils/tools'
|
||||||
import { useI18n } from '@/lang'
|
import { useI18n } from '@/lang'
|
||||||
|
|
||||||
interface ReadOptions {
|
export interface ReadOptions {
|
||||||
title: string
|
title: string
|
||||||
dirOnly?: boolean
|
dirOnly?: boolean
|
||||||
filter?: RegExp
|
filter?: RegExp
|
||||||
|
@ -63,6 +63,7 @@ export interface ConfirmAlertProps {
|
|||||||
cancelText?: string
|
cancelText?: string
|
||||||
confirmText?: string
|
confirmText?: string
|
||||||
showConfirm?: boolean
|
showConfirm?: boolean
|
||||||
|
disabledConfirm?: boolean
|
||||||
reverseBtn?: boolean
|
reverseBtn?: boolean
|
||||||
children?: React.ReactNode | React.ReactNode[]
|
children?: React.ReactNode | React.ReactNode[]
|
||||||
}
|
}
|
||||||
@ -83,6 +84,7 @@ export default forwardRef<ConfirmAlertType, ConfirmAlertProps>(({
|
|||||||
cancelText = '',
|
cancelText = '',
|
||||||
confirmText = '',
|
confirmText = '',
|
||||||
showConfirm = true,
|
showConfirm = true,
|
||||||
|
disabledConfirm = false,
|
||||||
children,
|
children,
|
||||||
reverseBtn = false,
|
reverseBtn = false,
|
||||||
}: ConfirmAlertProps, ref) => {
|
}: ConfirmAlertProps, ref) => {
|
||||||
@ -114,7 +116,7 @@ export default forwardRef<ConfirmAlertType, ConfirmAlertProps>(({
|
|||||||
<Text color={theme['c-button-font']}>{cancelText || t('cancel')}</Text>
|
<Text color={theme['c-button-font']}>{cancelText || t('cancel')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
{showConfirm
|
{showConfirm
|
||||||
? <Button style={{ ...styles.btn, ...(reverseBtn ? styles.btnReversedDirection : styles.btnDirection), backgroundColor: theme['c-button-background'] }} onPress={onConfirm}>
|
? <Button style={{ ...styles.btn, ...(reverseBtn ? styles.btnReversedDirection : styles.btnDirection), backgroundColor: theme['c-button-background'] }} onPress={onConfirm} disabled={disabledConfirm}>
|
||||||
<Text color={theme['c-button-font']}>{confirmText || t('confirm')}</Text>
|
<Text color={theme['c-button-font']}>{confirmText || t('confirm')}</Text>
|
||||||
</Button>
|
</Button>
|
||||||
: null}
|
: null}
|
||||||
|
33
src/components/common/FileSelect.tsx
Normal file
33
src/components/common/FileSelect.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import ChoosePath, { type ReadOptions, type ChoosePathType } from '@/components/common/ChoosePath'
|
||||||
|
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
export interface FileSelectType {
|
||||||
|
show: (options: ReadOptions, onSelect: typeof noop) => void
|
||||||
|
}
|
||||||
|
const noop = (path: string) => {}
|
||||||
|
export default forwardRef<FileSelectType, {}>((props, ref) => {
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
|
const choosePathRef = useRef<ChoosePathType>(null)
|
||||||
|
const onSelectRef = useRef<typeof noop>(noop)
|
||||||
|
// console.log('render import export')
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
show(options, onSelect) {
|
||||||
|
onSelectRef.current = onSelect ?? noop
|
||||||
|
if (visible) {
|
||||||
|
choosePathRef.current?.show(options)
|
||||||
|
} else {
|
||||||
|
setVisible(true)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
choosePathRef.current?.show(options)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
visible
|
||||||
|
? <ChoosePath ref={choosePathRef} onConfirm={onSelectRef.current} />
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
})
|
@ -12,7 +12,11 @@ const defaultHeaders = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Image = ({ url, resizeMode = FastImage.resizeMode.cover, ...props }: ImageProps) => {
|
const Image = ({ url, resizeMode = FastImage.resizeMode.cover, ...props }: ImageProps) => {
|
||||||
let uri = typeof url == 'number' ? _Image.resolveAssetSource(url).uri : url
|
let uri = typeof url == 'number'
|
||||||
|
? _Image.resolveAssetSource(url).uri
|
||||||
|
: url?.startsWith('/')
|
||||||
|
? 'file://' + url
|
||||||
|
: url
|
||||||
return (
|
return (
|
||||||
<FastImage
|
<FastImage
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"disagree_tip": "Cancelled...",
|
"disagree_tip": "Cancelled...",
|
||||||
"dislike": "Dislike",
|
"dislike": "Dislike",
|
||||||
"duplicate_list_tip": "You have previously favorited the list [{name}], do you want to update the songs?",
|
"duplicate_list_tip": "You have previously favorited the list [{name}], do you want to update the songs?",
|
||||||
|
"edit_metadata": "Edit tag",
|
||||||
"exit_app_tip": "Are you sure you want to quit the app?",
|
"exit_app_tip": "Are you sure you want to quit the app?",
|
||||||
"ignoring_battery_optimization_check_tip": "LX Music is not on the list of ignored battery optimization, which may cause the problem of being suspended by the system when playing music in the background. Do you want to add LX Music to the whitelist?",
|
"ignoring_battery_optimization_check_tip": "LX Music is not on the list of ignored battery optimization, which may cause the problem of being suspended by the system when playing music in the background. Do you want to add LX Music to the whitelist?",
|
||||||
"ignoring_battery_optimization_check_title": "Background running permission setting reminder",
|
"ignoring_battery_optimization_check_title": "Background running permission setting reminder",
|
||||||
@ -106,6 +107,20 @@
|
|||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
"location": "From {location}",
|
"location": "From {location}",
|
||||||
"lyric__load_error": "Failed to get lyrics",
|
"lyric__load_error": "Failed to get lyrics",
|
||||||
|
"metadata_edit_modal_confirm": "Save",
|
||||||
|
"metadata_edit_modal_file_path": "File path",
|
||||||
|
"metadata_edit_modal_form_album_name": "Album name",
|
||||||
|
"metadata_edit_modal_form_lyric": "LRC Lyrics",
|
||||||
|
"metadata_edit_modal_form_name": "Song name",
|
||||||
|
"metadata_edit_modal_form_pic": "Song cover",
|
||||||
|
"metadata_edit_modal_form_remove_pic": "Remove image",
|
||||||
|
"metadata_edit_modal_form_select_pic": "Select Image",
|
||||||
|
"metadata_edit_modal_form_select_pic_title": "Select song cover image",
|
||||||
|
"metadata_edit_modal_form_singer": "Artist",
|
||||||
|
"metadata_edit_modal_processing": "Writing...",
|
||||||
|
"metadata_edit_modal_success": "Saved successfully",
|
||||||
|
"metadata_edit_modal_tip": "Song name cannot be empty",
|
||||||
|
"metadata_edit_modal_title": "Edit song tags",
|
||||||
"move_to": "Move to...",
|
"move_to": "Move to...",
|
||||||
"name": "Name: {name}",
|
"name": "Name: {name}",
|
||||||
"nav_exit": "Exit application",
|
"nav_exit": "Exit application",
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"disagree_tip": "那算了... 🙄",
|
"disagree_tip": "那算了... 🙄",
|
||||||
"dislike": "不喜欢",
|
"dislike": "不喜欢",
|
||||||
"duplicate_list_tip": "你之前已收藏过该列表 [{name}],是否需要更新里面的歌曲?",
|
"duplicate_list_tip": "你之前已收藏过该列表 [{name}],是否需要更新里面的歌曲?",
|
||||||
|
"edit_metadata": "编辑标签",
|
||||||
"exit_app_tip": "确定要退出应用吗?",
|
"exit_app_tip": "确定要退出应用吗?",
|
||||||
"ignoring_battery_optimization_check_tip": "LX Music没有在忽略电池优化的名单中,这可能会导致在后台播放音乐时被系统暂停的问题,是否将LX Music加入该白名单中?",
|
"ignoring_battery_optimization_check_tip": "LX Music没有在忽略电池优化的名单中,这可能会导致在后台播放音乐时被系统暂停的问题,是否将LX Music加入该白名单中?",
|
||||||
"ignoring_battery_optimization_check_title": "后台运行权限设置提醒",
|
"ignoring_battery_optimization_check_title": "后台运行权限设置提醒",
|
||||||
@ -106,6 +107,20 @@
|
|||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
"location": "来自{location}",
|
"location": "来自{location}",
|
||||||
"lyric__load_error": "歌词获取失败",
|
"lyric__load_error": "歌词获取失败",
|
||||||
|
"metadata_edit_modal_confirm": "保存",
|
||||||
|
"metadata_edit_modal_file_path": "文件路径",
|
||||||
|
"metadata_edit_modal_form_album_name": "专辑名",
|
||||||
|
"metadata_edit_modal_form_lyric": "LRC 歌词",
|
||||||
|
"metadata_edit_modal_form_name": "歌曲名",
|
||||||
|
"metadata_edit_modal_form_pic": "歌曲封面",
|
||||||
|
"metadata_edit_modal_form_remove_pic": "移除图片",
|
||||||
|
"metadata_edit_modal_form_select_pic": "选择图片",
|
||||||
|
"metadata_edit_modal_form_select_pic_title": "选择歌曲封面图片",
|
||||||
|
"metadata_edit_modal_form_singer": "艺术家",
|
||||||
|
"metadata_edit_modal_processing": "写入中...",
|
||||||
|
"metadata_edit_modal_success": "保存成功",
|
||||||
|
"metadata_edit_modal_tip": "歌曲名不能为空",
|
||||||
|
"metadata_edit_modal_title": "编辑歌曲标签",
|
||||||
"move_to": "移动到...",
|
"move_to": "移动到...",
|
||||||
"name": "歌曲名:{name}",
|
"name": "歌曲名:{name}",
|
||||||
"nav_exit": "退出应用",
|
"nav_exit": "退出应用",
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { useMemo, useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
import { useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||||
import { useI18n } from '@/lang'
|
import { useI18n } from '@/lang'
|
||||||
import Menu, { type MenuType, type Position } from '@/components/common/Menu'
|
import Menu, { type Menus, type MenuType, type Position } from '@/components/common/Menu'
|
||||||
import { hasDislike } from '@/core/dislikeList'
|
import { hasDislike } from '@/core/dislikeList'
|
||||||
|
import { existsFile } from '@/utils/fs'
|
||||||
|
|
||||||
export interface SelectInfo {
|
export interface SelectInfo {
|
||||||
musicInfo: LX.Music.MusicInfo
|
musicInfo: LX.Music.MusicInfo
|
||||||
@ -17,6 +18,7 @@ export interface ListMenuProps {
|
|||||||
onPlayLater: (selectInfo: SelectInfo) => void
|
onPlayLater: (selectInfo: SelectInfo) => void
|
||||||
onAdd: (selectInfo: SelectInfo) => void
|
onAdd: (selectInfo: SelectInfo) => void
|
||||||
onMove: (selectInfo: SelectInfo) => void
|
onMove: (selectInfo: SelectInfo) => void
|
||||||
|
onEditMetadata: (selectInfo: SelectInfo) => void
|
||||||
onCopyName: (selectInfo: SelectInfo) => void
|
onCopyName: (selectInfo: SelectInfo) => void
|
||||||
onChangePosition: (selectInfo: SelectInfo) => void
|
onChangePosition: (selectInfo: SelectInfo) => void
|
||||||
onDislikeMusic: (selectInfo: SelectInfo) => void
|
onDislikeMusic: (selectInfo: SelectInfo) => void
|
||||||
@ -30,17 +32,21 @@ export type {
|
|||||||
Position,
|
Position,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasEditMetadata = async(musicInfo: LX.Music.MusicInfo) => {
|
||||||
|
if (musicInfo.source != 'local') return false
|
||||||
|
return existsFile(musicInfo.meta.filePath)
|
||||||
|
}
|
||||||
export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
|
export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const [visible, setVisible] = useState(false)
|
const [visible, setVisible] = useState(false)
|
||||||
const menuRef = useRef<MenuType>(null)
|
const menuRef = useRef<MenuType>(null)
|
||||||
const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)
|
const selectInfoRef = useRef<SelectInfo>(initSelectInfo as SelectInfo)
|
||||||
const [isDislikeMusic, setDislikeMusic] = useState(false)
|
const [menus, setMenus] = useState<Menus>([])
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
show(selectInfo, position) {
|
show(selectInfo, position) {
|
||||||
selectInfoRef.current = selectInfo
|
selectInfoRef.current = selectInfo
|
||||||
setDislikeMusic(hasDislike(selectInfo.musicInfo))
|
handleSetMenu(selectInfo.musicInfo)
|
||||||
if (visible) menuRef.current?.show(position)
|
if (visible) menuRef.current?.show(position)
|
||||||
else {
|
else {
|
||||||
setVisible(true)
|
setVisible(true)
|
||||||
@ -51,8 +57,9 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
|
|||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const menus = useMemo(() => {
|
const handleSetMenu = (musicInfo: LX.Music.MusicInfo) => {
|
||||||
return [
|
let edit_metadata = false
|
||||||
|
const menu = [
|
||||||
{ action: 'play', label: t('play') },
|
{ action: 'play', label: t('play') },
|
||||||
{ action: 'playLater', label: t('play_later') },
|
{ action: 'playLater', label: t('play_later') },
|
||||||
// { action: 'download', label: '下载' },
|
// { action: 'download', label: '下载' },
|
||||||
@ -60,10 +67,36 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
|
|||||||
{ action: 'move', label: t('move_to') },
|
{ action: 'move', label: t('move_to') },
|
||||||
{ action: 'copyName', label: t('copy_name') },
|
{ action: 'copyName', label: t('copy_name') },
|
||||||
{ action: 'changePosition', label: t('change_position') },
|
{ action: 'changePosition', label: t('change_position') },
|
||||||
{ action: 'dislike', label: t('dislike'), disabled: isDislikeMusic },
|
{ action: 'dislike', disabled: hasDislike(musicInfo), label: t('dislike') },
|
||||||
{ action: 'remove', label: t('delete') },
|
{ action: 'remove', label: t('delete') },
|
||||||
] as const
|
]
|
||||||
}, [t, isDislikeMusic])
|
if (musicInfo.source == 'local') menu.splice(4, 0, { action: 'editMetadata', disabled: !edit_metadata, label: t('edit_metadata') })
|
||||||
|
setMenus(menu)
|
||||||
|
void Promise.all([hasEditMetadata(musicInfo)]).then(([_edit_metadata]) => {
|
||||||
|
console.log(_edit_metadata)
|
||||||
|
let isUpdated = true
|
||||||
|
if (edit_metadata != _edit_metadata) {
|
||||||
|
edit_metadata = _edit_metadata
|
||||||
|
isUpdated ||= true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isUpdated) {
|
||||||
|
const menu = [
|
||||||
|
{ action: 'play', label: t('play') },
|
||||||
|
{ action: 'playLater', label: t('play_later') },
|
||||||
|
// { action: 'download', label: '下载' },
|
||||||
|
{ action: 'add', label: t('add_to') },
|
||||||
|
{ action: 'move', label: t('move_to') },
|
||||||
|
{ action: 'copyName', label: t('copy_name') },
|
||||||
|
{ action: 'changePosition', label: t('change_position') },
|
||||||
|
{ action: 'dislike', disabled: hasDislike(musicInfo), label: t('dislike') },
|
||||||
|
{ action: 'remove', label: t('delete') },
|
||||||
|
]
|
||||||
|
if (musicInfo.source == 'local') menu.splice(4, 0, { action: 'editMetadata', disabled: !edit_metadata, label: t('edit_metadata') })
|
||||||
|
setMenus(menu)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const handleMenuPress = ({ action }: typeof menus[number]) => {
|
const handleMenuPress = ({ action }: typeof menus[number]) => {
|
||||||
const selectInfo = selectInfoRef.current
|
const selectInfo = selectInfoRef.current
|
||||||
@ -89,6 +122,9 @@ export default forwardRef<ListMenuType, ListMenuProps>((props, ref) => {
|
|||||||
// ? setVisibleMusicMultiAddModal(true)
|
// ? setVisibleMusicMultiAddModal(true)
|
||||||
// : setVisibleMusicAddModal(true)
|
// : setVisibleMusicAddModal(true)
|
||||||
break
|
break
|
||||||
|
case 'editMetadata':
|
||||||
|
props.onEditMetadata(selectInfo)
|
||||||
|
break
|
||||||
case 'copyName':
|
case 'copyName':
|
||||||
props.onCopyName(selectInfo)
|
props.onCopyName(selectInfo)
|
||||||
break
|
break
|
||||||
|
@ -2,7 +2,7 @@ import { useCallback, useRef } from 'react'
|
|||||||
|
|
||||||
import listState from '@/store/list/state'
|
import listState from '@/store/list/state'
|
||||||
import ListMenu, { type ListMenuType, type Position, type SelectInfo } from './ListMenu'
|
import ListMenu, { type ListMenuType, type Position, type SelectInfo } from './ListMenu'
|
||||||
import { handleDislikeMusic, handlePlay, handlePlayLater, handleRemove, handleShare, handleUpdateMusicPosition } from './listAction'
|
import { handleDislikeMusic, handlePlay, handlePlayLater, handleRemove, handleShare, handleUpdateMusicInfo, handleUpdateMusicPosition } from './listAction'
|
||||||
import List, { type ListType } from './List'
|
import List, { type ListType } from './List'
|
||||||
import ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal'
|
import ListMusicAdd, { type MusicAddModalType as ListMusicAddType } from '@/components/MusicAddModal'
|
||||||
import ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal'
|
import ListMusicMultiAdd, { type MusicMultiAddModalType as ListAddMultiType } from '@/components/MusicMultiAddModal'
|
||||||
@ -13,6 +13,7 @@ import MultipleModeBar, { type SelectMode, type MultipleModeBarType } from './Mu
|
|||||||
import ListSearchBar, { type ListSearchBarType } from './ListSearchBar'
|
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'
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
@ -25,10 +26,12 @@ export default () => {
|
|||||||
const listMusicAddRef = useRef<ListMusicAddType>(null)
|
const listMusicAddRef = useRef<ListMusicAddType>(null)
|
||||||
const listMusicMultiAddRef = useRef<ListAddMultiType>(null)
|
const listMusicMultiAddRef = useRef<ListAddMultiType>(null)
|
||||||
const musicPositionModalRef = useRef<MusicPositionModalType>(null)
|
const musicPositionModalRef = useRef<MusicPositionModalType>(null)
|
||||||
|
const metadataEditTypeRef = useRef<MetadataEditType>(null)
|
||||||
const listMenuRef = useRef<ListMenuType>(null)
|
const listMenuRef = useRef<ListMenuType>(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)
|
||||||
|
const selectedInfoRef = useRef<SelectInfo>()
|
||||||
// console.log('render index list')
|
// console.log('render index list')
|
||||||
|
|
||||||
const hancelMultiSelect = useCallback(() => {
|
const hancelMultiSelect = useCallback(() => {
|
||||||
@ -103,6 +106,15 @@ export default () => {
|
|||||||
listMusicAddRef.current?.show({ musicInfo: info.musicInfo, listId: info.listId, isMove: true })
|
listMusicAddRef.current?.show({ musicInfo: info.musicInfo, listId: info.listId, isMove: true })
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
const handleEditMetadata = useCallback((info: SelectInfo) => {
|
||||||
|
if (info.musicInfo.source != 'local') return
|
||||||
|
selectedInfoRef.current = info
|
||||||
|
metadataEditTypeRef.current?.show(info.musicInfo.meta.filePath)
|
||||||
|
}, [])
|
||||||
|
const handleUpdateMetadata = useCallback<MetadataEditProps['onUpdate']>((info) => {
|
||||||
|
if (!selectedInfoRef.current || selectedInfoRef.current.musicInfo.source != 'local') return
|
||||||
|
handleUpdateMusicInfo(selectedInfoRef.current.listId, selectedInfoRef.current.musicInfo, info)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -146,8 +158,13 @@ export default () => {
|
|||||||
onCopyName={info => { handleShare(info.musicInfo) }}
|
onCopyName={info => { handleShare(info.musicInfo) }}
|
||||||
onAdd={handleAddMusic}
|
onAdd={handleAddMusic}
|
||||||
onMove={handleMoveMusic}
|
onMove={handleMoveMusic}
|
||||||
|
onEditMetadata={handleEditMetadata}
|
||||||
onChangePosition={info => musicPositionModalRef.current?.show(info)}
|
onChangePosition={info => musicPositionModalRef.current?.show(info)}
|
||||||
/>
|
/>
|
||||||
|
<MetadataEditModal
|
||||||
|
ref={metadataEditTypeRef}
|
||||||
|
onUpdate={handleUpdateMetadata}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { removeListMusics, updateListMusicPosition } from '@/core/list'
|
import { removeListMusics, updateListMusicPosition, updateListMusics } from '@/core/list'
|
||||||
import { playList, playNext } from '@/core/player/player'
|
import { playList, playNext } from '@/core/player/player'
|
||||||
import { addTempPlayList } from '@/core/player/tempPlayList'
|
import { addTempPlayList } from '@/core/player/tempPlayList'
|
||||||
import settingState from '@/store/setting/state'
|
import settingState from '@/store/setting/state'
|
||||||
@ -8,6 +8,7 @@ import { addDislikeInfo, hasDislike } from '@/core/dislikeList'
|
|||||||
import playerState from '@/store/player/state'
|
import playerState from '@/store/player/state'
|
||||||
|
|
||||||
import type { SelectInfo } from './ListMenu'
|
import type { SelectInfo } from './ListMenu'
|
||||||
|
import { type Metadata } from '@/components/MetadataEditModal'
|
||||||
|
|
||||||
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)
|
||||||
@ -46,6 +47,23 @@ export const handleUpdateMusicPosition = (position: number, listId: SelectInfo['
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const handleUpdateMusicInfo = (listId: SelectInfo['listId'], musicInfo: LX.Music.MusicInfoLocal, newInfo: Metadata) => {
|
||||||
|
void updateListMusics([
|
||||||
|
{
|
||||||
|
id: listId,
|
||||||
|
musicInfo: {
|
||||||
|
...musicInfo,
|
||||||
|
name: newInfo.name,
|
||||||
|
singer: newInfo.singer,
|
||||||
|
meta: {
|
||||||
|
...musicInfo.meta,
|
||||||
|
albumName: newInfo.albumName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const handleShare = (musicInfo: SelectInfo['musicInfo']) => {
|
export const handleShare = (musicInfo: SelectInfo['musicInfo']) => {
|
||||||
shareMusic(settingState.setting['common.shareType'], settingState.setting['download.fileName'], musicInfo)
|
shareMusic(settingState.setting['common.shareType'], settingState.setting['download.fileName'], musicInfo)
|
||||||
|
8
src/types/music.d.ts
vendored
8
src/types/music.d.ts
vendored
@ -100,14 +100,6 @@ declare namespace LX {
|
|||||||
lyrics: LyricInfo
|
lyrics: LyricInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
interface MusicFileMeta {
|
|
||||||
title: string
|
|
||||||
artist: string | null
|
|
||||||
album: string | null
|
|
||||||
APIC: string | null
|
|
||||||
lyrics: string | null
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MusicUrlInfo {
|
interface MusicUrlInfo {
|
||||||
id: string
|
id: string
|
||||||
url: string
|
url: string
|
||||||
|
@ -7,3 +7,4 @@ export { default as useDeviceOrientation } from './useDeviceOrientation'
|
|||||||
export { default as usePlayTime } from './usePlayTime'
|
export { default as usePlayTime } from './usePlayTime'
|
||||||
export { default as useAssertApiSupport } from './useAssertApiSupport'
|
export { default as useAssertApiSupport } from './useAssertApiSupport'
|
||||||
export { useDrag } from './useDrag'
|
export { useDrag } from './useDrag'
|
||||||
|
export { useUnmounted } from './useUnmounted'
|
||||||
|
13
src/utils/hooks/useUnmounted.tsx
Normal file
13
src/utils/hooks/useUnmounted.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export function useUnmounted() {
|
||||||
|
const isUnmountedRef = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
isUnmountedRef.current = false
|
||||||
|
return () => {
|
||||||
|
isUnmountedRef.current = true
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return isUnmountedRef.current
|
||||||
|
}
|
@ -1,5 +1,14 @@
|
|||||||
import { scanFiles } from 'react-native-local-media-metadata'
|
import { scanFiles } from 'react-native-local-media-metadata'
|
||||||
export { type MusicMetadata, readMetadata, readPic, readLyric } from 'react-native-local-media-metadata'
|
export {
|
||||||
|
type MusicMetadata,
|
||||||
|
type MusicMetadataFull,
|
||||||
|
readMetadata,
|
||||||
|
writeMetadata,
|
||||||
|
readPic,
|
||||||
|
writePic,
|
||||||
|
readLyric,
|
||||||
|
writeLyric,
|
||||||
|
} from 'react-native-local-media-metadata'
|
||||||
|
|
||||||
export const scanAudioFiles = async(dirPath: string): Promise<string[]> => {
|
export const scanAudioFiles = async(dirPath: string): Promise<string[]> => {
|
||||||
return scanFiles(dirPath, ['mp3', 'flac', 'ogg', 'wav'])
|
return scanFiles(dirPath, ['mp3', 'flac', 'ogg', 'wav'])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user