1import React from "react"; 2import { ScrollView, StyleSheet } from "react-native"; 3import ListItem, { ListItemHeader } from "@/components/base/listItem"; 4import Toast from "@/utils/toast"; 5import Backup from "@/core/backup"; 6import backup from "@/core/backup"; 7import { ROUTE_PATH, useNavigate } from "@/core/router"; 8 9import axios from "axios"; 10import { showDialog } from "@/components/dialogs/useDialog"; 11import { showPanel } from "@/components/panels/usePanel"; 12 13import { AuthType, createClient } from "webdav"; 14import Config from "@/core/config.ts"; 15import { writeInChunks } from "@/utils/fileUtils.ts"; 16import { getDocumentAsync } from "expo-document-picker"; 17import { readAsStringAsync } from "expo-file-system"; 18import sleep from "@/utils/sleep"; 19import { ResumeMode } from "@/constants/commonConst.ts"; 20import strings from "@/constants/strings.ts"; 21import { errorLog } from "@/utils/log.ts"; 22 23export default function BackupSetting() { 24 const navigate = useNavigate(); 25 26 const resumeMode = Config.useConfigValue('backup.resumeMode'); 27 const webdavUrl = Config.useConfigValue('webdav.url'); 28 const webdavUsername = Config.useConfigValue('webdav.username'); 29 const webdavPassword = Config.useConfigValue('webdav.password'); 30 31 32 const onBackupToLocal = async () => { 33 navigate(ROUTE_PATH.FILE_SELECTOR, { 34 fileType: 'folder', 35 multi: false, 36 actionText: '开始备份', 37 async onAction(selectedFiles) { 38 const raw = Backup.backup(); 39 const folder = selectedFiles[0]?.path; 40 return new Promise(resolve => { 41 showDialog('LoadingDialog', { 42 title: '备份本地音乐', 43 loadingText: '备份中...', 44 promise: writeInChunks( 45 `${folder}${ 46 folder?.endsWith('/') ? '' : '/' 47 }backup.json`, 48 raw, 49 ), 50 onResolve(_, hideDialog) { 51 Toast.success('备份成功~'); 52 hideDialog(); 53 resolve(true); 54 }, 55 onCancel(hideDialog) { 56 hideDialog(); 57 resolve(false); 58 }, 59 onReject(reason, hideDialog) { 60 hideDialog(); 61 resolve(false); 62 console.log(reason); 63 Toast.warn(`备份失败 ${reason?.message ?? reason}`); 64 }, 65 }); 66 }); 67 }, 68 }); 69 }; 70 71 async function onResumeFromLocal() { 72 try { 73 const pickResult = await getDocumentAsync({ 74 copyToCacheDirectory: true, 75 type: 'application/json', 76 }); 77 if (pickResult.canceled) { 78 return; 79 } 80 const result = await readAsStringAsync(pickResult.assets[0].uri); 81 return new Promise(resolve => { 82 showDialog('LoadingDialog', { 83 title: '从本地文件恢复', 84 loadingText: '恢复中...', 85 async task() { 86 await sleep(300); 87 return backup.resume(result, resumeMode); 88 }, 89 onResolve(_, hideDialog) { 90 Toast.success('恢复成功~'); 91 hideDialog(); 92 resolve(true); 93 }, 94 onCancel(hideDialog) { 95 hideDialog(); 96 resolve(false); 97 }, 98 onReject(reason, hideDialog) { 99 hideDialog(); 100 resolve(false); 101 console.log(reason); 102 Toast.warn(`恢复失败 ${reason?.message ?? reason}`); 103 }, 104 }); 105 }); 106 } catch (e: any) { 107 errorLog('恢复失败', e); 108 Toast.warn(`恢复失败 ${e?.message ?? e}`); 109 } 110 } 111 112 // const [webDavState, setWebDavState] = useState(''); 113 async function onResumeFromUrl() { 114 showPanel('SimpleInput', { 115 title: '恢复数据', 116 placeholder: '输入以json或txt结尾的URL', 117 maxLength: 1024, 118 async onOk(text, closePanel) { 119 try { 120 const url = text.trim(); 121 if (url.endsWith('.json') || url.endsWith('.txt')) { 122 const raw = (await axios.get(text)).data; 123 await Backup.resume(raw, resumeMode); 124 Toast.success('恢复成功~'); 125 closePanel(); 126 } else { 127 throw '无效的URL'; 128 } 129 } catch (e: any) { 130 Toast.warn(`恢复失败 ${e?.message ?? e}`); 131 } 132 }, 133 }); 134 } 135 136 async function onResumeFromWebdav() { 137 const url = Config.getConfig('webdav.url'); 138 const username = Config.getConfig('webdav.username'); 139 const password = Config.getConfig('webdav.password'); 140 141 if (!(username && password && url)) { 142 Toast.warn('请先在「Webdav设置」中完成配置,再执行恢复'); 143 return; 144 } 145 const client = createClient(url, { 146 authType: AuthType.Password, 147 username: username, 148 password: password, 149 }); 150 151 if (!(await client.exists('/MusicFree/MusicFreeBackup.json'))) { 152 Toast.warn('备份文件不存在'); 153 return; 154 } 155 156 try { 157 const resumeData = await client.getFileContents( 158 '/MusicFree/MusicFreeBackup.json', 159 { 160 format: 'text', 161 }, 162 ); 163 await Backup.resume( 164 resumeData, 165 Config.getConfig('backup.resumeMode'), 166 ); 167 Toast.success('恢复成功~'); 168 } catch (e: any) { 169 Toast.warn(`恢复失败: ${e?.message ?? e}`); 170 } 171 } 172 173 async function onBackupToWebdav() { 174 const username = Config.getConfig('webdav.username'); 175 const password = Config.getConfig('webdav.password'); 176 const url = Config.getConfig('webdav.url'); 177 if (!(username && password && url)) { 178 Toast.warn('请先在「Webdav设置」中完成配置,再执行恢复'); 179 return; 180 } 181 try { 182 const client = createClient(url, { 183 authType: AuthType.Password, 184 username: username, 185 password: password, 186 }); 187 188 const raw = Backup.backup(); 189 if (!(await client.exists('/MusicFree'))) { 190 await client.createDirectory('/MusicFree'); 191 } 192 // 临时文件 193 await client.putFileContents( 194 '/MusicFree/MusicFreeBackup.json', 195 raw, 196 { 197 overwrite: true, 198 }, 199 ); 200 Toast.success('备份成功'); 201 } catch (e: any) { 202 Toast.warn(`备份失败: ${e?.message ?? e}`); 203 } 204 } 205 206 return ( 207 <ScrollView style={style.wrapper}> 208 <ListItemHeader>备份&恢复设置</ListItemHeader> 209 210 <ListItem 211 withHorizontalPadding 212 onPress={() => { 213 showDialog('RadioDialog', { 214 title: '设置恢复方式', 215 content: [ 216 { 217 label: strings.settings[ResumeMode.Append], 218 value: ResumeMode.Append, 219 }, 220 { 221 label: strings.settings[ 222 ResumeMode.OverwriteDefault 223 ], 224 value: ResumeMode.OverwriteDefault, 225 }, 226 { 227 label: strings.settings[ResumeMode.Overwrite], 228 value: ResumeMode.Overwrite, 229 }, 230 ], 231 onOk(value) { 232 Config.setConfig( 233 'backup.resumeMode', 234 value as any, 235 ); 236 }, 237 }); 238 }}> 239 <ListItem.Content title="恢复方式" /> 240 <ListItem.ListItemText> 241 { 242 strings.settings[ 243 (resumeMode as ResumeMode) || 244 ResumeMode.Append 245 ] 246 } 247 </ListItem.ListItemText> 248 </ListItem> 249 <ListItemHeader>本地备份</ListItemHeader> 250 <ListItem withHorizontalPadding onPress={onBackupToLocal}> 251 <ListItem.Content title="备份到本地" /> 252 </ListItem> 253 <ListItem withHorizontalPadding onPress={onResumeFromLocal}> 254 <ListItem.Content title="从本地文件恢复" /> 255 </ListItem> 256 <ListItem withHorizontalPadding onPress={onResumeFromUrl}> 257 <ListItem.Content title="从远程URL中恢复" /> 258 </ListItem> 259 <ListItemHeader>Webdav</ListItemHeader> 260 <ListItem 261 withHorizontalPadding 262 onPress={() => { 263 showPanel('SetUserVariables', { 264 title: 'Webdav设置', 265 initValues: { 266 url: webdavUrl ?? '', 267 username: webdavUsername ?? '', 268 password: webdavPassword ?? '', 269 }, 270 variables: [ 271 { 272 key: 'url', 273 name: 'URL', 274 hint: 'webdav服务地址', 275 }, 276 { 277 key: 'username', 278 name: '用户名', 279 }, 280 { 281 key: 'password', 282 name: '密码', 283 }, 284 ], 285 onOk(values, closePanel) { 286 Config.setConfig('webdav.url', values?.url); 287 Config.setConfig('webdav.username', values?.username); 288 Config.setConfig('webdav.password', values?.password); 289 290 Toast.success('保存成功'); 291 closePanel(); 292 }, 293 }); 294 }}> 295 <ListItem.Content title="Webdav设置" /> 296 </ListItem> 297 <ListItem withHorizontalPadding onPress={onBackupToWebdav}> 298 <ListItem.Content title="备份到Webdav" /> 299 </ListItem> 300 <ListItem withHorizontalPadding onPress={onResumeFromWebdav}> 301 <ListItem.Content title="从Webdav中恢复" /> 302 </ListItem> 303 </ScrollView> 304 ); 305} 306 307const style = StyleSheet.create({ 308 wrapper: { 309 width: '100%', 310 flex: 1, 311 }, 312}); 313