mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-04 07:32:10 +08:00
新增单个列表导入/导出功能,新增删除列表前的确认弹窗,防止误删列表
This commit is contained in:
parent
d9179c745c
commit
83086356b3
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
- 新增歌曲评论显示,可在播放详情页进入。(与PC端一样,目前仅支持显示部分评论)
|
- 新增歌曲评论显示,可在播放详情页进入。(与PC端一样,目前仅支持显示部分评论)
|
||||||
- 新增播放、收藏整个排行榜功能,可长按排行榜名字后在弹出的菜单中操作
|
- 新增播放、收藏整个排行榜功能,可长按排行榜名字后在弹出的菜单中操作
|
||||||
|
- 新增单个列表导入/导出功能,可以方便分享歌曲列表,可在点击“我的列表”里的列表名右侧的按钮后弹出的菜单中使用
|
||||||
|
- 新增删除列表前的确认弹窗,防止误删列表
|
||||||
|
|
||||||
### 优化
|
### 优化
|
||||||
|
|
||||||
|
@ -23,3 +23,5 @@ export const NAV_VIEW_NAMES = {
|
|||||||
list: 3,
|
list: 3,
|
||||||
setting: 4,
|
setting: 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LXM_FILE_EXT_RXP = /\.(json|lxmc)$/
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
"date_format_minute": "{{num}} minutes ago",
|
"date_format_minute": "{{num}} minutes ago",
|
||||||
"date_format_second": "{{num}} seconds ago",
|
"date_format_second": "{{num}} seconds ago",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"dialog_cancel": "No",
|
||||||
|
"dialog_confirm": "OK",
|
||||||
"disagree": "Disagree",
|
"disagree": "Disagree",
|
||||||
"disagree_tip": "Cancelled...",
|
"disagree_tip": "Cancelled...",
|
||||||
"input_error": "Don't input indiscriminately 😡",
|
"input_error": "Don't input indiscriminately 😡",
|
||||||
@ -39,11 +41,20 @@
|
|||||||
"list_edit_action_tip_remove_success": "Removed successfully",
|
"list_edit_action_tip_remove_success": "Removed successfully",
|
||||||
"list_end": "In The End",
|
"list_end": "In The End",
|
||||||
"list_error": "Loading failed 😥",
|
"list_error": "Loading failed 😥",
|
||||||
|
"list_export": "Export",
|
||||||
|
"list_export_part_desc": "Choose where to save the list file",
|
||||||
|
"list_import": "Import",
|
||||||
|
"list_import_part_button_cancel": "No",
|
||||||
|
"list_import_part_button_confirm": "Overwrite",
|
||||||
|
"list_import_part_confirm": "The imported list ({{importName}}) has the same ID as the local list ({{localName}}). Do you overwrite the local list?",
|
||||||
|
"list_import_part_desc": "Select list file",
|
||||||
|
"list_import_part_tip_failed": "This does not seem to be a single list file",
|
||||||
"list_loading": "Loading...",
|
"list_loading": "Loading...",
|
||||||
"list_multi_add_title_first_add": "Add selected",
|
"list_multi_add_title_first_add": "Add selected",
|
||||||
"list_multi_add_title_first_move": "Move the selected one",
|
"list_multi_add_title_first_move": "Move the selected one",
|
||||||
"list_multi_add_title_last": "First song to...",
|
"list_multi_add_title_last": "First song to...",
|
||||||
"list_remove": "Remove",
|
"list_remove": "Remove",
|
||||||
|
"list_remove_tip_button": "Yes, that's right",
|
||||||
"list_rename": "Rename",
|
"list_rename": "Rename",
|
||||||
"list_rename_title": "Rename List",
|
"list_rename_title": "Rename List",
|
||||||
"list_select_all": "Select All",
|
"list_select_all": "Select All",
|
||||||
|
@ -25,6 +25,8 @@
|
|||||||
"date_format_minute": "{{num}}分钟前",
|
"date_format_minute": "{{num}}分钟前",
|
||||||
"date_format_second": "{{num}}秒前",
|
"date_format_second": "{{num}}秒前",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
"dialog_cancel": "我不",
|
||||||
|
"dialog_confirm": "好的",
|
||||||
"disagree": "我就不",
|
"disagree": "我就不",
|
||||||
"disagree_tip": "那算了... 🙄",
|
"disagree_tip": "那算了... 🙄",
|
||||||
"input_error": "不要乱输好吧😡",
|
"input_error": "不要乱输好吧😡",
|
||||||
@ -39,11 +41,21 @@
|
|||||||
"list_edit_action_tip_remove_success": "移除成功",
|
"list_edit_action_tip_remove_success": "移除成功",
|
||||||
"list_end": "到底啦~",
|
"list_end": "到底啦~",
|
||||||
"list_error": "加载失败😥",
|
"list_error": "加载失败😥",
|
||||||
|
"list_export": "导出",
|
||||||
|
"list_export_part_desc": "选择列表文件保存位置",
|
||||||
|
"list_import": "导入",
|
||||||
|
"list_import_part_button_cancel": "不要啊",
|
||||||
|
"list_import_part_button_confirm": "覆盖掉",
|
||||||
|
"list_import_part_confirm": "导入的列表({{importName}})与本地列表({{localName}})的ID相同,是否覆盖本地列表?",
|
||||||
|
"list_import_part_desc": "选择列表文件",
|
||||||
|
"list_import_part_tip_failed": "这似乎不是单个的列表文件哦",
|
||||||
"list_loading": "加载中...",
|
"list_loading": "加载中...",
|
||||||
"list_multi_add_title_first_add": "添加已选的",
|
"list_multi_add_title_first_add": "添加已选的",
|
||||||
"list_multi_add_title_first_move": "移动已选的",
|
"list_multi_add_title_first_move": "移动已选的",
|
||||||
"list_multi_add_title_last": "首歌曲到...",
|
"list_multi_add_title_last": "首歌曲到...",
|
||||||
"list_remove": "移除",
|
"list_remove": "移除",
|
||||||
|
"list_remove_tip": "你真的想要移除 {{name}} 吗?",
|
||||||
|
"list_remove_tip_button": "是的 没错",
|
||||||
"list_rename": "重命名",
|
"list_rename": "重命名",
|
||||||
"list_rename_title": "重命名列表",
|
"list_rename_title": "重命名列表",
|
||||||
"list_select_all": "全选",
|
"list_select_all": "全选",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { memo, useMemo, useEffect, useCallback, useState, useRef } from 'react'
|
import React, { memo, useMemo, useEffect, useCallback, useState, useRef } from 'react'
|
||||||
import { StyleSheet, Text, View, TouchableOpacity, ScrollView } from 'react-native'
|
import { StyleSheet, Text, View, TouchableOpacity, ScrollView, InteractionManager } from 'react-native'
|
||||||
|
|
||||||
import { useGetter, useDispatch } from '@/store'
|
import { useGetter, useDispatch } from '@/store'
|
||||||
import { useTranslation } from '@/plugins/i18n'
|
import { useTranslation } from '@/plugins/i18n'
|
||||||
@ -10,9 +10,39 @@ import { BorderWidths } from '@/theme'
|
|||||||
import Menu from '@/components/common/Menu'
|
import Menu from '@/components/common/Menu'
|
||||||
import ConfirmAlert from '@/components/common/ConfirmAlert'
|
import ConfirmAlert from '@/components/common/ConfirmAlert'
|
||||||
import Input from '@/components/common/Input'
|
import Input from '@/components/common/Input'
|
||||||
import { getListScrollPosition, saveListScrollPosition, toast } from '@/utils/tools'
|
import { filterFileName } from '@/utils'
|
||||||
import { LIST_SCROLL_POSITION_KEY } from '@/config/constant'
|
import { getListScrollPosition, saveListScrollPosition, toast, handleSaveFile, handleReadFile, confirmDialog } from '@/utils/tools'
|
||||||
|
import { LIST_SCROLL_POSITION_KEY, LXM_FILE_EXT_RXP } from '@/config/constant'
|
||||||
import musicSdk from '@/utils/music'
|
import musicSdk from '@/utils/music'
|
||||||
|
import ChoosePath from '@/components/common/ChoosePath'
|
||||||
|
import { log } from '@/utils/log'
|
||||||
|
|
||||||
|
const exportList = async(list, path) => {
|
||||||
|
const data = JSON.parse(JSON.stringify({
|
||||||
|
type: 'playListPart',
|
||||||
|
data: list,
|
||||||
|
}))
|
||||||
|
for (const item of data.data.list) {
|
||||||
|
if (item.otherSource) delete item.otherSource
|
||||||
|
if (item.lrc) delete item.lrc
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await handleSaveFile(path + `/lx_list_part_${filterFileName(list.name)}.lxmc`, data)
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error.stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const importList = async path => {
|
||||||
|
let listData
|
||||||
|
try {
|
||||||
|
listData = await handleReadFile(path)
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error.stack)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.log(listData.type)
|
||||||
|
return listData
|
||||||
|
}
|
||||||
|
|
||||||
const ListItem = ({ onPress, name, id, showMenu, activeId, loading, index }) => {
|
const ListItem = ({ onPress, name, id, showMenu, activeId, loading, index }) => {
|
||||||
const theme = useGetter('common', 'theme')
|
const theme = useGetter('common', 'theme')
|
||||||
@ -38,6 +68,89 @@ const ListItem = ({ onPress, name, id, showMenu, activeId, loading, index }) =>
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ImportExport = ({ actionType, visible, hide, selectedListRef }) => {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [dirOnly, setDirOnly] = useState(false)
|
||||||
|
const setList = useDispatch('list', 'setList')
|
||||||
|
const createUserList = useDispatch('list', 'createUserList')
|
||||||
|
const { t } = useTranslation()
|
||||||
|
useEffect(() => {
|
||||||
|
switch (actionType) {
|
||||||
|
case 'import':
|
||||||
|
setTitle(t('list_import_part_desc'))
|
||||||
|
setDirOnly(false)
|
||||||
|
break
|
||||||
|
case 'export':
|
||||||
|
default:
|
||||||
|
setTitle(t('list_export_part_desc'))
|
||||||
|
setDirOnly(true)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [actionType, t])
|
||||||
|
|
||||||
|
const onConfirmPath = useCallback(path => {
|
||||||
|
hide()
|
||||||
|
switch (actionType) {
|
||||||
|
case 'import':
|
||||||
|
toast(t('setting_backup_part_import_list_tip_unzip'))
|
||||||
|
importList(path).then(async listData => {
|
||||||
|
if (listData.type != 'playListPart') return toast(t('list_import_part_tip_failed'))
|
||||||
|
const targetList = global.allList[listData.data.id]
|
||||||
|
if (targetList) {
|
||||||
|
const confirm = await confirmDialog({
|
||||||
|
message: t('list_import_part_confirm', { importName: listData.data.name, localName: targetList.name }),
|
||||||
|
cancelButtonText: t('list_import_part_button_cancel'),
|
||||||
|
confirmButtonText: t('list_import_part_button_confirm'),
|
||||||
|
bgClose: false,
|
||||||
|
})
|
||||||
|
if (confirm) {
|
||||||
|
listData.data.name = targetList.name
|
||||||
|
setList({
|
||||||
|
name: listData.data.name,
|
||||||
|
id: listData.data.id,
|
||||||
|
list: listData.data.list,
|
||||||
|
source: listData.data.source,
|
||||||
|
sourceListId: listData.data.sourceListId,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
listData.data.id += `__${Date.now()}`
|
||||||
|
}
|
||||||
|
createUserList({
|
||||||
|
name: listData.data.name,
|
||||||
|
id: listData.data.id,
|
||||||
|
list: listData.data.list,
|
||||||
|
source: listData.data.source,
|
||||||
|
sourceListId: listData.data.sourceListId,
|
||||||
|
position: Math.max(selectedListRef.current.index, -1),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
case 'export':
|
||||||
|
InteractionManager.runAfterInteractions(() => {
|
||||||
|
toast(t('setting_backup_part_export_list_tip_zip'))
|
||||||
|
exportList(selectedListRef.current.listInfo, path).then(() => {
|
||||||
|
toast(t('setting_backup_part_export_list_tip_success'))
|
||||||
|
}).catch(err => {
|
||||||
|
log.error(err.message)
|
||||||
|
toast(t('setting_backup_part_export_list_tip_failed') + ': ' + err.message)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [actionType, createUserList, hide, setList, t])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChoosePath
|
||||||
|
visible={visible}
|
||||||
|
hide={hide}
|
||||||
|
title={title}
|
||||||
|
dirOnly={dirOnly}
|
||||||
|
filter={LXM_FILE_EXT_RXP}
|
||||||
|
onConfirm={onConfirmPath} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) => {
|
const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) => {
|
||||||
const theme = useGetter('common', 'theme')
|
const theme = useGetter('common', 'theme')
|
||||||
const defaultList = useGetter('list', 'defaultList')
|
const defaultList = useGetter('list', 'defaultList')
|
||||||
@ -61,6 +174,8 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
const getBoardListAll = useDispatch('top', 'getListAll')
|
const getBoardListAll = useDispatch('top', 'getListAll')
|
||||||
const getListDetailAll = useDispatch('songList', 'getListDetailAll')
|
const getListDetailAll = useDispatch('songList', 'getListDetailAll')
|
||||||
const [fetchingListStatus, setFetchingListStatus] = useState({})
|
const [fetchingListStatus, setFetchingListStatus] = useState({})
|
||||||
|
const [isShowChoosePath, setShowChoosePath] = useState(false)
|
||||||
|
const [actionType, setActionType] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
userListRef.current = userList
|
userListRef.current = userList
|
||||||
@ -75,6 +190,29 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
removeUserList({ id })
|
removeUserList({ id })
|
||||||
}, [removeUserList])
|
}, [removeUserList])
|
||||||
|
|
||||||
|
const getTargetListInfo = useCallback(index => {
|
||||||
|
let list
|
||||||
|
switch (index) {
|
||||||
|
case -2:
|
||||||
|
list = defaultList
|
||||||
|
break
|
||||||
|
case -1:
|
||||||
|
list = loveList
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
list = userListRef.current[index]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}, [defaultList, loveList])
|
||||||
|
|
||||||
|
const handleImportAndExportList = useCallback((type, index) => {
|
||||||
|
const list = getTargetListInfo(index)
|
||||||
|
if (!list) return
|
||||||
|
selectedListRef.current.listInfo = list
|
||||||
|
setActionType(type)
|
||||||
|
setShowChoosePath(true)
|
||||||
|
}, [getTargetListInfo])
|
||||||
|
|
||||||
const hideMenu = useCallback(() => {
|
const hideMenu = useCallback(() => {
|
||||||
setVisibleMenu(false)
|
setVisibleMenu(false)
|
||||||
@ -113,6 +251,12 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
setListNameText(selectedListRef.current.name)
|
setListNameText(selectedListRef.current.name)
|
||||||
setVisibleRename(true)
|
setVisibleRename(true)
|
||||||
break
|
break
|
||||||
|
case 'import':
|
||||||
|
handleImportAndExportList('import', selectedListRef.current.index)
|
||||||
|
break
|
||||||
|
case 'export':
|
||||||
|
handleImportAndExportList('export', selectedListRef.current.index)
|
||||||
|
break
|
||||||
case 'sync':
|
case 'sync':
|
||||||
handleSyncSourceList(selectedListRef.current.index)
|
handleSyncSourceList(selectedListRef.current.index)
|
||||||
break
|
break
|
||||||
@ -120,26 +264,50 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
|
|
||||||
// break
|
// break
|
||||||
case 'remove':
|
case 'remove':
|
||||||
|
confirmDialog({
|
||||||
|
message: t('list_remove_tip', { name: selectedListRef.current.name }),
|
||||||
|
confirmButtonText: t('list_remove_tip_button'),
|
||||||
|
}).then(isRemove => {
|
||||||
|
if (!isRemove) return
|
||||||
handleRemoveList(selectedListRef.current.id)
|
handleRemoveList(selectedListRef.current.id)
|
||||||
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [handleRemoveList, handleSyncSourceList])
|
}, [handleImportAndExportList, handleRemoveList, handleSyncSourceList, t])
|
||||||
|
|
||||||
const menus = useMemo(() => {
|
const menus = useMemo(() => {
|
||||||
const list = userList[selectedListIndex]
|
let list
|
||||||
|
let rename = false
|
||||||
|
let sync = false
|
||||||
|
let remove = false
|
||||||
|
switch (selectedListIndex) {
|
||||||
|
case -2:
|
||||||
|
list = defaultList
|
||||||
|
break
|
||||||
|
case -1:
|
||||||
|
list = loveList
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
list = userList[selectedListIndex]
|
||||||
|
rename = true
|
||||||
|
remove = true
|
||||||
|
sync = list.source && !!musicSdk[list.source].songList
|
||||||
|
break
|
||||||
|
}
|
||||||
if (!list) return []
|
if (!list) return []
|
||||||
const source = list.source
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ action: 'rename', label: t('list_rename') },
|
{ action: 'rename', disabled: !rename, label: t('list_rename') },
|
||||||
{ action: 'sync', label: t('list_sync'), disabled: !source || !musicSdk[source].songList },
|
{ action: 'sync', disabled: !sync, label: t('list_sync') },
|
||||||
|
{ action: 'import', label: t('list_import') },
|
||||||
|
{ action: 'export', label: t('list_export') },
|
||||||
// { action: 'changePosition', label: t('change_position') },
|
// { action: 'changePosition', label: t('change_position') },
|
||||||
{ action: 'remove', label: t('list_remove') },
|
{ action: 'remove', disabled: !remove, label: t('list_remove') },
|
||||||
]
|
]
|
||||||
}, [selectedListIndex, userList, t])
|
}, [selectedListIndex, t, defaultList, loveList, userList])
|
||||||
|
|
||||||
const handleCancelRename = useCallback(() => {
|
const handleCancelRename = useCallback(() => {
|
||||||
setVisibleRename(false)
|
setVisibleRename(false)
|
||||||
@ -150,13 +318,6 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
setVisibleRename(false)
|
setVisibleRename(false)
|
||||||
}, [listNameText, setUserListName])
|
}, [listNameText, setUserListName])
|
||||||
|
|
||||||
const handleToggleDefaultList = () => {
|
|
||||||
handleToggleList(defaultList)
|
|
||||||
}
|
|
||||||
const handleToggleLoveList = () => {
|
|
||||||
handleToggleList(loveList)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleScroll = useCallback(({ nativeEvent }) => {
|
const handleScroll = useCallback(({ nativeEvent }) => {
|
||||||
saveListScrollPosition(LIST_SCROLL_POSITION_KEY, nativeEvent.contentOffset.y)
|
saveListScrollPosition(LIST_SCROLL_POSITION_KEY, nativeEvent.contentOffset.y)
|
||||||
}, [])
|
}, [])
|
||||||
@ -167,7 +328,7 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
})
|
})
|
||||||
const showMenu = useCallback((id, name, index, position) => {
|
const showMenu = useCallback((id, name, index, position) => {
|
||||||
// console.log(position)
|
// console.log(position)
|
||||||
if (id == 'default' || id == 'love') return
|
// if (id == 'default' || id == 'love') return
|
||||||
setButtonPosition({ ...position })
|
setButtonPosition({ ...position })
|
||||||
selectedListRef.current.id = id
|
selectedListRef.current.id = id
|
||||||
selectedListRef.current.name = name
|
selectedListRef.current.name = name
|
||||||
@ -179,16 +340,8 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
<View style={{ ...styles.container, borderBottomColor: theme.secondary10 }}>
|
<View style={{ ...styles.container, borderBottomColor: theme.secondary10 }}>
|
||||||
<ScrollView style={{ flexShrink: 1, flexGrow: 0 }} onScroll={handleScroll} ref={scrollViewRef} keyboardShouldPersistTaps={'always'}>
|
<ScrollView style={{ flexShrink: 1, flexGrow: 0 }} onScroll={handleScroll} ref={scrollViewRef} keyboardShouldPersistTaps={'always'}>
|
||||||
<View style={{ ...styles.listContainer, backgroundColor: theme.primary }} onStartShouldSetResponder={() => true}>
|
<View style={{ ...styles.listContainer, backgroundColor: theme.primary }} onStartShouldSetResponder={() => true}>
|
||||||
<View style={{ ...styles.listItem, borderBottomColor: theme.secondary45 }}>
|
<ListItem name={defaultList.name} id={defaultList.id} index={-2} loading={false} onPress={() => handleToggleList(defaultList)} activeId={currentList.id} showMenu={showMenu} />
|
||||||
<TouchableOpacity style={styles.listName} onPress={handleToggleDefaultList}>
|
<ListItem name={loveList.name} id={loveList.id} index={-1} loading={false} onPress={() => handleToggleList(loveList)} activeId={currentList.id} showMenu={showMenu} />
|
||||||
<Text numberOfLines={1} style={{ color: theme.normal }}>{defaultList.name}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
<View style={{ ...styles.listItem, borderBottomColor: theme.secondary45 }}>
|
|
||||||
<TouchableOpacity style={styles.listName} onPress={handleToggleLoveList}>
|
|
||||||
<Text numberOfLines={1} style={{ color: theme.normal }}>{loveList.name}</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
{userList.map(({ id, name }, index) => <ListItem key={id} name={name} id={id} index={index} loading={fetchingListStatus[id]} onPress={() => handleToggleList({ id, name })} activeId={currentList.id} showMenu={showMenu} />)}
|
{userList.map(({ id, name }, index) => <ListItem key={id} name={name} id={id} index={index} loading={fetchingListStatus[id]} onPress={() => handleToggleList({ id, name })} activeId={currentList.id} showMenu={showMenu} />)}
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@ -208,6 +361,7 @@ const List = memo(({ setVisiblePanel, currentList, handleCancelMultiSelect }) =>
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ConfirmAlert>
|
</ConfirmAlert>
|
||||||
|
<ImportExport actionType={actionType} visible={isShowChoosePath} hide={() => setShowChoosePath(false)} selectedListRef={selectedListRef} />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { memo, useCallback, useState, useRef } from 'react'
|
import React, { memo, useCallback, useState, useRef } from 'react'
|
||||||
import { StyleSheet, View, Text, InteractionManager } from 'react-native'
|
import { StyleSheet, View, InteractionManager } from 'react-native'
|
||||||
import { readFile, writeFile, temporaryDirectoryPath, unlink } from '@/utils/fs'
|
|
||||||
import { log } from '@/utils/log'
|
import { log } from '@/utils/log'
|
||||||
|
import { LXM_FILE_EXT_RXP } from '@/config/constant'
|
||||||
|
|
||||||
import { useGetter, useDispatch } from '@/store'
|
import { useGetter, useDispatch } from '@/store'
|
||||||
// import { gzip, ungzip } from 'pako'
|
// import { gzip, ungzip } from 'pako'
|
||||||
@ -10,33 +10,9 @@ import SubTitle from '../components/SubTitle'
|
|||||||
import Button from '../components/Button'
|
import Button from '../components/Button'
|
||||||
import ChoosePath from '@/components/common/ChoosePath'
|
import ChoosePath from '@/components/common/ChoosePath'
|
||||||
import { useTranslation } from '@/plugins/i18n'
|
import { useTranslation } from '@/plugins/i18n'
|
||||||
import { toast } from '@/utils/tools'
|
import { toast, handleSaveFile, handleReadFile } from '@/utils/tools'
|
||||||
import { gzip, ungzip } from '@/utils/gzip'
|
|
||||||
|
|
||||||
const lxmFileExt = /\.(json|lxmc)$/
|
const exportAllList = async(allList, path) => {
|
||||||
|
|
||||||
const handleSaveFile = async(path, data) => {
|
|
||||||
// if (!path.endsWith('.json')) path += '.json'
|
|
||||||
// const buffer = gzip(data)
|
|
||||||
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
|
|
||||||
await writeFile(tempFilePath, data, 'utf8')
|
|
||||||
await gzip(tempFilePath, path)
|
|
||||||
await unlink(tempFilePath)
|
|
||||||
}
|
|
||||||
const handleReadFile = async(path) => {
|
|
||||||
let isJSON = path.endsWith('.json')
|
|
||||||
let data
|
|
||||||
if (isJSON) {
|
|
||||||
data = await readFile(path, 'utf8')
|
|
||||||
} else {
|
|
||||||
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
|
|
||||||
await ungzip(path, tempFilePath)
|
|
||||||
data = await readFile(tempFilePath, 'utf8')
|
|
||||||
await unlink(tempFilePath)
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
const exportList = async(allList, path) => {
|
|
||||||
const data = JSON.parse(JSON.stringify({
|
const data = JSON.parse(JSON.stringify({
|
||||||
type: 'playList',
|
type: 'playList',
|
||||||
data: allList,
|
data: allList,
|
||||||
@ -46,12 +22,16 @@ const exportList = async(allList, path) => {
|
|||||||
if (item.otherSource) delete item.otherSource
|
if (item.otherSource) delete item.otherSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await handleSaveFile(path + '/lx_list.lxmc', JSON.stringify(data))
|
try {
|
||||||
|
await handleSaveFile(path + '/lx_list.lxmc', data)
|
||||||
|
} catch (error) {
|
||||||
|
log.error(error.stack)
|
||||||
}
|
}
|
||||||
const importList = async path => {
|
}
|
||||||
|
const importAllList = async path => {
|
||||||
let listData
|
let listData
|
||||||
try {
|
try {
|
||||||
listData = JSON.parse(await handleReadFile(path))
|
listData = await handleReadFile(path)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error(error.stack)
|
log.error(error.stack)
|
||||||
return
|
return
|
||||||
@ -116,7 +96,7 @@ export default memo(() => {
|
|||||||
case 'import_list':
|
case 'import_list':
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
toast(t('setting_backup_part_import_list_tip_unzip'))
|
toast(t('setting_backup_part_import_list_tip_unzip'))
|
||||||
importList(path).then(listData => {
|
importAllList(path).then(listData => {
|
||||||
// 兼容0.6.2及以前版本的列表数据
|
// 兼容0.6.2及以前版本的列表数据
|
||||||
if (listData.type === 'defautlList') {
|
if (listData.type === 'defautlList') {
|
||||||
handleSetList(setList, [
|
handleSetList(setList, [
|
||||||
@ -164,7 +144,7 @@ export default memo(() => {
|
|||||||
case 'export_list':
|
case 'export_list':
|
||||||
InteractionManager.runAfterInteractions(() => {
|
InteractionManager.runAfterInteractions(() => {
|
||||||
toast(t('setting_backup_part_export_list_tip_zip'))
|
toast(t('setting_backup_part_export_list_tip_zip'))
|
||||||
exportList(allList, path).then(() => {
|
exportAllList(allList, path).then(() => {
|
||||||
toast(t('setting_backup_part_export_list_tip_success'))
|
toast(t('setting_backup_part_export_list_tip_success'))
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
log.error(err.message)
|
log.error(err.message)
|
||||||
@ -206,7 +186,7 @@ export default memo(() => {
|
|||||||
hide={() => setShowChoosePath(false)}
|
hide={() => setShowChoosePath(false)}
|
||||||
title={title}
|
title={title}
|
||||||
dirOnly={dirOnly}
|
dirOnly={dirOnly}
|
||||||
filter={lxmFileExt}
|
filter={LXM_FILE_EXT_RXP}
|
||||||
onConfirm={onConfirmPath} />
|
onConfirm={onConfirmPath} />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -256,14 +256,14 @@ export const updateMusicInfo = ({ listId, id, data, isSync }) => (dispatch, getS
|
|||||||
saveList(global.allList[listId])
|
saveList(global.allList[listId])
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createUserList = ({ name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) => async(dispatch, getState) => {
|
export const createUserList = ({ name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, position, isSync }) => async(dispatch, getState) => {
|
||||||
if (!isSync) {
|
if (!isSync) {
|
||||||
listSync.sendListAction('create_user_list', { name, id, list, source, sourceListId })
|
listSync.sendListAction('create_user_list', { name, id, list, source, sourceListId, position })
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: TYPES.createUserList,
|
type: TYPES.createUserList,
|
||||||
payload: { name, id, source, sourceListId },
|
payload: { name, id, source, sourceListId, position },
|
||||||
})
|
})
|
||||||
dispatch(listAddMultiple({ id, list, isSync: true }))
|
dispatch(listAddMultiple({ id, list, isSync: true }))
|
||||||
await saveList(global.allList[id])
|
await saveList(global.allList[id])
|
||||||
|
@ -241,7 +241,7 @@ const mutations = {
|
|||||||
return updateStateList({ ...state }, [listId])
|
return updateStateList({ ...state }, [listId])
|
||||||
},
|
},
|
||||||
|
|
||||||
[TYPES.createUserList](state, { name, id, source, sourceListId }) {
|
[TYPES.createUserList](state, { name, id, source, sourceListId, position }) {
|
||||||
let newList = state.userList.find(item => item.id === id)
|
let newList = state.userList.find(item => item.id === id)
|
||||||
if (newList) return state
|
if (newList) return state
|
||||||
const newState = { ...state }
|
const newState = { ...state }
|
||||||
@ -252,7 +252,13 @@ const mutations = {
|
|||||||
source,
|
source,
|
||||||
sourceListId,
|
sourceListId,
|
||||||
}
|
}
|
||||||
newState.userList = [...state.userList, newList]
|
const userList = [...state.userList]
|
||||||
|
if (position == null) {
|
||||||
|
userList.push(newList)
|
||||||
|
} else {
|
||||||
|
userList.splice(position + 1, 0, newList)
|
||||||
|
}
|
||||||
|
newState.userList = userList
|
||||||
allListUpdate(newList)
|
allListUpdate(newList)
|
||||||
return newState
|
return newState
|
||||||
},
|
},
|
||||||
|
@ -156,3 +156,6 @@ export const debounce = (fn, delay = 100) => {
|
|||||||
}, delay)
|
}, delay)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fileNameRxp = /[\\/:*?#"<>|]/g
|
||||||
|
export const filterFileName = name => name.replace(fileNameRxp, '')
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import { Platform, NativeModules, ToastAndroid, BackHandler, Linking, Dimensions } from 'react-native'
|
import { Platform, NativeModules, ToastAndroid, BackHandler, Linking, Dimensions, Alert } from 'react-native'
|
||||||
import ExtraDimensions from 'react-native-extra-dimensions-android'
|
import ExtraDimensions from 'react-native-extra-dimensions-android'
|
||||||
import { getData, setData, getAllKeys, removeData, removeDataMultiple, setDataMultiple, getDataMultiple } from '@/plugins/storage'
|
import { getData, setData, getAllKeys, removeData, removeDataMultiple, setDataMultiple, getDataMultiple } from '@/plugins/storage'
|
||||||
import { storageDataPrefix } from '@/config'
|
import { storageDataPrefix } from '@/config'
|
||||||
import { throttle } from './index'
|
import { throttle } from './index'
|
||||||
|
import { gzip, ungzip } from '@/utils/gzip'
|
||||||
|
import { readFile, writeFile, temporaryDirectoryPath, unlink } from '@/utils/fs'
|
||||||
|
|
||||||
const playInfoStorageKey = storageDataPrefix.playInfo
|
const playInfoStorageKey = storageDataPrefix.playInfo
|
||||||
const listPositionPrefix = storageDataPrefix.listPosition
|
const listPositionPrefix = storageDataPrefix.listPosition
|
||||||
@ -242,6 +244,57 @@ export const setSyncHost = async({ host, port }) => {
|
|||||||
|
|
||||||
export const exitApp = BackHandler.exitApp
|
export const exitApp = BackHandler.exitApp
|
||||||
|
|
||||||
|
export const handleSaveFile = async(path, data) => {
|
||||||
|
// if (!path.endsWith('.json')) path += '.json'
|
||||||
|
// const buffer = gzip(data)
|
||||||
|
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
|
||||||
|
await writeFile(tempFilePath, JSON.stringify(data), 'utf8')
|
||||||
|
await gzip(tempFilePath, path)
|
||||||
|
await unlink(tempFilePath)
|
||||||
|
}
|
||||||
|
export const handleReadFile = async(path) => {
|
||||||
|
let isJSON = path.endsWith('.json')
|
||||||
|
let data
|
||||||
|
if (isJSON) {
|
||||||
|
data = await readFile(path, 'utf8')
|
||||||
|
} else {
|
||||||
|
const tempFilePath = `${temporaryDirectoryPath}/tempFile.json`
|
||||||
|
await ungzip(path, tempFilePath)
|
||||||
|
data = await readFile(tempFilePath, 'utf8')
|
||||||
|
await unlink(tempFilePath)
|
||||||
|
}
|
||||||
|
return JSON.parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const confirmDialog = ({
|
||||||
|
message = '',
|
||||||
|
cancelButtonText = global.i18n.t('dialog_cancel'),
|
||||||
|
confirmButtonText = global.i18n.t('dialog_confirm'),
|
||||||
|
bgClose = true,
|
||||||
|
}) => {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
Alert.alert(null, message, [
|
||||||
|
{
|
||||||
|
text: cancelButtonText,
|
||||||
|
onPress() {
|
||||||
|
resolve(false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: confirmButtonText,
|
||||||
|
onPress() {
|
||||||
|
resolve(true)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
], {
|
||||||
|
cancelable: bgClose,
|
||||||
|
onDismiss() {
|
||||||
|
resolve(false)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
deviceLanguage,
|
deviceLanguage,
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user