From db2c2395de4ebfc1bb758fea5791c083e9e87051 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 10:35:21 +0800 Subject: [PATCH 01/37] fix importing data on windows --- lib/utils/data.dart | 10 +++++++--- lib/utils/io.dart | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/utils/data.dart b/lib/utils/data.dart index ec55e9b..0865244 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -54,16 +54,19 @@ Future importAppData(File file, [bool checkVersion = false]) async { } if(await historyFile.exists()) { HistoryManager().close(); - historyFile.copySync(FilePath.join(App.dataPath, "history.db")); + File(FilePath.join(App.dataPath, "history.db")).deleteIfExistsSync(); + historyFile.renameSync(FilePath.join(App.dataPath, "history.db")); HistoryManager().init(); } if(await localFavoriteFile.exists()) { LocalFavoritesManager().close(); - localFavoriteFile.copySync(FilePath.join(App.dataPath, "local_favorite.db")); + File(FilePath.join(App.dataPath, "local_favorite.db")).deleteIfExistsSync(); + localFavoriteFile.renameSync(FilePath.join(App.dataPath, "local_favorite.db")); LocalFavoritesManager().init(); } if(await appdataFile.exists()) { - await appdataFile.copy(FilePath.join(App.dataPath, "appdata.json")); + File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync(); + appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json")); appdata.init(); } var comicSourceDir = FilePath.join(cacheDirPath, "comic_source"); @@ -71,6 +74,7 @@ Future importAppData(File file, [bool checkVersion = false]) async { for(var file in Directory(comicSourceDir).listSync()) { if(file is File) { var targetFile = FilePath.join(App.dataPath, "comic_source", file.name); + File(targetFile).deleteIfExistsSync(); await file.copy(targetFile); } } diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 5431f18..34377c5 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -45,6 +45,18 @@ extension FileSystemEntityExt on FileSystemEntity { // ignore } } + + Future deleteIfExists({bool recursive = false}) async { + if (existsSync()) { + await delete(recursive: recursive); + } + } + + void deleteIfExistsSync({bool recursive = false}) { + if (existsSync()) { + deleteSync(recursive: recursive); + } + } } extension FileExtension on File { From f8eace4c3112560a610cca3fbe854212759e9b03 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 10:40:56 +0800 Subject: [PATCH 02/37] fix an issue where a deleted comic could not be displayed in a favorite folder. --- lib/components/comic.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index ab8c073..525bcaf 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -158,7 +158,10 @@ class ComicTile extends StatelessWidget { image = FileImage(File(comic.cover.substring(7))); } else if (comic.sourceKey == 'local') { var localComic = LocalManager().find(comic.id, ComicType.local); - image = FileImage(localComic!.coverFile); + if(localComic == null) { + return const SizedBox(); + } + image = FileImage(localComic.coverFile); } else { image = CachedImageProvider(comic.cover, sourceKey: comic.sourceKey); } From ea3cc8cc58b7e72a4cdc3d48246625f3716d0cb7 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 10:58:48 +0800 Subject: [PATCH 03/37] [windows] prevent multiple instances --- windows/runner/win32_window.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index 60608d0..4d452e8 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -123,6 +123,26 @@ Win32Window::~Win32Window() { bool Win32Window::Create(const std::wstring& title, const Point& origin, const Size& size) { + HWND hwnd = ::FindWindow(kWindowClassName, title.c_str()); + if (hwnd) { + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + SetForegroundWindow(hwnd); + switch (place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + return false; + } Destroy(); const wchar_t* window_class = From 193ecdb765ad354de7427e4fea8df417f60eb641 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 11:52:36 +0800 Subject: [PATCH 04/37] improve data sync --- lib/pages/reader/reader.dart | 4 ++++ lib/utils/data_sync.dart | 5 ++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 65a432e..6dea93f 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -20,6 +20,7 @@ import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/pages/settings/settings_page.dart'; +import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; @@ -122,6 +123,9 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { focusNode.dispose(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); stopVolumeEvent(); + Future.microtask(() { + DataSync().onDataChanged(); + }); super.dispose(); } diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index b277ae5..3dd1474 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -4,7 +4,6 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/favorites.dart'; -import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/network/app_dio.dart'; @@ -19,7 +18,6 @@ class DataSync with ChangeNotifier { if (isEnabled) { downloadData(); } - HistoryManager().addListener(onDataChanged); LocalFavoritesManager().addListener(onDataChanged); ComicSource.addListener(onDataChanged); } @@ -57,8 +55,9 @@ class DataSync with ChangeNotifier { } Future> uploadData() async { + if(isDownloading) return const Res(true); if (haveWaitingTask) return const Res(true); - while (isDownloading || isUploading) { + while (isUploading) { haveWaitingTask = true; await Future.delayed(const Duration(milliseconds: 100)); } From d875681c4bf5a4a58b923a9d8432f08945e1bfb6 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 14:23:24 +0800 Subject: [PATCH 05/37] update gitignore --- .gitignore | 5 +- linux/flutter/generated_plugin_registrant.cc | 43 ----------------- linux/flutter/generated_plugin_registrant.h | 15 ------ linux/flutter/generated_plugins.cmake | 34 ------------- macos/Flutter/GeneratedPluginRegistrant.swift | 30 ------------ pubspec.lock | 48 ------------------- .../flutter/generated_plugin_registrant.cc | 44 ----------------- windows/flutter/generated_plugin_registrant.h | 15 ------ windows/flutter/generated_plugins.cmake | 37 -------------- 9 files changed, 4 insertions(+), 267 deletions(-) delete mode 100644 linux/flutter/generated_plugin_registrant.cc delete mode 100644 linux/flutter/generated_plugin_registrant.h delete mode 100644 linux/flutter/generated_plugins.cmake delete mode 100644 macos/Flutter/GeneratedPluginRegistrant.swift delete mode 100644 windows/flutter/generated_plugin_registrant.cc delete mode 100644 windows/flutter/generated_plugin_registrant.h delete mode 100644 windows/flutter/generated_plugins.cmake diff --git a/.gitignore b/.gitignore index 291ef60..d0754d1 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ app.*.map.json /android/app/profile /android/app/release -add_translation.py \ No newline at end of file +add_translation.py + +*/*/generated_* +*/*/Generated* \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e9281b4..0000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,43 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); - desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); - g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); - file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) flutter_qjs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin"); - flutter_qjs_plugin_register_with_registrar(flutter_qjs_registrar); - g_autoptr(FlPluginRegistrar) gtk_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); - gtk_plugin_register_with_registrar(gtk_registrar); - g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); - screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); - g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); - sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); - g_autoptr(FlPluginRegistrar) window_manager_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); - window_manager_plugin_register_with_registrar(window_manager_registrar); -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47..0000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 3b7ed15..0000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,34 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - desktop_webview_window - file_selector_linux - flutter_qjs - gtk - screen_retriever_linux - sqlite3_flutter_libs - url_launcher_linux - window_manager -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - lodepng_flutter - rhttp - zip_flutter -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 4b07183..0000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,30 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import app_links -import desktop_webview_window -import file_selector_macos -import flutter_inappwebview_macos -import path_provider_foundation -import screen_retriever_macos -import share_plus -import sqlite3_flutter_libs -import url_launcher_macos -import window_manager - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) - DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) - FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) - SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) - Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) -} diff --git a/pubspec.lock b/pubspec.lock index 4fd2dae..c1c24c8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -593,54 +593,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - permission_handler: - dependency: "direct main" - description: - name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" - url: "https://pub.dev" - source: hosted - version: "11.3.1" - permission_handler_android: - dependency: transitive - description: - name: permission_handler_android - sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" - url: "https://pub.dev" - source: hosted - version: "12.0.13" - permission_handler_apple: - dependency: transitive - description: - name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 - url: "https://pub.dev" - source: hosted - version: "9.4.5" - permission_handler_html: - dependency: transitive - description: - name: permission_handler_html - sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 - url: "https://pub.dev" - source: hosted - version: "0.1.3+2" - permission_handler_platform_interface: - dependency: transitive - description: - name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 - url: "https://pub.dev" - source: hosted - version: "4.2.3" - permission_handler_windows: - dependency: transitive - description: - name: permission_handler_windows - sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" - url: "https://pub.dev" - source: hosted - version: "0.2.1" petitparser: dependency: transitive description: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 11d2b2f..0000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,44 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - AppLinksPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("AppLinksPluginCApi")); - DesktopWebviewWindowPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); - FileSelectorWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FileSelectorWindows")); - FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); - FlutterQjsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterQjsPlugin")); - PermissionHandlerWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); - ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); - SharePlusWindowsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); - Sqlite3FlutterLibsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); - WindowManagerPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("WindowManagerPlugin")); -} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d8..0000000 --- a/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake deleted file mode 100644 index e7ccd0e..0000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,37 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - app_links - desktop_webview_window - file_selector_windows - flutter_inappwebview_windows - flutter_qjs - permission_handler_windows - screen_retriever_windows - share_plus - sqlite3_flutter_libs - url_launcher_windows - window_manager -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST - lodepng_flutter - rhttp - zip_flutter -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) From 7cf55fcb8e4eed95f8916d7fd1bc74508aed785d Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 15:01:31 +0800 Subject: [PATCH 06/37] add onLoadFailed to imageLoadingConfig --- lib/network/images.dart | 120 +++++++++++++++++++++++++--------------- 1 file changed, 76 insertions(+), 44 deletions(-) diff --git a/lib/network/images.dart b/lib/network/images.dart index 29c9005..1c26f89 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; +import 'package:flutter_qjs/flutter_qjs.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; @@ -83,61 +84,92 @@ class ImageDownloader { ); } + Future?> Function()? onLoadFailed; + var configs = {}; if (sourceKey != null) { var comicSource = ComicSource.find(sourceKey); configs = (await comicSource!.getImageLoadingConfig - ?.call(imageKey, cid, eid)) ?? {}; + ?.call(imageKey, cid, eid)) ?? + {}; } - configs['headers'] ??= { - 'user-agent': webUA, - }; + var retryLimit = 5; + while (true) { + try { + configs['headers'] ??= { + 'user-agent': webUA, + }; - var dio = AppDio(BaseOptions( - headers: configs['headers'], - method: configs['method'] ?? 'GET', - responseType: ResponseType.stream, - )); + if (configs['onLoadFailed'] is JSInvokable) { + onLoadFailed = () async { + dynamic result = configs['onLoadFailed'](); + if (result is Future) { + result = await result; + } + if (result is! Map) return null; + return result; + }; + } - var req = await dio.request(configs['url'] ?? imageKey, - data: configs['data']); - var stream = req.data?.stream ?? (throw "Error: Empty response body."); - int? expectedBytes = req.data!.contentLength; - if (expectedBytes == -1) { - expectedBytes = null; - } - var buffer = []; - await for (var data in stream) { - buffer.addAll(data); - if (expectedBytes != null) { + var dio = AppDio(BaseOptions( + headers: configs['headers'], + method: configs['method'] ?? 'GET', + responseType: ResponseType.stream, + )); + + var req = await dio.request(configs['url'] ?? imageKey, + data: configs['data']); + var stream = req.data?.stream ?? (throw "Error: Empty response body."); + int? expectedBytes = req.data!.contentLength; + if (expectedBytes == -1) { + expectedBytes = null; + } + var buffer = []; + await for (var data in stream) { + buffer.addAll(data); + if (expectedBytes != null) { + yield ImageDownloadProgress( + currentBytes: buffer.length, + totalBytes: expectedBytes, + ); + } + } + + if (configs['onResponse'] != null) { + buffer = configs['onResponse'](buffer); + } + + var data = Uint8List.fromList(buffer); + buffer.clear(); + + if (configs['modifyImage'] != null) { + var newData = await modifyImageWithScript( + data, + configs['modifyImage'], + ); + data = newData; + } + + await CacheManager().writeCache(cacheKey, data); yield ImageDownloadProgress( - currentBytes: buffer.length, - totalBytes: expectedBytes, + currentBytes: data.length, + totalBytes: data.length, + imageBytes: data, ); + return; + } catch (e) { + if(retryLimit < 0 || onLoadFailed == null) { + rethrow; + } + var newConfig = await onLoadFailed(); + onLoadFailed = null; + if(newConfig == null) { + rethrow; + } + configs = newConfig; + retryLimit--; } } - - if (configs['onResponse'] != null) { - buffer = configs['onResponse'](buffer); - } - - var data = Uint8List.fromList(buffer); - buffer.clear(); - - if (configs['modifyImage'] != null) { - var newData = await modifyImageWithScript( - data, - configs['modifyImage'], - ); - data = newData; - } - - await CacheManager().writeCache(cacheKey, data); - yield ImageDownloadProgress( - currentBytes: data.length, - totalBytes: data.length, - imageBytes: data, - ); } } From 6ae3e50a5bf91fee3b35153a963320b536f72b23 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 17:18:56 +0800 Subject: [PATCH 07/37] improve network request --- lib/network/app_dio.dart | 16 ++++++++- lib/network/cache.dart | 76 ++++++++++++++++++---------------------- lib/network/images.dart | 22 ++++++++---- 3 files changed, 64 insertions(+), 50 deletions(-) diff --git a/lib/network/app_dio.dart b/lib/network/app_dio.dart index af21d6f..6de436f 100644 --- a/lib/network/app_dio.dart +++ b/lib/network/app_dio.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -176,6 +177,8 @@ class AppDio with DioMixin { return res; } + static final Map _requests = {}; + @override Future> request( String path, { @@ -186,6 +189,13 @@ class AppDio with DioMixin { ProgressCallback? onSendProgress, ProgressCallback? onReceiveProgress, }) async { + if(options?.headers?['prevent-parallel'] == 'true') { + while(_requests.containsKey(path)) { + await Future.delayed(const Duration(milliseconds: 20)); + } + _requests[path] = true; + options!.headers!.remove('prevent-parallel'); + } proxy = await getProxy(); if (_proxy != proxy) { Log.info("Network", "Proxy changed to $proxy"); @@ -196,7 +206,7 @@ class AppDio with DioMixin { : rhttp.ProxySettings.proxy(proxy!), )); } - return super.request( + var res = super.request( path, data: data, queryParameters: queryParameters, @@ -205,6 +215,10 @@ class AppDio with DioMixin { onSendProgress: onSendProgress, onReceiveProgress: onReceiveProgress, ); + if(_requests.containsKey(path)) { + _requests.remove(path); + } + return res; } } diff --git a/lib/network/cache.dart b/lib/network/cache.dart index 7beea4f..9d9df06 100644 --- a/lib/network/cache.dart +++ b/lib/network/cache.dart @@ -1,5 +1,4 @@ -import 'dart:async'; - +import 'dart:typed_data'; import 'package:dio/dio.dart'; class NetworkCache { @@ -43,7 +42,7 @@ class NetworkCacheManager implements Interceptor { static const _maxCacheSize = 10 * 1024 * 1024; void setCache(NetworkCache cache) { - while(size > _maxCacheSize){ + while (size > _maxCacheSize) { size -= _cache.values.first.size; _cache.remove(_cache.keys.first); } @@ -53,7 +52,7 @@ class NetworkCacheManager implements Interceptor { void removeCache(Uri uri) { var cache = _cache[uri]; - if(cache != null){ + if (cache != null) { size -= cache.size; } _cache.remove(uri); @@ -64,41 +63,29 @@ class NetworkCacheManager implements Interceptor { size = 0; } - var preventParallel = {}; - @override void onError(DioException err, ErrorInterceptorHandler handler) { - if(err.requestOptions.method != "GET"){ + if (err.requestOptions.method != "GET") { return handler.next(err); } - if(preventParallel[err.requestOptions.uri] != null){ - preventParallel[err.requestOptions.uri]!.complete(); - preventParallel.remove(err.requestOptions.uri); - } return handler.next(err); } @override void onRequest( RequestOptions options, RequestInterceptorHandler handler) async { - if(options.method != "GET"){ + if (options.method != "GET") { return handler.next(options); } - if(preventParallel[options.uri] != null){ - await preventParallel[options.uri]!.future; - } var cache = getCache(options.uri); - if (cache == null || !compareHeaders(options.headers, cache.requestHeaders)) { - if(options.headers['cache-time'] != null){ + if (cache == null || + !compareHeaders(options.headers, cache.requestHeaders)) { + if (options.headers['cache-time'] != null) { options.headers.remove('cache-time'); } - if(options.headers['prevent-parallel'] != null){ - options.headers.remove('prevent-parallel'); - preventParallel[options.uri] = Completer(); - } return handler.next(options); } else { - if(options.headers['cache-time'] == 'no'){ + if (options.headers['cache-time'] == 'no') { options.headers.remove('cache-time'); removeCache(options.uri); return handler.next(options); @@ -106,20 +93,21 @@ class NetworkCacheManager implements Interceptor { } var time = DateTime.now(); var diff = time.difference(cache.time); - if (options.headers['cache-time'] == 'long' - && diff < const Duration(hours: 2)) { + if (options.headers['cache-time'] == 'long' && + diff < const Duration(hours: 2)) { return handler.resolve(Response( requestOptions: options, data: cache.data, - headers: Headers.fromMap(cache.responseHeaders), + headers: Headers.fromMap(cache.responseHeaders) + ..set('venera-cache', 'true'), statusCode: 200, )); - } - else if (diff < const Duration(seconds: 5)) { + } else if (diff < const Duration(seconds: 5)) { return handler.resolve(Response( requestOptions: options, data: cache.data, - headers: Headers.fromMap(cache.responseHeaders), + headers: Headers.fromMap(cache.responseHeaders) + ..set('venera-cache', 'true'), statusCode: 200, )); } else if (diff < const Duration(hours: 1)) { @@ -133,7 +121,8 @@ class NetworkCacheManager implements Interceptor { return handler.resolve(Response( requestOptions: options, data: cache.data, - headers: Headers.fromMap(cache.responseHeaders), + headers: Headers.fromMap(cache.responseHeaders) + ..set('venera-cache', 'true'), statusCode: 200, )); } @@ -143,6 +132,10 @@ class NetworkCacheManager implements Interceptor { } static bool compareHeaders(Map a, Map b) { + a.remove('cache-time'); + a.remove('prevent-parallel'); + b.remove('cache-time'); + b.remove('prevent-parallel'); if (a.length != b.length) { return false; } @@ -160,11 +153,11 @@ class NetworkCacheManager implements Interceptor { if (response.requestOptions.method != "GET") { return handler.next(response); } - if(response.statusCode != null && response.statusCode! >= 400){ + if (response.statusCode != null && response.statusCode! >= 400) { return handler.next(response); } var size = _calculateSize(response.data); - if(size != null && size < 1024 * 1024 && size > 0) { + if (size != null && size < 1024 * 1024 && size > 0) { var cache = NetworkCache( uri: response.requestOptions.uri, requestHeaders: response.requestOptions.headers, @@ -175,30 +168,29 @@ class NetworkCacheManager implements Interceptor { ); setCache(cache); } - if(preventParallel[response.requestOptions.uri] != null){ - preventParallel[response.requestOptions.uri]!.complete(); - preventParallel.remove(response.requestOptions.uri); - } handler.next(response); } - static int? _calculateSize(Object? data){ - if(data == null){ + static int? _calculateSize(Object? data) { + if (data == null) { return 0; } - if(data is List) { + if (data is List) { return data.length; } - if(data is String) { - if(data.trim().isEmpty){ + if (data is Uint8List) { + return data.length; + } + if (data is String) { + if (data.trim().isEmpty) { return 0; } - if(data.length < 512 && data.contains("IP address")){ + if (data.length < 512 && data.contains("IP address")) { return 0; } return data.length * 4; } - if(data is Map) { + if (data is Map) { return data.toString().length * 4; } return null; diff --git a/lib/network/images.dart b/lib/network/images.dart index 1c26f89..096296f 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -58,8 +58,9 @@ class ImageDownloader { } } - if (configs['onResponse'] != null) { - buffer = configs['onResponse'](buffer); + if (configs['onResponse'] is JSInvokable) { + buffer = (configs['onResponse'] as JSInvokable)([buffer]); + (configs['onResponse'] as JSInvokable).free(); } await CacheManager().writeCache(cacheKey, buffer); @@ -102,7 +103,7 @@ class ImageDownloader { if (configs['onLoadFailed'] is JSInvokable) { onLoadFailed = () async { - dynamic result = configs['onLoadFailed'](); + dynamic result = (configs['onLoadFailed'] as JSInvokable)([]); if (result is Future) { result = await result; } @@ -135,8 +136,9 @@ class ImageDownloader { } } - if (configs['onResponse'] != null) { - buffer = configs['onResponse'](buffer); + if (configs['onResponse'] is JSInvokable) { + buffer = (configs['onResponse'] as JSInvokable)([buffer]); + (configs['onResponse'] as JSInvokable).free(); } var data = Uint8List.fromList(buffer); @@ -158,17 +160,23 @@ class ImageDownloader { ); return; } catch (e) { - if(retryLimit < 0 || onLoadFailed == null) { + if (retryLimit < 0 || onLoadFailed == null) { rethrow; } var newConfig = await onLoadFailed(); + (configs['onLoadFailed'] as JSInvokable).free(); onLoadFailed = null; - if(newConfig == null) { + if (newConfig == null) { rethrow; } configs = newConfig; retryLimit--; } + finally { + if(onLoadFailed != null) { + (configs['onLoadFailed'] as JSInvokable).free(); + } + } } } } From a0e3cc720acd70cc14c71b96ef27f18cb9d0294a Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 17:36:42 +0800 Subject: [PATCH 08/37] add ImageLoadingConfig constructor --- assets/init.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/assets/init.js b/assets/init.js index 932590c..6d08177 100644 --- a/assets/init.js +++ b/assets/init.js @@ -940,6 +940,33 @@ function Comment({userName, avatar, content, time, replyCount, id, isLiked, scor this.voteStatus = voteStatus; } +/** + * Create image loading config + * @param url {string?} + * @param method {string?} - http method, uppercase + * @param data {any} - request data, may be null + * @param headers {Object?} - request headers + * @param onResponse {((ArrayBuffer) => ArrayBuffer)?} - modify response data + * @param modifyImage {string?} + * A js script string. + * The script will be executed in a new Isolate. + * A function named `modifyImage` should be defined in the script, which receives an [Image] as the only argument, and returns an [Image].. + * @param onLoadFailed {(() => ImageLoadingConfig)?} - called when the image loading failed + * @constructor + * @since 1.0.5 + * + * To keep the compatibility with the old version, do not use the constructor. Consider creating a new object with the properties directly. + */ +function ImageLoadingConfig({url, method, data, headers, onResponse, modifyImage, onLoadFailed}) { + this.url = url; + this.method = method; + this.data = data; + this.headers = headers; + this.onResponse = onResponse; + this.modifyImage = modifyImage; + this.onLoadFailed = onLoadFailed; +} + class ComicSource { name = "" From e1df69e785709f2b66576a79ba0841dae9c87f2d Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 17:46:11 +0800 Subject: [PATCH 09/37] [import data] proxy settings should be kept --- lib/utils/data.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 0865244..5895a2b 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -65,9 +65,13 @@ Future importAppData(File file, [bool checkVersion = false]) async { LocalFavoritesManager().init(); } if(await appdataFile.exists()) { + // proxy settings should be kept + var proxySettings = appdata.settings["proxy"]; File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync(); appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json")); - appdata.init(); + await appdata.init(); + appdata.settings["proxy"] = proxySettings; + appdata.saveData(); } var comicSourceDir = FilePath.join(cacheDirPath, "comic_source"); if(Directory(comicSourceDir).existsSync()) { From 05bbef0b8aef606bd50a0b783ea8b6db90dcd65a Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 11 Nov 2024 18:43:32 +0800 Subject: [PATCH 10/37] fix #30 --- lib/components/window_frame.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/components/window_frame.dart b/lib/components/window_frame.dart index fda1520..9caefd5 100644 --- a/lib/components/window_frame.dart +++ b/lib/components/window_frame.dart @@ -485,8 +485,15 @@ class WindowPlacement { } } + static Rect? lastValidRect; + static Future get current async { var rect = await windowManager.getBounds(); + if(validate(rect)) { + lastValidRect = rect; + } else { + rect = lastValidRect ?? defaultPlacement.rect; + } var isMaximized = await windowManager.isMaximized(); return WindowPlacement(rect, isMaximized); } @@ -501,9 +508,6 @@ class WindowPlacement { static void loop() async { timer ??= Timer.periodic(const Duration(milliseconds: 100), (timer) async { var placement = await WindowPlacement.current; - if (!validate(placement.rect)) { - return; - } if (placement.rect != cache.rect || placement.isMaximized != cache.isMaximized) { cache = placement; From bc4e0f79a50fe3f26ede9ad5b72a1110a8b1fc03 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:27:40 +0800 Subject: [PATCH 11/37] Added clock & battery widgets in reader. --- lib/pages/reader/reader.dart | 1 + lib/pages/reader/scaffold.dart | 201 ++++++++++++++++++++++++++++++++- pubspec.lock | 32 ++++++ pubspec.yaml | 1 + 4 files changed, 234 insertions(+), 1 deletion(-) diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 6dea93f..3cc1838 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -26,6 +26,7 @@ import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; import 'package:venera/utils/volume.dart'; import 'package:window_manager/window_manager.dart'; +import 'package:battery_plus/battery_plus.dart'; part 'scaffold.dart'; part 'images.dart'; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 36ecea3..a0bf05c 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -131,10 +131,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { child: widget.child, ), buildPageInfoText(), + buildStatusInfo(), AnimatedPositioned( duration: const Duration(milliseconds: 180), right: 16, - bottom: showFloatingButtonValue == 0 ? -58 : 16, + bottom: showFloatingButtonValue == 0 ? -58 : 36, child: buildEpChangeButton(), ), AnimatedPositioned( @@ -424,6 +425,20 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); } + Widget buildStatusInfo() { + return Positioned( + bottom: 13, + right: 25, + child: Row( + children: [ + _ClockWidget(), + const SizedBox(width: 10), + _BatteryWidget(), + ], + ), + ); + } + void openChapterDrawer() { showSideBar( context, @@ -569,6 +584,190 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } } +class _BatteryWidget extends StatefulWidget { + @override + _BatteryWidgetState createState() => _BatteryWidgetState(); +} + +class _BatteryWidgetState extends State<_BatteryWidget> { + late Battery _battery; + late int _batteryLevel; + Timer? _timer; + bool _hasBattery = false; + + @override + void initState() { + super.initState(); + _battery = Battery(); + _checkBatteryAvailability(); + if(_hasBattery) { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { + final batteryLevel = await _battery.batteryLevel; + if(_batteryLevel != batteryLevel) { + setState(() { + _batteryLevel = batteryLevel; + }); + } + }); + } else { + _timer = null; + } + } + + void _checkBatteryAvailability() async { + try { + _batteryLevel = await _battery.batteryLevel; + if (_batteryLevel != -1) { + setState(() { + _hasBattery = true; + }); + } else { + setState(() { + _hasBattery = false; + }); + } + } catch (e) { + setState(() { + _hasBattery = false; + }); + } + } + + @override + Widget build(BuildContext context) { + if (!_hasBattery) { + return const SizedBox.shrink(); //Empty Widget + } + + return _batteryInfo(_batteryLevel); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + Widget _batteryInfo(int batteryLevel) { + IconData batteryIcon; + Color batteryColor = Colors.black; + + if (batteryLevel >= 96) { + batteryIcon = Icons.battery_full_sharp; + } else if (batteryLevel >= 84) { + batteryIcon = Icons.battery_6_bar_sharp; + } else if (batteryLevel >= 72) { + batteryIcon = Icons.battery_5_bar_sharp; + } else if (batteryLevel >= 60) { + batteryIcon = Icons.battery_4_bar_sharp; + } else if (batteryLevel >= 48) { + batteryIcon = Icons.battery_3_bar_sharp; + } else if (batteryLevel >= 36) { + batteryIcon = Icons.battery_2_bar_sharp; + } else if (batteryLevel >= 24) { + batteryIcon = Icons.battery_1_bar_sharp; + } else if (batteryLevel >= 12) { + batteryIcon = Icons.battery_0_bar_sharp; + } else { + batteryIcon = Icons.battery_alert_sharp; + batteryColor = Colors.red; + } + + return Row( + children: [ + Icon( + batteryIcon, + size: 16, + color: batteryColor, + // Stroke + shadows: List.generate(9, + (index) { + if(index == 4) { + return null; + } + double offsetX = (index % 3 - 1) * 0.8; + double offsetY = ((index / 3).floor() - 1) * 0.8; + return Shadow( + color: context.colorScheme.onInverseSurface, + offset: Offset(offsetX, offsetY), + ); + }, + ).whereType().toList(), + ), + Stack( + children: [ + Text( + '$batteryLevel%', + style: TextStyle( + fontSize: 14, + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.4 + ..color = context.colorScheme.onInverseSurface, + ), + ), + Text('$batteryLevel%'), + ], + ), + ], + ); + } +} + +class _ClockWidget extends StatefulWidget { + @override + _ClockWidgetState createState() => _ClockWidgetState(); +} + +class _ClockWidgetState extends State<_ClockWidget> { + late String _currentTime; + late Timer _timer; + + @override + void initState() { + super.initState(); + _currentTime = _getCurrentTime(); + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + final time = _getCurrentTime(); + if(_currentTime != time) { + setState(() { + _currentTime = time; + }); + } + }); + } + + String _getCurrentTime() { + final now = DateTime.now(); + return "${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}"; + } + + @override + void dispose() { + _timer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + Text( + _currentTime, + style: TextStyle( + fontSize: 14, + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.4 + ..color = context.colorScheme.onInverseSurface, + ), + ), + Text(_currentTime), + ], + ); + } +} + class _ChaptersView extends StatefulWidget { const _ChaptersView(this.reader); diff --git a/pubspec.lock b/pubspec.lock index c1c24c8..87a2ebf 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: "220c8f1961efb01d6870493b5ac5a80afaeaffc8757f7a11ed3025a8570d29e7" + url: "https://pub.dev" + source: hosted + version: "6.2.0" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 + url: "https://pub.dev" + source: hosted + version: "2.0.1" boolean_selector: dependency: transitive description: @@ -121,6 +137,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" desktop_webview_window: dependency: "direct main" description: @@ -800,6 +824,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.dev" + source: hosted + version: "0.7.0" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index aad2ff3..79e4d25 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: git: url: https://github.com/wgh136/webdav_client ref: 285f87f15bccd2d5d5ff443761348c6ee47b98d1 + battery_plus: ^6.2.0 dev_dependencies: flutter_test: From 53b9bc79ddc6b2821b8bea8a276d44eebb351376 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:58:44 +0800 Subject: [PATCH 12/37] Fix battery update issue. --- lib/pages/reader/scaffold.dart | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index a0bf05c..110b5fd 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -600,18 +600,6 @@ class _BatteryWidgetState extends State<_BatteryWidget> { super.initState(); _battery = Battery(); _checkBatteryAvailability(); - if(_hasBattery) { - _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { - final batteryLevel = await _battery.batteryLevel; - if(_batteryLevel != batteryLevel) { - setState(() { - _batteryLevel = batteryLevel; - }); - } - }); - } else { - _timer = null; - } } void _checkBatteryAvailability() async { @@ -620,6 +608,15 @@ class _BatteryWidgetState extends State<_BatteryWidget> { if (_batteryLevel != -1) { setState(() { _hasBattery = true; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + _battery.batteryLevel.then((level) =>{ + if(_batteryLevel != level) { + setState(() { + _batteryLevel = level; + }) + } + }); + }); }); } else { setState(() { @@ -639,7 +636,17 @@ class _BatteryWidgetState extends State<_BatteryWidget> { return const SizedBox.shrink(); //Empty Widget } - return _batteryInfo(_batteryLevel); + return FutureBuilder( + future: _battery.batteryLevel, + builder: (context, snapshot) { + if(snapshot.connectionState != ConnectionState.waiting + && !snapshot.hasError + && snapshot.data != -1) { + int batteryLevel = snapshot.data!; + return _batteryInfo(batteryLevel); + } + return const SizedBox.shrink(); + }); } @override From 189dfe5a43ba055109898f3fef979e574d068018 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Mon, 11 Nov 2024 21:58:44 +0800 Subject: [PATCH 13/37] Fix battery update issue. --- lib/pages/reader/scaffold.dart | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index a0bf05c..110b5fd 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -600,18 +600,6 @@ class _BatteryWidgetState extends State<_BatteryWidget> { super.initState(); _battery = Battery(); _checkBatteryAvailability(); - if(_hasBattery) { - _timer = Timer.periodic(const Duration(seconds: 1), (timer) async { - final batteryLevel = await _battery.batteryLevel; - if(_batteryLevel != batteryLevel) { - setState(() { - _batteryLevel = batteryLevel; - }); - } - }); - } else { - _timer = null; - } } void _checkBatteryAvailability() async { @@ -620,6 +608,15 @@ class _BatteryWidgetState extends State<_BatteryWidget> { if (_batteryLevel != -1) { setState(() { _hasBattery = true; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + _battery.batteryLevel.then((level) =>{ + if(_batteryLevel != level) { + setState(() { + _batteryLevel = level; + }) + } + }); + }); }); } else { setState(() { @@ -639,7 +636,17 @@ class _BatteryWidgetState extends State<_BatteryWidget> { return const SizedBox.shrink(); //Empty Widget } - return _batteryInfo(_batteryLevel); + return FutureBuilder( + future: _battery.batteryLevel, + builder: (context, snapshot) { + if(snapshot.connectionState != ConnectionState.waiting + && !snapshot.hasError + && snapshot.data != -1) { + int batteryLevel = snapshot.data!; + return _batteryInfo(batteryLevel); + } + return const SizedBox.shrink(); + }); } @override From b3e95d7162af98353576b0620921435fcc44ae8a Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:13:03 +0800 Subject: [PATCH 14/37] Fix widget blinking caused by future builder. --- lib/pages/reader/scaffold.dart | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 110b5fd..89e0944 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -591,7 +591,7 @@ class _BatteryWidget extends StatefulWidget { class _BatteryWidgetState extends State<_BatteryWidget> { late Battery _battery; - late int _batteryLevel; + late int _batteryLevel = 100; Timer? _timer; bool _hasBattery = false; @@ -635,18 +635,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> { if (!_hasBattery) { return const SizedBox.shrink(); //Empty Widget } - - return FutureBuilder( - future: _battery.batteryLevel, - builder: (context, snapshot) { - if(snapshot.connectionState != ConnectionState.waiting - && !snapshot.hasError - && snapshot.data != -1) { - int batteryLevel = snapshot.data!; - return _batteryInfo(batteryLevel); - } - return const SizedBox.shrink(); - }); + return _batteryInfo(_batteryLevel); } @override From 93bf99daa57f8c9115b963e52ec39f9ee0efeccb Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Mon, 11 Nov 2024 22:40:46 +0800 Subject: [PATCH 15/37] Add option to hide time and battery info. --- assets/translation.json | 6 ++++-- lib/foundation/appdata.dart | 1 + lib/pages/reader/scaffold.dart | 26 +++++++++++++++----------- lib/pages/settings/reader.dart | 7 +++++++ 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index f213c0a..73539a0 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -189,7 +189,8 @@ "Quick Favorite": "快速收藏", "Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹", "Added": "已添加", - "Turn page by volume keys": "使用音量键翻页" + "Turn page by volume keys": "使用音量键翻页", + "Display time & battery info in reader":"在阅读器中显示时间和电量信息" }, "zh_TW": { "Home": "首頁", @@ -381,6 +382,7 @@ "Quick Favorite": "快速收藏", "Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾", "Added": "已添加", - "Turn page by volume keys": "使用音量鍵翻頁" + "Turn page by volume keys": "使用音量鍵翻頁", + "Display time & battery info in reader":"在閱讀器中顯示時間和電量信息" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index dc93fb8..afd1522 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -118,6 +118,7 @@ class _Settings with ChangeNotifier { 'dataVersion': 0, 'quickFavorite': null, 'enableTurnPageByVolumeKey': true, + 'enableClockAndBatteryInfoInReader': true, }; operator [](String key) { diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 89e0944..265fc11 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -426,17 +426,21 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } Widget buildStatusInfo() { - return Positioned( - bottom: 13, - right: 25, - child: Row( - children: [ - _ClockWidget(), - const SizedBox(width: 10), - _BatteryWidget(), - ], - ), - ); + if(appdata.settings['enableClockAndBatteryInfoInReader']) { + return Positioned( + bottom: 13, + right: 25, + child: Row( + children: [ + _ClockWidget(), + const SizedBox(width: 10), + _BatteryWidget(), + ], + ), + ); + } else { + return const SizedBox.shrink(); + } } void openChapterDrawer() { diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 3635800..e982dc3 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -77,6 +77,13 @@ class _ReaderSettingsState extends State { widget.onChanged?.call('enableTurnPageByVolumeKey'); }, ).toSliver(), + _SwitchSetting( + title: "Display time & battery info in reader".tl, + settingKey: "enableClockAndBatteryInfoInReader", + onChanged: () { + widget.onChanged?.call("enableClockAndBatteryInfoInReader"); + }, + ).toSliver(), ], ); } From c4f531a4638227f5705652a1492b5da3056cd917 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 12 Nov 2024 16:36:02 +0800 Subject: [PATCH 16/37] Exported data should contain cookies --- lib/foundation/comic_source/comic_source.dart | 2 + lib/utils/data.dart | 38 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/lib/foundation/comic_source/comic_source.dart b/lib/foundation/comic_source/comic_source.dart index f4c64df..758d8f3 100644 --- a/lib/foundation/comic_source/comic_source.dart +++ b/lib/foundation/comic_source/comic_source.dart @@ -10,6 +10,7 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/res.dart'; +import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; @@ -236,6 +237,7 @@ class ComicSource { } await file.writeAsString(jsonEncode(data)); _isSaving = false; + DataSync().uploadData(); } Future reLogin() async { diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 5895a2b..9a13426 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -6,6 +6,7 @@ import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; +import 'package:venera/network/cookie_jar.dart'; import 'package:zip_flutter/zip_flutter.dart'; import 'io.dart'; @@ -15,7 +16,7 @@ Future exportAppData() async { var cacheFilePath = FilePath.join(App.cachePath, '$time.venera'); var cacheFile = File(cacheFilePath); var dataPath = App.dataPath; - if(await cacheFile.exists()) { + if (await cacheFile.exists()) { await cacheFile.delete(); } await Isolate.run(() { @@ -23,11 +24,14 @@ Future exportAppData() async { var historyFile = FilePath.join(dataPath, "history.db"); var localFavoriteFile = FilePath.join(dataPath, "local_favorite.db"); var appdata = FilePath.join(dataPath, "appdata.json"); + var cookies = FilePath.join(dataPath, "cookie.db"); zipFile.addFile("history.db", historyFile); zipFile.addFile("local_favorite.db", localFavoriteFile); zipFile.addFile("appdata.json", appdata); - for(var file in Directory(FilePath.join(dataPath, "comic_source")).listSync()) { - if(file is File) { + zipFile.addFile("cookie.db", cookies); + for (var file + in Directory(FilePath.join(dataPath, "comic_source")).listSync()) { + if (file is File) { zipFile.addFile("comic_source/${file.name}", file.path); } } @@ -45,26 +49,28 @@ Future importAppData(File file, [bool checkVersion = false]) async { var historyFile = cacheDir.joinFile("history.db"); var localFavoriteFile = cacheDir.joinFile("local_favorite.db"); var appdataFile = cacheDir.joinFile("appdata.json"); - if(checkVersion && appdataFile.existsSync()) { + var cookieFile = cacheDir.joinFile("cookie.db"); + if (checkVersion && appdataFile.existsSync()) { var data = jsonDecode(await appdataFile.readAsString()); var version = data["settings"]["dataVersion"]; - if(version is int && version <= appdata.settings["dataVersion"]) { + if (version is int && version <= appdata.settings["dataVersion"]) { return; } } - if(await historyFile.exists()) { + if (await historyFile.exists()) { HistoryManager().close(); File(FilePath.join(App.dataPath, "history.db")).deleteIfExistsSync(); historyFile.renameSync(FilePath.join(App.dataPath, "history.db")); HistoryManager().init(); } - if(await localFavoriteFile.exists()) { + if (await localFavoriteFile.exists()) { LocalFavoritesManager().close(); File(FilePath.join(App.dataPath, "local_favorite.db")).deleteIfExistsSync(); - localFavoriteFile.renameSync(FilePath.join(App.dataPath, "local_favorite.db")); + localFavoriteFile + .renameSync(FilePath.join(App.dataPath, "local_favorite.db")); LocalFavoritesManager().init(); } - if(await appdataFile.exists()) { + if (await appdataFile.exists()) { // proxy settings should be kept var proxySettings = appdata.settings["proxy"]; File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync(); @@ -73,10 +79,18 @@ Future importAppData(File file, [bool checkVersion = false]) async { appdata.settings["proxy"] = proxySettings; appdata.saveData(); } + if (await cookieFile.exists()) { + SingleInstanceCookieJar.instance?.dispose(); + File(FilePath.join(App.dataPath, "cookie.db")).deleteIfExistsSync(); + cookieFile.renameSync(FilePath.join(App.dataPath, "cookie.db")); + SingleInstanceCookieJar.instance = + SingleInstanceCookieJar(FilePath.join(App.dataPath, "cookie.db")) + ..init(); + } var comicSourceDir = FilePath.join(cacheDirPath, "comic_source"); - if(Directory(comicSourceDir).existsSync()) { - for(var file in Directory(comicSourceDir).listSync()) { - if(file is File) { + if (Directory(comicSourceDir).existsSync()) { + for (var file in Directory(comicSourceDir).listSync()) { + if (file is File) { var targetFile = FilePath.join(App.dataPath, "comic_source", file.name); File(targetFile).deleteIfExistsSync(); await file.copy(targetFile); From a427bcdf84b0e3a42331da9618212478885178bd Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 12 Nov 2024 17:37:29 +0800 Subject: [PATCH 17/37] fix search action --- lib/components/navigation_bar.dart | 6 +++--- lib/pages/main_page.dart | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/components/navigation_bar.dart b/lib/components/navigation_bar.dart index 5620505..73e89ec 100644 --- a/lib/components/navigation_bar.dart +++ b/lib/components/navigation_bar.dart @@ -27,7 +27,7 @@ class NaviPane extends StatefulWidget { required this.paneActions, required this.pageBuilder, this.initialPage = 0, - this.onPageChange, + this.onPageChanged, required this.observer, required this.navigatorKey, super.key}); @@ -38,7 +38,7 @@ class NaviPane extends StatefulWidget { final Widget Function(int page) pageBuilder; - final void Function(int index)? onPageChange; + final void Function(int index)? onPageChanged; final int initialPage; @@ -59,7 +59,7 @@ class _NaviPaneState extends State set currentPage(int value) { if (value == _currentPage) return; _currentPage = value; - widget.onPageChange?.call(value); + widget.onPageChanged?.call(value); } void Function()? mainViewUpdateHandler; diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index e074cad..3cf50ea 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -78,6 +78,11 @@ class _MainPageState extends State { activeIcon: Icons.category, ), ], + onPageChanged: (i) { + setState(() { + index = i; + }); + }, paneActions: [ if(index != 0) PaneActionEntry( From 293040f374cccb1d4d573683f66b489c88010793 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 12 Nov 2024 17:43:37 +0800 Subject: [PATCH 18/37] fix subtitle --- lib/foundation/comic_source/models.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 59b7fd7..65384b3 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -92,7 +92,7 @@ class Comic { Comic.fromJson(Map json, this.sourceKey) : title = json["title"], - subtitle = json["subTitle"] ?? "", + subtitle = json["subtitle"] ?? "", cover = json["cover"], id = json["id"], tags = List.from(json["tags"] ?? []), From 775ab471f5c003929a53b3035ba0e97e09bf9856 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 12 Nov 2024 17:49:02 +0800 Subject: [PATCH 19/37] fix subtitle --- assets/init.js | 4 +++- lib/foundation/comic_source/models.dart | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/init.js b/assets/init.js index 6d08177..03ee00a 100644 --- a/assets/init.js +++ b/assets/init.js @@ -850,6 +850,7 @@ let console = { * @param id {string} * @param title {string} * @param subtitle {string} + * @param subTitle {string} - equal to subtitle * @param cover {string} * @param tags {string[]} * @param description {string} @@ -859,10 +860,11 @@ let console = { * @param stars {number?} - 0-5, double * @constructor */ -function Comic({id, title, subtitle, cover, tags, description, maxPage, language, favoriteId, stars}) { +function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage, language, favoriteId, stars}) { this.id = id; this.title = title; this.subtitle = subtitle; + this.subTitle = subTitle; this.cover = cover; this.tags = tags; this.description = description; diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 65384b3..042682b 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -92,7 +92,7 @@ class Comic { Comic.fromJson(Map json, this.sourceKey) : title = json["title"], - subtitle = json["subtitle"] ?? "", + subtitle = json["subtitle"] ?? json["subTitle"] ?? "", cover = json["cover"], id = json["id"], tags = List.from(json["tags"] ?? []), From 9b9807515302c7206509191ff6fed9b9343159fb Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 12 Nov 2024 17:51:20 +0800 Subject: [PATCH 20/37] fix multiple setting pages and search pages --- lib/foundation/favorites.dart | 7 +++++++ lib/pages/main_page.dart | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 4ed1cd1..a2aff14 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -167,6 +167,13 @@ class LocalFavoritesManager with ChangeNotifier { order_value int ); """); + _db.execute(""" + create table if not exists folder_sync ( + folder_name text primary key, + source_key text, + source_folder text + ); + """); } List find(String id, ComicType type) { diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 3cf50ea..4610da6 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -89,14 +89,14 @@ class _MainPageState extends State { icon: Icons.search, label: "Search".tl, onTap: () { - to(() => const SearchPage()); + to(() => const SearchPage(), preventDuplicate: true); }, ), PaneActionEntry( icon: Icons.settings, label: "Settings".tl, onTap: () { - to(() => const SettingsPage()); + to(() => const SettingsPage(), preventDuplicate: true); }, ) ], From 5119beb1feb272983de3d78f040fbc8a9e5d450c Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:44:05 +0800 Subject: [PATCH 21/37] Fix battery forground color. --- lib/pages/reader/scaffold.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 265fc11..514351f 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -650,7 +650,7 @@ class _BatteryWidgetState extends State<_BatteryWidget> { Widget _batteryInfo(int batteryLevel) { IconData batteryIcon; - Color batteryColor = Colors.black; + Color batteryColor = context.colorScheme.onSurface; if (batteryLevel >= 96) { batteryIcon = Icons.battery_full_sharp; From abd9afad6badf49ddf52bc9f7a3815aa5f9d401b Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:45:27 +0800 Subject: [PATCH 22/37] Fix local comic cover display logic. --- lib/pages/favorites/local_favorites_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 62230c8..7d87726 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -207,7 +207,7 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> { e.author, e.tags, "${e.time} | ${comicSource?.name ?? "Unknown"}", - comicSource?.key ?? "Unknown", + comicSource?.key ?? (e.type == ComicType.local ? "local" : "Unknown"), null, null, ), From 389403c11dffd94af8fe9a4dbf8ac7b57b436d57 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:48:15 +0800 Subject: [PATCH 23/37] Ignore files starting with a dot when fetching local comic images, and improve local comic delete logic. --- lib/foundation/local.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 8876b84..cc2fde8 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/download.dart'; import 'package:venera/pages/reader/reader.dart'; @@ -346,6 +347,10 @@ class LocalManager with ChangeNotifier { comic.cover) { continue; } + //Hidden file in some file system + if(entity.name.startsWith('.')) { + continue; + } files.add(entity); } } @@ -439,9 +444,20 @@ class LocalManager with ChangeNotifier { downloadingTasks.first.resume(); } - void deleteComic(LocalComic c) { - var dir = Directory(FilePath.join(path, c.directory)); - dir.deleteIgnoreError(recursive: true); + void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) { + if(removeFileOnDisk) { + var dir = Directory(FilePath.join(path, c.directory)); + dir.deleteIgnoreError(recursive: true); + } + //Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted. + if(HistoryManager().findSync(c.id, c.comicType) != null) { + HistoryManager().remove(c.id, c.comicType); + } + assert(c.comicType == ComicType.local); + var folders = LocalFavoritesManager().find(c.id, c.comicType); + for (var f in folders) { + LocalFavoritesManager().deleteComicWithId(f, c.id, c.comicType); + } remove(c.id, c.comicType); notifyListeners(); } From 5825f88e78303debe1e041c67d275bf463fcc028 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:50:53 +0800 Subject: [PATCH 24/37] Allow custom creation time of favorite items, add LocalFavoritesManager.existsFolder function. --- lib/foundation/favorites.dart | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index a2aff14..3482601 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -11,8 +11,8 @@ import 'app.dart'; import 'comic_source/comic_source.dart'; import 'comic_type.dart'; -String _getCurTime() { - return DateTime.now() +String _getTimeString(DateTime time) { + return time .toIso8601String() .replaceFirst("T", " ") .substring(0, 19); @@ -27,7 +27,7 @@ class FavoriteItem implements Comic { @override String id; String coverPath; - String time = _getCurTime(); + late String time; FavoriteItem({ required this.id, @@ -36,7 +36,11 @@ class FavoriteItem implements Comic { required this.author, required this.type, required this.tags, - }); + DateTime? favoriteTime + }) { + var t = favoriteTime ?? DateTime.now(); + time = _getTimeString(t); + } FavoriteItem.fromRow(Row row) : name = row["name"], @@ -296,12 +300,16 @@ class LocalFavoritesManager with ChangeNotifier { return res; } + bool existsFolder(String name) { + return folderNames.contains(name); + } + /// create a folder String createFolder(String name, [bool renameWhenInvalidName = false]) { if (name.isEmpty) { if (renameWhenInvalidName) { int i = 0; - while (folderNames.contains(i.toString())) { + while (existsFolder(i.toString())) { i++; } name = i.toString(); @@ -309,11 +317,11 @@ class LocalFavoritesManager with ChangeNotifier { throw "name is empty!"; } } - if (folderNames.contains(name)) { + if (existsFolder(name)) { if (renameWhenInvalidName) { var prevName = name; int i = 0; - while (folderNames.contains(i.toString())) { + while (existsFolder(i.toString())) { i++; } name = prevName + i.toString(); @@ -362,7 +370,7 @@ class LocalFavoritesManager with ChangeNotifier { /// This method will download cover to local, to avoid problems like changing url void addComic(String folder, FavoriteItem comic, [int? order]) async { _modifiedAfterLastCache = true; - if (!folderNames.contains(folder)) { + if (!existsFolder(folder)) { throw Exception("Folder does not exists"); } var res = _db.select(""" @@ -431,7 +439,7 @@ class LocalFavoritesManager with ChangeNotifier { } void reorder(List newFolder, String folder) async { - if (!folderNames.contains(folder)) { + if (!existsFolder(folder)) { throw Exception("Failed to reorder: folder not found"); } deleteFolder(folder); @@ -443,7 +451,7 @@ class LocalFavoritesManager with ChangeNotifier { } void rename(String before, String after) { - if (folderNames.contains(after)) { + if (existsFolder(after)) { throw "Name already exists!"; } if (after.contains('"')) { @@ -598,9 +606,9 @@ class LocalFavoritesManager with ChangeNotifier { if (folder == null || folder is! String) { throw "Invalid data"; } - if (folderNames.contains(folder)) { + if (existsFolder(folder)) { int i = 0; - while (folderNames.contains("$folder($i)")) { + while (existsFolder("$folder($i)")) { i++; } folder = "$folder($i)"; From c94438d7c4405fd2901cc81d0bfc4550a5afb82a Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:52:34 +0800 Subject: [PATCH 25/37] Add EhViewer database import support. --- lib/pages/home_page.dart | 175 ++++++++++++++++++++++++++++++++------- 1 file changed, 144 insertions(+), 31 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index dc22b35..427c709 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -22,6 +22,8 @@ import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; +import 'package:sqlite3/sqlite3.dart' as sql; +import 'dart:math'; import 'local_comics_page.dart'; @@ -495,7 +497,14 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { "Select a directory which contains the comic files.".tl, "Select a directory which contains the comic directories.".tl, "Select a cbz file.".tl, + "Select an EhViewer database and a download folder.".tl ][type]; + List importMethods = [ + "Single Comic".tl, + "Multiple Comics".tl, + "A cbz file".tl, + "EhViewer downloads".tl + ]; return ContentDialog( dismissible: !loading, @@ -513,36 +522,18 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(width: 600), - RadioListTile( - title: Text("Single Comic".tl), - value: 0, - groupValue: type, - onChanged: (value) { - setState(() { - type = value as int; - }); - }, - ), - RadioListTile( - title: Text("Multiple Comics".tl), - value: 1, - groupValue: type, - onChanged: (value) { - setState(() { - type = value as int; - }); - }, - ), - RadioListTile( - title: Text("A cbz file".tl), - value: 2, - groupValue: type, - onChanged: (value) { - setState(() { - type = value as int; - }); - }, - ), + ...List.generate(importMethods.length, (index) { + return RadioListTile( + title: Text(importMethods[index]), + value: index, + groupValue: type, + onChanged: (value) { + setState(() { + type = value as int; + }); + }, + ); + }), ListTile( title: Text("Add to favorites".tl), trailing: Select( @@ -587,8 +578,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { help += '${"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used.".tl}\n\n'; help += - "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." + "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" .tl; + help +="If you import an EhViewer's database, program will automatically create folders according to the download label in that database.".tl; return ContentDialog( title: "Help".tl, content: Text(help).paddingHorizontal(16), @@ -641,6 +633,127 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { } controller.close(); return; + } else if (type == 3) { + var dbFile = await selectFile(ext: ['db']); + final picker = DirectoryPicker(); + final comicSrc = await picker.pickDirectory(); + if (dbFile == null || comicSrc == null) { + return; + } + var controller = showLoadingDialog(context, allowCancel: false); + + try { + var cache = FilePath.join(App.cachePath, dbFile.name); + await dbFile.saveTo(cache); + var db = sql.sqlite3.open(cache); + + Future addTagComics(String destFolder, List comics) async { + for(var comic in comics) { + var comicDir = Directory(FilePath.join(comicSrc.path, comic['DIRNAME'] as String)); + if(!(await comicDir.exists())) { + continue; + } + String titleJP = comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String; + String title = titleJP == "" ? comic['TITLE'] as String : titleJP; + if (LocalManager().findByName(title) != null) { + Log.info("Import Comic", "Comic already exists: $title"); + continue; + } + + String coverURL = await comicDir.joinFile(".thumb").exists() ? + comicDir.joinFile(".thumb").path : + (comic['THUMB'] as String).replaceAll('s.exhentai.org', 'ehgt.org'); + int downloadedTimeStamp = comic['TIME'] as int; + DateTime downloadedTime = + downloadedTimeStamp != 0 ? + DateTime.fromMillisecondsSinceEpoch(downloadedTimeStamp) : DateTime.now(); + var comicObj = LocalComic( + id: LocalManager().findValidId(ComicType.local), + title: title, + subtitle: '', + tags: [ + //1 >> x + [ + "MISC", + "DOUJINSHI", + "MANGA", + "ARTISTCG", + "GAMECG", + "IMAGE SET", + "COSPLAY", + "ASIAN PORN", + "NON-H", + "WESTERN", + ][(log(comic['CATEGORY'] as int) / ln2).floor()] + ], + directory: comicDir.path, + chapters: null, + cover: coverURL, + comicType: ComicType.local, + downloadedChapters: [], + createdAt: downloadedTime, + ); + LocalManager().add(comicObj, comicObj.id); + LocalFavoritesManager().addComic( + destFolder, + FavoriteItem( + id: comicObj.id, + name: comicObj.title, + coverPath: comicObj.cover, + author: comicObj.subtitle, + type: comicObj.comicType, + tags: comicObj.tags, + favoriteTime: downloadedTime + ), + ); + } + } + + //default folder + { + var defaultFolderName = '(EhViewer)Default'.tl; + if(!LocalFavoritesManager().existsFolder(defaultFolderName)) { + LocalFavoritesManager().createFolder(defaultFolderName); + } + var comicList = db.select(""" + SELECT * + FROM DOWNLOAD_DIRNAME DN + LEFT JOIN DOWNLOADS DL + ON DL.GID = DN.GID + WHERE DL.LABEL IS NULL AND DL.STATE = 3 + ORDER BY DL.TIME DESC + """).toList(); + await addTagComics(defaultFolderName, comicList); + } + + var folders = db.select(""" + SELECT * FROM DOWNLOAD_LABELS; + """); + + for (var folder in folders) { + var label = folder["LABEL"] as String; + var folderName = '(EhViewer)$label'; + if(!LocalFavoritesManager().existsFolder(folderName)) { + LocalFavoritesManager().createFolder(folderName); + } + var comicList = db.select(""" + SELECT * + FROM DOWNLOAD_DIRNAME DN + LEFT JOIN DOWNLOADS DL + ON DL.GID = DN.GID + WHERE DL.LABEL = ? AND DL.STATE = 3 + ORDER BY DL.TIME DESC + """, [label]).toList(); + await addTagComics(folderName, comicList); + } + db.dispose(); + await File(cache).deleteIgnoreError(); + } catch (e, s) { + Log.error("Import Comic", e.toString(), s); + context.showMessage(message: e.toString()); + } + controller.close(); + return; } height = key.currentContext!.size!.height; setState(() { From 601ef68ad36fea03297392c78031004495646ca6 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:54:34 +0800 Subject: [PATCH 26/37] Improve local comics selection logic. --- lib/components/comic.dart | 43 ++++++++-- lib/pages/local_comics_page.dart | 132 ++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 26 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 525bcaf..430d96f 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -581,10 +581,13 @@ class SliverGridComics extends StatefulWidget { this.badgeBuilder, this.menuBuilder, this.onTap, + this.selections }); final List comics; + final Map? selections; + final void Function()? onLastItemBuild; final String? Function(Comic)? badgeBuilder; @@ -638,6 +641,7 @@ class _SliverGridComicsState extends State { Widget build(BuildContext context) { return _SliverGridComics( comics: comics, + selection: widget.selections, onLastItemBuild: widget.onLastItemBuild, badgeBuilder: widget.badgeBuilder, menuBuilder: widget.menuBuilder, @@ -653,10 +657,13 @@ class _SliverGridComics extends StatelessWidget { this.badgeBuilder, this.menuBuilder, this.onTap, + this.selection, }); final List comics; + final Map? selection; + final void Function()? onLastItemBuild; final String? Function(Comic)? badgeBuilder; @@ -674,11 +681,37 @@ class _SliverGridComics extends StatelessWidget { onLastItemBuild?.call(); } var badge = badgeBuilder?.call(comics[index]); - return ComicTile( - comic: comics[index], - badge: badge, - menuOptions: menuBuilder?.call(comics[index]), - onTap: onTap != null ? () => onTap!(comics[index]) : null, + return Stack( + children: [ + ComicTile( + comic: comics[index], + badge: badge, + menuOptions: menuBuilder?.call(comics[index]), + onTap: onTap != null ? () => onTap!(comics[index]) : null, + ), + Positioned( + bottom: 10, + right: 8, + child: Visibility( + visible: selection == null ? false : selection![comics[index]] ?? false, + child: Stack( + children: [ + Transform.scale( + scale: 0.9, + child: const Icon( + Icons.circle_rounded, + color: Colors.white, + ) + ), + const Icon( + Icons.check_circle_rounded, + color: Colors.green, + ) + ], + ) + ) + ) + ], ); }, childCount: comics.length, diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 29e4d38..151f0db 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/utils/cbz.dart'; @@ -26,7 +27,7 @@ class _LocalComicsPageState extends State { bool multiSelectMode = false; - Map selectedComics = {}; + Map selectedComics = {}; void update() { if (keyword.isEmpty) { @@ -166,10 +167,66 @@ class _LocalComicsPageState extends State { ) else if (multiSelectMode) SliverAppbar( - title: Text("Selected ${selectedComics.length} comics"), + title: Text("Selected @c comics".tlParams({"c": selectedComics.length})), actions: [ + IconButton( + icon: const Icon(Icons.check_box_rounded), + tooltip: "Select All".tl, + onPressed: () { + setState(() { + selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + }); + }, + ), + IconButton( + icon: const Icon(Icons.check_box_outline_blank_outlined), + tooltip: "Deselect".tl, + onPressed: () { + setState(() { + selectedComics.clear(); + }); + }, + ), + IconButton( + icon: const Icon(Icons.check_box_outlined), + tooltip: "Invert Selection".tl, + onPressed: () { + setState(() { + comics.asMap().forEach((k, v) { + selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + }); + selectedComics.removeWhere((k, v) => !v); + }); + }, + ), + + IconButton( + icon: const Icon(Icons.indeterminate_check_box_rounded), + tooltip: "Select in range".tl, + onPressed: () { + setState(() { + List l = []; + selectedComics.forEach((k, v) { + l.add(comics.indexOf(k as LocalComic)); + }); + if(l.isEmpty) { + return; + } + l.sort(); + int start = l.first; + int end = l.last; + selectedComics.clear(); + selectedComics.addEntries( + List.generate(end - start + 1, (i) { + return MapEntry(comics[start + i], true); + }) + ); + }); + }, + ), IconButton( icon: const Icon(Icons.close), + tooltip: "Exit Multi-Select".tl, onPressed: () { setState(() { multiSelectMode = false; @@ -177,6 +234,7 @@ class _LocalComicsPageState extends State { }); }, ), + ], ) else if (searchMode) @@ -207,13 +265,14 @@ class _LocalComicsPageState extends State { ), SliverGridComics( comics: comics, + selections: selectedComics, onTap: multiSelectMode ? (c) { setState(() { if (selectedComics.containsKey(c as LocalComic)) { - selectedComics.remove(c as LocalComic); + selectedComics.remove(c); } else { - selectedComics[c as LocalComic] = true; + selectedComics[c] = true; } }); } @@ -226,23 +285,54 @@ class _LocalComicsPageState extends State { icon: Icons.delete, text: "Delete".tl, onClick: () { - if (multiSelectMode) { - showConfirmDialog( - context: context, - title: "Delete".tl, - content: "Delete selected comics?".tl, - onConfirm: () { - for (var comic in selectedComics.keys) { - LocalManager().deleteComic(comic); + showDialog( + context: context, + builder: (context) { + bool removeComicFile = true; + return StatefulBuilder( + builder: (context, state) { + return ContentDialog( + title: "Delete".tl, + content: Column( + children: [ + Text("Delete selected comics?".tl).paddingVertical(8), + Transform.scale( + scale: 0.9, + child: CheckboxListTile( + title: Text("Also remove files on disk".tl), + value: removeComicFile, + onChanged: (v) { + state(() { + removeComicFile = !removeComicFile; + }); + } + ) + ), + ], + ).paddingHorizontal(16).paddingVertical(8), + actions: [ + FilledButton( + onPressed: () { + context.pop(); + if(multiSelectMode) { + for (var comic in selectedComics.keys) { + LocalManager().deleteComic(comic as LocalComic, removeComicFile); + } + setState(() { + selectedComics.clear(); + }); + } else { + LocalManager().deleteComic(c as LocalComic, removeComicFile); + } + }, + child: Text("Confirm".tl), + ), + ], + ); } - setState(() { - selectedComics.clear(); - }); - }, - ); - } else { - LocalManager().deleteComic(c as LocalComic); - } + ); + } + ); }), MenuEntry( icon: Icons.outbox_outlined, @@ -255,7 +345,7 @@ class _LocalComicsPageState extends State { try { if (multiSelectMode) { for (var comic in selectedComics.keys) { - var file = await CBZ.export(comic); + var file = await CBZ.export(comic as LocalComic); await saveFile(filename: file.name, file: file); await file.delete(); } From 057d6a2f549889d10cd90618667f113c0b75267b Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 19:54:47 +0800 Subject: [PATCH 27/37] Update translation. --- assets/translation.json | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 73539a0..721d07f 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -141,7 +141,7 @@ "1. The directory only contains image files." : "1. 目录只包含图片文件。", "2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目录包含多个包含图片文件的目录。每个目录被视为一个章节。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。", - "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。", + "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n", "Export as cbz": "导出为cbz", "Select a cbz file." : "选择一个cbz文件", "A cbz file" : "一个cbz文件", @@ -190,7 +190,18 @@ "Long press on the favorite button to quickly add to this folder": "长按收藏按钮快速添加到这个文件夹", "Added": "已添加", "Turn page by volume keys": "使用音量键翻页", - "Display time & battery info in reader":"在阅读器中显示时间和电量信息" + "Display time & battery info in reader":"在阅读器中显示时间和电量信息", + "EhViewer downloads":"EhViewer下载", + "Select an EhViewer database and a download folder.":"选择EhViewer的下载数据(导出的db文件)与存放下载内容的目录", + "(EhViewer)Default": "(EhViewer)默认", + "If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若通过EhViewer数据库导入漫画,程序将会按其中的下载标签自动创建收藏文件夹。", + "Multi-Select": "进入多选模式", + "Exit Multi-Select": "退出多选模式", + "Selected @c comics": "已选择 @c 本漫画", + "Select All": "全选", + "Deselect": "取消选择", + "Invert Selection": "反选", + "Select in range": "区间选择" }, "zh_TW": { "Home": "首頁", @@ -334,7 +345,7 @@ "1. The directory only contains image files." : "1. 目錄只包含圖片文件。", "2. The directory contains directories which contain image files. Each directory is considered as a chapter." : "2. 目錄包含多個包含圖片文件的目錄。每個目錄被視為一個章節。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。", - "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles." : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。", + "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n", "Export as cbz": "匯出為cbz", "Select a cbz file." : "選擇一個cbz文件", "A cbz file" : "一個cbz文件", @@ -383,6 +394,17 @@ "Long press on the favorite button to quickly add to this folder": "長按收藏按鈕快速添加到這個文件夾", "Added": "已添加", "Turn page by volume keys": "使用音量鍵翻頁", - "Display time & battery info in reader":"在閱讀器中顯示時間和電量信息" + "Display time & battery info in reader": "在閱讀器中顯示時間和電量信息", + "EhViewer downloads": "EhViewer下載", + "Select an EhViewer database and a download folder.": "選擇EhViewer的下載資料(匯出的db檔案)與存放下載內容的目錄", + "(EhViewer)Default": "(EhViewer)預設", + "If you import an EhViewer's database, program will automatically create folders according to the download label in that database.": "若透過EhViewer資料庫匯入漫畫,程式將會按其中的下載標籤自動建立收藏資料夾。", + "Multi-Select": "進入多選模式", + "Exit Multi-Select": "退出多選模式", + "Selected @c comics": "已選擇 @c 本漫畫", + "Select All": "全選", + "Deselect": "取消選擇", + "Invert Selection": "反選", + "Select in range": "區間選擇" } } \ No newline at end of file From 4ff1140bf66e5dab610147ed2e7ee9b5402956a0 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 21:28:07 +0800 Subject: [PATCH 28/37] Add cancellation to ehviewer import. --- lib/pages/home_page.dart | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 427c709..6d36958 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -640,7 +640,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { if (dbFile == null || comicSrc == null) { return; } - var controller = showLoadingDialog(context, allowCancel: false); + + bool cancelled = false; + var controller = showLoadingDialog(context, onCancel: () { cancelled = true; }); try { var cache = FilePath.join(App.cachePath, dbFile.name); @@ -649,6 +651,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { Future addTagComics(String destFolder, List comics) async { for(var comic in comics) { + if(cancelled) { + return; + } var comicDir = Directory(FilePath.join(comicSrc.path, comic['DIRNAME'] as String)); if(!(await comicDir.exists())) { continue; @@ -731,6 +736,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { """); for (var folder in folders) { + if(cancelled) { + break; + } var label = folder["LABEL"] as String; var folderName = '(EhViewer)$label'; if(!LocalFavoritesManager().existsFolder(folderName)) { From 1636c959d01bfdcecaf9aa0557f52a88c7c44705 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 12 Nov 2024 22:37:46 +0800 Subject: [PATCH 29/37] fix #33 --- lib/pages/comic_page.dart | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 62b086b..c12e67c 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:sliver_tools/sliver_tools.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; @@ -1021,6 +1022,8 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { String? error; + bool isLoading = false; + @override void didChangeDependencies() { state = context.findAncestorStateOfType<_ComicPageState>()!; @@ -1034,6 +1037,11 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { if (!isInitialLoading && next == null) { return; } + Future.microtask(() { + setState(() { + isLoading = true; + }); + }); var res = await state.comicSource.loadComicThumbnail!(state.comic.id, next); if (res.success) { thumbnails.addAll(res.data); @@ -1042,13 +1050,15 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { } else { error = res.errorMessage; } - setState(() {}); + setState(() { + isLoading = false; + }); } @override Widget build(BuildContext context) { - return SliverMainAxisGroup( - slivers: [ + return MultiSliver( + children: [ SliverToBoxAdapter( child: ListTile( title: Text("Preview".tl), @@ -1148,7 +1158,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { ], ), ) - else if (next != null || isInitialLoading) + else if (isLoading) const SliverToBoxAdapter( child: ListLoadingIndicator(), ), From acb9c47657d8a935b6cd8042a478f2008e03f800 Mon Sep 17 00:00:00 2001 From: pkuislm <69719051+pkuislm@users.noreply.github.com> Date: Tue, 12 Nov 2024 23:09:53 +0800 Subject: [PATCH 30/37] Improve selection button display on small screen devices. --- lib/pages/local_comics_page.dart | 174 +++++++++++++++++++------------ 1 file changed, 110 insertions(+), 64 deletions(-) diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 151f0db..b8f95bb 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -116,6 +116,106 @@ class _LocalComicsPageState extends State { @override Widget build(BuildContext context) { + final double screenWidth = MediaQuery.of(context).size.width; + final bool isScreenSmall = screenWidth < 500.0; + + void selectAll(){ + setState(() { + selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + }); + } + void deSelect() { + setState(() { + selectedComics.clear(); + }); + } + void invertSelection() { + setState(() { + comics.asMap().forEach((k, v) { + selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + }); + selectedComics.removeWhere((k, v) => !v); + }); + } + void selectRange() { + setState(() { + List l = []; + selectedComics.forEach((k, v) { + l.add(comics.indexOf(k as LocalComic)); + }); + if(l.isEmpty) { + return; + } + l.sort(); + int start = l.first; + int end = l.last; + selectedComics.clear(); + selectedComics.addEntries( + List.generate(end - start + 1, (i) { + return MapEntry(comics[start + i], true); + }) + ); + }); + } + + List selectActions = []; + if(isScreenSmall) { + selectActions.add( + IconButton( + onPressed: () { + showMenu( + context: context, + position: RelativeRect.fromLTRB(screenWidth, App.isMobile ? 64 : 96, 0, 0), + items: [ + PopupMenuItem( + onTap: selectAll, + child: Text("Select All".tl), + ), + PopupMenuItem( + onTap: deSelect, + child: Text("Deselect".tl), + ), + PopupMenuItem( + onTap: invertSelection, + child: Text("Invert Selection".tl), + ), + PopupMenuItem( + onTap: selectRange, + child: Text("Select in range".tl), + ) + ] + ); + }, + icon: const Icon( + Icons.list + )) + ); + }else { + selectActions = [ + IconButton( + icon: const Icon(Icons.check_box_rounded), + tooltip: "Select All".tl, + onPressed: selectAll + ), + IconButton( + icon: const Icon(Icons.check_box_outline_blank_outlined), + tooltip: "Deselect".tl, + onPressed: deSelect + ), + IconButton( + icon: const Icon(Icons.check_box_outlined), + tooltip: "Invert Selection".tl, + onPressed: invertSelection + ), + + IconButton( + icon: const Icon(Icons.indeterminate_check_box_rounded), + tooltip: "Select in range".tl, + onPressed: selectRange + ), + ]; + } + return Scaffold( body: SmoothCustomScrollView( slivers: [ @@ -169,71 +269,17 @@ class _LocalComicsPageState extends State { SliverAppbar( title: Text("Selected @c comics".tlParams({"c": selectedComics.length})), actions: [ - IconButton( - icon: const Icon(Icons.check_box_rounded), - tooltip: "Select All".tl, - onPressed: () { - setState(() { - selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); - }); - }, - ), - IconButton( - icon: const Icon(Icons.check_box_outline_blank_outlined), - tooltip: "Deselect".tl, - onPressed: () { - setState(() { - selectedComics.clear(); - }); - }, - ), - IconButton( - icon: const Icon(Icons.check_box_outlined), - tooltip: "Invert Selection".tl, - onPressed: () { - setState(() { - comics.asMap().forEach((k, v) { - selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + ...selectActions, + IconButton( + icon: const Icon(Icons.close), + tooltip: "Exit Multi-Select".tl, + onPressed: () { + setState(() { + multiSelectMode = false; + selectedComics.clear(); }); - selectedComics.removeWhere((k, v) => !v); - }); - }, - ), - - IconButton( - icon: const Icon(Icons.indeterminate_check_box_rounded), - tooltip: "Select in range".tl, - onPressed: () { - setState(() { - List l = []; - selectedComics.forEach((k, v) { - l.add(comics.indexOf(k as LocalComic)); - }); - if(l.isEmpty) { - return; - } - l.sort(); - int start = l.first; - int end = l.last; - selectedComics.clear(); - selectedComics.addEntries( - List.generate(end - start + 1, (i) { - return MapEntry(comics[start + i], true); - }) - ); - }); - }, - ), - IconButton( - icon: const Icon(Icons.close), - tooltip: "Exit Multi-Select".tl, - onPressed: () { - setState(() { - multiSelectMode = false; - selectedComics.clear(); - }); - }, - ), + }, + ), ], ) From 8e99e94620cf9e9fb43b5e4b9bb7e15c83b0dd2e Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 13 Nov 2024 08:57:37 +0800 Subject: [PATCH 31/37] Add the feature for updating local favorites info --- assets/translation.json | 10 +- lib/pages/favorites/favorite_actions.dart | 120 +++++++++++++++++- lib/pages/favorites/local_favorites_page.dart | 12 ++ 3 files changed, 136 insertions(+), 6 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 721d07f..92816e6 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -201,7 +201,10 @@ "Select All": "全选", "Deselect": "取消选择", "Invert Selection": "反选", - "Select in range": "区间选择" + "Select in range": "区间选择", + "Finished": "已完成", + "Updating": "更新中", + "Update Comics Info": "更新漫画信息" }, "zh_TW": { "Home": "首頁", @@ -405,6 +408,9 @@ "Select All": "全選", "Deselect": "取消選擇", "Invert Selection": "反選", - "Select in range": "區間選擇" + "Select in range": "區間選擇", + "Finished": "已完成", + "Updating": "更新中", + "Update Comics Info": "更新漫畫信息" } } \ No newline at end of file diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 2b625d6..21d1634 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -34,12 +34,11 @@ Future newFolder() async { child: Text("Import from file".tl), onPressed: () async { var file = await selectFile(ext: ['json']); - if(file == null) return; + if (file == null) return; var data = await file.readAsBytes(); try { LocalFavoritesManager().fromJson(utf8.decode(data)); - } - catch(e) { + } catch (e) { context.showMessage(message: "Failed to import".tl); return; } @@ -113,7 +112,9 @@ void addFavorite(Comic comic) { name: comic.title, coverPath: comic.cover, author: comic.subtitle ?? '', - type: ComicType((comic.sourceKey == 'local' ? 0 : comic.sourceKey.hashCode)), + type: ComicType((comic.sourceKey == 'local' + ? 0 + : comic.sourceKey.hashCode)), tags: comic.tags ?? [], ), ); @@ -128,3 +129,114 @@ void addFavorite(Comic comic) { }, ); } + +Future> updateComicsInfo(String folder) async { + var comics = LocalFavoritesManager().getAllComics(folder); + + Future updateSingleComic(int index) async { + int retry = 3; + + while (true) { + try { + var c = comics[index]; + var comicSource = c.type.comicSource; + if (comicSource == null) return; + + var newInfo = (await comicSource.loadComicInfo!(c.id)).data; + + comics[index] = FavoriteItem( + id: c.id, + name: newInfo.title, + coverPath: newInfo.cover, + author: newInfo.subTitle ?? + newInfo.tags['author']?.firstOrNull ?? + c.author, + type: c.type, + tags: c.tags, + ); + + LocalFavoritesManager().updateInfo(folder, comics[index]); + return; + } catch (e) { + retry--; + if(retry == 0) { + rethrow; + } + continue; + } + } + } + + var finished = ValueNotifier(0); + + var errors = 0; + + var index = 0; + + bool isCanceled = false; + + showDialog( + context: App.rootContext, + builder: (context) { + return ValueListenableBuilder( + valueListenable: finished, + builder: (context, value, child) { + var isFinished = value == comics.length; + return ContentDialog( + title: isFinished ? "Finished".tl : "Updating".tl, + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + LinearProgressIndicator( + value: value / comics.length, + ), + const SizedBox(height: 4), + Text("$value/${comics.length}"), + const SizedBox(height: 4), + if (errors > 0) Text("Errors: $errors"), + ], + ).paddingHorizontal(16), + actions: [ + Button.filled( + color: isFinished ? null : context.colorScheme.error, + onPressed: () { + isCanceled = true; + context.pop(); + }, + child: isFinished ?Text("OK".tl) : Text("Cancel".tl), + ), + ], + ); + }, + ); + }, + ).then((_) { + isCanceled = true; + }); + + while(index < comics.length) { + var futures = []; + const maxConcurrency = 4; + + if(isCanceled) { + return comics; + } + + for (var i = 0; i < maxConcurrency; i++) { + if (index+i >= comics.length) break; + futures.add(updateSingleComic(index + i).then((v) { + finished.value++; + }, onError: (_) { + errors++; + finished.value++; + })); + } + + await Future.wait(futures); + index += maxConcurrency; + } + + return comics; +} diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 7d87726..d3bb263 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -123,6 +123,18 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { filename: "${widget.folder}.json", ); }), + MenuEntry( + icon: Icons.update, + text: "Update Comics Info".tl, + onClick: () { + updateComicsInfo(widget.folder).then((newComics) { + if(mounted) { + setState(() { + comics = newComics; + }); + } + }); + }), ], ), ], From 9bdcba1270fc5023a2298010245297666ff91bdb Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 13 Nov 2024 12:21:57 +0800 Subject: [PATCH 32/37] improve ui --- lib/components/button.dart | 3 +- lib/components/comic.dart | 96 ++++---- .../image_provider/cached_image.dart | 8 +- lib/network/download.dart | 4 + lib/network/images.dart | 20 +- lib/pages/downloading_page.dart | 4 +- lib/pages/home_page.dart | 3 +- lib/pages/local_comics_page.dart | 212 ++++++++---------- lib/pages/settings/about.dart | 6 +- lib/pages/settings/settings_page.dart | 3 +- lib/pages/webview.dart | 52 ++--- 11 files changed, 199 insertions(+), 212 deletions(-) diff --git a/lib/components/button.dart b/lib/components/button.dart index 2a1f6e6..8a16de0 100644 --- a/lib/components/button.dart +++ b/lib/components/button.dart @@ -156,7 +156,7 @@ class _ButtonState extends State