mirror of
https://github.com/ikun0014/lx-music-mobile.git
synced 2025-07-04 09:32:09 +08:00
新增在线自定义源导入功能,允许通过http/https链接导入自定义源
This commit is contained in:
parent
5e8c889ed4
commit
69135fa45e
@ -1,7 +1,8 @@
|
||||
### 新增
|
||||
|
||||
- 新增棕色主题“泥牛入海”
|
||||
- 新增设置-基本设置-总是保留状态栏高度设置,如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况,可以启用该设置以始终为系统状态栏保留空间,
|
||||
- 新增设置-基本设置-总是保留状态栏高度设置,如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况,可以启用该设置以始终为系统状态栏保留空间
|
||||
- 新增在线自定义源导入功能,允许通过http/https链接导入自定义源
|
||||
|
||||
### 优化
|
||||
|
||||
|
@ -2,11 +2,12 @@ import { useRef } from 'react'
|
||||
// import { View } from 'react-native'
|
||||
|
||||
import Menu, { type MenuType, type MenuProps, type Menus } from './Menu'
|
||||
import Button, { type BtnType } from './Button'
|
||||
import Button, { type BtnType, type BtnProps } from './Button'
|
||||
// import { useLayout } from '@/utils/hooks'
|
||||
|
||||
export interface DorpDownMenuProps<T extends Menus> extends Omit<MenuProps<T>, 'width'> {
|
||||
children: React.ReactNode
|
||||
btnStyle?: BtnProps['style']
|
||||
}
|
||||
|
||||
export default <T extends Menus>({
|
||||
@ -17,6 +18,7 @@ export default <T extends Menus>({
|
||||
center,
|
||||
children,
|
||||
activeId,
|
||||
btnStyle,
|
||||
}: DorpDownMenuProps<T>) => {
|
||||
const buttonRef = useRef<BtnType>(null)
|
||||
const menuRef = useRef<MenuType>(null)
|
||||
@ -32,7 +34,7 @@ export default <T extends Menus>({
|
||||
}
|
||||
|
||||
return (
|
||||
<Button ref={buttonRef} onPress={showMenu}>
|
||||
<Button style={btnStyle} ref={buttonRef} onPress={showMenu}>
|
||||
{children}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
|
@ -458,9 +458,14 @@
|
||||
"user_api_add_failed_tip": "Invalid custom source file",
|
||||
"user_api_allow_show_update_alert": "Allow update popups to be displayed",
|
||||
"user_api_btn_import": "Import",
|
||||
"user_api_btn_import_local": "Local import",
|
||||
"user_api_btn_import_online": "Online import",
|
||||
"user_api_btn_import_online_input_confirm": "Import",
|
||||
"user_api_btn_import_online_input_loading": "Importing...",
|
||||
"user_api_btn_import_online_input_tip": "Please enter HTTP link",
|
||||
"user_api_empty": "It’s actually empty here 😲",
|
||||
"user_api_import_desc": "Select custom source file",
|
||||
"user_api_import_failed_tip": "Import failed:",
|
||||
"user_api_import_failed_tip": "Custom source import failed: \n{message}",
|
||||
"user_api_import_success_tip": "Import successful 🎉",
|
||||
"user_api_max_tip": "A maximum of 20 sources can exist at the same time🤪\n\nIf you want to continue importing, please remove some old sources to make room.",
|
||||
"user_api_note": "Tip: Although we have isolated the running environment of the script as much as possible, importing scripts containing malicious behaviors may still affect your system, so please import with caution.",
|
||||
|
@ -458,9 +458,14 @@
|
||||
"user_api_add_failed_tip": "无效的自定义源文件",
|
||||
"user_api_allow_show_update_alert": "允许显示更新弹窗",
|
||||
"user_api_btn_import": "导入",
|
||||
"user_api_btn_import_local": "本地导入",
|
||||
"user_api_btn_import_online": "在线导入",
|
||||
"user_api_btn_import_online_input_confirm": "导入",
|
||||
"user_api_btn_import_online_input_loading": "导入中...",
|
||||
"user_api_btn_import_online_input_tip": "请输入 HTTP 链接",
|
||||
"user_api_empty": "这里竟然是空的 😲",
|
||||
"user_api_import_desc": "选择自定义源文件",
|
||||
"user_api_import_failed_tip": "导入失败:",
|
||||
"user_api_import_failed_tip": "自定义源导入失败:\n{message}",
|
||||
"user_api_import_success_tip": "导入成功 🎉",
|
||||
"user_api_max_tip": "最多只能同时存在20个源哦🤪\n想要继续导入的话,请先移除一些旧的源腾出位置吧",
|
||||
"user_api_note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",
|
||||
|
@ -0,0 +1,62 @@
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
import DorpDownMenu, { type DorpDownMenuProps as _DorpDownMenuProps } from '@/components/common/DorpDownMenu'
|
||||
import Text from '@/components/common/Text'
|
||||
import { useI18n } from '@/lang'
|
||||
import ScriptImportExport, { type ScriptImportExportType } from './ScriptImportExport'
|
||||
import ScriptImportOnline, { type ScriptImportOnlineType } from './ScriptImportOnline'
|
||||
import { state } from '@/store/userApi'
|
||||
import { tipDialog } from '@/utils/tools'
|
||||
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
|
||||
interface BtnProps {
|
||||
btnStyle?: _DorpDownMenuProps<any[]>['btnStyle']
|
||||
}
|
||||
|
||||
|
||||
export default ({ btnStyle }: BtnProps) => {
|
||||
const t = useI18n()
|
||||
const theme = useTheme()
|
||||
const scriptImportExportRef = useRef<ScriptImportExportType>(null)
|
||||
const scriptImportOnlineRef = useRef<ScriptImportOnlineType>(null)
|
||||
|
||||
const importTypes = useMemo(() => {
|
||||
return [
|
||||
{ action: 'local', label: t('user_api_btn_import_local') },
|
||||
{ action: 'online', label: t('user_api_btn_import_online') },
|
||||
] as const
|
||||
}, [t])
|
||||
|
||||
type DorpDownMenuProps = _DorpDownMenuProps<typeof importTypes>
|
||||
|
||||
const handleAction: DorpDownMenuProps['onPress'] = ({ action }) => {
|
||||
if (state.list.length > 20) {
|
||||
void tipDialog({
|
||||
message: t('user_api_max_tip'),
|
||||
btnText: t('ok'),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (action == 'local') {
|
||||
scriptImportExportRef.current?.import()
|
||||
} else {
|
||||
scriptImportOnlineRef.current?.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<DorpDownMenu
|
||||
btnStyle={btnStyle}
|
||||
menus={importTypes}
|
||||
center
|
||||
onPress={handleAction}
|
||||
>
|
||||
<Text size={14} color={theme['c-button-font']}>{t('user_api_btn_import')}</Text>
|
||||
<ScriptImportExport ref={scriptImportExportRef} />
|
||||
<ScriptImportOnline ref={scriptImportOnlineRef} />
|
||||
</DorpDownMenu>
|
||||
)
|
||||
}
|
@ -125,6 +125,7 @@ export default () => {
|
||||
|
||||
const styles = createStyle({
|
||||
scrollView: {
|
||||
paddingHorizontal: 7,
|
||||
flexGrow: 0,
|
||||
},
|
||||
list: {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath'
|
||||
import { USER_API_SOURCE_FILE_EXT_RXP } from '@/config/constant'
|
||||
import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { handleImport } from './action'
|
||||
import { handleImportLocalFile } from './action'
|
||||
|
||||
export interface SelectInfo {
|
||||
// listInfo: LX.List.MyListInfo
|
||||
@ -82,7 +82,7 @@ export default forwardRef<ScriptImportExportType, {}>((props, ref) => {
|
||||
const onConfirmPath = (path: string) => {
|
||||
switch (selectInfoRef.current.action) {
|
||||
case 'import':
|
||||
handleImport(path)
|
||||
handleImportLocalFile(path)
|
||||
break
|
||||
// case 'export':
|
||||
// handleExport(selectInfoRef.current.listInfo, path)
|
||||
|
@ -0,0 +1,142 @@
|
||||
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 Input, { type InputType } from '@/components/common/Input'
|
||||
import { createStyle, toast } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useI18n } from '@/lang'
|
||||
import { httpFetch } from '@/utils/request'
|
||||
import { handleImportScript } from './action'
|
||||
|
||||
interface UrlInputType {
|
||||
setText: (text: string) => void
|
||||
getText: () => string
|
||||
focus: () => void
|
||||
}
|
||||
const UrlInput = forwardRef<UrlInputType, {}>((props, ref) => {
|
||||
const theme = useTheme()
|
||||
const [text, setText] = useState('')
|
||||
const [placeholder, setPlaceholder] = useState('')
|
||||
const inputRef = useRef<InputType>(null)
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getText() {
|
||||
return text.trim()
|
||||
},
|
||||
setText(text) {
|
||||
setText(text)
|
||||
setPlaceholder(global.i18n.t('user_api_btn_import_online_input_tip'))
|
||||
},
|
||||
focus() {
|
||||
inputRef.current?.focus()
|
||||
},
|
||||
}))
|
||||
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={placeholder}
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
style={{ ...styles.input, backgroundColor: theme['c-primary-input-background'] }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
export interface ScriptImportOnlineType {
|
||||
show: () => void
|
||||
}
|
||||
|
||||
|
||||
export default forwardRef<ScriptImportOnlineType, {}>((props, ref) => {
|
||||
const t = useI18n()
|
||||
const alertRef = useRef<ConfirmAlertType>(null)
|
||||
const urlInputRef = useRef<UrlInputType>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [btn, setBtn] = useState({ disabled: false, text: t('user_api_btn_import_online_input_confirm') })
|
||||
|
||||
const handleShow = () => {
|
||||
alertRef.current?.setVisible(true)
|
||||
setBtn({ disabled: false, text: t('user_api_btn_import_online_input_confirm') })
|
||||
requestAnimationFrame(() => {
|
||||
urlInputRef.current?.setText('')
|
||||
setTimeout(() => {
|
||||
urlInputRef.current?.focus()
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
useImperativeHandle(ref, () => ({
|
||||
show() {
|
||||
if (visible) handleShow()
|
||||
else {
|
||||
setVisible(true)
|
||||
requestAnimationFrame(() => {
|
||||
handleShow()
|
||||
})
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
const handleImport = async() => {
|
||||
let url = urlInputRef.current?.getText() ?? ''
|
||||
if (!/^https?:\/\//.test(url)) {
|
||||
url = ''
|
||||
urlInputRef.current?.setText('')
|
||||
}
|
||||
if (!url.length) return
|
||||
setBtn({ disabled: true, text: t('user_api_btn_import_online_input_loading') })
|
||||
let script: string
|
||||
try {
|
||||
script = await httpFetch(url).promise.then(resp => resp.body) as string
|
||||
} catch (err: any) {
|
||||
toast(t('user_api_import_failed_tip', { message: err.message }), 'long')
|
||||
return
|
||||
} finally {
|
||||
setBtn({ disabled: false, text: t('user_api_btn_import_online_input_confirm') })
|
||||
}
|
||||
if (script.length > 9_000_000) {
|
||||
toast(t('user_api_import_failed_tip', { message: 'Too large script' }), 'long')
|
||||
return
|
||||
}
|
||||
void handleImportScript(script)
|
||||
|
||||
alertRef.current?.setVisible(false)
|
||||
}
|
||||
|
||||
return (
|
||||
visible
|
||||
? <ConfirmAlert
|
||||
ref={alertRef}
|
||||
onConfirm={handleImport}
|
||||
disabledConfirm={btn.disabled}
|
||||
confirmText={btn.text}
|
||||
>
|
||||
<View style={styles.reurlContent}>
|
||||
<Text style={{ marginBottom: 5 }}>{ t('user_api_btn_import_online')}</Text>
|
||||
<UrlInput ref={urlInputRef} />
|
||||
</View>
|
||||
</ConfirmAlert>
|
||||
: null
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
const styles = createStyle({
|
||||
reurlContent: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
flexDirection: 'column',
|
||||
},
|
||||
input: {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
minWidth: 290,
|
||||
borderRadius: 4,
|
||||
// paddingTop: 2,
|
||||
// paddingBottom: 2,
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -4,15 +4,22 @@ import { log } from '@/utils/log'
|
||||
import { toast } from '@/utils/tools'
|
||||
|
||||
|
||||
export const handleImport = (path: string) => {
|
||||
// toast(global.i18n.t('setting_backup_part_import_list_tip_unzip'))
|
||||
void readFile(path).then(async script => {
|
||||
if (script == null) throw new Error('Read file failed')
|
||||
await importUserApi(script)
|
||||
export const handleImportScript = async(script: string) => {
|
||||
await importUserApi(script).then(() => {
|
||||
toast(global.i18n.t('user_api_import_success_tip'))
|
||||
}).catch((error: any) => {
|
||||
log.error(error.stack)
|
||||
toast(global.i18n.t('user_api_import_failed_tip') + '\n' + error.message)
|
||||
toast(global.i18n.t('user_api_import_failed_tip', { message: error.message }), 'long')
|
||||
})
|
||||
}
|
||||
|
||||
export const handleImportLocalFile = (path: string) => {
|
||||
// toast(global.i18n.t('setting_backup_part_import_list_tip_unzip'))
|
||||
void readFile(path).then(async script => {
|
||||
if (script == null) throw new Error('Read file failed')
|
||||
void handleImportScript(script)
|
||||
}).catch((error: any) => {
|
||||
toast(global.i18n.t('user_api_import_failed_tip', { message: error.message }), 'long')
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { useRef, useImperativeHandle, forwardRef, useState } from 'react'
|
||||
import Text from '@/components/common/Text'
|
||||
import { View, TouchableOpacity } from 'react-native'
|
||||
import { createStyle, openUrl, tipDialog } from '@/utils/tools'
|
||||
import { createStyle, openUrl } from '@/utils/tools'
|
||||
import { useTheme } from '@/store/theme/hook'
|
||||
import { useI18n } from '@/lang'
|
||||
import Dialog, { type DialogType } from '@/components/common/Dialog'
|
||||
import Button from '@/components/common/Button'
|
||||
import List from './List'
|
||||
import ScriptImportExport, { type ScriptImportExportType } from './ScriptImportExport'
|
||||
import { state } from '@/store/userApi'
|
||||
import ImportBtn from './ImportBtn'
|
||||
|
||||
// interface UrlInputType {
|
||||
// setText: (text: string) => void
|
||||
@ -64,7 +63,6 @@ export interface UserApiEditModalType {
|
||||
|
||||
export default forwardRef<UserApiEditModalType, {}>((props, ref) => {
|
||||
const dialogRef = useRef<DialogType>(null)
|
||||
const scriptImportExportRef = useRef<ScriptImportExportType>(null)
|
||||
// const sourceSelectorRef = useRef<SourceSelectorType>(null)
|
||||
// const inputRef = useRef<UrlInputType>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
@ -96,16 +94,7 @@ export default forwardRef<UserApiEditModalType, {}>((props, ref) => {
|
||||
const handleCancel = () => {
|
||||
dialogRef.current?.setVisible(false)
|
||||
}
|
||||
const handleImport = () => {
|
||||
if (state.list.length > 20) {
|
||||
void tipDialog({
|
||||
message: t('user_api_max_tip'),
|
||||
btnText: t('ok'),
|
||||
})
|
||||
return
|
||||
}
|
||||
scriptImportExportRef.current?.import()
|
||||
}
|
||||
|
||||
const openFAQPage = () => {
|
||||
void openUrl('https://lyswhut.github.io/lx-music-doc/mobile/custom-source')
|
||||
}
|
||||
@ -125,17 +114,16 @@ export default forwardRef<UserApiEditModalType, {}>((props, ref) => {
|
||||
<TouchableOpacity onPress={openFAQPage}>
|
||||
<Text style={{ ...styles.tipsText, textDecorationLine: 'underline' }} size={12} color={theme['c-primary-font']}>FAQ</Text>
|
||||
</TouchableOpacity>
|
||||
<View>
|
||||
<Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text>
|
||||
</View>
|
||||
<View style={styles.btns}>
|
||||
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>
|
||||
<Text size={14} color={theme['c-button-font']}>{t('close')}</Text>
|
||||
</Button>
|
||||
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleImport}>
|
||||
<Text size={14} color={theme['c-button-font']}>{t('user_api_btn_import')}</Text>
|
||||
</Button>
|
||||
<ScriptImportExport ref={scriptImportExportRef} />
|
||||
<ImportBtn btnStyle={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} />
|
||||
</View>
|
||||
</Dialog>
|
||||
) : null
|
||||
@ -147,7 +135,7 @@ const styles = createStyle({
|
||||
content: {
|
||||
// flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingHorizontal: 15,
|
||||
paddingHorizontal: 8,
|
||||
paddingTop: 15,
|
||||
paddingBottom: 10,
|
||||
flexDirection: 'column',
|
||||
@ -158,6 +146,7 @@ const styles = createStyle({
|
||||
// backgroundColor: 'rgba(0, 0, 0, 0.2)',
|
||||
},
|
||||
tips: {
|
||||
paddingHorizontal: 7,
|
||||
marginTop: 15,
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
|
Loading…
x
Reference in New Issue
Block a user