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(), _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'); } }