新增在线自定义源导入功能,允许通过http/https链接导入自定义源

This commit is contained in:
lyswhut 2024-03-12 14:45:18 +08:00
parent 5e8c889ed4
commit 69135fa45e
10 changed files with 247 additions and 33 deletions

View File

@ -1,7 +1,8 @@
### 新增 ### 新增
- 新增棕色主题“泥牛入海” - 新增棕色主题“泥牛入海”
- 新增设置-基本设置-总是保留状态栏高度设置,如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况,可以启用该设置以始终为系统状态栏保留空间, - 新增设置-基本设置-总是保留状态栏高度设置,如果在你的设备上出现软件可交互内容与状态栏内容显示重叠的情况,可以启用该设置以始终为系统状态栏保留空间
- 新增在线自定义源导入功能允许通过http/https链接导入自定义源
### 优化 ### 优化

View File

@ -2,11 +2,12 @@ import { useRef } from 'react'
// import { View } from 'react-native' // import { View } from 'react-native'
import Menu, { type MenuType, type MenuProps, type Menus } from './Menu' 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' // import { useLayout } from '@/utils/hooks'
export interface DorpDownMenuProps<T extends Menus> extends Omit<MenuProps<T>, 'width'> { export interface DorpDownMenuProps<T extends Menus> extends Omit<MenuProps<T>, 'width'> {
children: React.ReactNode children: React.ReactNode
btnStyle?: BtnProps['style']
} }
export default <T extends Menus>({ export default <T extends Menus>({
@ -17,6 +18,7 @@ export default <T extends Menus>({
center, center,
children, children,
activeId, activeId,
btnStyle,
}: DorpDownMenuProps<T>) => { }: DorpDownMenuProps<T>) => {
const buttonRef = useRef<BtnType>(null) const buttonRef = useRef<BtnType>(null)
const menuRef = useRef<MenuType>(null) const menuRef = useRef<MenuType>(null)
@ -32,7 +34,7 @@ export default <T extends Menus>({
} }
return ( return (
<Button ref={buttonRef} onPress={showMenu}> <Button style={btnStyle} ref={buttonRef} onPress={showMenu}>
{children} {children}
<Menu <Menu
ref={menuRef} ref={menuRef}

View File

@ -458,9 +458,14 @@
"user_api_add_failed_tip": "Invalid custom source file", "user_api_add_failed_tip": "Invalid custom source file",
"user_api_allow_show_update_alert": "Allow update popups to be displayed", "user_api_allow_show_update_alert": "Allow update popups to be displayed",
"user_api_btn_import": "Import", "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": "Its actually empty here 😲", "user_api_empty": "Its actually empty here 😲",
"user_api_import_desc": "Select custom source file", "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_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_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.", "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.",

View File

@ -458,9 +458,14 @@
"user_api_add_failed_tip": "无效的自定义源文件", "user_api_add_failed_tip": "无效的自定义源文件",
"user_api_allow_show_update_alert": "允许显示更新弹窗", "user_api_allow_show_update_alert": "允许显示更新弹窗",
"user_api_btn_import": "导入", "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_empty": "这里竟然是空的 😲",
"user_api_import_desc": "选择自定义源文件", "user_api_import_desc": "选择自定义源文件",
"user_api_import_failed_tip": "导入失败:", "user_api_import_failed_tip": "自定义源导入失败:\n{message}",
"user_api_import_success_tip": "导入成功 🎉", "user_api_import_success_tip": "导入成功 🎉",
"user_api_max_tip": "最多只能同时存在20个源哦🤪\n想要继续导入的话请先移除一些旧的源腾出位置吧", "user_api_max_tip": "最多只能同时存在20个源哦🤪\n想要继续导入的话请先移除一些旧的源腾出位置吧",
"user_api_note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。", "user_api_note": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",

View File

@ -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>
)
}

View File

@ -125,6 +125,7 @@ export default () => {
const styles = createStyle({ const styles = createStyle({
scrollView: { scrollView: {
paddingHorizontal: 7,
flexGrow: 0, flexGrow: 0,
}, },
list: { list: {

View File

@ -1,7 +1,7 @@
import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath' import ChoosePath, { type ChoosePathType } from '@/components/common/ChoosePath'
import { USER_API_SOURCE_FILE_EXT_RXP } from '@/config/constant' import { USER_API_SOURCE_FILE_EXT_RXP } from '@/config/constant'
import { forwardRef, useImperativeHandle, useRef, useState } from 'react' import { forwardRef, useImperativeHandle, useRef, useState } from 'react'
import { handleImport } from './action' import { handleImportLocalFile } from './action'
export interface SelectInfo { export interface SelectInfo {
// listInfo: LX.List.MyListInfo // listInfo: LX.List.MyListInfo
@ -82,7 +82,7 @@ export default forwardRef<ScriptImportExportType, {}>((props, ref) => {
const onConfirmPath = (path: string) => { const onConfirmPath = (path: string) => {
switch (selectInfoRef.current.action) { switch (selectInfoRef.current.action) {
case 'import': case 'import':
handleImport(path) handleImportLocalFile(path)
break break
// case 'export': // case 'export':
// handleExport(selectInfoRef.current.listInfo, path) // handleExport(selectInfoRef.current.listInfo, path)

View File

@ -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,
},
})

View File

@ -4,15 +4,22 @@ import { log } from '@/utils/log'
import { toast } from '@/utils/tools' import { toast } from '@/utils/tools'
export const handleImport = (path: string) => { export const handleImportScript = async(script: string) => {
// toast(global.i18n.t('setting_backup_part_import_list_tip_unzip')) await importUserApi(script).then(() => {
void readFile(path).then(async script => {
if (script == null) throw new Error('Read file failed')
await importUserApi(script)
toast(global.i18n.t('user_api_import_success_tip')) toast(global.i18n.t('user_api_import_success_tip'))
}).catch((error: any) => { }).catch((error: any) => {
log.error(error.stack) 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')
}) })
} }

View File

@ -1,14 +1,13 @@
import { useRef, useImperativeHandle, forwardRef, useState } from 'react' import { useRef, useImperativeHandle, forwardRef, useState } from 'react'
import Text from '@/components/common/Text' import Text from '@/components/common/Text'
import { View, TouchableOpacity } from 'react-native' 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 { useTheme } from '@/store/theme/hook'
import { useI18n } from '@/lang' import { useI18n } from '@/lang'
import Dialog, { type DialogType } from '@/components/common/Dialog' import Dialog, { type DialogType } from '@/components/common/Dialog'
import Button from '@/components/common/Button' import Button from '@/components/common/Button'
import List from './List' import List from './List'
import ScriptImportExport, { type ScriptImportExportType } from './ScriptImportExport' import ImportBtn from './ImportBtn'
import { state } from '@/store/userApi'
// interface UrlInputType { // interface UrlInputType {
// setText: (text: string) => void // setText: (text: string) => void
@ -64,7 +63,6 @@ export interface UserApiEditModalType {
export default forwardRef<UserApiEditModalType, {}>((props, ref) => { export default forwardRef<UserApiEditModalType, {}>((props, ref) => {
const dialogRef = useRef<DialogType>(null) const dialogRef = useRef<DialogType>(null)
const scriptImportExportRef = useRef<ScriptImportExportType>(null)
// const sourceSelectorRef = useRef<SourceSelectorType>(null) // const sourceSelectorRef = useRef<SourceSelectorType>(null)
// const inputRef = useRef<UrlInputType>(null) // const inputRef = useRef<UrlInputType>(null)
const [visible, setVisible] = useState(false) const [visible, setVisible] = useState(false)
@ -96,16 +94,7 @@ export default forwardRef<UserApiEditModalType, {}>((props, ref) => {
const handleCancel = () => { const handleCancel = () => {
dialogRef.current?.setVisible(false) 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 = () => { const openFAQPage = () => {
void openUrl('https://lyswhut.github.io/lx-music-doc/mobile/custom-source') 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}> <TouchableOpacity onPress={openFAQPage}>
<Text style={{ ...styles.tipsText, textDecorationLine: 'underline' }} size={12} color={theme['c-primary-font']}>FAQ</Text> <Text style={{ ...styles.tipsText, textDecorationLine: 'underline' }} size={12} color={theme['c-primary-font']}>FAQ</Text>
</TouchableOpacity> </TouchableOpacity>
</View> <View>
<Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text> <Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text>
</View> </View>
</View>
</View>
<View style={styles.btns}> <View style={styles.btns}>
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}> <Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleCancel}>
<Text size={14} color={theme['c-button-font']}>{t('close')}</Text> <Text size={14} color={theme['c-button-font']}>{t('close')}</Text>
</Button> </Button>
<Button style={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} onPress={handleImport}> <ImportBtn btnStyle={{ ...styles.btn, backgroundColor: theme['c-button-background'] }} />
<Text size={14} color={theme['c-button-font']}>{t('user_api_btn_import')}</Text>
</Button>
<ScriptImportExport ref={scriptImportExportRef} />
</View> </View>
</Dialog> </Dialog>
) : null ) : null
@ -147,7 +135,7 @@ const styles = createStyle({
content: { content: {
// flexGrow: 1, // flexGrow: 1,
flexShrink: 1, flexShrink: 1,
paddingHorizontal: 15, paddingHorizontal: 8,
paddingTop: 15, paddingTop: 15,
paddingBottom: 10, paddingBottom: 10,
flexDirection: 'column', flexDirection: 'column',
@ -158,6 +146,7 @@ const styles = createStyle({
// backgroundColor: 'rgba(0, 0, 0, 0.2)', // backgroundColor: 'rgba(0, 0, 0, 0.2)',
}, },
tips: { tips: {
paddingHorizontal: 7,
marginTop: 15, marginTop: 15,
flexDirection: 'row', flexDirection: 'row',
flexWrap: 'wrap', flexWrap: 'wrap',