添加打开手动输入存储路径的功能

This commit is contained in:
lyswhut 2023-12-20 13:23:39 +08:00
parent 906e2eb9f8
commit adb9c7c582
11 changed files with 353 additions and 109 deletions

View File

@ -15,6 +15,7 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.concurrent.Callable;
public class Utils {
@ -34,9 +35,10 @@ public class Utils {
}
// https://gist.github.com/PauloLuan/4bcecc086095bce28e22?permalink_comment_id=2591001#gistcomment-2591001
public static String getExternalStoragePath(ReactApplicationContext mContext, boolean is_removable) {
public static ArrayList<String> getExternalStoragePath(ReactApplicationContext mContext, boolean is_removable) {
StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE);
Class<?> storageVolumeClazz;
ArrayList<String> paths = new ArrayList<>();
try {
storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
@ -49,13 +51,13 @@ public class Utils {
String path = (String) getPath.invoke(storageVolumeElement);
boolean removable = (Boolean) isRemovable.invoke(storageVolumeElement);
if (is_removable == removable) {
return path;
paths.add(path);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
return paths;
}
public static String convertStreamToString(InputStream is) throws Exception {

View File

@ -35,6 +35,7 @@ import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeArray;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
@ -398,7 +399,10 @@ public class UtilsModule extends ReactContextBaseJavaModule {
@ReactMethod
public void getExternalStoragePath(Promise promise) {
promise.resolve(Utils.getExternalStoragePath(reactContext, true));
WritableArray arr = Arguments.createArray();
ArrayList<String> paths = Utils.getExternalStoragePath(reactContext, true);
for (String p: paths) arr.pushString(p);
promise.resolve(arr);
}
}

View File

@ -14,7 +14,7 @@
- 优化播放详情页歌曲封面、控制按钮对各尺寸屏幕的适配,修改横屏下的控制栏按钮布局
- 优化横竖屏界面的暂时判断,现在趋于方屏的屏幕按竖屏的方式显示,横屏下的播放栏添加上一曲切歌按钮
- 添加对wy源某些歌曲有问题的歌词进行修复#370
- 文件选择器允许(在旧系统)选择外置存储设备上的路径
- 文件选择器允许(在旧系统)选择外置存储设备上的路径,长按存储卡按钮可显示手动输入存储路径的窗口
- 图片显示改用第三方的图片组件支持gif类型的图片显示尝试解决某些设备上图片过多导致的应用崩溃问题
- 歌曲评论内容过长时自动折叠,需手动展开
- 改进本地音乐在线信息的匹配机制

View File

@ -1,52 +1,17 @@
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useRef } from 'react'
import { View, TouchableOpacity } from 'react-native'
import Input, { type InputType } from '@/components/common/Input'
import Text from '@/components/common/Text'
import { Icon } from '@/components/common/Icon'
import ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'
import { createStyle, toast } from '@/utils/tools'
import { mkdir, readDir } from '@/utils/fs'
import { createStyle } from '@/utils/tools'
import { readDir } from '@/utils/fs'
import { useTheme } from '@/store/theme/hook'
import { scaleSizeH } from '@/utils/pixelRatio'
import { getExternalStoragePath } from '@/utils/nativeModules/utils'
import { useUnmounted } from '@/utils/hooks'
import { useStatusbarHeight } from '@/store/common/hook'
const filterFileName = /[\\/:*?#"<>|]/
import NewFolderModal, { type NewFolderType } from './NewFolderModal'
import OpenStorageModal, { type OpenDirModalType } from './OpenStorageModal'
interface NameInputType {
setName: (text: string) => void
getText: () => string
focus: () => void
}
const NameInput = forwardRef<NameInputType, {}>((props, ref) => {
const theme = useTheme()
const [text, setText] = useState('')
const inputRef = useRef<InputType>(null)
useImperativeHandle(ref, () => ({
getText() {
return text.trim()
},
setName(text) {
setText(text)
},
focus() {
inputRef.current?.focus()
},
}))
return (
<Input
ref={inputRef}
placeholder={global.i18n.t('create_new_folder_tip')}
value={text}
onChangeText={setText}
style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}
/>
)
})
export default memo(({
title,
path,
@ -57,24 +22,21 @@ export default memo(({
onRefreshDir: (dir: string) => Promise<void>
}) => {
const theme = useTheme()
const confirmAlertRef = useRef<ConfirmAlertType>(null)
const nameInputRef = useRef<NameInputType>(null)
const storagePathRef = useRef('')
const [isShowStorage, setIsShowStorage] = useState(false)
const isUnmounted = useUnmounted()
const newFolderTypeRef = useRef<NewFolderType>(null)
const openDirModalTypeRef = useRef<OpenDirModalType>(null)
const storagePathsRef = useRef<string[]>([])
const statusBarHeight = useStatusbarHeight()
const checkExternalStoragePath = useCallback(() => {
void getExternalStoragePath().then((storagePath) => {
if (storagePath) {
void readDir(storagePath).then(() => {
if (isUnmounted.current) return
storagePathRef.current = storagePath
setIsShowStorage(true)
}).catch(() => {
setIsShowStorage(false)
})
} else setIsShowStorage(false)
// storagePathsRef.current = []
void getExternalStoragePath().then(async(storagePaths) => {
for (const path of storagePaths) {
try {
await readDir(path)
} catch { continue }
storagePathsRef.current.push(path)
break
}
})
}, [])
useEffect(() => {
@ -87,38 +49,18 @@ export default memo(({
}
const toggleStorageDir = () => {
void onRefreshDir(storagePathRef.current)
}
const handleShow = () => {
confirmAlertRef.current?.setVisible(true)
requestAnimationFrame(() => {
setTimeout(() => {
nameInputRef.current?.focus()
}, 300)
})
}
const handleHideNewFolderAlert = () => {
nameInputRef.current?.setName('')
}
const handleConfirmNewFolderAlert = () => {
const text = nameInputRef.current?.getText() ?? ''
if (!text) return
if (filterFileName.test(text)) {
toast(global.i18n.t('create_new_folder_error_tip'), 'long')
if (storagePathsRef.current.length) {
void onRefreshDir(storagePathsRef.current[0])
return
}
const newPath = `${path}/${text}`
mkdir(newPath).then(() => {
void onRefreshDir(path).then(() => {
void onRefreshDir(newPath)
})
nameInputRef.current?.setName('')
}).catch((err: any) => {
toast('Create failed: ' + (err.message as string))
})
confirmAlertRef.current?.setVisible(false)
openStorage()
}
const openStorage = () => {
openDirModalTypeRef.current?.show(storagePathsRef.current)
}
const handleShowNewFolderModal = () => {
newFolderTypeRef.current?.show(path)
}
return (
@ -134,14 +76,10 @@ export default memo(({
<Text style={styles.subTitle} color={theme['c-primary-font']} size={13} numberOfLines={1}>{path}</Text>
</View>
<View style={styles.actions}>
{
isShowStorage ? (
<TouchableOpacity style={styles.actionBtn} onPress={toggleStorageDir}>
<Icon name="sd-card" color={theme['c-primary-font']} size={22} />
</TouchableOpacity>
) : null
}
<TouchableOpacity style={styles.actionBtn} onPress={handleShow}>
<TouchableOpacity style={styles.actionBtn} onPress={toggleStorageDir} onLongPress={openStorage}>
<Icon name="sd-card" color={theme['c-primary-font']} size={22} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionBtn} onPress={handleShowNewFolderModal}>
<Icon name="add_folder" color={theme['c-primary-font']} size={22} />
</TouchableOpacity>
<TouchableOpacity style={styles.actionBtn} onPress={refresh}>
@ -149,16 +87,8 @@ export default memo(({
</TouchableOpacity>
</View>
</View>
<ConfirmAlert
onHide={handleHideNewFolderAlert}
onConfirm={handleConfirmNewFolderAlert}
ref={confirmAlertRef}
>
<View style={styles.newFolderContent}>
<Text style={styles.newFolderTitle}>{global.i18n.t('create_new_folder')}</Text>
<NameInput ref={nameInputRef} />
</View>
</ConfirmAlert>
<OpenStorageModal ref={openDirModalTypeRef} onRefreshDir={onRefreshDir} />
<NewFolderModal ref={newFolderTypeRef} onRefreshDir={onRefreshDir} />
</>
)
})

View File

@ -0,0 +1,118 @@
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
import { View } from 'react-native'
import Input, { type InputType } from '@/components/common/Input'
import Text from '@/components/common/Text'
import ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'
import { createStyle, toast } from '@/utils/tools'
import { mkdir } from '@/utils/fs'
import { useTheme } from '@/store/theme/hook'
const filterFileName = /[\\/:*?#"<>|]/
interface NameInputType {
setName: (text: string) => void
getText: () => string
focus: () => void
}
const NameInput = forwardRef<NameInputType, {}>((props, ref) => {
const theme = useTheme()
const [text, setText] = useState('')
const inputRef = useRef<InputType>(null)
useImperativeHandle(ref, () => ({
getText() {
return text.trim()
},
setName(text) {
setText(text)
},
focus() {
inputRef.current?.focus()
},
}))
return (
<Input
ref={inputRef}
placeholder={global.i18n.t('create_new_folder_tip')}
value={text}
onChangeText={setText}
style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}
/>
)
})
export interface NewFolderType {
show: (path: string) => void
}
export default forwardRef<NewFolderType, { onRefreshDir: (dir: string) => Promise<void> }>(({ onRefreshDir }, ref) => {
const confirmAlertRef = useRef<ConfirmAlertType>(null)
const nameInputRef = useRef<NameInputType>(null)
const pathRef = useRef<string>('')
useImperativeHandle(ref, () => ({
show(path) {
pathRef.current = path
confirmAlertRef.current?.setVisible(true)
requestAnimationFrame(() => {
setTimeout(() => {
nameInputRef.current?.focus()
}, 300)
})
},
}))
const handleHideNewFolderAlert = () => {
nameInputRef.current?.setName('')
}
const handleConfirmNewFolderAlert = () => {
const text = nameInputRef.current?.getText() ?? ''
if (!text) return
if (filterFileName.test(text)) {
toast(global.i18n.t('create_new_folder_error_tip'), 'long')
return
}
const newPath = `${pathRef.current}/${text}`
mkdir(newPath).then(() => {
void onRefreshDir(pathRef.current).then(() => {
void onRefreshDir(newPath)
})
nameInputRef.current?.setName('')
}).catch((err: any) => {
toast('Create failed: ' + (err.message as string))
})
confirmAlertRef.current?.setVisible(false)
}
return (
<ConfirmAlert
onHide={handleHideNewFolderAlert}
onConfirm={handleConfirmNewFolderAlert}
ref={confirmAlertRef}
>
<View style={styles.newFolderContent}>
<Text style={styles.newFolderTitle}>{global.i18n.t('create_new_folder')}</Text>
<NameInput ref={nameInputRef} />
</View>
</ConfirmAlert>
)
})
const styles = createStyle({
newFolderContent: {
flexShrink: 1,
flexDirection: 'column',
},
newFolderTitle: {
marginBottom: 5,
},
input: {
flexGrow: 1,
flexShrink: 1,
minWidth: 240,
borderRadius: 4,
paddingTop: 2,
paddingBottom: 2,
},
})

View File

@ -0,0 +1,161 @@
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
import { View } from 'react-native'
import Input, { type InputType } from '@/components/common/Input'
import Text from '@/components/common/Text'
import ConfirmAlert, { type ConfirmAlertType } from '@/components/common/ConfirmAlert'
import { createStyle, toast } from '@/utils/tools'
import { readDir } from '@/utils/fs'
import { useTheme } from '@/store/theme/hook'
import { getOpenStoragePath, saveOpenStoragePath } from '@/utils/data'
import Button from '../../Button'
const filterFileName = /[\\:*?#"<>|]/
interface PathInputType {
setPath: (text: string) => void
getText: () => string
focus: () => void
}
const PathInput = forwardRef<PathInputType, {}>((props, ref) => {
const theme = useTheme()
const [text, setText] = useState('')
const inputRef = useRef<InputType>(null)
useImperativeHandle(ref, () => ({
getText() {
return text.trim()
},
setPath(text) {
setText(text)
},
focus() {
inputRef.current?.focus()
},
}))
return (
<Input
ref={inputRef}
placeholder={global.i18n.t('open_storage_tip')}
value={text}
onChangeText={setText}
multiline
numberOfLines={3}
textAlignVertical='top'
style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}
/>
)
})
export interface OpenDirModalType {
show: (paths: string[]) => void
}
export default forwardRef<OpenDirModalType, { onRefreshDir: (dir: string) => Promise<void> }>(({
onRefreshDir,
}, ref) => {
const confirmAlertRef = useRef<ConfirmAlertType>(null)
const inputRef = useRef<PathInputType>(null)
const [paths, setPaths] = useState<string[]>([])
useImperativeHandle(ref, () => ({
show(paths) {
setPaths(paths)
confirmAlertRef.current?.setVisible(true)
requestAnimationFrame(() => {
void getOpenStoragePath().then(path => {
if (path) inputRef.current?.setPath(path)
})
setTimeout(() => {
inputRef.current?.focus()
}, 300)
})
},
}))
// const handleHideAlert = () => {
// inputRef.current?.setPath('')
// }
const handleConfirmAlert = async() => {
const text = inputRef.current?.getText() ?? ''
if (text) {
if (!text.startsWith('/') || filterFileName.test(text)) {
toast(global.i18n.t('open_storage_error_tip'), 'long')
return
}
try {
await readDir(text)
} catch (err: any) {
toast('Open failed: ' + err.message, 'long')
return
}
void onRefreshDir(text)
}
void saveOpenStoragePath(text)
confirmAlertRef.current?.setVisible(false)
}
return (
<ConfirmAlert
onConfirm={handleConfirmAlert}
ref={confirmAlertRef}
>
<View style={styles.newFolderContent} onStartShouldSetResponder={() => true}>
<Text style={styles.newFolderTitle}>
{
paths.length
? global.i18n.t('open_storage_title')
: global.i18n.t('open_storage_not_found_title')
}
</Text>
<PathInput ref={inputRef} />
{
paths.length ? (
<View style={styles.list}>
{
paths.map(path => {
return (
<Button style={styles.listItem} key={path} onPress={() => inputRef.current?.setPath(path)}>
<Text size={12}>{path}</Text>
</Button>
)
})
}
</View>
) : null
}
</View>
</ConfirmAlert>
)
})
const styles = createStyle({
newFolderContent: {
flexGrow: 0,
flexShrink: 1,
flexDirection: 'column',
},
newFolderTitle: {
marginBottom: 5,
width: 300,
maxWidth: '100%',
},
input: {
flexGrow: 1,
flexShrink: 1,
minWidth: 240,
borderRadius: 4,
paddingTop: 3,
paddingBottom: 3,
height: 'auto',
},
list: {
flexGrow: 1,
flexShrink: 1,
marginTop: 10,
},
listItem: {
paddingVertical: 10,
paddingHorizontal: 5,
},
})

View File

@ -55,6 +55,7 @@ export const storageDataPrefix = {
syncHost: '@sync_host',
syncHostHistory: '@sync_host_history',
openStoragePath: '@open_storage_path',
notificationTipEnable: '@notification_tip_enable',
ignoringBatteryOptimizationTipEnable: '@ignoring_battery_optimization_tip_enable',

View File

@ -138,6 +138,10 @@
"notifications_check_tip": "You have not allowed LX Music to display notifications, or the Music Service in the LX Music notification settings has been disabled, which will prevent you from using the notification bar to pause, switch songs, etc. Do you want to enable it?",
"notifications_check_title": "Notification permission reminder",
"ok": "OK",
"open_storage_error_tip": "The entered path is illegal",
"open_storage_not_found_title": "External memory card not found, please manually enter the path below to specify the external memory",
"open_storage_tip": "Enter storage path",
"open_storage_title": "Please manually enter the path below to specify the external storage",
"parent_dir_name": "Parent directory",
"pause": "Pause",
"play": "Play",

View File

@ -138,6 +138,10 @@
"notifications_check_tip": "你没有允许LX Music显示通知或LX Music通知设置里的Music Service通知被禁用这将无法使用通知栏进行暂停、切歌等操作是否去开启",
"notifications_check_title": "通知权限提醒",
"ok": "我知道了",
"open_storage_error_tip": "输入的路径不合法",
"open_storage_not_found_title": "未找到外置存储卡,请手动在下方输入路径以指定外置存储器",
"open_storage_tip": "输入存储路径",
"open_storage_title": "请手动在下方输入路径以指定外置存储器",
"parent_dir_name": "父级目录",
"pause": "暂停",
"play": "播放",

View File

@ -27,6 +27,7 @@ const syncHostHistoryPrefix = storageDataPrefix.syncHostHistory
const listPrefix = storageDataPrefix.list
const dislikeListPrefix = storageDataPrefix.dislikeList
const userApiPrefix = storageDataPrefix.userApi
const openStoragePathPrefix = storageDataPrefix.openStoragePath
// const defaultListKey = listPrefix + 'default'
// const loveListKey = listPrefix + 'love'
@ -195,6 +196,25 @@ export const getIgnoreVersionFailTipTime = async() => {
return ignoreVersionFailTipTime ?? 0
}
let openStoragePath: string | null = ''
export const saveOpenStoragePath = async(path: string) => {
if (path) {
openStoragePath = path
await saveData(openStoragePathPrefix, path)
} else {
if (!openStoragePath) return
openStoragePath = null
await removeData(openStoragePathPrefix)
}
}
// 获取上次打开的存储路径
export const getOpenStoragePath = async() => {
if (openStoragePath === '') {
// eslint-disable-next-line require-atomic-updates
openStoragePath = await getData<string | null>(openStoragePathPrefix)
}
return openStoragePath
}
export const getSearchSetting = async() => {
// eslint-disable-next-line require-atomic-updates

View File

@ -70,7 +70,7 @@ export const onScreenStateChange = (handler: (state: 'ON' | 'OFF') => void): ()
}
}
export const getExternalStoragePath = async(): Promise<string | null> => {
export const getExternalStoragePath = async(): Promise<string[]> => {
return UtilsModule.getExternalStoragePath()
}