新增在线自定义源导入功能,允许通过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 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}

View File

@ -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": "Its 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.",

View File

@ -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": "提示:虽然我们已经尽可能地隔离了脚本的运行环境,但导入包含恶意行为的脚本仍可能会影响你的系统,请谨慎导入。",

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({
scrollView: {
paddingHorizontal: 7,
flexGrow: 0,
},
list: {

View File

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

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

View File

@ -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>
<View>
<Text style={styles.tipsText} size={12}>{t('user_api_note')}</Text>
</View>
</View>
</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',