From adb9c7c5827a9d269e4c9c84015b35958e4339b0 Mon Sep 17 00:00:00 2001 From: lyswhut Date: Wed, 20 Dec 2023 13:23:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=93=E5=BC=80=E6=89=8B?= =?UTF-8?q?=E5=8A=A8=E8=BE=93=E5=85=A5=E5=AD=98=E5=82=A8=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E7=9A=84=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cn/toside/music/mobile/utils/Utils.java | 8 +- .../music/mobile/utils/UtilsModule.java | 6 +- publish/changeLog.md | 2 +- .../common/ChoosePath/components/Header.tsx | 136 ++++----------- .../ChoosePath/components/NewFolderModal.tsx | 118 +++++++++++++ .../components/OpenStorageModal.tsx | 161 ++++++++++++++++++ src/config/constant.ts | 1 + src/lang/en_us.json | 4 + src/lang/zh_cn.json | 4 + src/utils/data.ts | 20 +++ src/utils/nativeModules/utils.ts | 2 +- 11 files changed, 353 insertions(+), 109 deletions(-) create mode 100644 src/components/common/ChoosePath/components/NewFolderModal.tsx create mode 100644 src/components/common/ChoosePath/components/OpenStorageModal.tsx diff --git a/android/app/src/main/java/cn/toside/music/mobile/utils/Utils.java b/android/app/src/main/java/cn/toside/music/mobile/utils/Utils.java index d3e6ec8..8208514 100644 --- a/android/app/src/main/java/cn/toside/music/mobile/utils/Utils.java +++ b/android/app/src/main/java/cn/toside/music/mobile/utils/Utils.java @@ -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 getExternalStoragePath(ReactApplicationContext mContext, boolean is_removable) { StorageManager mStorageManager = (StorageManager) mContext.getSystemService(Context.STORAGE_SERVICE); Class storageVolumeClazz; + ArrayList 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 { diff --git a/android/app/src/main/java/cn/toside/music/mobile/utils/UtilsModule.java b/android/app/src/main/java/cn/toside/music/mobile/utils/UtilsModule.java index 2d46d48..e8c846a 100644 --- a/android/app/src/main/java/cn/toside/music/mobile/utils/UtilsModule.java +++ b/android/app/src/main/java/cn/toside/music/mobile/utils/UtilsModule.java @@ -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 paths = Utils.getExternalStoragePath(reactContext, true); + for (String p: paths) arr.pushString(p); + promise.resolve(arr); } } diff --git a/publish/changeLog.md b/publish/changeLog.md index e5d84d5..581d4f8 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -14,7 +14,7 @@ - 优化播放详情页歌曲封面、控制按钮对各尺寸屏幕的适配,修改横屏下的控制栏按钮布局 - 优化横竖屏界面的暂时判断,现在趋于方屏的屏幕按竖屏的方式显示,横屏下的播放栏添加上一曲切歌按钮 - 添加对wy源某些歌曲有问题的歌词进行修复(#370) -- 文件选择器允许(在旧系统)选择外置存储设备上的路径 +- 文件选择器允许(在旧系统)选择外置存储设备上的路径,长按存储卡按钮可显示手动输入存储路径的窗口 - 图片显示改用第三方的图片组件,支持gif类型的图片显示,尝试解决某些设备上图片过多导致的应用崩溃问题 - 歌曲评论内容过长时自动折叠,需手动展开 - 改进本地音乐在线信息的匹配机制 diff --git a/src/components/common/ChoosePath/components/Header.tsx b/src/components/common/ChoosePath/components/Header.tsx index 35369dc..5a0f91f 100644 --- a/src/components/common/ChoosePath/components/Header.tsx +++ b/src/components/common/ChoosePath/components/Header.tsx @@ -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((props, ref) => { - const theme = useTheme() - const [text, setText] = useState('') - const inputRef = useRef(null) - - useImperativeHandle(ref, () => ({ - getText() { - return text.trim() - }, - setName(text) { - setText(text) - }, - focus() { - inputRef.current?.focus() - }, - })) - - return ( - - ) -}) - export default memo(({ title, path, @@ -57,24 +22,21 @@ export default memo(({ onRefreshDir: (dir: string) => Promise }) => { const theme = useTheme() - const confirmAlertRef = useRef(null) - const nameInputRef = useRef(null) - const storagePathRef = useRef('') - const [isShowStorage, setIsShowStorage] = useState(false) - const isUnmounted = useUnmounted() + const newFolderTypeRef = useRef(null) + const openDirModalTypeRef = useRef(null) + const storagePathsRef = useRef([]) 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(({ {path} - { - isShowStorage ? ( - - - - ) : null - } - + + + + @@ -149,16 +87,8 @@ export default memo(({ - - - {global.i18n.t('create_new_folder')} - - - + + ) }) diff --git a/src/components/common/ChoosePath/components/NewFolderModal.tsx b/src/components/common/ChoosePath/components/NewFolderModal.tsx new file mode 100644 index 0000000..d9d5a56 --- /dev/null +++ b/src/components/common/ChoosePath/components/NewFolderModal.tsx @@ -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((props, ref) => { + const theme = useTheme() + const [text, setText] = useState('') + const inputRef = useRef(null) + + useImperativeHandle(ref, () => ({ + getText() { + return text.trim() + }, + setName(text) { + setText(text) + }, + focus() { + inputRef.current?.focus() + }, + })) + + return ( + + ) +}) + +export interface NewFolderType { + show: (path: string) => void +} +export default forwardRef Promise }>(({ onRefreshDir }, ref) => { + const confirmAlertRef = useRef(null) + const nameInputRef = useRef(null) + const pathRef = useRef('') + + 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 ( + + + {global.i18n.t('create_new_folder')} + + + + ) +}) + +const styles = createStyle({ + newFolderContent: { + flexShrink: 1, + flexDirection: 'column', + }, + newFolderTitle: { + marginBottom: 5, + }, + input: { + flexGrow: 1, + flexShrink: 1, + minWidth: 240, + borderRadius: 4, + paddingTop: 2, + paddingBottom: 2, + }, +}) + diff --git a/src/components/common/ChoosePath/components/OpenStorageModal.tsx b/src/components/common/ChoosePath/components/OpenStorageModal.tsx new file mode 100644 index 0000000..220c0d0 --- /dev/null +++ b/src/components/common/ChoosePath/components/OpenStorageModal.tsx @@ -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((props, ref) => { + const theme = useTheme() + const [text, setText] = useState('') + const inputRef = useRef(null) + + useImperativeHandle(ref, () => ({ + getText() { + return text.trim() + }, + setPath(text) { + setText(text) + }, + focus() { + inputRef.current?.focus() + }, + })) + + return ( + + ) +}) + +export interface OpenDirModalType { + show: (paths: string[]) => void +} +export default forwardRef Promise }>(({ + onRefreshDir, +}, ref) => { + const confirmAlertRef = useRef(null) + const inputRef = useRef(null) + const [paths, setPaths] = useState([]) + + 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 ( + + true}> + + { + paths.length + ? global.i18n.t('open_storage_title') + : global.i18n.t('open_storage_not_found_title') + } + + + { + paths.length ? ( + + { + paths.map(path => { + return ( + + ) + }) + } + + ) : null + } + + + ) +}) + +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, + }, +}) + diff --git a/src/config/constant.ts b/src/config/constant.ts index 6a9fc99..28fc15d 100644 --- a/src/config/constant.ts +++ b/src/config/constant.ts @@ -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', diff --git a/src/lang/en_us.json b/src/lang/en_us.json index d5efd33..26b64b9 100644 --- a/src/lang/en_us.json +++ b/src/lang/en_us.json @@ -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", diff --git a/src/lang/zh_cn.json b/src/lang/zh_cn.json index 280e3fc..f1f6e0d 100644 --- a/src/lang/zh_cn.json +++ b/src/lang/zh_cn.json @@ -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": "播放", diff --git a/src/utils/data.ts b/src/utils/data.ts index fdd7c8b..40b52d4 100644 --- a/src/utils/data.ts +++ b/src/utils/data.ts @@ -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(openStoragePathPrefix) + } + return openStoragePath +} export const getSearchSetting = async() => { // eslint-disable-next-line require-atomic-updates diff --git a/src/utils/nativeModules/utils.ts b/src/utils/nativeModules/utils.ts index 13a2c41..2fdcae9 100644 --- a/src/utils/nativeModules/utils.ts +++ b/src/utils/nativeModules/utils.ts @@ -70,7 +70,7 @@ export const onScreenStateChange = (handler: (state: 'ON' | 'OFF') => void): () } } -export const getExternalStoragePath = async(): Promise => { +export const getExternalStoragePath = async(): Promise => { return UtilsModule.getExternalStoragePath() }