part of 'settings_page.dart'; class AppSettings extends StatefulWidget { const AppSettings({super.key}); @override State createState() => _AppSettingsState(); } class _AppSettingsState extends State { @override Widget build(BuildContext context) { return SmoothCustomScrollView( slivers: [ SliverAppbar(title: Text("App".tl)), _SettingPartTitle( title: "Data".tl, icon: Icons.storage, ), ListTile( title: Text("Storage Path for local comics".tl), subtitle: Text(LocalManager().path, softWrap: false), ).toSliver(), _CallbackSetting( title: "Set New Storage Path".tl, actionTitle: "Set".tl, callback: () async { if (App.isMobile) { context.showMessage(message: "Not supported".tl); return; } var result = await selectDirectory(); if (result == null) return; var loadingDialog = showLoadingDialog( App.rootContext, barrierDismissible: false, allowCancel: false, ); var res = await LocalManager().setNewPath(result); loadingDialog.close(); if (res != null) { context.showMessage(message: res); } else { context.showMessage(message: "Path set successfully".tl); setState(() {}); } }, ).toSliver(), ListTile( title: Text("Cache Size".tl), subtitle: Text(bytesToReadableString(CacheManager().currentSize)), ).toSliver(), _CallbackSetting( title: "Clear Cache".tl, actionTitle: "Clear".tl, callback: () async { var loadingDialog = showLoadingDialog( App.rootContext, barrierDismissible: false, allowCancel: false, ); await CacheManager().clear(); loadingDialog.close(); context.showMessage(message: "Cache cleared".tl); setState(() {}); }, ).toSliver(), _CallbackSetting( title: "Cache Limit".tl, subtitle: "${appdata.settings['cacheSize']} MB", callback: () { showInputDialog( context: context, title: "Set Cache Limit".tl, hintText: "Size in MB".tl, inputValidator: RegExp(r"^\d+$"), onConfirm: (value) { appdata.settings['cacheSize'] = int.parse(value); appdata.saveData(); setState(() {}); CacheManager().setLimitSize(appdata.settings['cacheSize']); return null; }, ); }, actionTitle: 'Set'.tl, ).toSliver(), _CallbackSetting( title: "Export App Data".tl, callback: () async { var controller = showLoadingDialog(context); var file = await exportAppData(); await saveFile(filename: "data.venera", file: file); controller.close(); }, actionTitle: 'Export'.tl, ).toSliver(), _CallbackSetting( title: "Import App Data".tl, callback: () async { var controller = showLoadingDialog(context); var file = await selectFile(ext: ['venera']); if (file != null) { var cacheFile = File(FilePath.join(App.cachePath, "temp.venera")); await file.saveTo(cacheFile.path); try { await importAppData(cacheFile); } catch (e, s) { Log.error("Import data", e.toString(), s); context.showMessage(message: "Failed to import data".tl); } } controller.close(); }, actionTitle: 'Import'.tl, ).toSliver(), _CallbackSetting( title: "Data Sync".tl, callback: () async { showPopUpWidget(context, const _WebdavSetting()); }, actionTitle: 'Set'.tl, ).toSliver(), _SettingPartTitle( title: "Log".tl, icon: Icons.error_outline, ), _CallbackSetting( title: "Open Log".tl, callback: () { context.to(() => const LogsPage()); }, actionTitle: 'Open'.tl, ).toSliver(), _SettingPartTitle( title: "User".tl, icon: Icons.person_outline, ), SelectSetting( title: "Language".tl, settingKey: "language", optionTranslation: const { "system": "System", "zh-CN": "简体中文", "zh-TW": "繁體中文", "en-US": "English", }, onChanged: () { App.forceRebuild(); }, ).toSliver(), ], ); } } class LogsPage extends StatefulWidget { const LogsPage({super.key}); @override State createState() => _LogsPageState(); } class _LogsPageState extends State { @override Widget build(BuildContext context) { return Scaffold( appBar: Appbar( title: const Text("Logs"), actions: [ IconButton( onPressed: () => setState(() { final RelativeRect position = RelativeRect.fromLTRB( MediaQuery.of(context).size.width, MediaQuery.of(context).padding.top + kToolbarHeight, 0.0, 0.0, ); showMenu(context: context, position: position, items: [ PopupMenuItem( child: Text("Clear".tl), onTap: () => setState(() => Log.clear()), ), PopupMenuItem( child: Text("Disable Length Limitation".tl), onTap: () { Log.ignoreLimitation = true; context.showMessage( message: "Only valid for this run"); }, ), PopupMenuItem( child: Text("Export".tl), onTap: () => saveLog(Log().toString()), ), ]); }), icon: const Icon(Icons.more_horiz)) ], ), body: ListView.builder( reverse: true, controller: ScrollController(), itemCount: Log.logs.length, itemBuilder: (context, index) { index = Log.logs.length - index - 1; return Padding( padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), child: SelectionArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( decoration: BoxDecoration( color: Theme.of(context) .colorScheme .surfaceContainerHighest, borderRadius: const BorderRadius.all(Radius.circular(16)), ), child: Padding( padding: const EdgeInsets.fromLTRB(5, 0, 5, 1), child: Text(Log.logs[index].title), ), ), const SizedBox( width: 3, ), Container( decoration: BoxDecoration( color: [ Theme.of(context).colorScheme.error, Theme.of(context).colorScheme.errorContainer, Theme.of(context).colorScheme.primaryContainer ][Log.logs[index].level.index], borderRadius: const BorderRadius.all(Radius.circular(16)), ), child: Padding( padding: const EdgeInsets.fromLTRB(5, 0, 5, 1), child: Text( Log.logs[index].level.name, style: TextStyle( color: Log.logs[index].level.index == 0 ? Colors.white : Colors.black), ), ), ), ], ), Text(Log.logs[index].content), Text(Log.logs[index].time .toString() .replaceAll(RegExp(r"\.\w+"), "")), TextButton( onPressed: () { Clipboard.setData( ClipboardData(text: Log.logs[index].content)); }, child: Text("Copy".tl), ), const Divider(), ], ), ), ); }, ), ); } void saveLog(String log) async { saveFile(data: utf8.encode(log), filename: 'log.txt'); } } class _WebdavSetting extends StatefulWidget { const _WebdavSetting(); @override State<_WebdavSetting> createState() => _WebdavSettingState(); } class _WebdavSettingState extends State<_WebdavSetting> { String url = ""; String user = ""; String pass = ""; bool isTesting = false; bool upload = true; @override void initState() { super.initState(); if (appdata.settings['webdav'] is! List) { appdata.settings['webdav'] = []; } var configs = appdata.settings['webdav'] as List; if (configs.whereType().length != 3) { return; } url = configs[0]; user = configs[1]; pass = configs[2]; } @override Widget build(BuildContext context) { return PopUpWidgetScaffold( title: "Webdav", body: SingleChildScrollView( child: Column( children: [ const SizedBox(height: 12), TextField( decoration: const InputDecoration( labelText: "URL", border: OutlineInputBorder(), ), controller: TextEditingController(text: url), onChanged: (value) => url = value, ), const SizedBox(height: 12), TextField( decoration: InputDecoration( labelText: "Username".tl, border: const OutlineInputBorder(), ), controller: TextEditingController(text: user), onChanged: (value) => user = value, ), const SizedBox(height: 12), TextField( decoration: InputDecoration( labelText: "Password".tl, border: const OutlineInputBorder(), ), controller: TextEditingController(text: pass), onChanged: (value) => pass = value, ), const SizedBox(height: 12), Row( children: [ Text("Operation".tl), Radio( groupValue: upload, value: true, onChanged: (value) { setState(() { upload = value!; }); }, ), Text("Upload".tl), Radio( groupValue: upload, value: false, onChanged: (value) { setState(() { upload = value!; }); }, ), Text("Download".tl), ], ), const SizedBox(height: 16), Center( child: Button.filled( isLoading: isTesting, onPressed: () async { var oldConfig = appdata.settings['webdav']; appdata.settings['webdav'] = [url, user, pass]; setState(() { isTesting = true; }); var testResult = upload ? await DataSync().uploadData() : await DataSync().downloadData(); if (testResult.error) { setState(() { isTesting = false; }); appdata.settings['webdav'] = oldConfig; context.showMessage(message: testResult.errorMessage!); return; } appdata.saveData(); context.showMessage(message: "Saved".tl); App.rootPop(); }, child: Text("Continue".tl), ), ) ], ).paddingHorizontal(16), ), ); } }