From a26e5e20de683587384f54063b4ac1fa9e655115 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 11 Oct 2024 21:47:50 +0800 Subject: [PATCH] settings page --- assets/app_icon.png | Bin 0 -> 63239 bytes lib/components/appbar.dart | 8 +- lib/components/select.dart | 2 +- lib/foundation/appdata.dart | 29 +- lib/foundation/local.dart | 33 +- lib/init.dart | 2 +- lib/main.dart | 12 + lib/network/app_dio.dart | 2 +- lib/pages/main_page.dart | 5 +- lib/pages/settings/about.dart | 121 +++++++ lib/pages/settings/app.dart | 219 +++++++++++++ lib/pages/settings/appearance.dart | 44 +++ lib/pages/settings/explore_settings.dart | 182 +++++++++++ lib/pages/settings/local_favorites.dart | 36 +++ lib/pages/settings/network.dart | 236 ++++++++++++++ lib/pages/settings/reader.dart | 2 +- lib/pages/settings/setting_components.dart | 222 +++++++++++++ lib/pages/settings/settings_page.dart | 355 ++++++++++++++++++++- lib/utils/io.dart | 12 + pubspec.lock | 16 + pubspec.yaml | 3 + 21 files changed, 1515 insertions(+), 26 deletions(-) create mode 100644 assets/app_icon.png create mode 100644 lib/pages/settings/about.dart create mode 100644 lib/pages/settings/app.dart create mode 100644 lib/pages/settings/appearance.dart create mode 100644 lib/pages/settings/explore_settings.dart create mode 100644 lib/pages/settings/local_favorites.dart create mode 100644 lib/pages/settings/network.dart diff --git a/assets/app_icon.png b/assets/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e64eb409a82fba5d9550640f30d323d5eefad6d0 GIT binary patch literal 63239 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

=$E)CiUU#q0Osqx11_Pq?D%FO53=JY9SY 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 literal 0 HcmV?d00001 diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 13487ba..a3c9d3a 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -83,7 +83,7 @@ class _AppbarState extends State { message: "Back".tl, child: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.maybePop(context), ), ), const SizedBox( @@ -187,7 +187,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate { message: "Back".tl, child: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.pop(context), + onPressed: () => Navigator.maybePop(context), ), ) : const SizedBox()), @@ -322,7 +322,9 @@ class _FilledTabBarState extends State { scrollDirection: Axis.horizontal, padding: EdgeInsets.zero, controller: controller, - physics: physics, + physics: physics is BouncingScrollPhysics + ? const ClampingScrollPhysics() + : physics, child: CustomPaint( painter: painter, child: _TabRow( diff --git a/lib/components/select.dart b/lib/components/select.dart index 9e63467..3ac4852 100644 --- a/lib/components/select.dart +++ b/lib/components/select.dart @@ -60,7 +60,7 @@ class Select extends StatelessWidget { children: [ Text(current, style: ts.s14), const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down), + Icon(Icons.arrow_drop_down, color: context.colorScheme.primary), ], ).padding(const EdgeInsets.symmetric(horizontal: 12, vertical: 4)), ), diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 6be3432..d1d11fa 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:path_provider/path_provider.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/utils/io.dart'; @@ -9,9 +10,9 @@ class _Appdata { var searchHistory = []; bool _isSavingData = false; - + Future saveData() async { - if(_isSavingData) { + if (_isSavingData) { await Future.doWhile(() async { await Future.delayed(const Duration(milliseconds: 20)); return _isSavingData; @@ -25,11 +26,11 @@ class _Appdata { } void addSearchHistory(String keyword) { - if(searchHistory.contains(keyword)) { + if (searchHistory.contains(keyword)) { searchHistory.remove(keyword); } searchHistory.insert(0, keyword); - if(searchHistory.length > 50) { + if (searchHistory.length > 50) { searchHistory.removeLast(); } saveData(); @@ -46,13 +47,16 @@ class _Appdata { } Future init() async { - var file = File(FilePath.join(App.dataPath, 'appdata.json')); - if(!await file.exists()) { + var file = File(FilePath.join( + (await getApplicationSupportDirectory()).path, + 'appdata.json', + )); + if (!await file.exists()) { return; } var json = jsonDecode(await file.readAsString()); - for(var key in (json['settings'] as Map).keys) { - if(json['settings'][key] != null) { + for (var key in (json['settings'] as Map).keys) { + if (json['settings'][key] != null) { settings[key] = json['settings'][key]; } } @@ -74,7 +78,7 @@ class _Settings { final _data = { 'comicDisplayMode': 'detailed', // detailed, brief - 'comicTileScale': 1.0, // 0.8-1.2 + 'comicTileScale': 1.00, // 0.75-1.25 'color': 'blue', // red, pink, purple, green, orange, blue 'theme_mode': 'system', // light, dark, system 'newFavoriteAddTo': 'end', // start, end @@ -91,13 +95,14 @@ class _Settings { 'readerMode': 'galleryLeftToRight', // values of [ReaderMode] 'enableTapToTurnPages': true, 'enablePageAnimation': true, + 'language': 'system', // system, zh-CN, zh-TW, en-US }; - operator[](String key) { + operator [](String key) { return _data[key]; } - operator[]=(String key, dynamic value) { + operator []=(String key, dynamic value) { _data[key] = value; } @@ -105,4 +110,4 @@ class _Settings { String toString() { return _data.toString(); } -} \ No newline at end of file +} diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 90de45e..5033e89 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -100,6 +100,29 @@ class LocalManager with ChangeNotifier { late String path; + // return error message if failed + Future setNewPath(String newPath) async { + var newDir = Directory(newPath); + if(!await newDir.exists()) { + return "Directory does not exist"; + } + if(!await newDir.list().isEmpty) { + return "Directory is not empty"; + } + try { + await copyDirectory( + Directory(path), + newDir, + ); + await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(path); + } catch (e) { + return e.toString(); + } + await Directory(path).deleteIgnoreError(); + path = newPath; + return null; + } + Future init() async { _db = sqlite3.open( '${App.dataPath}/local.db', @@ -118,18 +141,18 @@ class LocalManager with ChangeNotifier { PRIMARY KEY (id, comic_type) ); '''); - if (File('${App.dataPath}/local_path').existsSync()) { - path = File('${App.dataPath}/local_path').readAsStringSync(); + if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) { + path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync(); } else { if (App.isAndroid) { var external = await getExternalStorageDirectories(); if (external != null && external.isNotEmpty) { - path = '${external.first.path}/local'; + path = FilePath.join(external.first.path, 'local_path'); } else { - path = '${App.dataPath}/local'; + path = FilePath.join(App.dataPath, 'local_path'); } } else { - path = '${App.dataPath}/local'; + path = FilePath.join(App.dataPath, 'local_path'); } } if (!Directory(path).existsSync()) { diff --git a/lib/init.dart b/lib/init.dart index 040b8e6..9ad25b9 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -12,8 +12,8 @@ import 'foundation/appdata.dart'; Future init() async { await AppTranslation.init(); - await App.init(); await appdata.init(); + await App.init(); await HistoryManager().init(); await LocalManager().init(); await LocalFavoritesManager().init(); diff --git a/lib/main.dart b/lib/main.dart index ae34ad4..b74278b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -104,6 +104,18 @@ class _MyAppState extends State { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], + locale: () { + var lang = appdata.settings['language']; + if(lang == 'system') { + return null; + } + return switch(lang) { + 'zh-CN' => const Locale('zh', 'CN'), + 'zh-TW' => const Locale('zh', 'TW'), + 'en-US' => const Locale('en'), + _ => null + }; + }(), supportedLocales: const [ Locale('en'), Locale('zh', 'CN'), diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index e0e9d40..ba4b62e 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -112,7 +112,7 @@ class AppDio with DioMixin { static HttpClient createHttpClient() { final client = HttpClient(); client.connectionTimeout = const Duration(seconds: 5); - client.findProxy = (uri) => proxy ?? "DIRECT"; + client.findProxy = (uri) => proxy == null ? "DIRECT" : "PROXY $proxy"; client.idleTimeout = const Duration(seconds: 100); client.badCertificateCallback = (X509Certificate cert, String host, int port) { if (host.contains("cdn")) return true; diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 750b888..f2f50e9 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:venera/pages/categories_page.dart'; import 'package:venera/pages/search_page.dart'; +import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/translations.dart'; import '../components/components.dart'; @@ -89,7 +90,9 @@ class _MainPageState extends State { PaneActionEntry( icon: Icons.settings, label: "Settings".tl, - onTap: () {}, + onTap: () { + to(() => const SettingsPage()); + }, ) ], pageBuilder: (index) { diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart new file mode 100644 index 0000000..f9a46da --- /dev/null +++ b/lib/pages/settings/about.dart @@ -0,0 +1,121 @@ +part of 'settings_page.dart'; + +class AboutSettings extends StatefulWidget { + const AboutSettings({super.key}); + + @override + State createState() => _AboutSettingsState(); +} + +class _AboutSettingsState extends State { + bool isCheckingUpdate = false; + + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView( + slivers: [ + SliverAppbar(title: Text("About".tl)), + SizedBox( + height: 136, + width: double.infinity, + child: Center( + child: Container( + width: 136, + height: 136, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(136), + ), + clipBehavior: Clip.antiAlias, + child: const Image( + image: AssetImage("assets/app_icon.png"), + filterQuality: FilterQuality.medium, + ), + ), + ), + ).paddingTop(16).toSliver(), + Column( + children: [ + const SizedBox(height: 8), + Text( + "V${App.version}", + style: const TextStyle(fontSize: 16), + ), + Text("Venera is a free and open-source app for comic reading.".tl), + const SizedBox(height: 8), + ], + ).toSliver(), + ListTile( + title: Text("Check for updates".tl), + trailing: Button.filled( + isLoading: isCheckingUpdate, + child: Text("Check".tl), + onPressed: () { + setState(() { + isCheckingUpdate = true; + }); + checkUpdate().then((value) { + if (value) { + showDialog( + context: App.rootContext, + builder: (context) { + return ContentDialog( + title: "New version available".tl, + content: Text( + "A new version is available. Do you want to update now?" + .tl), + actions: [ + Button.text( + onPressed: () { + Navigator.pop(context); + launchUrlString( + "https://github.com/venera-app/venera/releases"); + }, + child: Text("Update".tl), + ), + ]); + }); + } else { + context.showMessage(message: "No new version available".tl); + } + setState(() { + isCheckingUpdate = false; + }); + }); + }, + ).fixHeight(32), + ).toSliver(), + ListTile( + title: const Text("Github"), + trailing: const Icon(Icons.open_in_new), + onTap: () { + launchUrlString("https://github.com/venera-app/venera"); + }, + ).toSliver(), + ], + ); + } +} + +Future checkUpdate() async { + var res = await AppDio().get( + "https://raw.githubusercontent.com/venera-app/venera/refs/heads/master/pubspec.yaml"); + if (res.statusCode == 200) { + var data = loadYaml(res.data); + if (data["version"] != null) { + return _compareVersion(data["version"].split("+")[0], App.version); + } + } + return false; +} + +/// return true if version1 > version2 +bool _compareVersion(String version1, String version2) { + var v1 = version1.split("."); + var v2 = version2.split("."); + for (var i = 0; i < v1.length; i++) { + if (int.parse(v1[i]) > int.parse(v2[i])) { + return true; + } + } + return false; +} diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart new file mode 100644 index 0000000..30ff8ff --- /dev/null +++ b/lib/pages/settings/app.dart @@ -0,0 +1,219 @@ +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 { + var picker = FilePicker.platform; + var result = await picker.getDirectoryPath(); + 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(), + _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'); + } +} diff --git a/lib/pages/settings/appearance.dart b/lib/pages/settings/appearance.dart new file mode 100644 index 0000000..af3f013 --- /dev/null +++ b/lib/pages/settings/appearance.dart @@ -0,0 +1,44 @@ +part of 'settings_page.dart'; + +class AppearanceSettings extends StatefulWidget { + const AppearanceSettings({super.key}); + + @override + State createState() => _AppearanceSettingsState(); +} + +class _AppearanceSettingsState extends State { + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView( + slivers: [ + SliverAppbar(title: Text("Appearance".tl)), + SelectSetting( + title: "Theme Mode".tl, + settingKey: "theme_mode", + optionTranslation: { + "system": "System".tl, + "light": "Light".tl, + "dark": "Dark".tl, + }, + ).toSliver(), + SelectSetting( + title: "Theme Color".tl, + settingKey: "color", + optionTranslation: { + "red": "Red".tl, + "pink": "Pink".tl, + "purple": "Purple".tl, + "green": "Green".tl, + "orange": "Orange".tl, + "blue": "Blue".tl, + }, + onChanged: () async { + await App.init(); + App.forceRebuild(); + }, + ).toSliver(), + ], + ); + } +} diff --git a/lib/pages/settings/explore_settings.dart b/lib/pages/settings/explore_settings.dart new file mode 100644 index 0000000..00a979d --- /dev/null +++ b/lib/pages/settings/explore_settings.dart @@ -0,0 +1,182 @@ +part of 'settings_page.dart'; + +class ExploreSettings extends StatefulWidget { + const ExploreSettings({super.key}); + + @override + State createState() => _ExploreSettingsState(); +} + +class _ExploreSettingsState extends State { + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView( + slivers: [ + SliverAppbar(title: Text("Explore".tl)), + SelectSetting( + title: "Display mode of comic tile".tl, + settingKey: "comicDisplayMode", + optionTranslation: { + "detailed": "Detailed".tl, + "brief": "Brief".tl, + }, + ).toSliver(), + _SliderSetting( + title: "Size of comic tile".tl, + settingsIndex: "comicTileScale", + interval: 0.05, + min: 0.75, + max: 1.25, + ).toSliver(), + _PopupWindowSetting( + title: "Explore Pages".tl, + builder: () { + var pages = {}; + for (var c in ComicSource.all()) { + for (var page in c.explorePages) { + pages[page.title] = page.title; + } + } + return _MultiPagesFilter( + title: "Explore Pages".tl, + settingsIndex: "explore_pages", + pages: pages, + ); + }, + ).toSliver(), + _PopupWindowSetting( + title: "Category Pages".tl, + builder: () { + var pages = {}; + for (var c in ComicSource.all()) { + if (c.categoryData != null) { + pages[c.categoryData!.key] = c.categoryData!.title; + } + } + return _MultiPagesFilter( + title: "Category Pages".tl, + settingsIndex: "categories", + pages: pages, + ); + }, + ).toSliver(), + _PopupWindowSetting( + title: "Explore Pages".tl, + builder: () { + var pages = {}; + for (var c in ComicSource.all()) { + if (c.favoriteData != null) { + pages[c.favoriteData!.key] = c.favoriteData!.title; + } + } + return _MultiPagesFilter( + title: "Network Favorite Pages".tl, + settingsIndex: "favorites", + pages: pages, + ); + }, + ).toSliver(), + _SwitchSetting( + title: "Show favorite status on comic tile".tl, + settingKey: "showFavoriteStatusOnTile", + ).toSliver(), + _SwitchSetting( + title: "Show history on comic tile".tl, + settingKey: "showHistoryStatusOnTile", + ).toSliver(), + _PopupWindowSetting( + title: "Keyword blocking".tl, + builder: () => const _ManageBlockingWordView(), + ).toSliver(), + ], + ); + } +} + +class _ManageBlockingWordView extends StatefulWidget { + const _ManageBlockingWordView({super.key}); + + @override + State<_ManageBlockingWordView> createState() => + _ManageBlockingWordViewState(); +} + +class _ManageBlockingWordViewState extends State<_ManageBlockingWordView> { + @override + Widget build(BuildContext context) { + assert(appdata.settings["blockedWords"] is List); + return PopUpWidgetScaffold( + title: "Keyword blocking".tl, + tailing: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: add, + ), + ], + body: ListView.builder( + itemCount: appdata.settings["blockedWords"].length, + itemBuilder: (context, index) { + return ListTile( + title: Text(appdata.settings["blockedWords"][index]), + trailing: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + appdata.settings["blockedWords"].removeAt(index); + appdata.saveData(); + setState(() {}); + }, + ), + ); + }, + ), + ); + } + + void add() { + showDialog( + context: App.rootContext, + barrierColor: Colors.black.withOpacity(0.1), + builder: (context) { + var controller = TextEditingController(); + String? error; + return StatefulBuilder(builder: (context, setState) { + return ContentDialog( + title: "Add keyword".tl, + content: TextField( + controller: controller, + decoration: InputDecoration( + border: const OutlineInputBorder(), + label: Text("Keyword".tl), + errorText: error, + ), + onChanged: (s) { + if(error != null){ + setState(() { + error = null; + }); + } + }, + ).paddingHorizontal(12), + actions: [ + Button.filled( + onPressed: () { + if(appdata.settings["blockedWords"].contains(controller.text)){ + setState(() { + error = "Keyword already exists".tl; + }); + return; + } + appdata.settings["blockedWords"].add(controller.text); + appdata.saveData(); + this.setState(() {}); + context.pop(); + }, + child: Text("Add".tl), + ), + ], + ); + }); + }, + ); + } +} diff --git a/lib/pages/settings/local_favorites.dart b/lib/pages/settings/local_favorites.dart new file mode 100644 index 0000000..6169b4f --- /dev/null +++ b/lib/pages/settings/local_favorites.dart @@ -0,0 +1,36 @@ +part of 'settings_page.dart'; + +class LocalFavoritesSettings extends StatefulWidget { + const LocalFavoritesSettings({super.key}); + + @override + State createState() => _LocalFavoritesSettingsState(); +} + +class _LocalFavoritesSettingsState extends State { + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView( + slivers: [ + SliverAppbar(title: Text("Local Favorites".tl)), + const SelectSetting( + title: "Add new favorite to", + settingKey: "newFavoriteAddTo", + optionTranslation: { + "start": "Start", + "end": "End", + }, + ).toSliver(), + const SelectSetting( + title: "Move favorite after read", + settingKey: "moveFavoriteAfterRead", + optionTranslation: { + "none": "None", + "end": "End", + "start": "Start", + }, + ).toSliver(), + ], + ); + } +} diff --git a/lib/pages/settings/network.dart b/lib/pages/settings/network.dart new file mode 100644 index 0000000..0cc2545 --- /dev/null +++ b/lib/pages/settings/network.dart @@ -0,0 +1,236 @@ +part of 'settings_page.dart'; + +class NetworkSettings extends StatefulWidget { + const NetworkSettings({super.key}); + + @override + State createState() => _NetworkSettingsState(); +} + +class _NetworkSettingsState extends State { + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView( + slivers: [ + SliverAppbar(title: Text("Network".tl)), + _PopupWindowSetting( + title: "Proxy".tl, + builder: () => const _ProxySettingView(), + ).toSliver(), + ], + ); + } +} + +class _ProxySettingView extends StatefulWidget { + const _ProxySettingView(); + + @override + State<_ProxySettingView> createState() => _ProxySettingViewState(); +} + +class _ProxySettingViewState extends State<_ProxySettingView> { + String type = ''; + + String host = ''; + + String port = ''; + + String username = ''; + + String password = ''; + + // USERNAME:PASSWORD@HOST:PORT + String toProxyStr() { + if(type == 'direct') { + return 'direct'; + } else if(type == 'system') { + return 'system'; + } + var res = ''; + if(username.isNotEmpty) { + res += username; + if(password.isNotEmpty) { + res += ':$password'; + } + res += '@'; + } + res += host; + if(port.isNotEmpty) { + res += ':$port'; + } + return res; + } + + void parseProxyString(String proxy) { + if(proxy == 'direct') { + type = 'direct'; + return; + } else if(proxy == 'system') { + type = 'system'; + return; + } + type = 'manual'; + var parts = proxy.split('@'); + if(parts.length == 2) { + var auth = parts[0].split(':'); + if(auth.length == 2) { + username = auth[0]; + password = auth[1]; + } + parts = parts[1].split(':'); + if(parts.length == 2) { + host = parts[0]; + port = parts[1]; + } + } else { + parts = proxy.split(':'); + if(parts.length == 2) { + host = parts[0]; + port = parts[1]; + } + } + } + + @override + void initState() { + var proxy = appdata.settings['proxy']; + parseProxyString(proxy); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return PopUpWidgetScaffold( + title: "Proxy".tl, + body: SingleChildScrollView( + child: Column( + children: [ + RadioListTile( + title: Text("Direct".tl), + value: 'direct', + groupValue: type, + onChanged: (v) { + setState(() { + type = v!; + }); + appdata.settings['proxy'] = toProxyStr(); + appdata.saveData(); + }, + ), + RadioListTile( + title: Text("System".tl), + value: 'system', + groupValue: type, + onChanged: (v) { + setState(() { + type = v!; + }); + appdata.settings['proxy'] = toProxyStr(); + appdata.saveData(); + }, + ), + RadioListTile( + title: Text("Manual".tl), + value: 'manual', + groupValue: type, + onChanged: (v) { + setState(() { + type = v!; + }); + }, + ), + if(type == 'manual') buildManualProxy(), + ], + ), + ), + ); + } + + var formKey = GlobalKey(); + + Widget buildManualProxy() { + return Form( + key: formKey, + child: Column( + children: [ + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Host".tl, + ), + controller: TextEditingController(text: host), + onChanged: (v) { + host = v; + }, + validator: (v) { + if(v?.isEmpty ?? false) { + return "Host cannot be empty".tl; + } + return null; + }, + ), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Port".tl, + ), + controller: TextEditingController(text: port), + onChanged: (v) { + port = v; + }, + validator: (v) { + if(v?.isEmpty ?? true) { + return null; + } + if(int.tryParse(v!) == null) { + return "Port must be a number".tl; + } + return null; + }, + ), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Username".tl, + ), + controller: TextEditingController(text: username), + onChanged: (v) { + username = v; + }, + validator: (v) { + if((v?.isEmpty ?? false) && password.isNotEmpty) { + return "Username cannot be empty".tl; + } + return null; + }, + ), + const SizedBox(height: 8), + TextFormField( + decoration: InputDecoration( + border: const OutlineInputBorder(), + labelText: "Password".tl, + ), + controller: TextEditingController(text: password), + onChanged: (v) { + password = v; + }, + ), + const SizedBox(height: 16), + FilledButton( + onPressed: () { + if(formKey.currentState?.validate() ?? false) { + appdata.settings['proxy'] = toProxyStr(); + appdata.saveData(); + App.rootContext.pop(); + } + }, + child: Text("Save".tl), + ), + ], + ), + ).paddingHorizontal(16).paddingTop(16); + } +} diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 5185d4c..4704d24 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -14,7 +14,7 @@ class _ReaderSettingsState extends State { Widget build(BuildContext context) { return SmoothCustomScrollView( slivers: [ - SliverAppbar(title: Text("Settings".tl)), + SliverAppbar(title: Text("Reading".tl)), _SwitchSetting( title: "Tap to turn Pages".tl, settingKey: "enableTapToTurnPages", diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart index 02d4fc9..04ea79f 100644 --- a/lib/pages/settings/setting_components.dart +++ b/lib/pages/settings/setting_components.dart @@ -251,3 +251,225 @@ class _SliderSettingState extends State<_SliderSetting> { ); } } + +class _PopupWindowSetting extends StatelessWidget { + const _PopupWindowSetting({required this.title, required this.builder}); + + final Widget Function() builder; + + final String title; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + trailing: const Icon(Icons.arrow_right), + onTap: () { + showPopUpWidget(App.rootContext, builder()); + }, + ); + } +} + +class _MultiPagesFilter extends StatefulWidget { + const _MultiPagesFilter({ + required this.title, + required this.settingsIndex, + required this.pages, + }); + + final String title; + + final String settingsIndex; + + // key - name + final Map pages; + + @override + State<_MultiPagesFilter> createState() => _MultiPagesFilterState(); +} + +class _MultiPagesFilterState extends State<_MultiPagesFilter> { + late List keys; + + @override + void initState() { + keys = List.from(appdata.settings[widget.settingsIndex]); + keys.remove(""); + super.initState(); + } + + var reorderWidgetKey = UniqueKey(); + var scrollController = ScrollController(); + final _key = GlobalKey(); + + @override + Widget build(BuildContext context) { + var tiles = keys.map((e) => buildItem(e)).toList(); + + var view = ReorderableBuilder( + key: reorderWidgetKey, + scrollController: scrollController, + longPressDelay: App.isDesktop + ? const Duration(milliseconds: 100) + : const Duration(milliseconds: 500), + dragChildBoxDecoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainer, + boxShadow: const [ + BoxShadow( + color: Colors.black12, + blurRadius: 5, + offset: Offset(0, 2), + spreadRadius: 2) + ], + ), + onReorder: (reorderFunc) { + setState(() { + keys = List.from(reorderFunc(keys)); + }); + updateSetting(); + }, + children: tiles, + builder: (children) { + return GridView( + key: _key, + controller: scrollController, + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 1, + mainAxisExtent: 48, + ), + children: children, + ); + }, + ); + + return PopUpWidgetScaffold( + title: widget.title, + tailing: [ + if (keys.length < widget.pages.length) + IconButton(onPressed: showAddDialog, icon: const Icon(Icons.add)) + ], + body: view, + ); + } + + Widget buildItem(String key) { + Widget removeButton = Padding( + padding: const EdgeInsets.only(right: 8), + child: IconButton( + onPressed: () { + setState(() { + keys.remove(key); + }); + updateSetting(); + }, + icon: const Icon(Icons.delete)), + ); + + return ListTile( + title: Text(widget.pages[key] ?? "(Invalid) $key"), + key: Key(key), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + removeButton, + const Icon(Icons.drag_handle), + ], + ), + ); + } + + void showAddDialog() { + var canAdd = {}; + widget.pages.forEach((key, value) { + if (!keys.contains(key)) { + canAdd[key] = value; + } + }); + showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: const Text("Add"), + children: canAdd.entries + .map((e) => InkWell( + child: ListTile(title: Text(e.value), key: Key(e.key)), + onTap: () { + context.pop(); + setState(() { + keys.add(e.key); + }); + updateSetting(); + }, + )) + .toList(), + ); + }); + } + + void updateSetting() { + appdata.settings[widget.settingsIndex] = keys; + appdata.saveData(); + } +} + +class _CallbackSetting extends StatelessWidget { + const _CallbackSetting({ + required this.title, + required this.callback, + required this.actionTitle, + this.subtitle, + }); + + final String title; + + final String? subtitle; + + final VoidCallback callback; + + final String actionTitle; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + subtitle: subtitle == null ? null : Text(subtitle!), + trailing: FilledButton( + onPressed: callback, + child: Text(actionTitle), + ).fixHeight(28), + onTap: callback, + ); + } +} + +class _SettingPartTitle extends StatelessWidget { + const _SettingPartTitle({required this.title, required this.icon}); + + final String title; + + final IconData icon; + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.only(left: 16, top: 16, bottom: 8), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: context.colorScheme.onSurface.withOpacity(0.1), + ), + ), + ), + child: Row( + children: [ + Icon(icon, size: 24), + const SizedBox(width: 8), + Text(title, style: ts.s18), + ], + ), + ), + ); + } +} diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index ba2beca..3dbbc5b 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -1,8 +1,361 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; +import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/cache_manager.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; +import 'package:venera/foundation/consts.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/foundation/log.dart'; +import 'package:venera/network/app_dio.dart'; +import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; +import 'package:yaml/yaml.dart'; part 'reader.dart'; -part 'setting_components.dart'; \ No newline at end of file +part 'explore_settings.dart'; +part 'setting_components.dart'; +part 'appearance.dart'; +part 'local_favorites.dart'; +part 'app.dart'; +part 'about.dart'; +part 'network.dart'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({this.initialPage = -1, super.key}); + + final int initialPage; + + @override + State createState() => _SettingsPageState(); +} + +class _SettingsPageState extends State implements PopEntry { + int currentPage = -1; + + ColorScheme get colors => Theme.of(context).colorScheme; + + bool get enableTwoViews => context.width > changePoint; + + final categories = [ + "Explore", + "Reading", + "Appearance", + "Local Favorites", + "APP", + "Network", + "About", + ]; + + final icons = [ + Icons.explore, + Icons.book, + Icons.color_lens, + Icons.collections_bookmark_rounded, + Icons.apps, + Icons.public, + Icons.info + ]; + + double offset = 0; + + late final HorizontalDragGestureRecognizer gestureRecognizer; + + ModalRoute? _route; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final ModalRoute? nextRoute = ModalRoute.of(context); + if (nextRoute != _route) { + _route?.unregisterPopEntry(this); + _route = nextRoute; + _route?.registerPopEntry(this); + } + } + + @override + void initState() { + currentPage = widget.initialPage; + gestureRecognizer = HorizontalDragGestureRecognizer(debugOwner: this) + ..onUpdate = ((details) => setState(() => offset += details.delta.dx)) + ..onEnd = (details) async { + if (details.velocity.pixelsPerSecond.dx.abs() > 1 && + details.velocity.pixelsPerSecond.dx >= 0) { + setState(() { + Future.delayed(const Duration(milliseconds: 300), () => offset = 0); + currentPage = -1; + }); + } else if (offset > MediaQuery.of(context).size.width / 2) { + setState(() { + Future.delayed(const Duration(milliseconds: 300), () => offset = 0); + currentPage = -1; + }); + } else { + int i = 10; + while (offset != 0) { + setState(() { + offset -= i; + i *= 10; + if (offset < 0) { + offset = 0; + } + }); + await Future.delayed(const Duration(milliseconds: 10)); + } + } + } + ..onCancel = () async { + int i = 10; + while (offset != 0) { + setState(() { + offset -= i; + i *= 10; + if (offset < 0) { + offset = 0; + } + }); + await Future.delayed(const Duration(milliseconds: 10)); + } + }; + super.initState(); + } + + @override + dispose() { + super.dispose(); + gestureRecognizer.dispose(); + _route?.unregisterPopEntry(this); + } + + @override + Widget build(BuildContext context) { + if (currentPage != -1) { + canPop.value = false; + } else { + canPop.value = true; + } + return Material( + child: buildBody(), + ); + } + + Widget buildBody() { + if (enableTwoViews) { + return Row( + children: [ + SizedBox( + width: 280, + height: double.infinity, + child: buildLeft(), + ), + Container( + height: double.infinity, + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: context.colorScheme.outlineVariant, + width: 0.6, + ), + ), + ), + ), + Expanded(child: buildRight()) + ], + ); + } else { + return Stack( + children: [ + Positioned.fill(child: buildLeft()), + Positioned( + left: offset, + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Listener( + onPointerDown: handlePointerDown, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + switchInCurve: Curves.fastOutSlowIn, + switchOutCurve: Curves.fastOutSlowIn, + transitionBuilder: (child, animation) { + var tween = Tween( + begin: const Offset(1, 0), end: const Offset(0, 0)); + + return SlideTransition( + position: tween.animate(animation), + child: child, + ); + }, + child: currentPage == -1 + ? const SizedBox( + key: Key("1"), + ) + : buildRight(), + ), + ), + ) + ], + ); + } + } + + void handlePointerDown(PointerDownEvent event) { + if (event.position.dx < 20) { + gestureRecognizer.addPointer(event); + } + } + + Widget buildLeft() { + return Material( + child: Column( + children: [ + SizedBox( + height: MediaQuery.of(context).padding.top, + ), + SizedBox( + height: 56, + child: Row(children: [ + const SizedBox( + width: 8, + ), + Tooltip( + message: "Back", + child: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: context.pop, + ), + ), + const SizedBox( + width: 24, + ), + Text( + "Settings".tl, + style: ts.s20, + ) + ]), + ), + const SizedBox( + height: 4, + ), + Expanded( + child: buildCategories(), + ) + ], + ), + ); + } + + Widget buildCategories() { + Widget buildItem(String name, int id) { + final bool selected = id == currentPage; + + Widget content = AnimatedContainer( + key: ValueKey(id), + duration: const Duration(milliseconds: 200), + width: double.infinity, + height: 46, + padding: const EdgeInsets.fromLTRB(12, 0, 12, 0), + decoration: BoxDecoration( + color: selected ? colors.primaryContainer.withOpacity(0.36) : null, + border: Border( + left: BorderSide( + color: selected ? colors.primary : Colors.transparent, + width: 2, + ), + ), + ), + child: Row(children: [ + Icon(icons[id]), + const SizedBox(width: 16), + Text( + name, + style: ts.s16, + ), + const Spacer(), + if (selected) const Icon(Icons.arrow_right) + ]), + ); + + return Padding( + padding: enableTwoViews + ? const EdgeInsets.fromLTRB(8, 0, 8, 0) + : EdgeInsets.zero, + child: InkWell( + onTap: () => setState(() => currentPage = id), + child: content, + ).paddingVertical(4), + ); + } + + return ListView.builder( + padding: EdgeInsets.zero, + itemCount: categories.length, + itemBuilder: (context, index) => buildItem(categories[index].tl, index), + ); + } + + Widget buildRight() { + final Widget body = switch (currentPage) { + -1 => const SizedBox(), + 0 => const ExploreSettings(), + 1 => const ReaderSettings(), + 2 => const AppearanceSettings(), + 3 => const LocalFavoritesSettings(), + 4 => const AppSettings(), + 5 => const NetworkSettings(), + 6 => const AboutSettings(), + _ => throw UnimplementedError() + }; + + return Material( + child: body, + ); + } + + var canPop = ValueNotifier(true); + + @override + ValueListenable get canPopNotifier => canPop; + + /* + flutter >=3.24.0 api + + @override + void onPopInvokedWithResult(bool didPop, result) { + if (currentPage != -1) { + setState(() { + currentPage = -1; + }); + } + } + + @override + void onPopInvoked(bool didPop) { + if (currentPage != -1) { + setState(() { + currentPage = -1; + }); + } + } + */ + + // flutter <3.24.0 api + @override + PopInvokedCallback? get onPopInvoked => (bool didPop) { + if (currentPage != -1) { + setState(() { + currentPage = -1; + }); + } + }; +} diff --git a/lib/utils/io.dart b/lib/utils/io.dart index c99e98b..931bbe5 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -186,3 +186,15 @@ class Share { s.Share.share(text); } } + +String bytesToReadableString(int bytes) { + if (bytes < 1024) { + return "$bytes B"; + } else if (bytes < 1024 * 1024) { + return "${(bytes / 1024).toStringAsFixed(2)} KB"; + } else if (bytes < 1024 * 1024 * 1024) { + return "${(bytes / 1024 / 1024).toStringAsFixed(2)} MB"; + } else { + return "${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB"; + } +} diff --git a/pubspec.lock b/pubspec.lock index 6a98f95..541e5d8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -164,6 +164,14 @@ packages: url: "https://github.com/wgh136/flutter_qjs" source: git version: "0.3.7" + flutter_reorderable_grid_view: + dependency: "direct main" + description: + name: flutter_reorderable_grid_view + sha256: "40abcc5bff228ebff119326502e7357ee6399956b60b80b17385e9770b7458c0" + url: "https://pub.dev" + source: hosted + version: "5.0.1" flutter_test: dependency: "direct dev" description: flutter @@ -597,6 +605,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + yaml: + dependency: "direct main" + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" sdks: dart: ">=3.4.4 <4.0.0" flutter: ">=3.22.3" diff --git a/pubspec.yaml b/pubspec.yaml index 5c7fae0..d00547e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,8 @@ dependencies: url: https://github.com/venera-app/flutter.widgets ref: 09e756b1f1b04e6298318d99ec20a787fb360f59 path: packages/scrollable_positioned_list + flutter_reorderable_grid_view: 5.0.1 + yaml: any dev_dependencies: flutter_test: @@ -51,4 +53,5 @@ flutter: assets: - assets/translation.json - assets/init.js + - assets/app_icon.png