From 430b6eeb3acb5a5ed24ae2bbbc2d35f5a87c120b Mon Sep 17 00:00:00 2001 From: nyne <67669799+wgh136@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:33:28 +0800 Subject: [PATCH 01/50] Feat/saf (#81) * [Android] Use SAF to change local path * Use IOOverrides to replace openDirectoryPlatform and openFilePlatform * fix io --- lib/components/components.dart | 1 - .../image_provider/base_image_provider.dart | 3 +- .../image_provider/cached_image.dart | 3 +- lib/foundation/local.dart | 24 ++--- lib/main.dart | 70 +++++++------- lib/network/download.dart | 39 ++++---- lib/pages/favorites/favorites_page.dart | 3 - lib/pages/reader/images.dart | 2 +- lib/pages/reader/scaffold.dart | 4 +- lib/pages/settings/app.dart | 21 +--- lib/utils/cbz.dart | 2 +- lib/utils/import_comic.dart | 8 +- lib/utils/io.dart | 95 ++++++++++++------- pubspec.lock | 4 +- 14 files changed, 146 insertions(+), 133 deletions(-) diff --git a/lib/components/components.dart b/lib/components/components.dart index 90c7d93..b4b2dd8 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -25,7 +25,6 @@ import 'package:venera/network/cloudflare.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/favorites/favorites_page.dart'; import 'package:venera/utils/ext.dart'; -import 'package:venera/utils/io.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index 57dd120..f7c981b 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -1,5 +1,4 @@ import 'dart:async' show Future, StreamController, scheduleMicrotask; -import 'dart:collection'; import 'dart:convert'; import 'dart:ui' as ui show Codec; import 'dart:ui'; @@ -108,7 +107,7 @@ abstract class BaseImageProvider> } } - static final _cache = LinkedHashMap(); + static final _cache = {}; static var _cacheSize = 0; diff --git a/lib/foundation/image_provider/cached_image.dart b/lib/foundation/image_provider/cached_image.dart index a962f3d..701eb76 100644 --- a/lib/foundation/image_provider/cached_image.dart +++ b/lib/foundation/image_provider/cached_image.dart @@ -1,5 +1,4 @@ import 'dart:async' show Future, StreamController; -import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/network/images.dart'; @@ -25,7 +24,7 @@ class CachedImageProvider @override Future load(StreamController chunkEvents) async { if(url.startsWith("file://")) { - var file = openFilePlatform(url.substring(7)); + var file = File(url.substring(7)); return file.readAsBytes(); } await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 41bc3a5..6558df9 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -71,7 +71,7 @@ class LocalComic with HistoryMixin implements Comic { downloadedChapters = List.from(jsonDecode(row[8] as String)), createdAt = DateTime.fromMillisecondsSinceEpoch(row[9] as int); - File get coverFile => openFilePlatform(FilePath.join( + File get coverFile => File(FilePath.join( baseDir, cover, )); @@ -151,6 +151,8 @@ class LocalManager with ChangeNotifier { /// path to the directory where all the comics are stored late String path; + Directory get directory => Directory(path); + // return error message if failed Future setNewPath(String newPath) async { var newDir = Directory(newPath); @@ -162,7 +164,7 @@ class LocalManager with ChangeNotifier { } try { await copyDirectoryIsolate( - Directory(path), + directory, newDir, ); await File(FilePath.join(App.dataPath, 'local_path')).writeAsString(newPath); @@ -170,7 +172,7 @@ class LocalManager with ChangeNotifier { Log.error("IO", e, s); return e.toString(); } - await Directory(path).deleteIgnoreError(recursive:true); + await directory.deleteContents(recursive: true); path = newPath; return null; } @@ -217,15 +219,15 @@ class LocalManager with ChangeNotifier { '''); if (File(FilePath.join(App.dataPath, 'local_path')).existsSync()) { path = File(FilePath.join(App.dataPath, 'local_path')).readAsStringSync(); - if (!Directory(path).existsSync()) { + if (!directory.existsSync()) { path = await findDefaultPath(); } } else { path = await findDefaultPath(); } try { - if (!Directory(path).existsSync()) { - await Directory(path).create(); + if (!directory.existsSync()) { + await directory.create(); } } catch(e, s) { @@ -354,12 +356,12 @@ class LocalManager with ChangeNotifier { throw "Invalid ep"; } var comic = find(id, type) ?? (throw "Comic Not Found"); - var directory = openDirectoryPlatform(comic.baseDir); + var directory = Directory(comic.baseDir); if (comic.chapters != null) { var cid = ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String); - directory = openDirectoryPlatform(FilePath.join(directory.path, cid)); + directory = Directory(FilePath.join(directory.path, cid)); } var files = []; await for (var entity in directory.list()) { @@ -406,10 +408,10 @@ class LocalManager with ChangeNotifier { String id, ComicType type, String name) async { var comic = find(id, type); if (comic != null) { - return openDirectoryPlatform(FilePath.join(path, comic.directory)); + return Directory(FilePath.join(path, comic.directory)); } var dir = findValidDirectoryName(path, name); - return openDirectoryPlatform(FilePath.join(path, dir)).create().then((value) => value); + return Directory(FilePath.join(path, dir)).create().then((value) => value); } void completeTask(DownloadTask task) { @@ -468,7 +470,7 @@ class LocalManager with ChangeNotifier { void deleteComic(LocalComic c, [bool removeFileOnDisk = true]) { if(removeFileOnDisk) { - var dir = openDirectoryPlatform(FilePath.join(path, c.directory)); + 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. diff --git a/lib/main.dart b/lib/main.dart index 8d11045..01cd575 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,40 +20,42 @@ void main(List args) { if (runWebViewTitleBarWidget(args)) { return; } - runZonedGuarded(() async { - await Rhttp.init(); - WidgetsFlutterBinding.ensureInitialized(); - await init(); - if (App.isAndroid) { - handleLinks(); - } - FlutterError.onError = (details) { - Log.error( - "Unhandled Exception", "${details.exception}\n${details.stack}"); - }; - runApp(const MyApp()); - if (App.isDesktop) { - await windowManager.ensureInitialized(); - windowManager.waitUntilReadyToShow().then((_) async { - await windowManager.setTitleBarStyle( - TitleBarStyle.hidden, - windowButtonVisibility: App.isMacOS, - ); - if (App.isLinux) { - await windowManager.setBackgroundColor(Colors.transparent); - } - await windowManager.setMinimumSize(const Size(500, 600)); - if (!App.isLinux) { - // https://github.com/leanflutter/window_manager/issues/460 - var placement = await WindowPlacement.loadFromFile(); - await placement.applyToWindow(); - await windowManager.show(); - WindowPlacement.loop(); - } - }); - } - }, (error, stack) { - Log.error("Unhandled Exception", "$error\n$stack"); + overrideIO(() { + runZonedGuarded(() async { + await Rhttp.init(); + WidgetsFlutterBinding.ensureInitialized(); + await init(); + if (App.isAndroid) { + handleLinks(); + } + FlutterError.onError = (details) { + Log.error( + "Unhandled Exception", "${details.exception}\n${details.stack}"); + }; + runApp(const MyApp()); + if (App.isDesktop) { + await windowManager.ensureInitialized(); + windowManager.waitUntilReadyToShow().then((_) async { + await windowManager.setTitleBarStyle( + TitleBarStyle.hidden, + windowButtonVisibility: App.isMacOS, + ); + if (App.isLinux) { + await windowManager.setBackgroundColor(Colors.transparent); + } + await windowManager.setMinimumSize(const Size(500, 600)); + if (!App.isLinux) { + // https://github.com/leanflutter/window_manager/issues/460 + var placement = await WindowPlacement.loadFromFile(); + await placement.applyToWindow(); + await windowManager.show(); + WindowPlacement.loop(); + } + }); + } + }, (error, stack) { + Log.error("Unhandled Exception", "$error\n$stack"); + }); }); } diff --git a/lib/network/download.dart b/lib/network/download.dart index da2b93f..7657549 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -235,20 +235,21 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { } if (path == null) { - var dir = await LocalManager().findValidDirectory( - comicId, - comicType, - comic!.title, - ); - if (!(await dir.exists())) { - try { + try { + var dir = await LocalManager().findValidDirectory( + comicId, + comicType, + comic!.title, + ); + if (!(await dir.exists())) { await dir.create(); - } catch (e) { - _setError("Error: $e"); - return; } + path = dir.path; + } catch (e, s) { + Log.error("Download", e.toString(), s); + _setError("Error: $e"); + return; } - path = dir.path; } await LocalManager().saveCurrentDownloadingTasks(); @@ -266,11 +267,13 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { throw "Failed to download cover"; } var fileType = detectFileType(data); - var file = File(FilePath.join(path!, "cover${fileType.ext}")); + var file = + File(FilePath.join(path!, "cover${fileType.ext}")); file.writeAsBytesSync(data); return "file://${file.path}"; }); if (res.error) { + Log.error("Download", res.errorMessage!); _setError("Error: ${res.errorMessage}"); return; } else { @@ -294,6 +297,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { return; } if (res.error) { + Log.error("Download", res.errorMessage!); _setError("Error: ${res.errorMessage}"); return; } else { @@ -323,6 +327,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { return; } if (res.error) { + Log.error("Download", res.errorMessage!); _setError("Error: ${res.errorMessage}"); return; } else { @@ -347,6 +352,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { return; } if (task.error != null) { + Log.error("Download", task.error.toString()); _setError("Error: ${task.error}"); return; } @@ -375,7 +381,6 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { _message = message; notifyListeners(); stopRecorder(); - Log.error("Download", message); } @override @@ -448,7 +453,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { }).toList(), directory: Directory(path!).name, chapters: comic!.chapters, - cover: File(_cover!.split("file://").last).uri.pathSegments.last, + cover: + File(_cover!.split("file://").last).name, comicType: ComicType(source.key.hashCode), downloadedChapters: chapters ?? [], createdAt: DateTime.now(), @@ -721,13 +727,12 @@ class ArchiveDownloadTask extends DownloadTask { _currentBytes = status.downloadedBytes; _expectedBytes = status.totalBytes; _message = - "${bytesToReadableString(_currentBytes)}/${bytesToReadableString(_expectedBytes)}"; + "${bytesToReadableString(_currentBytes)}/${bytesToReadableString(_expectedBytes)}"; _speed = status.bytesPerSecond; isDownloaded = status.isFinished; notifyListeners(); } - } - catch(e) { + } catch (e) { _setError("Error: $e"); return; } diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index f71eb17..76542fd 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart'; import 'package:venera/components/components.dart'; @@ -11,9 +10,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.dart'; -import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/res.dart'; -import 'package:venera/network/download.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index a202591..d273eca 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -604,7 +604,7 @@ ImageProvider _createImageProvider(int page, BuildContext context) { var reader = context.reader; var imageKey = reader.images![page - 1]; if (imageKey.startsWith('file://')) { - return FileImage(openFilePlatform(imageKey.replaceFirst("file://", ''))); + return FileImage(File(imageKey.replaceFirst("file://", ''))); } else { return ReaderImageProvider( imageKey, diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 804f32c..95043c3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -469,7 +469,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ImageProvider image; var imageKey = images[index]; if (imageKey.startsWith('file://')) { - image = FileImage(openFilePlatform(imageKey.replaceFirst("file://", ''))); + image = FileImage(File(imageKey.replaceFirst("file://", ''))); } else { image = ReaderImageProvider( imageKey, @@ -515,7 +515,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } } if (imageKey.startsWith("file://")) { - return await openFilePlatform(imageKey.substring(7)).readAsBytes(); + return await File(imageKey.substring(7)).readAsBytes(); } else { return (await CacheManager().findCache( "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index b35301f..234340c 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -34,25 +34,8 @@ class _AppSettingsState extends State { callback: () async { String? result; if (App.isAndroid) { - var channel = const MethodChannel("venera/storage"); - var permission = await channel.invokeMethod(''); - if (permission != true) { - context.showMessage(message: "Permission denied".tl); - return; - } - var path = await selectDirectory(); - if (path != null) { - // check if the path is writable - var testFile = File(FilePath.join(path, "test")); - try { - await testFile.writeAsBytes([1]); - await testFile.delete(); - } catch (e) { - context.showMessage(message: "Permission denied".tl); - return; - } - result = path; - } + var picker = DirectoryPicker(); + result = (await picker.pickDirectory())?.path; } else if (App.isIOS) { result = await selectDirectoryIOS(); } else { diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 35a9fd6..7bc6ba1 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -187,7 +187,7 @@ abstract class CBZ { } int i = 1; for (var image in allImages) { - var src = openFilePlatform(image); + var src = File(image); var width = allImages.length.toString().length; var dstName = '${i.toString().padLeft(width, '0')}.${image.split('.').last}'; diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index cbcecbc..21eb5cf 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -60,7 +60,7 @@ class ImportComic { if (cancelled) { return imported; } - var comicDir = openDirectoryPlatform( + var comicDir = Directory( FilePath.join(comicSrc.path, comic['DIRNAME'] as String)); String titleJP = comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String; @@ -217,7 +217,7 @@ class ImportComic { chapters.sort(); if (hasChapters && coverPath == '') { // use the first image in the first chapter as the cover - var firstChapter = openDirectoryPlatform('${directory.path}/${chapters.first}'); + var firstChapter = Directory('${directory.path}/${chapters.first}'); await for (var entry in firstChapter.list()) { if (entry is File) { coverPath = entry.name; @@ -248,8 +248,8 @@ class ImportComic { var destination = data['destination'] as String; Map result = {}; for (var dir in toBeCopied) { - var source = openDirectoryPlatform(dir); - var dest = openDirectoryPlatform("$destination/${source.name}"); + var source = Directory(dir); + var dest = Directory("$destination/${source.name}"); if (dest.existsSync()) { // The destination directory already exists, and it is not managed by the app. // Rename the old directory to avoid conflicts. diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 9df3d29..2a959ce 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -81,7 +81,7 @@ extension DirectoryExtension on Directory { int total = 0; for (var f in listSync(recursive: true)) { if (FileSystemEntity.typeSync(f.path) == FileSystemEntityType.file) { - total += await openFilePlatform(f.path).length(); + total += await File(f.path).length(); } } return total; @@ -93,7 +93,21 @@ extension DirectoryExtension on Directory { } File joinFile(String name) { - return openFilePlatform(FilePath.join(path, name)); + return File(FilePath.join(path, name)); + } + + void deleteContentsSync({recursive = true}) { + if (!existsSync()) return; + for (var f in listSync()) { + f.deleteIfExistsSync(recursive: recursive); + } + } + + Future deleteContents({recursive = true}) async { + if (!existsSync()) return; + for (var f in listSync()) { + await f.deleteIfExists(recursive: recursive); + } } } @@ -124,14 +138,15 @@ String sanitizeFileName(String fileName) { Future copyDirectory(Directory source, Directory destination) async { List contents = source.listSync(); for (FileSystemEntity content in contents) { - String newPath = destination.path + - Platform.pathSeparator + - content.path.split(Platform.pathSeparator).last; + String newPath = FilePath.join(destination.path, content.name); if (content is File) { - content.copySync(newPath); + var resultFile = File(newPath); + resultFile.createSync(); + var data = content.readAsBytesSync(); + resultFile.writeAsBytesSync(data); } else if (content is Directory) { - Directory newDirectory = openDirectoryPlatform(newPath); + Directory newDirectory = Directory(newPath); newDirectory.createSync(); copyDirectory(content.absolute, newDirectory.absolute); } @@ -140,18 +155,18 @@ Future copyDirectory(Directory source, Directory destination) async { Future copyDirectoryIsolate( Directory source, Directory destination) async { - await Isolate.run(() { - copyDirectory(source, destination); + await Isolate.run(() async { + await copyDirectory(source, destination); }); } String findValidDirectoryName(String path, String directory) { var name = sanitizeFileName(directory); - var dir = openDirectoryPlatform("$path/$name"); + var dir = Directory("$path/$name"); var i = 1; while (dir.existsSync() && dir.listSync().isNotEmpty) { name = sanitizeFileName("$directory($i)"); - dir = openDirectoryPlatform("$path/$name"); + dir = Directory("$path/$name"); i++; } return name; @@ -184,11 +199,12 @@ class DirectoryPicker { directory = (await AndroidDirectory.pickDirectory())?.path; } else { // ios, macos - directory = await _methodChannel.invokeMethod("getDirectoryPath"); + directory = + await _methodChannel.invokeMethod("getDirectoryPath"); } if (directory == null) return null; _finalizer.attach(this, directory); - return openDirectoryPlatform(directory); + return Directory(directory); } finally { Future.delayed(const Duration(milliseconds: 100), () { IO._isSelectingFiles = false; @@ -311,33 +327,44 @@ Future saveFile( } } -Directory openDirectoryPlatform(String path) { - if(App.isAndroid) { - var dir = AndroidDirectory.fromPathSync(path); - if(dir == null) { - return Directory(path); +class _IOOverrides extends IOOverrides { + @override + Directory createDirectory(String path) { + if (App.isAndroid) { + var dir = AndroidDirectory.fromPathSync(path); + if (dir == null) { + return super.createDirectory(path); + } + return dir; + } else { + return super.createDirectory(path); } - return dir; - } else { - return Directory(path); } -} -File openFilePlatform(String path) { - if(path.startsWith("file://")) { - path = path.substring(7); - } - if(App.isAndroid) { - var f = AndroidFile.fromPathSync(path); - if(f == null) { - return File(path); + @override + File createFile(String path) { + if (path.startsWith("file://")) { + path = path.substring(7); + } + if (App.isAndroid) { + var f = AndroidFile.fromPathSync(path); + if (f == null) { + return super.createFile(path); + } + return f; + } else { + return super.createFile(path); } - return f; - } else { - return File(path); } } +void overrideIO(void Function() f) { + IOOverrides.runWithIOOverrides( + f, + _IOOverrides(), + ); +} + class Share { static void shareFile({ required Uint8List data, @@ -396,4 +423,4 @@ class FileSelectResult { } String get name => File(path).name; -} \ No newline at end of file +} diff --git a/pubspec.lock b/pubspec.lock index eb64a71..8128abe 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -393,8 +393,8 @@ packages: dependency: "direct main" description: path: "." - ref: "829a566b738a26ea98e523807f49838e21308543" - resolved-ref: "829a566b738a26ea98e523807f49838e21308543" + ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d + resolved-ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d url: "https://github.com/pkuislm/flutter_saf.git" source: git version: "0.0.1" From bbfe87fff20b5f4829d7f88208f3b770ff58ad3c Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 30 Nov 2024 10:07:03 +0800 Subject: [PATCH 02/50] fix #77 --- lib/pages/settings/settings_page.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index 04a0215..8f8e0bd 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -178,8 +178,9 @@ class _SettingsPageState extends State implements PopEntry { Positioned.fill(child: buildLeft()), Positioned( left: offset, - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, + right: 0, + top: 0, + bottom: 0, child: Listener( onPointerDown: handlePointerDown, child: AnimatedSwitcher( From 91b765ffbae9b8209fb370d3bdb67bbc985c9e74 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 30 Nov 2024 13:50:07 +0800 Subject: [PATCH 03/50] fix subtitle --- assets/init.js | 5 ++++- lib/foundation/comic_source/models.dart | 6 ++++-- lib/pages/comic_page.dart | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/assets/init.js b/assets/init.js index 8ea0e8b..7994a0b 100644 --- a/assets/init.js +++ b/assets/init.js @@ -877,6 +877,8 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage /** * Create a comic details object * @param title {string} + * @param subtitle {string} + * @param subTitle {string} - equal to subtitle * @param cover {string} * @param description {string?} * @param tags {Map | {} | null | undefined} @@ -897,8 +899,9 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage * @param comments {Comment[]?}- `since 1.0.7` App will display comments in the details page. * @constructor */ -function ComicDetails({title, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) { +function ComicDetails({title, subtitle, subTitle, cover, description, tags, chapters, isFavorite, subId, thumbnails, recommend, commentCount, likesCount, isLiked, uploader, updateTime, uploadTime, url, stars, maxPage, comments}) { this.title = title; + this.subtitle = subtitle ?? subTitle; this.cover = cover; this.description = description; this.tags = tags; diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 9155765..2f263a2 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -172,7 +172,7 @@ class ComicDetails with HistoryMixin { ComicDetails.fromJson(Map json) : title = json["title"], - subTitle = json["subTitle"], + subTitle = json["subtitle"], cover = json["cover"], description = json["description"], tags = _generateMap(json["tags"]), @@ -198,7 +198,9 @@ class ComicDetails with HistoryMixin { maxPage = json["maxPage"], comments = (json["comments"] as List?) ?.map((e) => Comment.fromJson(e)) - .toList(); + .toList(){ + print(json); + } Map toJson() { return { diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 2305e76..d740ce2 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -223,7 +223,7 @@ class _ComicPageState extends LoadingState children: [ SelectableText(comic.title, style: ts.s18), if (comic.subTitle != null) - SelectableText(comic.subTitle!, style: ts.s14), + SelectableText(comic.subTitle!, style: ts.s14).paddingVertical(4), Text( (ComicSource.find(comic.sourceKey)?.name) ?? '', style: ts.s12, From 2063eee82b37ec349880a085fe44f1c6aa4d391c Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 30 Nov 2024 20:52:55 +0800 Subject: [PATCH 04/50] fix #76 --- assets/translation.json | 4 ++-- lib/pages/home_page.dart | 2 +- lib/utils/import_comic.dart | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 788b30b..031c63d 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -152,7 +152,7 @@ "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.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n", "Export as cbz": "导出为cbz", - "Select a cbz file." : "选择一个cbz文件", + "Select a cbz/zip file." : "选择一个cbz/zip文件", "A cbz file" : "一个cbz文件", "Fullscreen": "全屏", "Exit": "退出", @@ -399,7 +399,7 @@ "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.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n", "Export as cbz": "匯出為cbz", - "Select a cbz file." : "選擇一個cbz文件", + "Select a cbz/zip file." : "選擇一個cbz/zip文件", "A cbz file" : "一個cbz文件", "Fullscreen": "全螢幕", "Exit": "退出", diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 6d4614d..018076b 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -511,7 +511,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { String info = [ "Select a directory which contains the comic files.".tl, "Select a directory which contains the comic directories.".tl, - "Select a cbz file.".tl, + "Select a cbz/zip file.".tl, "Select an EhViewer database and a download folder.".tl ][type]; List importMethods = [ diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index 21eb5cf..157e306 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -20,7 +20,7 @@ class ImportComic { const ImportComic({this.selectedFolder, this.copyToLocal = true}); Future cbz() async { - var file = await selectFile(ext: ['cbz']); + var file = await selectFile(ext: ['cbz', 'zip']); Map> imported = {}; if(file == null) { return false; From 9fb348247416b458aecec99ab0191593b5ff006e Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 30 Nov 2024 21:05:35 +0800 Subject: [PATCH 05/50] fix #73 --- lib/components/comic.dart | 2 +- lib/components/components.dart | 1 + .../image_provider/local_comic_image.dart | 63 +++++++++++++++++++ lib/pages/home_page.dart | 5 +- 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 lib/foundation/image_provider/local_comic_image.dart diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 28d53a7..6420308 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -163,7 +163,7 @@ class ComicTile extends StatelessWidget { Widget buildImage(BuildContext context) { ImageProvider image; if (comic is LocalComic) { - image = FileImage((comic as LocalComic).coverFile); + image = LocalComicImageProvider(comic as LocalComic); } else if (comic.sourceKey == 'local') { var localComic = LocalManager().find(comic.id, ComicType.local); if (localComic == null) { diff --git a/lib/components/components.dart b/lib/components/components.dart index b4b2dd8..ea377c1 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -19,6 +19,7 @@ import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/cached_image.dart'; +import 'package:venera/foundation/image_provider/local_comic_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/network/cloudflare.dart'; diff --git a/lib/foundation/image_provider/local_comic_image.dart b/lib/foundation/image_provider/local_comic_image.dart new file mode 100644 index 0000000..cfb1c22 --- /dev/null +++ b/lib/foundation/image_provider/local_comic_image.dart @@ -0,0 +1,63 @@ +import 'dart:async' show Future, StreamController; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/network/images.dart'; +import 'package:venera/utils/io.dart'; +import 'base_image_provider.dart'; +import 'local_comic_image.dart' as image_provider; + +class LocalComicImageProvider + extends BaseImageProvider { + /// Image provider for normal image. + /// + /// [url] is the url of the image. Local file path is also supported. + const LocalComicImageProvider(this.comic); + + final LocalComic comic; + + @override + Future load(StreamController chunkEvents) async { + File? file = comic.coverFile; + if(! await file.exists()) { + file = null; + var dir = Directory(comic.directory); + if (! await dir.exists()) { + throw "Error: Comic not found."; + } + Directory? firstDir; + await for (var entity in dir.list()) { + if(entity is File) { + if(["jpg", "jpeg", "png", "webp", "gif", "jpe", "jpeg"].contains(entity.extension)) { + file = entity; + break; + } + } else if(entity is Directory) { + firstDir ??= entity; + } + } + if(file == null && firstDir != null) { + await for (var entity in firstDir.list()) { + if(entity is File) { + if(["jpg", "jpeg", "png", "webp", "gif", "jpe", "jpeg"].contains(entity.extension)) { + file = entity; + break; + } + } + } + } + } + if(file == null) { + throw "Error: Cover not found."; + } + return file.readAsBytes(); + } + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + String get key => "local${comic.id}${comic.comicType.value}"; +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 018076b..32ca613 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -7,6 +7,7 @@ import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/cached_image.dart'; +import 'package:venera/foundation/image_provider/local_comic_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/pages/accounts_page.dart'; import 'package:venera/pages/comic_page.dart'; @@ -418,8 +419,8 @@ class _LocalState extends State<_Local> { ), clipBehavior: Clip.antiAlias, child: AnimatedImage( - image: FileImage( - local[index].coverFile, + image: LocalComicImageProvider( + local[index], ), width: 96, height: 128, From 2f4927f7190fae6166408ed47a3afaeec8d7858f Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 30 Nov 2024 21:30:39 +0800 Subject: [PATCH 06/50] prevent too many image loading at save time --- .../image_provider/cached_image.dart | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/foundation/image_provider/cached_image.dart b/lib/foundation/image_provider/cached_image.dart index 701eb76..be5cd4a 100644 --- a/lib/foundation/image_provider/cached_image.dart +++ b/lib/foundation/image_provider/cached_image.dart @@ -21,22 +21,35 @@ class CachedImageProvider final String? cid; + static int loadingCount = 0; + + static const _kMaxLoadingCount = 8; + @override Future load(StreamController chunkEvents) async { - if(url.startsWith("file://")) { - var file = File(url.substring(7)); - return file.readAsBytes(); + while(loadingCount > _kMaxLoadingCount) { + await Future.delayed(const Duration(milliseconds: 100)); } - await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { - chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: progress.currentBytes, - expectedTotalBytes: progress.totalBytes, - )); - if(progress.imageBytes != null) { - return progress.imageBytes!; + loadingCount++; + try { + if(url.startsWith("file://")) { + var file = File(url.substring(7)); + return file.readAsBytes(); } + await for (var progress in ImageDownloader.loadThumbnail(url, sourceKey, cid)) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: progress.currentBytes, + expectedTotalBytes: progress.totalBytes, + )); + if(progress.imageBytes != null) { + return progress.imageBytes!; + } + } + throw "Error: Empty response body."; + } + finally { + loadingCount--; } - throw "Error: Empty response body."; } @override From 30b2aa2f995707bf9dcbebef18fd5eea386389b8 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 30 Nov 2024 21:36:23 +0800 Subject: [PATCH 07/50] fix #52 --- lib/components/comic.dart | 4 +++ lib/components/navigation_bar.dart | 30 ++++++++++++++++--- lib/foundation/state_controller.dart | 17 +++++++---- lib/pages/explore_page.dart | 45 ++++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 11 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 6420308..d01f63a 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -829,6 +829,7 @@ class ComicList extends StatefulWidget { this.trailingSliver, this.errorLeading, this.menuBuilder, + this.controller, }); final Future>> Function(int page)? loadPage; @@ -843,6 +844,8 @@ class ComicList extends StatefulWidget { final List Function(Comic)? menuBuilder; + final ScrollController? controller; + @override State createState() => ComicListState(); } @@ -1064,6 +1067,7 @@ class ComicListState extends State { ); } return SmoothCustomScrollView( + controller: widget.controller, slivers: [ if (widget.leadingSliver != null) widget.leadingSliver!, if (_maxPage != 1) _buildSliverPageSelector(), diff --git a/lib/components/navigation_bar.dart b/lib/components/navigation_bar.dart index 73e89ec..7965921 100644 --- a/lib/components/navigation_bar.dart +++ b/lib/components/navigation_bar.dart @@ -47,10 +47,16 @@ class NaviPane extends StatefulWidget { final GlobalKey navigatorKey; @override - State createState() => _NaviPaneState(); + State createState() => NaviPaneState(); + + static NaviPaneState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } } -class _NaviPaneState extends State +typedef NaviItemTapListener = void Function(int); + +class NaviPaneState extends State with SingleTickerProviderStateMixin { late int _currentPage = widget.initialPage; @@ -66,6 +72,16 @@ class _NaviPaneState extends State late AnimationController controller; + final _naviItemTapListeners = []; + + void addNaviItemTapListener(NaviItemTapListener listener) { + _naviItemTapListeners.add(listener); + } + + void removeNaviItemTapListener(NaviItemTapListener listener) { + _naviItemTapListeners.remove(listener); + } + static const _kBottomBarHeight = 58.0; static const _kFoldedSideBarWidth = 80.0; @@ -85,9 +101,15 @@ class _NaviPaneState extends State } void updatePage(int index) { + for (var listener in _naviItemTapListeners) { + listener(index); + } if (widget.observer.routes.length > 1) { widget.navigatorKey.currentState!.popUntil((route) => route.isFirst); } + if (currentPage == index) { + return; + } setState(() { currentPage = index; }); @@ -670,14 +692,14 @@ class _NaviPopScope extends StatelessWidget { class _NaviMainView extends StatefulWidget { const _NaviMainView({required this.state}); - final _NaviPaneState state; + final NaviPaneState state; @override State<_NaviMainView> createState() => _NaviMainViewState(); } class _NaviMainViewState extends State<_NaviMainView> { - _NaviPaneState get state => widget.state; + NaviPaneState get state => widget.state; @override void initState() { diff --git a/lib/foundation/state_controller.dart b/lib/foundation/state_controller.dart index a4e0c9d..4e5fff3 100644 --- a/lib/foundation/state_controller.dart +++ b/lib/foundation/state_controller.dart @@ -1,14 +1,18 @@ import 'package:flutter/material.dart'; class SimpleController extends StateController { - final void Function()? refresh_; + final void Function()? refreshFunction; - SimpleController({this.refresh_}); + final Map Function()? control; + + SimpleController({this.refreshFunction, this.control}); @override void refresh() { - (refresh_ ?? super.refresh)(); + (refreshFunction ?? super.refresh)(); } + + Map get controlMap => control?.call() ?? {}; } abstract class StateController { @@ -71,8 +75,8 @@ abstract class StateController { static SimpleController putSimpleController( void Function() onUpdate, Object? tag, - {void Function()? refresh}) { - var controller = SimpleController(refresh_: refresh); + {void Function()? refresh, Map Function()? control}) { + var controller = SimpleController(refreshFunction: refresh, control: control); controller.stateUpdaters.add(Pair(null, onUpdate)); _controllers.add(StateControllerWrapped(controller, false, tag)); return controller; @@ -202,6 +206,7 @@ abstract class StateWithController extends State { }, tag, refresh: refresh, + control: () => control, ); super.initState(); } @@ -218,6 +223,8 @@ abstract class StateWithController extends State { } Object? get tag; + + Map get control => {}; } class Pair{ diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 125bd7d..abd7618 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -46,6 +46,18 @@ class _ExplorePageState extends State } } + void onNaviItemTapped(int index) { + if (index == 2) { + int page = controller.index; + String currentPageId = pages[page]; + StateController.find(tag: currentPageId) + .control!()['toTop'] + ?.call(); + } + } + + NaviPaneState? naviPane; + @override void initState() { pages = List.from(appdata.settings["explore_pages"]); @@ -59,13 +71,21 @@ class _ExplorePageState extends State vsync: this, ); appdata.settings.addListener(onSettingsChanged); + NaviPane.of(context).addNaviItemTapListener(onNaviItemTapped); super.initState(); } + @override + void didChangeDependencies() { + naviPane = NaviPane.of(context); + super.didChangeDependencies(); + } + @override void dispose() { controller.dispose(); appdata.settings.removeListener(onSettingsChanged); + naviPane?.removeNaviItemTapListener(onNaviItemTapped); super.dispose(); } @@ -95,7 +115,7 @@ class _ExplorePageState extends State Widget buildEmpty() { var msg = "No Explore Pages".tl; msg += '\n'; - if(ComicSource.isEmpty) { + if (ComicSource.isEmpty) { msg += "Add a comic source in home page".tl; } else { msg += "Please check your settings".tl; @@ -232,6 +252,8 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> bool _wantKeepAlive = true; + var scrollController = ScrollController(); + void onSettingsChanged() { var explorePages = appdata.settings["explore_pages"]; if (!explorePages.contains(widget.title)) { @@ -274,6 +296,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> data, comicSourceKey, key: ValueKey(key), + controller: scrollController, ); } else { return const Center( @@ -287,6 +310,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> loadPage: data.loadPage, loadNext: data.loadNext, key: ValueKey(key), + controller: scrollController, ); } @@ -323,6 +347,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> Widget buildPage() { return SmoothCustomScrollView( + controller: scrollController, slivers: _buildPage().toList(), ); } @@ -352,15 +377,30 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> @override bool get wantKeepAlive => _wantKeepAlive; + + void toTop() { + if (scrollController.hasClients) { + scrollController.animateTo( + scrollController.position.minScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.easeInOut, + ); + } + } + + @override + Map get control => {"toTop": toTop}; } class _MixedExplorePage extends StatefulWidget { - const _MixedExplorePage(this.data, this.sourceKey, {super.key}); + const _MixedExplorePage(this.data, this.sourceKey, {super.key, this.controller}); final ExplorePageData data; final String sourceKey; + final ScrollController? controller; + @override State<_MixedExplorePage> createState() => _MixedExplorePageState(); } @@ -394,6 +434,7 @@ class _MixedExplorePageState @override Widget buildContent(BuildContext context, List data) { return SmoothCustomScrollView( + controller: widget.controller, slivers: [ ...buildSlivers(context, data), if (haveNextPage) const ListLoadingIndicator().toSliver() From de4503a2dea6f8ed85db5849363507494d2f089d Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 18:05:59 +0800 Subject: [PATCH 08/50] fix selecting file on Android --- .../com/github/wgh136/venera/MainActivity.kt | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt b/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt index 4a3f677..65bae8b 100644 --- a/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt +++ b/android/app/src/main/kotlin/com/github/wgh136/venera/MainActivity.kt @@ -9,6 +9,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings +import android.util.Log import android.view.KeyEvent import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultLauncher @@ -324,8 +325,25 @@ class MainActivity : FlutterFragmentActivity() { } } // use copy method - val filePath = FileUtils.getPathFromCopyOfFileFromUri(this, uri) - result.success(filePath) + val tmp = File(cacheDir, fileName) + if(tmp.exists()) { + tmp.delete() + } + Log.i("Venera", "copy file (${fileName}) to ${tmp.absolutePath}") + Thread { + try { + contentResolver.openInputStream(uri)?.use { input -> + FileOutputStream(tmp).use { output -> + input.copyTo(output, bufferSize = DEFAULT_BUFFER_SIZE) + output.flush() + } + } + result.success(tmp.absolutePath) + } + catch (e: Exception) { + result.error("copy error", e.message, null) + } + }.start() } } } From a2f628001a9ac7a1279d9e2ebae32c5bc1b83022 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 18:06:19 +0800 Subject: [PATCH 09/50] import pica data --- lib/pages/settings/app.dart | 13 ++- lib/utils/data.dart | 209 +++++++++++++++++++++++++++--------- lib/utils/io.dart | 4 +- 3 files changed, 171 insertions(+), 55 deletions(-) diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 234340c..c59e19e 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -110,16 +110,23 @@ class _AppSettingsState extends State { title: "Import App Data".tl, callback: () async { var controller = showLoadingDialog(context); - var file = await selectFile(ext: ['venera']); + var file = await selectFile(ext: ['venera', 'picadata']); if (file != null) { - var cacheFile = File(FilePath.join(App.cachePath, "temp.venera")); + var cacheFile = File(FilePath.join(App.cachePath, "import_data_temp")); await file.saveTo(cacheFile.path); try { - await importAppData(cacheFile); + if(file.name.endsWith('picadata')) { + await importPicaData(cacheFile); + } else { + await importAppData(cacheFile); + } } catch (e, s) { Log.error("Import data", e.toString(), s); context.showMessage(message: "Failed to import data".tl); } + finally { + cacheFile.deleteIgnoreError(); + } } controller.close(); }, diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 00f4264..788191d 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -1,11 +1,14 @@ import 'dart:convert'; import 'dart:isolate'; +import 'package:sqlite3/sqlite3.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/comic_type.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/network/cookie_jar.dart'; import 'package:zip_flutter/zip_flutter.dart'; @@ -43,61 +46,165 @@ Future exportAppData() async { Future importAppData(File file, [bool checkVersion = false]) async { var cacheDirPath = FilePath.join(App.cachePath, 'temp_data'); var cacheDir = Directory(cacheDirPath); - await Isolate.run(() { - ZipFile.openAndExtract(file.path, cacheDirPath); - }); - var historyFile = cacheDir.joinFile("history.db"); - var localFavoriteFile = cacheDir.joinFile("local_favorite.db"); - var appdataFile = cacheDir.joinFile("appdata.json"); - 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"]) { - return; - } + if (cacheDir.existsSync()) { + cacheDir.deleteSync(recursive: true); } - 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()) { - LocalFavoritesManager().close(); - File(FilePath.join(App.dataPath, "local_favorite.db")).deleteIfExistsSync(); - localFavoriteFile - .renameSync(FilePath.join(App.dataPath, "local_favorite.db")); - LocalFavoritesManager().init(); - } - if (await appdataFile.exists()) { - // proxy settings & authorization setting should be kept - var proxySettings = appdata.settings["proxy"]; - var authSettings = appdata.settings["authorizationRequired"]; - File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync(); - appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json")); - await appdata.init(); - appdata.settings["proxy"] = proxySettings; - appdata.settings["authorizationRequired"] = authSettings; - appdata.saveData(); + cacheDir.createSync(); + try { + await Isolate.run(() { + ZipFile.openAndExtract(file.path, cacheDirPath); + }); + var historyFile = cacheDir.joinFile("history.db"); + var localFavoriteFile = cacheDir.joinFile("local_favorite.db"); + var appdataFile = cacheDir.joinFile("appdata.json"); + 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"]) { + return; + } + } + 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()) { + LocalFavoritesManager().close(); + File(FilePath.join(App.dataPath, "local_favorite.db")) + .deleteIfExistsSync(); + localFavoriteFile + .renameSync(FilePath.join(App.dataPath, "local_favorite.db")); + LocalFavoritesManager().init(); + } + if (await appdataFile.exists()) { + // proxy settings & authorization setting should be kept + var proxySettings = appdata.settings["proxy"]; + var authSettings = appdata.settings["authorizationRequired"]; + File(FilePath.join(App.dataPath, "appdata.json")).deleteIfExistsSync(); + appdataFile.renameSync(FilePath.join(App.dataPath, "appdata.json")); + await appdata.init(); + appdata.settings["proxy"] = proxySettings; + appdata.settings["authorizationRequired"] = authSettings; + 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) { + var targetFile = + FilePath.join(App.dataPath, "comic_source", file.name); + File(targetFile).deleteIfExistsSync(); + await file.copy(targetFile); + } + } + await ComicSource.reload(); + } + } finally { + cacheDir.deleteIgnoreError(recursive: true); } - 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(); +} + +Future importPicaData(File file) async { + var cacheDirPath = FilePath.join(App.cachePath, 'temp_data'); + var cacheDir = Directory(cacheDirPath); + if (cacheDir.existsSync()) { + cacheDir.deleteSync(recursive: true); } - var comicSourceDir = FilePath.join(cacheDirPath, "comic_source"); - 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); + cacheDir.createSync(); + try { + await Isolate.run(() { + ZipFile.openAndExtract(file.path, cacheDirPath); + }); + var localFavoriteFile = cacheDir.joinFile("local_favorite.db"); + if (localFavoriteFile.existsSync()) { + var db = sqlite3.open(localFavoriteFile.path); + try { + var folderNames = db + .select("SELECT name FROM sqlite_master WHERE type='table';") + .map((e) => e["name"] as String) + .toList(); + folderNames.removeWhere((e) => e == "folder_order" || e == "folder_sync"); + for (var folderName in folderNames) { + if (!LocalFavoritesManager().existsFolder(folderName)) { + LocalFavoritesManager().createFolder(folderName); + } + for (var comic in db.select("SELECT * FROM \"$folderName\";")) { + LocalFavoritesManager().addComic( + folderName, + FavoriteItem( + id: comic['target'], + name: comic['name'], + coverPath: comic['cover_path'], + author: comic['author'], + type: ComicType(switch(comic['type']) { + 0 => 'picacg'.hashCode, + 1 => 'ehentai'.hashCode, + 2 => 'jm'.hashCode, + 3 => 'hitomi'.hashCode, + 4 => 'wnacg'.hashCode, + 6 => 'nhentai'.hashCode, + _ => comic['type'] + }), + tags: comic['tags'].split(','), + ), + ); + } + } + } + catch(e) { + Log.error("Import Data", "Failed to import local favorite: $e"); + } + finally { + db.dispose(); + } + } + var historyFile = cacheDir.joinFile("history.db"); + if (historyFile.existsSync()) { + var db = sqlite3.open(historyFile.path); + try { + for (var comic in db.select("SELECT * FROM history;")) { + HistoryManager().addHistory( + History.fromMap({ + "type": switch(comic['type']) { + 0 => 'picacg'.hashCode, + 1 => 'ehentai'.hashCode, + 2 => 'jm'.hashCode, + 3 => 'hitomi'.hashCode, + 4 => 'wnacg'.hashCode, + 6 => 'nhentai'.hashCode, + _ => comic['type'] + }, + "id": comic['target'], + "maxPage": comic["max_page"], + "ep": comic["ep"], + "page": comic["page"], + "time": comic["time"], + "title": comic["title"], + "subtitle": comic["subtitle"], + "cover": comic["cover"], + }), + ); + } + } + catch(e) { + Log.error("Import Data", "Failed to import history: $e"); + } + finally { + db.dispose(); } } - await ComicSource.reload(); + } finally { + cacheDir.deleteIgnoreError(recursive: true); } } diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 2a959ce..cb167ae 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -265,7 +265,9 @@ Future selectFile({required List ext}) async { file = FileSelectResult(xFile.path); } if (!ext.contains(file.path.split(".").last)) { - App.rootContext.showMessage(message: "Invalid file type"); + App.rootContext.showMessage( + message: "Invalid file type: ${file.path.split(".").last}", + ); return null; } return file; From 2ee2a01550cc30c01d2a2f4fc634a83617f66a8f Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 18:54:17 +0800 Subject: [PATCH 10/50] export comic as pdf --- assets/translation.json | 6 ++- lib/foundation/local.dart | 2 +- lib/pages/local_comics_page.dart | 33 +++++++++++- lib/utils/io.dart | 4 +- lib/utils/pdf.dart | 92 ++++++++++++++++++++++++++++++++ pubspec.lock | 56 +++++++++++++++++++ pubspec.yaml | 1 + 7 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 lib/utils/pdf.dart diff --git a/assets/translation.json b/assets/translation.json index 031c63d..d03fb34 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -244,7 +244,8 @@ "Deleted @a favorite items.": "已删除 @a 条无效收藏", "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", - "No new version available": "没有新版本可用" + "No new version available": "没有新版本可用", + "Export as pdf": "导出为pdf" }, "zh_TW": { "Home": "首頁", @@ -491,6 +492,7 @@ "Deleted @a favorite items.": "已刪除 @a 條無效收藏", "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", - "No new version available": "沒有新版本可用" + "No new version available": "沒有新版本可用", + "Export as pdf": "匯出為pdf" } } \ No newline at end of file diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 6558df9..1759102 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -76,7 +76,7 @@ class LocalComic with HistoryMixin implements Comic { cover, )); - String get baseDir => directory.contains("/") ? directory : FilePath.join(LocalManager().path, directory); + String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory); @override String get description => ""; diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 5c5aa21..a679dfe 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -4,9 +4,11 @@ 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/foundation/log.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/utils/cbz.dart'; import 'package:venera/utils/io.dart'; +import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/translations.dart'; class LocalComicsPage extends StatefulWidget { @@ -299,8 +301,7 @@ class _LocalComicsPageState extends State { return ContentDialog( title: "Delete".tl, content: CheckboxListTile( - title: - Text("Also remove files on disk".tl), + title: Text("Also remove files on disk".tl), value: removeComicFile, onChanged: (v) { state(() { @@ -361,6 +362,34 @@ class _LocalComicsPageState extends State { } controller.close(); }), + if (!multiSelectMode) + MenuEntry( + icon: Icons.picture_as_pdf_outlined, + text: "Export as pdf".tl, + onClick: () async { + var cache = FilePath.join(App.cachePath, 'temp.pdf'); + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + try { + await createPdfFromComicIsolate( + comic: c as LocalComic, + savePath: cache, + ); + await saveFile( + file: File(cache), + filename: "${c.title}.pdf", + ); + } catch (e, s) { + Log.error("PDF Export", e, s); + context.showMessage(message: e.toString()); + } finally { + controller.close(); + File(cache).deleteIgnoreError(); + } + }, + ) ]; }, ), diff --git a/lib/utils/io.dart b/lib/utils/io.dart index cb167ae..6cee959 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -360,8 +360,8 @@ class _IOOverrides extends IOOverrides { } } -void overrideIO(void Function() f) { - IOOverrides.runWithIOOverrides( +T overrideIO(T Function() f) { + return IOOverrides.runWithIOOverrides( f, _IOOverrides(), ); diff --git a/lib/utils/pdf.dart b/lib/utils/pdf.dart new file mode 100644 index 0000000..95c8e2b --- /dev/null +++ b/lib/utils/pdf.dart @@ -0,0 +1,92 @@ +import 'dart:isolate'; + +import 'package:pdf/widgets.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/utils/io.dart'; + +Future _createPdfFromComic({ + required LocalComic comic, + required String savePath, + required String localPath, +}) async { + final pdf = Document( + title: comic.title, + author: comic.subTitle ?? "", + producer: "Venera", + ); + + pdf.document.outline; + + var baseDir = comic.directory.contains('/') || comic.directory.contains('\\') + ? comic.directory + : FilePath.join(localPath, comic.directory); + + // add cover + var imageData = File(FilePath.join(baseDir, comic.cover)).readAsBytesSync(); + pdf.addPage(Page( + build: (Context context) { + return Image(MemoryImage(imageData), fit: BoxFit.contain); + }, + )); + + bool multiChapters = comic.chapters != null; + + void reorderFiles(List files) { + files.removeWhere( + (element) => element is! File || element.path.startsWith('cover')); + files.sort((a, b) { + var aName = (a as File).name; + var bName = (b as File).name; + var aNumber = int.tryParse(aName); + var bNumber = int.tryParse(bName); + if (aNumber != null && bNumber != null) { + return aNumber.compareTo(bNumber); + } + return aName.compareTo(bName); + }); + } + + if (!multiChapters) { + var files = Directory(baseDir).listSync(); + reorderFiles(files); + + for (var file in files) { + var imageData = (file as File).readAsBytesSync(); + pdf.addPage(Page( + build: (Context context) { + return Image(MemoryImage(imageData), fit: BoxFit.contain); + }, + )); + } + } else { + for (var chapter in comic.chapters!.keys) { + var files = Directory(FilePath.join(baseDir, chapter)).listSync(); + reorderFiles(files); + for (var file in files) { + var imageData = (file as File).readAsBytesSync(); + pdf.addPage(Page( + build: (Context context) { + return Image(MemoryImage(imageData), fit: BoxFit.contain); + }, + )); + } + } + } + + final file = File(savePath); + file.writeAsBytesSync(await pdf.save()); +} + +Future createPdfFromComicIsolate({ + required LocalComic comic, + required String savePath, +}) async { + var localPath = LocalManager().path; + return Isolate.run(() => overrideIO(() async { + return await _createPdfFromComic( + comic: comic, + savePath: savePath, + localPath: localPath, + ); + })); +} diff --git a/pubspec.lock b/pubspec.lock index 8128abe..167bbaa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" battery_plus: dependency: "direct main" description: @@ -65,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + bidi: + dependency: transitive + description: + name: bidi + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" + url: "https://pub.dev" + source: hosted + version: "2.0.12" boolean_selector: dependency: transitive description: @@ -465,6 +489,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" intl: dependency: "direct main" description: @@ -626,6 +658,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -674,6 +714,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" + url: "https://pub.dev" + source: hosted + version: "3.11.1" petitparser: dependency: transitive description: @@ -715,6 +763,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" rhttp: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f20cec6..713a57c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: git: url: https://github.com/pkuislm/flutter_saf.git ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d + pdf: ^3.11.1 dev_dependencies: flutter_test: From 60f7b4d3b0e79ae42f01a6446f6f763e180a736d Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 18:57:35 +0800 Subject: [PATCH 11/50] update version code --- lib/foundation/app.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 5428cac..9c94bd4 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -10,7 +10,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.0.7"; + final version = "1.0.8"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index 713a57c..d0fd106 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.0.7+107 +version: 1.0.8+108 environment: sdk: '>=3.5.0 <4.0.0' From 95c98eeaede56e8cc95016734f0ec3dcf5353cca Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Sun, 1 Dec 2024 19:56:38 +0800 Subject: [PATCH 12/50] =?UTF-8?q?Feat=20=E4=B8=BA=E7=94=BB=E5=BB=8A?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F=E6=B7=BB=E5=8A=A0=E6=AF=8F=E9=A1=B5=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E5=9B=BE=E7=89=87=E6=95=B0=E9=87=8F=E7=9A=84=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Feat: Add dynamic image-per-page configuration for gallery mode - Implemented a slider to configure the number of images displayed per page (1-5) in gallery mode. - Updated the reader to dynamically reflect changes in the `imagesPerPage` setting without requiring a mode switch or reopening. - Ensured compatibility with existing continuous reading mode. * fix currentImagesPerPage * fix Continuous mode * improve readerScreenPicNumber setting disable view * improve PhotoViewController --- assets/translation.json | 2 + lib/foundation/appdata.dart | 1 + lib/pages/reader/images.dart | 97 +++++++++++++++++++++++++--------- lib/pages/reader/reader.dart | 29 +++++++++- lib/pages/settings/reader.dart | 24 +++++++++ 5 files changed, 128 insertions(+), 25 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index d03fb34..985ab59 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -105,6 +105,7 @@ "Continuous (Right to Left)": "连续(从右到左)", "Continuous (Top to Bottom)": "连续(从上到下)", "Auto page turning interval": "自动翻页间隔", + "The number of pic in screen (Only Gallery Mode)": "同屏幕图片数量(仅画廊模式)", "Theme Mode": "主题模式", "System": "系统", "Light": "浅色", @@ -353,6 +354,7 @@ "Continuous (Right to Left)": "連續(從右到左)", "Continuous (Top to Bottom)": "連續(從上到下)", "Auto page turning interval": "自動翻頁間隔", + "The number of pic in screen (Only Gallery Mode)": "同螢幕圖片數量(僅畫廊模式)", "Theme Mode": "主題模式", "System": "系統", "Light": "浅色", diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 8c5d347..c113a00 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -106,6 +106,7 @@ class _Settings with ChangeNotifier { 'defaultSearchTarget': null, 'autoPageTurningInterval': 5, // in seconds 'readerMode': 'galleryLeftToRight', // values of [ReaderMode] + 'readerScreenPicNumber': 1, // 1 - 5 'enableTapToTurnPages': true, 'enablePageAnimation': true, 'language': 'system', // system, zh-CN, zh-TW, en-US diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index d273eca..4d12a64 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -83,7 +83,8 @@ class _ReaderImagesState extends State<_ReaderImages> { ); } else { if (reader.mode.isGallery) { - return _GalleryMode(key: Key(reader.mode.key)); + return _GalleryMode( + key: Key('${reader.mode.key}_${reader.imagesPerPage}')); } else { return _ContinuousMode(key: Key(reader.mode.key)); } @@ -110,6 +111,10 @@ class _GalleryModeState extends State<_GalleryMode> late _ReaderState reader; + int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) / + reader.imagesPerPage) + .ceil(); + @override void initState() { reader = context.reader; @@ -124,8 +129,14 @@ class _GalleryModeState extends State<_GalleryMode> void cache(int current) { for (int i = current + 1; i <= current + preCacheCount; i++) { - if (i <= reader.maxPage && !cached[i]) { - _precacheImage(i, context); + if (i <= totalPages && !cached[i]) { + int startIndex = (i - 1) * reader.imagesPerPage; + int endIndex = + math.min(startIndex + reader.imagesPerPage, reader.images!.length); + for (int i = startIndex; i < endIndex; i++) { + precacheImage( + _createImageProviderFromKey(reader.images![i], context), context); + } cached[i] = true; } } @@ -141,32 +152,34 @@ class _GalleryModeState extends State<_GalleryMode> scrollDirection: reader.mode == ReaderMode.galleryTopToBottom ? Axis.vertical : Axis.horizontal, - itemCount: reader.images!.length + 2, + itemCount: totalPages + 2, builder: (BuildContext context, int index) { - ImageProvider? imageProvider; - if (index != 0 && index != reader.images!.length + 1) { - imageProvider = _createImageProvider(index, context); - } else { + if (index == 0 || index == totalPages + 1) { return PhotoViewGalleryPageOptions.customChild( scaleStateController: PhotoViewScaleStateController(), child: const SizedBox(), ); - } + } else { + int pageIndex = index - 1; + int startIndex = pageIndex * reader.imagesPerPage; + int endIndex = math.min(startIndex + reader.imagesPerPage, reader.images!.length); + List pageImages = reader.images!.sublist(startIndex, endIndex); - cached[index] = true; - cache(index); + cached[index] = true; + cache(index); - photoViewControllers[index] ??= PhotoViewController(); + photoViewControllers[index] = PhotoViewController(); - return PhotoViewGalleryPageOptions( - filterQuality: FilterQuality.medium, - controller: photoViewControllers[index], - imageProvider: imageProvider, - fit: BoxFit.contain, - errorBuilder: (_, error, s, retry) { - return NetworkError(message: error.toString(), retry: retry); - }, - ); + return PhotoViewGalleryPageOptions.customChild( + child: PhotoView.customChild( + key: ValueKey('photo_view_$index'), + controller: photoViewControllers[index], + minScale: PhotoViewComputedScale.contained * 1.0, + maxScale: PhotoViewComputedScale.covered * 10.0, + child: buildPageImages(pageImages), + ), + ); + } }, pageController: controller, loadingBuilder: (context, event) => Center( @@ -186,9 +199,9 @@ class _GalleryModeState extends State<_GalleryMode> if (!reader.toPrevChapter()) { reader.toPage(1); } - } else if (i == reader.maxPage + 1) { + } else if (i == totalPages + 1) { if (!reader.toNextChapter()) { - reader.toPage(reader.maxPage); + reader.toPage(totalPages); } } else { reader.setPage(i); @@ -198,9 +211,30 @@ class _GalleryModeState extends State<_GalleryMode> ); } + Widget buildPageImages(List images) { + Axis axis = (reader.mode == ReaderMode.galleryTopToBottom) + ? Axis.vertical + : Axis.horizontal; + + List imageWidgets = images.map((imageKey) { + ImageProvider imageProvider = + _createImageProviderFromKey(imageKey, context); + return Expanded( + child: Image( + image: imageProvider, + fit: BoxFit.contain, + ), + ); + }).toList(); + + return axis == Axis.vertical + ? Column(children: imageWidgets) + : Row(children: imageWidgets); + } + @override Future animateToPage(int page) { - if ((page - controller.page!).abs() > 1) { + if ((page - controller.page!.round()).abs() > 1) { controller.jumpToPage(page > controller.page! ? page - 1 : page + 1); } return controller.animateToPage( @@ -600,6 +634,21 @@ class _ContinuousModeState extends State<_ContinuousMode> } } +ImageProvider _createImageProviderFromKey( + String imageKey, BuildContext context) { + if (imageKey.startsWith('file://')) { + return FileImage(File(imageKey.replaceFirst("file://", ''))); + } else { + var reader = context.reader; + return ReaderImageProvider( + imageKey, + reader.type.comicSource!.key, + reader.cid, + reader.eid, + ); + } +} + ImageProvider _createImageProvider(int page, BuildContext context) { var reader = context.reader; var imageKey = reader.images![page - 1]; diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 5a828de..8f91306 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -1,6 +1,7 @@ library venera_reader; import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -82,7 +83,8 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } @override - int get maxPage => images?.length ?? 1; + int get maxPage => + ((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage; ComicType get type => widget.type; @@ -94,6 +96,30 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { late ReaderMode mode; + int get imagesPerPage => appdata.settings['readerScreenPicNumber'] ?? 1; + + int _lastImagesPerPage = appdata.settings['readerScreenPicNumber'] ?? 1; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _checkImagesPerPageChange(); + } + + void _checkImagesPerPageChange() { + int currentImagesPerPage = imagesPerPage; + if (_lastImagesPerPage != currentImagesPerPage) { + _adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage); + _lastImagesPerPage = currentImagesPerPage; + } + } + + void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) { + int previousImageIndex = (page - 1) * oldImagesPerPage; + int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; + page = newPage; + } + History? history; @override @@ -133,6 +159,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { @override Widget build(BuildContext context) { + _checkImagesPerPageChange(); return KeyboardListener( focusNode: focusNode, autofocus: true, diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 2bba6b9..810734a 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -41,6 +41,11 @@ class _ReaderSettingsState extends State { "continuousTopToBottom": "Continuous (Top to Bottom)".tl, }, onChanged: () { + var readerMode = appdata.settings['readerMode']; + if (readerMode?.toLowerCase().startsWith('continuous') ?? false) { + appdata.settings['readerScreenPicNumber'] = 1; + widget.onChanged?.call('readerScreenPicNumber'); + } widget.onChanged?.call("readerMode"); }, ).toSliver(), @@ -54,6 +59,25 @@ class _ReaderSettingsState extends State { widget.onChanged?.call("autoPageTurningInterval"); }, ).toSliver(), + SliverToBoxAdapter( + child: AbsorbPointer( + absorbing: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false), + child: AnimatedOpacity( + opacity: (appdata.settings['readerMode']?.toLowerCase().startsWith('continuous') ?? false) ? 0.5 : 1.0, + duration: Duration(milliseconds: 300), + child: _SliderSetting( + title: "The number of pic in screen (Only Gallery Mode)".tl, + settingsIndex: "readerScreenPicNumber", + interval: 1, + min: 1, + max: 5, + onChanged: () { + widget.onChanged?.call("readerScreenPicNumber"); + }, + ), + ), + ), + ), _SwitchSetting( title: 'Long press to zoom'.tl, settingKey: 'enableLongPressToZoom', From b425eec561dc14481979c1725098b89bcf47d33c Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 20:22:33 +0800 Subject: [PATCH 13/50] update reader --- lib/pages/reader/gesture.dart | 3 +++ lib/pages/reader/images.dart | 23 ++++++++++++++++------- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index a97e17e..05f90d6 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -103,6 +103,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { } void onMouseWheel(bool forward) { + if (HardwareKeyboard.instance.isControlPressed) { + return; + } if (context.reader.mode.key.startsWith('gallery')) { if (forward) { if (!context.reader.toNextPage()) { diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 4d12a64..f3eb839 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -170,14 +170,23 @@ class _GalleryModeState extends State<_GalleryMode> photoViewControllers[index] = PhotoViewController(); - return PhotoViewGalleryPageOptions.customChild( - child: PhotoView.customChild( - key: ValueKey('photo_view_$index'), + if(reader.imagesPerPage == 1) { + return PhotoViewGalleryPageOptions( + filterQuality: FilterQuality.medium, controller: photoViewControllers[index], - minScale: PhotoViewComputedScale.contained * 1.0, - maxScale: PhotoViewComputedScale.covered * 10.0, - child: buildPageImages(pageImages), - ), + imageProvider: _createImageProviderFromKey(pageImages[0], context), + fit: BoxFit.contain, + errorBuilder: (_, error, s, retry) { + return NetworkError(message: error.toString(), retry: retry); + }, + ); + } + + return PhotoViewGalleryPageOptions.customChild( + controller: photoViewControllers[index], + minScale: PhotoViewComputedScale.contained * 1.0, + maxScale: PhotoViewComputedScale.covered * 10.0, + child: buildPageImages(pageImages), ); } }, From 070c803f973a5dbca2d640eba252803b98f3c76f Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 20:25:32 +0800 Subject: [PATCH 14/50] add telegram link --- lib/pages/settings/about.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/settings/about.dart b/lib/pages/settings/about.dart index 6a0c70a..c65b87e 100644 --- a/lib/pages/settings/about.dart +++ b/lib/pages/settings/about.dart @@ -68,6 +68,13 @@ class _AboutSettingsState extends State { launchUrlString("https://github.com/venera-app/venera"); }, ).toSliver(), + ListTile( + title: const Text("Telegram"), + trailing: const Icon(Icons.open_in_new), + onTap: () { + launchUrlString("https://t.me/venera_release"); + }, + ).toSliver(), ], ); } From 24188b51c054a5f6b35e7796cd23af7fabccb101 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 1 Dec 2024 21:10:51 +0800 Subject: [PATCH 15/50] fix copyDirectoryIsolate --- lib/utils/io.dart | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 6cee959..12617b9 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -155,9 +155,7 @@ Future copyDirectory(Directory source, Directory destination) async { Future copyDirectoryIsolate( Directory source, Directory destination) async { - await Isolate.run(() async { - await copyDirectory(source, destination); - }); + await Isolate.run(() => overrideIO(() => copyDirectory(source, destination))); } String findValidDirectoryName(String path, String directory) { @@ -358,6 +356,7 @@ class _IOOverrides extends IOOverrides { return super.createFile(path); } } + } T overrideIO(T Function() f) { From 6c5df476637589746c9cc906a6f5668b126cba7b Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 2 Dec 2024 11:19:06 +0800 Subject: [PATCH 16/50] Add HistoryImageProvider --- lib/components/comic.dart | 2 + lib/components/components.dart | 1 + lib/foundation/history.dart | 49 +++++++++++++++- .../history_image_provider.dart | 57 +++++++++++++++++++ lib/pages/history_page.dart | 28 +-------- lib/pages/home_page.dart | 18 +----- 6 files changed, 111 insertions(+), 44 deletions(-) create mode 100644 lib/foundation/image_provider/history_image_provider.dart diff --git a/lib/components/comic.dart b/lib/components/comic.dart index d01f63a..d175649 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -164,6 +164,8 @@ class ComicTile extends StatelessWidget { ImageProvider image; if (comic is LocalComic) { image = LocalComicImageProvider(comic as LocalComic); + } else if (comic is History) { + image = HistoryImageProvider(comic as History); } else if (comic.sourceKey == 'local') { var localComic = LocalManager().find(comic.id, ComicType.local); if (localComic == null) { diff --git a/lib/components/components.dart b/lib/components/components.dart index ea377c1..719bc79 100644 --- a/lib/components/components.dart +++ b/lib/components/components.dart @@ -19,6 +19,7 @@ import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/cached_image.dart'; +import 'package:venera/foundation/image_provider/history_image_provider.dart'; import 'package:venera/foundation/image_provider/local_comic_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/res.dart'; diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 71ef40d..d5def70 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:sqlite3/sqlite3.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/utils/translations.dart'; import 'app.dart'; @@ -22,15 +24,18 @@ abstract mixin class HistoryMixin { HistoryType get historyType; } -class History { +class History implements Comic { HistoryType type; DateTime time; + @override String title; + @override String subtitle; + @override String cover; int ep; @@ -44,6 +49,7 @@ class History { /// The number of episodes is 1-based. Set readEpisode; + @override int? maxPage; History.fromModel( @@ -137,6 +143,47 @@ class History { @override int get hashCode => Object.hash(id, type); + + @override + String get description { + var res = ""; + if (ep >= 1) { + res += "Chapter @ep".tlParams({ + "ep": ep, + }); + } + if (page >= 1) { + if (ep >= 1) { + res += " - "; + } + res += "Page @page".tlParams({ + "page": page, + }); + } + return res; + } + + @override + String? get favoriteId => null; + + @override + String? get language => null; + + @override + String get sourceKey => type == ComicType.local + ? 'local' + : type.comicSource?.key ?? "Unknown:${type.value}"; + + @override + double? get stars => null; + + @override + List? get tags => null; + + @override + Map toJson() { + throw UnimplementedError(); + } } class HistoryManager with ChangeNotifier { diff --git a/lib/foundation/image_provider/history_image_provider.dart b/lib/foundation/image_provider/history_image_provider.dart new file mode 100644 index 0000000..4e2475a --- /dev/null +++ b/lib/foundation/image_provider/history_image_provider.dart @@ -0,0 +1,57 @@ +import 'dart:async' show Future, StreamController; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/network/images.dart'; +import '../history.dart'; +import 'base_image_provider.dart'; +import 'history_image_provider.dart' as image_provider; + +class HistoryImageProvider + extends BaseImageProvider { + /// Image provider for normal image. + /// + /// [url] is the url of the image. Local file path is also supported. + const HistoryImageProvider(this.history); + + final History history; + + @override + Future load(StreamController chunkEvents) async { + var url = history.cover; + if (!url.contains('/')) { + var localComic = LocalManager().find(history.id, history.type); + if (localComic != null) { + return localComic.coverFile.readAsBytes(); + } + var comicSource = + history.type.comicSource ?? (throw "Comic source not found."); + var comic = await comicSource.loadComicInfo!(history.id); + url = comic.data.cover; + history.cover = url; + HistoryManager().addHistory(history); + } + await for (var progress in ImageDownloader.loadThumbnail( + url, + history.type.sourceKey, + history.id, + )) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: progress.currentBytes, + expectedTotalBytes: progress.totalBytes, + )); + if (progress.imageBytes != null) { + return progress.imageBytes!; + } + } + throw "Error: Empty response body."; + } + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } + + @override + String get key => "history${history.id}${history.type.value}"; +} diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index 37e8c4e..36a7165 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -78,33 +78,7 @@ class _HistoryPageState extends State { ], ), SliverGridComics( - comics: comics.map( - (e) { - var cover = e.cover; - if (!cover.isURL) { - var localComic = LocalManager().find( - e.id, - e.type, - ); - if(localComic != null) { - cover = "file://${localComic.coverFile.path}"; - } - } - return Comic( - e.title, - cover, - e.id, - e.subtitle, - null, - getDescription(e), - e.type == ComicType.local - ? 'local' - : e.type.comicSource?.key ?? "Unknown:${e.type.value}", - null, - null, - ); - }, - ).toList(), + comics: comics, badgeBuilder: (c) { return ComicSource.find(c.sourceKey)?.name; }, diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 32ca613..d4e0ee8 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -7,6 +7,7 @@ import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/cached_image.dart'; +import 'package:venera/foundation/image_provider/history_image_provider.dart'; import 'package:venera/foundation/image_provider/local_comic_image.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/pages/accounts_page.dart'; @@ -265,21 +266,6 @@ class _HistoryState extends State<_History> { scrollDirection: Axis.horizontal, itemCount: history.length, itemBuilder: (context, index) { - var cover = history[index].cover; - ImageProvider imageProvider = CachedImageProvider( - cover, - sourceKey: history[index].type.comicSource?.key, - cid: history[index].id, - ); - if (!cover.isURL) { - var localComic = LocalManager().find( - history[index].id, - history[index].type, - ); - if (localComic != null) { - imageProvider = FileImage(localComic.coverFile); - } - } return InkWell( onTap: () { context.to( @@ -302,7 +288,7 @@ class _HistoryState extends State<_History> { ), clipBehavior: Clip.antiAlias, child: AnimatedImage( - image: imageProvider, + image: HistoryImageProvider(history[index]), width: 96, height: 128, fit: BoxFit.cover, From 153f1a9dfef15fd190dcd8affee6084acdf7ca41 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 2 Dec 2024 11:39:28 +0800 Subject: [PATCH 17/50] rollback android storage setting --- lib/pages/settings/app.dart | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index c59e19e..69978f7 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -34,8 +34,25 @@ class _AppSettingsState extends State { callback: () async { String? result; if (App.isAndroid) { - var picker = DirectoryPicker(); - result = (await picker.pickDirectory())?.path; + var channel = const MethodChannel("venera/storage"); + var permission = await channel.invokeMethod(''); + if (permission != true) { + context.showMessage(message: "Permission denied".tl); + return; + } + var path = await selectDirectory(); + if (path != null) { + // check if the path is writable + var testFile = File(FilePath.join(path, "test")); + try { + await testFile.writeAsBytes([1]); + await testFile.delete(); + } catch (e) { + context.showMessage(message: "Permission denied".tl); + return; + } + result = path; + } } else if (App.isIOS) { result = await selectDirectoryIOS(); } else { From 674b5c9636862dfed217137501a42fbd52223f70 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 2 Dec 2024 15:30:59 +0800 Subject: [PATCH 18/50] Update saf --- .../image_provider/local_comic_image.dart | 7 +++++-- lib/pages/settings/app.dart | 21 ++----------------- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 10 insertions(+), 24 deletions(-) diff --git a/lib/foundation/image_provider/local_comic_image.dart b/lib/foundation/image_provider/local_comic_image.dart index cfb1c22..591b840 100644 --- a/lib/foundation/image_provider/local_comic_image.dart +++ b/lib/foundation/image_provider/local_comic_image.dart @@ -2,7 +2,6 @@ import 'dart:async' show Future, StreamController; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/foundation/local.dart'; -import 'package:venera/network/images.dart'; import 'package:venera/utils/io.dart'; import 'base_image_provider.dart'; import 'local_comic_image.dart' as image_provider; @@ -50,7 +49,11 @@ class LocalComicImageProvider if(file == null) { throw "Error: Cover not found."; } - return file.readAsBytes(); + var data = await file.readAsBytes(); + if(data.isEmpty) { + throw "Exception: Empty file(${file.path})."; + } + return data; } @override diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index 69978f7..c59e19e 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -34,25 +34,8 @@ class _AppSettingsState extends State { callback: () async { String? result; if (App.isAndroid) { - var channel = const MethodChannel("venera/storage"); - var permission = await channel.invokeMethod(''); - if (permission != true) { - context.showMessage(message: "Permission denied".tl); - return; - } - var path = await selectDirectory(); - if (path != null) { - // check if the path is writable - var testFile = File(FilePath.join(path, "test")); - try { - await testFile.writeAsBytes([1]); - await testFile.delete(); - } catch (e) { - context.showMessage(message: "Permission denied".tl); - return; - } - result = path; - } + var picker = DirectoryPicker(); + result = (await picker.pickDirectory())?.path; } else if (App.isIOS) { result = await selectDirectoryIOS(); } else { diff --git a/pubspec.lock b/pubspec.lock index 167bbaa..9f64ace 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -417,8 +417,8 @@ packages: dependency: "direct main" description: path: "." - ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d - resolved-ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d + ref: "3315082b9f7055655610e4f6f136b69e48228c05" + resolved-ref: "3315082b9f7055655610e4f6f136b69e48228c05" url: "https://github.com/pkuislm/flutter_saf.git" source: git version: "0.0.1" diff --git a/pubspec.yaml b/pubspec.yaml index d0fd106..da623e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: flutter_saf: git: url: https://github.com/pkuislm/flutter_saf.git - ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d + ref: 3315082b9f7055655610e4f6f136b69e48228c05 pdf: ^3.11.1 dev_dependencies: From 7aed61a65ec30ad000c6cda37caa7d1825d05e79 Mon Sep 17 00:00:00 2001 From: Naomi <33375791+Henvy-Mango@users.noreply.github.com> Date: Mon, 2 Dec 2024 16:27:40 +0800 Subject: [PATCH 19/50] =?UTF-8?q?feat:=20=E6=BC=AB=E7=94=BB=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E9=A1=B5=E6=9C=AC=E5=9C=B0=E6=94=B6=E8=97=8F=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=80=89=E6=8B=A9=E9=BB=98=E8=AE=A4=E6=94=B6=E8=97=8F?= =?UTF-8?q?=E5=A4=B9=20(#84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Naomi <33375791+Henvy-Mango@users.noreply.github.com> --- lib/pages/favorites/favorite_actions.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index e19686f..c6f494a 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -1,3 +1,5 @@ +import 'package:venera/foundation/appdata.dart'; + part of 'favorites_page.dart'; /// Open a dialog to create a new favorite folder. @@ -83,7 +85,7 @@ void addFavorite(Comic comic) { showDialog( context: App.rootContext, builder: (context) { - String? selectedFolder; + String? selectedFolder = appdata.settings['quickFavorite']; return StatefulBuilder(builder: (context, setState) { return ContentDialog( From 8f07c8a2bb66dc1212006e97d4574dfb7dc08bec Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 2 Dec 2024 16:47:13 +0800 Subject: [PATCH 20/50] comment button --- lib/pages/comic_page.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index d740ce2..12d84f5 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -288,8 +288,7 @@ class _ComicPageState extends LoadingState onLongPressed: quickFavorite, iconColor: context.useTextColor(Colors.purple), ), - if (comicSource.commentsLoader != null && - (comic.comments == null || comic.comments!.isEmpty)) + if (comicSource.commentsLoader != null) _ActionButton( icon: const Icon(Icons.comment), text: (comic.commentsCount ?? 'Comments'.tl).toString(), From 867b2a4b64afc4bb4a07ee261f0319de6d20b71f Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 2 Dec 2024 17:45:26 +0800 Subject: [PATCH 21/50] fix TabBar --- lib/components/appbar.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index c5341f1..0550e60 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -332,7 +332,7 @@ class _FilledTabBarState extends State { @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: _controller, + animation: _controller.animation ?? _controller, builder: buildTabBar, ); } @@ -427,7 +427,7 @@ class _FilledTabBarState extends State { padding: const EdgeInsets.symmetric(horizontal: 16), child: DefaultTextStyle( style: DefaultTextStyle.of(context).style.copyWith( - color: i == _controller.index + color: i == _controller.animation?.value.round() ? context.colorScheme.primary : context.colorScheme.onSurface, fontWeight: FontWeight.w500, From 9b821f1b4652fc6406e3425154a2176ce41fab50 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 2 Dec 2024 20:55:47 +0800 Subject: [PATCH 22/50] fix cbz import --- lib/utils/cbz.dart | 17 +++-- lib/utils/file_type.dart | 2 +- lib/utils/import_comic.dart | 126 ++++++++++++++++++------------------ lib/utils/io.dart | 9 +++ 4 files changed, 81 insertions(+), 73 deletions(-) diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 7bc6ba1..f6c3b67 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -104,14 +104,14 @@ abstract class CBZ { FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)), ); dest.createSync(); - coverFile.copy( - FilePath.join(dest.path, 'cover.${coverFile.path.split('.').last}')); + coverFile.copyMem( + FilePath.join(dest.path, 'cover.${coverFile.extension}')); if (metaData.chapters == null) { for (var i = 0; i < files.length; i++) { var src = files[i]; var dst = File( FilePath.join(dest.path, '${i + 1}.${src.path.split('.').last}')); - await src.copy(dst.path); + await src.copyMem(dst.path); } } else { dest.createSync(); @@ -129,7 +129,7 @@ abstract class CBZ { var src = chapter.value[i]; var dst = File(FilePath.join( chapterDir.path, '${i + 1}.${src.path.split('.').last}')); - await src.copy(dst.path); + await src.copyMem(dst.path); } } } @@ -142,10 +142,9 @@ abstract class CBZ { directory: dest.name, chapters: cpMap, downloadedChapters: cpMap?.keys.toList() ?? [], - cover: 'cover.${coverFile.path.split('.').last}', + cover: 'cover.${coverFile.extension}', createdAt: DateTime.now(), ); - LocalManager().add(comic); await cache.delete(recursive: true); return comic; } @@ -164,7 +163,7 @@ abstract class CBZ { var dstName = '${i.toString().padLeft(width, '0')}.${image.split('.').last}'; var dst = File(FilePath.join(cache.path, dstName)); - await src.copy(dst.path); + await src.copyMem(dst.path); i++; } } else { @@ -192,13 +191,13 @@ abstract class CBZ { var dstName = '${i.toString().padLeft(width, '0')}.${image.split('.').last}'; var dst = File(FilePath.join(cache.path, dstName)); - await src.copy(dst.path); + await src.copyMem(dst.path); i++; } } var cover = comic.coverFile; await cover - .copy(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}')); + .copyMem(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}')); await File(FilePath.join(cache.path, 'metadata.json')).writeAsString( jsonEncode( ComicMetaData( diff --git a/lib/utils/file_type.dart b/lib/utils/file_type.dart index f75ddf0..86538e1 100644 --- a/lib/utils/file_type.dart +++ b/lib/utils/file_type.dart @@ -13,7 +13,7 @@ class FileType { var mime = lookupMimeType('no-file.$ext') ?? 'application/octet-stream'; // Android doesn't support some mime types mime = switch(mime) { - 'text/javascript' => 'application/javascript', + 'text/javascript' => 'application/octet-stream', 'application/x-cbr' => 'application/octet-stream', _ => mime, }; diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index 157e306..d2db6ff 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -22,7 +22,7 @@ class ImportComic { Future cbz() async { var file = await selectFile(ext: ['cbz', 'zip']); Map> imported = {}; - if(file == null) { + if (file == null) { return false; } var controller = showLoadingDialog(App.rootContext, allowCancel: false); @@ -34,7 +34,7 @@ class ImportComic { App.rootContext.showMessage(message: e.toString()); } controller.close(); - return registerComics(imported, true); + return registerComics(imported, false); } Future ehViewer() async { @@ -63,7 +63,7 @@ class ImportComic { var comicDir = Directory( FilePath.join(comicSrc.path, comic['DIRNAME'] as String)); String titleJP = - comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String; + comic['TITLE_JPN'] == null ? "" : comic['TITLE_JPN'] as String; String title = titleJP == "" ? comic['TITLE'] as String : titleJP; int timeStamp = comic['TIME'] as int; DateTime downloadTime = timeStamp != 0 @@ -105,8 +105,7 @@ class ImportComic { if (cancelled) { break; } - var folderName = - tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag'; + var folderName = tag == '' ? '(EhViewer)Default'.tl : '(EhViewer)$tag'; var comicList = db.select(""" SELECT * FROM DOWNLOAD_DIRNAME DN @@ -133,7 +132,7 @@ class ImportComic { App.rootContext.showMessage(message: e.toString()); } controller.close(); - if(cancelled) return false; + if (cancelled) return false; return registerComics(imported, copyToLocal); } @@ -173,11 +172,10 @@ class ImportComic { //Automatically search for cover image and chapters Future _checkSingleComic(Directory directory, {String? id, - String? title, - String? subtitle, - List? tags, - DateTime? createTime}) - async { + String? title, + String? subtitle, + List? tags, + DateTime? createTime}) async { if (!(await directory.exists())) return null; var name = title ?? directory.name; if (LocalManager().findByName(name) != null) { @@ -207,12 +205,13 @@ class ImportComic { } } - if(fileList.isEmpty) { + if (fileList.isEmpty) { return null; } fileList.sort(); - coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ?? fileList.first; + coverPath = fileList.firstWhereOrNull((l) => l.startsWith('cover')) ?? + fileList.first; chapters.sort(); if (hasChapters && coverPath == '') { @@ -243,26 +242,29 @@ class ImportComic { ); } - static Future> _copyDirectories(Map data) async { - var toBeCopied = data['toBeCopied'] as List; - var destination = data['destination'] as String; - Map result = {}; - for (var dir in toBeCopied) { - var source = Directory(dir); - var dest = Directory("$destination/${source.name}"); - if (dest.existsSync()) { - // The destination directory already exists, and it is not managed by the app. - // Rename the old directory to avoid conflicts. - Log.info("Import Comic", - "Directory already exists: ${source.name}\nRenaming the old directory."); - await dest.rename( - findValidDirectoryName(dest.parent.path, "${dest.path}_old")); + static Future> _copyDirectories( + Map data) async { + return overrideIO(() async { + var toBeCopied = data['toBeCopied'] as List; + var destination = data['destination'] as String; + Map result = {}; + for (var dir in toBeCopied) { + var source = Directory(dir); + var dest = Directory("$destination/${source.name}"); + if (dest.existsSync()) { + // The destination directory already exists, and it is not managed by the app. + // Rename the old directory to avoid conflicts. + Log.info("Import Comic", + "Directory already exists: ${source.name}\nRenaming the old directory."); + dest.renameSync( + findValidDirectoryName(dest.parent.path, "${dest.path}_old")); + } + dest.createSync(); + await copyDirectory(source, dest); + result[source.path] = dest.path; } - dest.createSync(); - await copyDirectory(source, dest); - result[source.path] = dest.path; - } - return result; + return result; + }); } Future>> _copyComicsToLocalDir( @@ -284,36 +286,36 @@ class ImportComic { // copy the comics to the local directory var pathMap = await compute, Map>( _copyDirectories, { - 'toBeCopied': comics[favoriteFolder]!.map((e) => e.directory).toList(), + 'toBeCopied': + comics[favoriteFolder]!.map((e) => e.directory).toList(), 'destination': destPath, }); //Construct a new object since LocalComic.directory is a final String for (var c in comics[favoriteFolder]!) { - result[favoriteFolder]!.add( - LocalComic( - id: c.id, - title: c.title, - subtitle: c.subtitle, - tags: c.tags, - directory: pathMap[c.directory]!, - chapters: c.chapters, - cover: c.cover, - comicType: c.comicType, - downloadedChapters: c.downloadedChapters, - createdAt: c.createdAt - ) - ); + result[favoriteFolder]!.add(LocalComic( + id: c.id, + title: c.title, + subtitle: c.subtitle, + tags: c.tags, + directory: pathMap[c.directory]!, + chapters: c.chapters, + cover: c.cover, + comicType: c.comicType, + downloadedChapters: c.downloadedChapters, + createdAt: c.createdAt, + )); } - } catch (e) { + } catch (e, s) { App.rootContext.showMessage(message: "Failed to copy comics".tl); - Log.error("Import Comic", e.toString()); + Log.error("Import Comic", e.toString(), s); return result; } } return result; } - Future registerComics(Map> importedComics, bool copy) async { + Future registerComics( + Map> importedComics, bool copy) async { try { if (copy) { importedComics = await _copyComicsToLocalDir(importedComics); @@ -328,25 +330,23 @@ class ImportComic { LocalFavoritesManager().addComic( folder, FavoriteItem( - id: id, - name: comic.title, - coverPath: comic.cover, - author: comic.subtitle, - type: comic.comicType, - tags: comic.tags, - favoriteTime: comic.createdAt - ) - ); + id: id, + name: comic.title, + coverPath: comic.cover, + author: comic.subtitle, + type: comic.comicType, + tags: comic.tags, + favoriteTime: comic.createdAt)); } } } App.rootContext.showMessage( message: "Imported @a comics".tlParams({ - 'a': importedCount, - })); - } catch(e) { + 'a': importedCount, + })); + } catch (e, s) { App.rootContext.showMessage(message: "Failed to register comics".tl); - Log.error("Import Comic", e.toString()); + Log.error("Import Comic", e.toString(), s); return false; } return true; diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 12617b9..b577eeb 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -73,6 +73,15 @@ extension FileSystemEntityExt on FileSystemEntity { extension FileExtension on File { String get extension => path.split('.').last; + + /// Copy the file to the specified path using memory. + /// + /// This method prevents errors caused by files from different file systems. + Future copyMem(String newPath) async { + var newFile = File(newPath); + // Stream is not usable since [AndroidFile] does not support [openRead]. + await newFile.writeAsBytes(await readAsBytes()); + } } extension DirectoryExtension on Directory { From 425cbed8a1b9dea9acb0fa15a9d5fc240b159622 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 7 Dec 2024 20:04:22 +0800 Subject: [PATCH 23/50] fix #90: export comic as epub --- assets/translation.json | 6 +- lib/components/message.dart | 39 ++-- lib/pages/favorites/favorite_actions.dart | 2 - lib/pages/local_comics_page.dart | 28 +++ lib/utils/epub.dart | 208 ++++++++++++++++++++++ 5 files changed, 263 insertions(+), 20 deletions(-) create mode 100644 lib/utils/epub.dart diff --git a/assets/translation.json b/assets/translation.json index 985ab59..1114ff3 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -246,7 +246,8 @@ "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", "No new version available": "没有新版本可用", - "Export as pdf": "导出为pdf" + "Export as pdf": "导出为pdf", + "Export as epub": "导出为epub" }, "zh_TW": { "Home": "首頁", @@ -495,6 +496,7 @@ "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", "No new version available": "沒有新版本可用", - "Export as pdf": "匯出為pdf" + "Export as pdf": "匯出為pdf", + "Export as epub": "匯出為epub" } } \ No newline at end of file diff --git a/lib/components/message.dart b/lib/components/message.dart index 0024956..3618e40 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -46,21 +46,28 @@ class _ToastOverlay extends StatelessWidget { child: IconTheme( data: IconThemeData( color: Theme.of(context).colorScheme.onInverseSurface), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) icon!.paddingRight(8), - Text( - message, - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w500), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - if (trailing != null) trailing!.paddingLeft(8) - ], + child: IntrinsicWidth( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), + constraints: BoxConstraints( + maxWidth: context.width - 32, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) icon!.paddingRight(8), + Expanded( + child: Text( + message, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + if (trailing != null) trailing!.paddingLeft(8) + ], + ), ), ), ), @@ -220,7 +227,7 @@ LoadingDialogController showLoadingDialog(BuildContext context, ); }); - var navigator = Navigator.of(context); + var navigator = Navigator.of(context, rootNavigator: true); navigator.push(loadingDialogRoute).then((value) => controller.closed = true); diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index c6f494a..23f13f0 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -1,5 +1,3 @@ -import 'package:venera/foundation/appdata.dart'; - part of 'favorites_page.dart'; /// Open a dialog to create a new favorite folder. diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index a679dfe..cbbc979 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -7,6 +7,7 @@ import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/utils/cbz.dart'; +import 'package:venera/utils/epub.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/translations.dart'; @@ -389,6 +390,33 @@ class _LocalComicsPageState extends State { File(cache).deleteIgnoreError(); } }, + ), + if (!multiSelectMode) + MenuEntry( + icon: Icons.import_contacts_outlined, + text: "Export as epub".tl, + onClick: () async { + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + File? file; + try { + file = await createEpubWithLocalComic( + c as LocalComic, + ); + await saveFile( + file: file, + filename: "${c.title}.epub", + ); + } catch (e, s) { + Log.error("EPUB Export", e, s); + context.showMessage(message: e.toString()); + } finally { + controller.close(); + file?.deleteIgnoreError(); + } + }, ) ]; }, diff --git a/lib/utils/epub.dart b/lib/utils/epub.dart new file mode 100644 index 0000000..ed5ae98 --- /dev/null +++ b/lib/utils/epub.dart @@ -0,0 +1,208 @@ +import 'dart:isolate'; + +import 'package:uuid/uuid.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/utils/file_type.dart'; +import 'package:venera/utils/io.dart'; +import 'package:zip_flutter/zip_flutter.dart'; + +class EpubData { + final String title; + + final String author; + + final File cover; + + final Map> chapters; + + const EpubData({ + required this.title, + required this.author, + required this.cover, + required this.chapters, + }); +} + +Future createEpubComic(EpubData data, String cacheDir) async { + final workingDir = Directory(FilePath.join(cacheDir, 'epub')); + if (workingDir.existsSync()) { + workingDir.deleteSync(recursive: true); + } + workingDir.createSync(recursive: true); + + // mimetype + workingDir.joinFile('mimetype').writeAsStringSync('application/epub+zip'); + + // META-INF + Directory(FilePath.join(workingDir.path, 'META-INF')).createSync(); + File(FilePath.join(workingDir.path, 'META-INF', 'container.xml')) + .writeAsStringSync(''' + + + + + + + '''); + + Directory(FilePath.join(workingDir.path, 'OEBPS')).createSync(); + + // copy images, create html files + final imageDir = Directory(FilePath.join(workingDir.path, 'OEBPS', 'images')); + imageDir.createSync(); + final coverExt = data.cover.extension; + final coverMime = FileType.fromExtension(coverExt).mime; + imageDir + .joinFile('cover.$coverExt') + .writeAsBytesSync(data.cover.readAsBytesSync()); + int imgIndex = 0; + int chapterIndex = 0; + var manifestStrBuilder = StringBuffer(); + manifestStrBuilder.writeln( + ' '); + manifestStrBuilder.writeln( + ' '); + for (final chapter in data.chapters.keys) { + var images = []; + for (final image in data.chapters[chapter]!) { + final ext = image.extension; + imageDir + .joinFile('img$imgIndex.$ext') + .writeAsBytesSync(image.readAsBytesSync()); + images.add('images/img$imgIndex.$ext'); + var mime = FileType.fromExtension(ext).mime; + manifestStrBuilder.writeln( + ' '); + imgIndex++; + } + var html = + File(FilePath.join(workingDir.path, 'OEBPS', '$chapterIndex.html')); + html.writeAsStringSync(''' + + + + $chapter + + + +

$chapter

+
+${images.map((e) => ' $e').join('\n')} +
+ + + '''); + manifestStrBuilder.writeln( + ' '); + chapterIndex++; + } + + // content.opf + final contentOpf = + File(FilePath.join(workingDir.path, 'content.opf')); + final uuid = const Uuid().v4(); + var spineStrBuilder = StringBuffer(); + for (var i = 0; i < chapterIndex; i++) { + var idRef = 'idref="chapter$i"'; + spineStrBuilder.writeln(' '); + } + contentOpf.writeAsStringSync(''' + + + + ${data.title} + ${data.author} + urn:uuid:$uuid + + + +${manifestStrBuilder.toString()} + + +${spineStrBuilder.toString()} + + + '''); + + // toc.ncx + final tocNcx = File(FilePath.join(workingDir.path, 'toc.ncx')); + var navMapStrBuilder = StringBuffer(); + var playOrder = 2; + final chapterNames = data.chapters.keys.toList(); + for (var i = 0; i < chapterIndex; i++) { + navMapStrBuilder + .writeln(' '); + navMapStrBuilder.writeln( + ' ${chapterNames[i]}'); + navMapStrBuilder.writeln(' '); + navMapStrBuilder.writeln(' '); + playOrder++; + } + + tocNcx.writeAsStringSync(''' + + + + + + + + + + + ${data.title} + + +${navMapStrBuilder.toString()} + + + '''); + + // zip + final zipPath = FilePath.join(cacheDir, '${data.title}.epub'); + ZipFile.compressFolder(workingDir.path, zipPath); + + return File(zipPath); +} + +Future createEpubWithLocalComic(LocalComic comic) async { + var chapters = >{}; + if (comic.chapters == null) { + chapters[comic.title] = + (await LocalManager().getImages(comic.id, comic.comicType, 0)) + .map((e) => File(e)) + .toList(); + } else { + for (var chapter in comic.chapters!.keys) { + chapters[comic.chapters![chapter]!] = (await LocalManager() + .getImages(comic.id, comic.comicType, chapter)) + .map((e) => File(e)) + .toList(); + } + } + var data = EpubData( + title: comic.title, + author: comic.subtitle, + cover: comic.coverFile, + chapters: chapters, + ); + + final cacheDir = App.cachePath; + + return Isolate.run(() => overrideIO(() async { + return createEpubComic(data, cacheDir); + })); +} From e999652a3e0588a69d3c47a18c04c0c208c93cc2 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 7 Dec 2024 20:11:11 +0800 Subject: [PATCH 24/50] delete cache --- lib/utils/epub.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/utils/epub.dart b/lib/utils/epub.dart index ed5ae98..7505f00 100644 --- a/lib/utils/epub.dart +++ b/lib/utils/epub.dart @@ -175,6 +175,8 @@ ${navMapStrBuilder.toString()} final zipPath = FilePath.join(cacheDir, '${data.title}.epub'); ZipFile.compressFolder(workingDir.path, zipPath); + workingDir.deleteSync(recursive: true); + return File(zipPath); } From ef435dcaa5cd5ae50810ee2425c220c9caed3dd2 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 8 Dec 2024 17:56:30 +0800 Subject: [PATCH 25/50] fix #61 --- lib/components/comic.dart | 12 +- lib/components/menu.dart | 8 +- lib/components/navigation_bar.dart | 307 +++++++++++------------------ lib/foundation/app.dart | 13 -- lib/foundation/appdata.dart | 2 +- lib/main.dart | 178 +++++++++-------- lib/pages/settings/appearance.dart | 1 + pubspec.lock | 8 + pubspec.yaml | 1 + 9 files changed, 237 insertions(+), 293 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index d175649..b8e5061 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -1012,11 +1012,15 @@ class ComicListState extends State { while (_data[page] == null) { await _fetchNext(); } - setState(() {}); + if(mounted) { + setState(() {}); + } } catch (e) { - setState(() { - _error = e.toString(); - }); + if(mounted) { + setState(() { + _error = e.toString(); + }); + } } } } finally { diff --git a/lib/components/menu.dart b/lib/components/menu.dart index 1e13448..7ba17f0 100644 --- a/lib/components/menu.dart +++ b/lib/components/menu.dart @@ -51,12 +51,10 @@ class _MenuRoute extends PopupRoute { ], ), child: BlurEffect( - borderRadius: BorderRadius.circular(4), + borderRadius: BorderRadius.circular(8), child: Material( - color: context.brightness == Brightness.light - ? const Color(0xFFFAFAFA).withOpacity(0.82) - : const Color(0xFF090909).withOpacity(0.82), - borderRadius: BorderRadius.circular(4), + color: context.colorScheme.surface.withOpacity(0.82), + borderRadius: BorderRadius.circular(8), child: Container( width: width, padding: diff --git a/lib/components/navigation_bar.dart b/lib/components/navigation_bar.dart index 7965921..09dc06e 100644 --- a/lib/components/navigation_bar.dart +++ b/lib/components/navigation_bar.dart @@ -23,14 +23,15 @@ class PaneActionEntry { } class NaviPane extends StatefulWidget { - const NaviPane({required this.paneItems, - required this.paneActions, - required this.pageBuilder, - this.initialPage = 0, - this.onPageChanged, - required this.observer, - required this.navigatorKey, - super.key}); + const NaviPane( + {required this.paneItems, + required this.paneActions, + required this.pageBuilder, + this.initialPage = 0, + this.onPageChanged, + required this.observer, + required this.navigatorKey, + super.key}); final List paneItems; @@ -84,17 +85,14 @@ class NaviPaneState extends State static const _kBottomBarHeight = 58.0; - static const _kFoldedSideBarWidth = 80.0; + static const _kFoldedSideBarWidth = 72.0; - static const _kSideBarWidth = 256.0; + static const _kSideBarWidth = 224.0; static const _kTopBarHeight = 48.0; double get bottomBarHeight => - _kBottomBarHeight + MediaQuery - .of(context) - .padding - .bottom; + _kBottomBarHeight + MediaQuery.of(context).padding.bottom; void onNavigatorStateChange() { onRebuild(context); @@ -136,10 +134,7 @@ class NaviPaneState extends State } double targetFormContext(BuildContext context) { - var width = MediaQuery - .of(context) - .size - .width; + var width = MediaQuery.of(context).size.width; double target = 0; if (width > changePoint) { target = 2; @@ -208,14 +203,13 @@ class NaviPaneState extends State return Navigator( observers: [widget.observer], key: widget.navigatorKey, - onGenerateRoute: (settings) => - AppPageRoute( - preventRebuild: false, - isRootRoute: true, - builder: (context) { - return _NaviMainView(state: this); - }, - ), + onGenerateRoute: (settings) => AppPageRoute( + preventRebuild: false, + isRootRoute: true, + builder: (context) { + return _NaviMainView(state: this); + }, + ), ); } @@ -252,20 +246,14 @@ class NaviPaneState extends State Widget buildBottom() { return Material( - textStyle: Theme - .of(context) - .textTheme - .labelSmall, + textStyle: Theme.of(context).textTheme.labelSmall, elevation: 0, child: Container( height: _kBottomBarHeight, decoration: BoxDecoration( border: Border( top: BorderSide( - color: Theme - .of(context) - .colorScheme - .outlineVariant, + color: Theme.of(context).colorScheme.outlineVariant, width: 1, ), ), @@ -273,7 +261,7 @@ class NaviPaneState extends State child: Row( children: List.generate( widget.paneItems.length, - (index) { + (index) { return Expanded( child: _SingleBottomNaviWidget( enabled: currentPage == index, @@ -293,7 +281,7 @@ class NaviPaneState extends State Widget buildLeft() { final value = controller.value; - const paddingHorizontal = 16.0; + const paddingHorizontal = 12.0; return Material( child: Container( width: _kFoldedSideBarWidth + @@ -303,57 +291,39 @@ class NaviPaneState extends State decoration: BoxDecoration( border: Border( right: BorderSide( - color: Theme - .of(context) - .colorScheme - .outlineVariant, - width: 1, + color: Theme.of(context).colorScheme.outlineVariant, + width: 1.0, ), ), ), - child: Row( + child: Column( children: [ - SizedBox( - width: value == 3 - ? (_kSideBarWidth - paddingHorizontal * 2 - 1) - : (_kFoldedSideBarWidth - paddingHorizontal * 2 - 1), - child: Column( - children: [ - const SizedBox(height: 16), - SizedBox(height: MediaQuery - .of(context) - .padding - .top), - ...List.generate( - widget.paneItems.length, - (index) => - _SideNaviWidget( - enabled: currentPage == index, - entry: widget.paneItems[index], - showTitle: value == 3, - onTap: () { - updatePage(index); - }, - key: ValueKey(index), - ), - ), - const Spacer(), - ...List.generate( - widget.paneActions.length, - (index) => - _PaneActionWidget( - entry: widget.paneActions[index], - showTitle: value == 3, - key: ValueKey(index + widget.paneItems.length), - ), - ), - const SizedBox( - height: 16, - ) - ], + const SizedBox(height: 16), + SizedBox(height: MediaQuery.of(context).padding.top), + ...List.generate( + widget.paneItems.length, + (index) => _SideNaviWidget( + enabled: currentPage == index, + entry: widget.paneItems[index], + showTitle: value == 3, + onTap: () { + updatePage(index); + }, + key: ValueKey(index), ), ), const Spacer(), + ...List.generate( + widget.paneActions.length, + (index) => _PaneActionWidget( + entry: widget.paneActions[index], + showTitle: value == 3, + key: ValueKey(index + widget.paneItems.length), + ), + ), + const SizedBox( + height: 16, + ) ], ), ), @@ -361,12 +331,13 @@ class NaviPaneState extends State } } -class _SideNaviWidget extends StatefulWidget { - const _SideNaviWidget({required this.enabled, - required this.entry, - required this.onTap, - required this.showTitle, - super.key}); +class _SideNaviWidget extends StatelessWidget { + const _SideNaviWidget( + {required this.enabled, + required this.entry, + required this.onTap, + required this.showTitle, + super.key}); final bool enabled; @@ -376,60 +347,37 @@ class _SideNaviWidget extends StatefulWidget { final bool showTitle; - @override - State<_SideNaviWidget> createState() => _SideNaviWidgetState(); -} - -class _SideNaviWidgetState extends State<_SideNaviWidget> { - bool isHovering = false; - @override Widget build(BuildContext context) { - final colorScheme = Theme - .of(context) - .colorScheme; - final icon = - Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon); - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (details) => setState(() => isHovering = true), - onExit: (details) => setState(() => isHovering = false), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: widget.onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - margin: const EdgeInsets.symmetric(vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12), - width: double.infinity, - height: 42, - decoration: BoxDecoration( - color: widget.enabled - ? colorScheme.primaryContainer - : isHovering - ? colorScheme.surfaceContainerHigh - : null, - borderRadius: BorderRadius.circular(8), - ), - child: widget.showTitle - ? Row( - children: [ - icon, - const SizedBox( - width: 12, - ), - Text(widget.entry.label) - ], - ) - : Center( - child: icon, - )), + final colorScheme = Theme.of(context).colorScheme; + final icon = Icon(enabled ? entry.activeIcon : entry.icon); + return InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 12), + height: 38, + decoration: BoxDecoration( + color: enabled ? colorScheme.primaryContainer : null, + borderRadius: BorderRadius.circular(12), + ), + child: showTitle ? Row( + children: [ + icon, + const SizedBox(width: 12), + Text(entry.label) + ], + ) : Align( + alignment: Alignment.centerLeft, + child: icon, + ), ), - ); + ).paddingVertical(4); } } -class _PaneActionWidget extends StatefulWidget { +class _PaneActionWidget extends StatelessWidget { const _PaneActionWidget( {required this.entry, required this.showTitle, super.key}); @@ -437,58 +385,37 @@ class _PaneActionWidget extends StatefulWidget { final bool showTitle; - @override - State<_PaneActionWidget> createState() => _PaneActionWidgetState(); -} - -class _PaneActionWidgetState extends State<_PaneActionWidget> { - bool isHovering = false; - @override Widget build(BuildContext context) { - final colorScheme = Theme - .of(context) - .colorScheme; - final icon = Icon(widget.entry.icon); - return MouseRegion( - cursor: SystemMouseCursors.click, - onEnter: (details) => setState(() => isHovering = true), - onExit: (details) => setState(() => isHovering = false), - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: widget.entry.onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - margin: const EdgeInsets.symmetric(vertical: 4), - padding: const EdgeInsets.symmetric(horizontal: 12), - width: double.infinity, - height: 42, - decoration: BoxDecoration( - color: isHovering ? colorScheme.surfaceContainerHigh : null, - borderRadius: BorderRadius.circular(8)), - child: widget.showTitle - ? Row( - children: [ - icon, - const SizedBox( - width: 12, - ), - Text(widget.entry.label) - ], - ) - : Center( - child: icon, - )), + final icon = Icon(entry.icon); + return InkWell( + onTap: entry.onTap, + borderRadius: BorderRadius.circular(12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 12), + height: 38, + child: showTitle ? Row( + children: [ + icon, + const SizedBox(width: 12), + Text(entry.label) + ], + ) : Align( + alignment: Alignment.centerLeft, + child: icon, + ), ), - ); + ).paddingVertical(4); } } class _SingleBottomNaviWidget extends StatefulWidget { - const _SingleBottomNaviWidget({required this.enabled, - required this.entry, - required this.onTap, - super.key}); + const _SingleBottomNaviWidget( + {required this.enabled, + required this.entry, + required this.onTap, + super.key}); final bool enabled; @@ -556,11 +483,9 @@ class _SingleBottomNaviWidgetState extends State<_SingleBottomNaviWidget> Widget buildContent() { final value = controller.value; - final colorScheme = Theme - .of(context) - .colorScheme; + final colorScheme = Theme.of(context).colorScheme; final icon = - Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon); + Icon(widget.enabled ? widget.entry.activeIcon : widget.entry.icon); return Center( child: Container( width: 64, @@ -661,12 +586,12 @@ class _NaviPopScope extends StatelessWidget { Widget res = App.isIOS ? child : PopScope( - canPop: App.isAndroid ? false : true, - onPopInvokedWithResult: (value, result) { - action(); - }, - child: child, - ); + canPop: App.isAndroid ? false : true, + onPopInvokedWithResult: (value, result) { + action(); + }, + child: child, + ); if (popGesture) { res = GestureDetector( onPanStart: (details) { @@ -725,8 +650,8 @@ class _NaviMainViewState extends State<_NaviMainView> { ), ), ), - if (shouldShowAppBar) state.buildBottom().paddingBottom( - context.padding.bottom), + if (shouldShowAppBar) + state.buildBottom().paddingBottom(context.padding.bottom), ], ); } diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 9c94bd4..22e2da1 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -63,22 +63,9 @@ class _App { } } - var mainColor = Colors.blue; - Future init() async { cachePath = (await getApplicationCacheDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path; - mainColor = switch (appdata.settings['color']) { - 'red' => Colors.red, - 'pink' => Colors.pink, - 'purple' => Colors.purple, - 'green' => Colors.green, - 'orange' => Colors.orange, - 'blue' => Colors.blue, - 'yellow' => Colors.yellow, - 'cyan' => Colors.cyan, - _ => Colors.blue, - }; } Function? _forceRebuildHandler; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index c113a00..128d0bf 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -92,7 +92,7 @@ class _Settings with ChangeNotifier { final _data = { 'comicDisplayMode': 'detailed', // detailed, brief 'comicTileScale': 1.00, // 0.75-1.25 - 'color': 'blue', // red, pink, purple, green, orange, blue + 'color': 'system', // red, pink, purple, green, orange, blue 'theme_mode': 'system', // light, dark, system 'newFavoriteAddTo': 'end', // start, end 'moveFavoriteAfterRead': 'none', // none, end, start diff --git a/lib/main.dart b/lib/main.dart index 01cd575..100c974 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; @@ -128,6 +129,20 @@ class _MyAppState extends State with WidgetsBindingObserver { setState(() {}); } + Color translateColorSetting() { + return switch (appdata.settings['color']) { + 'red' => Colors.red, + 'pink' => Colors.pink, + 'purple' => Colors.purple, + 'green' => Colors.green, + 'orange' => Colors.orange, + 'blue' => Colors.blue, + 'yellow' => Colors.yellow, + 'cyan' => Colors.cyan, + _ => Colors.blue, + }; + } + @override Widget build(BuildContext context) { Widget home; @@ -140,90 +155,95 @@ class _MyAppState extends State with WidgetsBindingObserver { } else { home = const MainPage(); } - return MaterialApp( - home: home, - debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: App.mainColor, - surface: Colors.white, - primary: App.mainColor.shade600, - // ignore: deprecated_member_use - background: Colors.white, - ), - fontFamily: App.isWindows ? "Microsoft YaHei" : null, - ), - navigatorKey: App.rootNavigatorKey, - darkTheme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: App.mainColor, + return DynamicColorBuilder(builder: (light, dark) { + if (appdata.settings['color'] != 'system' || light == null || dark == null) { + var color = translateColorSetting(); + light = ColorScheme.fromSeed( + seedColor: color, + ); + dark = ColorScheme.fromSeed( + seedColor: color, brightness: Brightness.dark, - surface: Colors.black, - primary: App.mainColor.shade400, - // ignore: deprecated_member_use - background: Colors.black, + ); + } + return MaterialApp( + home: home, + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: light.copyWith( + surface: Colors.white, + background: Colors.white, + ), + fontFamily: App.isWindows ? "Microsoft YaHei" : null, ), - fontFamily: App.isWindows ? "Microsoft YaHei" : null, - ), - themeMode: switch (appdata.settings['theme_mode']) { - 'light' => ThemeMode.light, - 'dark' => ThemeMode.dark, - _ => ThemeMode.system - }, - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - locale: () { - var lang = appdata.settings['language']; - if (lang == 'system') { - return null; - } - return switch (lang) { - 'zh-CN' => const Locale('zh', 'CN'), - 'zh-TW' => const Locale('zh', 'TW'), - 'en-US' => const Locale('en'), - _ => null - }; - }(), - supportedLocales: const [ - Locale('en'), - Locale('zh', 'CN'), - Locale('zh', 'TW'), - ], - builder: (context, widget) { - ErrorWidget.builder = (details) { - Log.error( - "Unhandled Exception", "${details.exception}\n${details.stack}"); - return Material( - child: Center( - child: Text(details.exception.toString()), - ), - ); - }; - if (widget != null) { - widget = OverlayWidget(widget); - if (App.isDesktop) { - widget = Shortcuts( - shortcuts: { - LogicalKeySet(LogicalKeyboardKey.escape): VoidCallbackIntent( - App.pop, - ), - }, - child: MouseBackDetector( - onTapDown: App.pop, - child: WindowFrame(widget), + navigatorKey: App.rootNavigatorKey, + darkTheme: ThemeData( + colorScheme: dark.copyWith( + surface: Colors.black, + background: Colors.black, + ), + fontFamily: App.isWindows ? "Microsoft YaHei" : null, + ), + themeMode: switch (appdata.settings['theme_mode']) { + 'light' => ThemeMode.light, + 'dark' => ThemeMode.dark, + _ => ThemeMode.system + }, + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + locale: () { + var lang = appdata.settings['language']; + if (lang == 'system') { + return null; + } + return switch (lang) { + 'zh-CN' => const Locale('zh', 'CN'), + 'zh-TW' => const Locale('zh', 'TW'), + 'en-US' => const Locale('en'), + _ => null + }; + }(), + supportedLocales: const [ + Locale('en'), + Locale('zh', 'CN'), + Locale('zh', 'TW'), + ], + builder: (context, widget) { + ErrorWidget.builder = (details) { + Log.error( + "Unhandled Exception", "${details.exception}\n${details.stack}"); + return Material( + child: Center( + child: Text(details.exception.toString()), ), ); + }; + if (widget != null) { + widget = OverlayWidget(widget); + if (App.isDesktop) { + widget = Shortcuts( + shortcuts: { + LogicalKeySet(LogicalKeyboardKey.escape): VoidCallbackIntent( + App.pop, + ), + }, + child: MouseBackDetector( + onTapDown: App.pop, + child: WindowFrame(widget), + ), + ); + } + return _SystemUiProvider(Material( + child: widget, + )); } - return _SystemUiProvider(Material( - child: widget, - )); - } - throw ('widget is null'); - }, - ); + throw ('widget is null'); + }, + ); + }); } } diff --git a/lib/pages/settings/appearance.dart b/lib/pages/settings/appearance.dart index 2f96bd8..fcc7a17 100644 --- a/lib/pages/settings/appearance.dart +++ b/lib/pages/settings/appearance.dart @@ -29,6 +29,7 @@ class _AppearanceSettingsState extends State { title: "Theme Color".tl, settingKey: "color", optionTranslation: { + "system": "System".tl, "red": "Red".tl, "pink": "Pink".tl, "purple": "Purple".tl, diff --git a/pubspec.lock b/pubspec.lock index 9f64ace..e85ba84 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -194,6 +194,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" fake_async: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index da623e6..7b4097e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: url: https://github.com/pkuislm/flutter_saf.git ref: 3315082b9f7055655610e4f6f136b69e48228c05 pdf: ^3.11.1 + dynamic_color: ^1.7.0 dev_dependencies: flutter_test: From 835b40860d1260001b5091d8a62513a3f71cac7b Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 9 Dec 2024 17:56:44 +0800 Subject: [PATCH 26/50] aggregated search --- assets/translation.json | 6 +- lib/components/gesture.dart | 45 +++++- lib/components/scroll.dart | 3 + lib/components/select.dart | 7 +- lib/pages/aggregated_search_page.dart | 213 ++++++++++++++++++++++++++ lib/pages/search_page.dart | 53 +++++-- lib/pages/search_result_page.dart | 6 +- pubspec.lock | 8 + pubspec.yaml | 1 + 9 files changed, 320 insertions(+), 22 deletions(-) create mode 100644 lib/pages/aggregated_search_page.dart diff --git a/assets/translation.json b/assets/translation.json index 1114ff3..c10221f 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -247,7 +247,8 @@ "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", "No new version available": "没有新版本可用", "Export as pdf": "导出为pdf", - "Export as epub": "导出为epub" + "Export as epub": "导出为epub", + "Aggregated Search": "聚合搜索" }, "zh_TW": { "Home": "首頁", @@ -497,6 +498,7 @@ "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", "No new version available": "沒有新版本可用", "Export as pdf": "匯出為pdf", - "Export as epub": "匯出為epub" + "Export as epub": "匯出為epub", + "Aggregated Search": "聚合搜索" } } \ No newline at end of file diff --git a/lib/components/gesture.dart b/lib/components/gesture.dart index bd345f2..5efc32f 100644 --- a/lib/components/gesture.dart +++ b/lib/components/gesture.dart @@ -1,7 +1,8 @@ part of 'components.dart'; class MouseBackDetector extends StatelessWidget { - const MouseBackDetector({super.key, required this.onTapDown, required this.child}); + const MouseBackDetector( + {super.key, required this.onTapDown, required this.child}); final Widget child; @@ -20,3 +21,45 @@ class MouseBackDetector extends StatelessWidget { ); } } + +class AnimatedTapRegion extends StatefulWidget { + const AnimatedTapRegion({ + super.key, + required this.child, + required this.onTap, + this.borderRadius = 0, + }); + + final Widget child; + + final void Function() onTap; + + final double borderRadius; + + @override + State createState() => _AnimatedTapRegionState(); +} + +class _AnimatedTapRegionState extends State { + bool isHovered = false; + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: (_) => setState(() => isHovered = true), + onExit: (_) => setState(() => isHovered = false), + child: GestureDetector( + onTap: widget.onTap, + child: ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + clipBehavior: Clip.antiAlias, + child: AnimatedScale( + duration: _fastAnimationDuration, + scale: isHovered ? 1.1 : 1, + child: widget.child, + ), + ), + ), + ); + } +} diff --git a/lib/components/scroll.dart b/lib/components/scroll.dart index 9cbe82c..98fd287 100644 --- a/lib/components/scroll.dart +++ b/lib/components/scroll.dart @@ -78,6 +78,9 @@ class _SmoothScrollProviderState extends State { }, onPointerSignal: (pointerSignal) { if (pointerSignal is PointerScrollEvent) { + if (HardwareKeyboard.instance.isShiftPressed) { + return; + } if (pointerSignal.kind == PointerDeviceKind.mouse && !_isMouseScroll) { setState(() { diff --git a/lib/components/select.dart b/lib/components/select.dart index e2b9e05..201c78f 100644 --- a/lib/components/select.dart +++ b/lib/components/select.dart @@ -267,13 +267,14 @@ class OptionChip extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( + return AnimatedContainer( + duration: _fastAnimationDuration, decoration: BoxDecoration( color: isSelected - ? context.colorScheme.primaryContainer + ? context.colorScheme.secondaryContainer : context.colorScheme.surface, border: isSelected - ? Border.all(color: context.colorScheme.primaryContainer) + ? Border.all(color: context.colorScheme.secondaryContainer) : Border.all(color: context.colorScheme.outline), borderRadius: BorderRadius.circular(8), ), diff --git a/lib/pages/aggregated_search_page.dart b/lib/pages/aggregated_search_page.dart new file mode 100644 index 0000000..10883ad --- /dev/null +++ b/lib/pages/aggregated_search_page.dart @@ -0,0 +1,213 @@ +import "package:flutter/material.dart"; +import "package:shimmer/shimmer.dart"; +import "package:venera/components/components.dart"; +import "package:venera/foundation/app.dart"; +import "package:venera/foundation/comic_source/comic_source.dart"; +import "package:venera/foundation/image_provider/cached_image.dart"; +import "package:venera/pages/search_result_page.dart"; + +import "comic_page.dart"; + +class AggregatedSearchPage extends StatefulWidget { + const AggregatedSearchPage({super.key, required this.keyword}); + + final String keyword; + + @override + State createState() => _AggregatedSearchPageState(); +} + +class _AggregatedSearchPageState extends State { + late final List sources; + + late final SearchBarController controller; + + var _keyword = ""; + + @override + void initState() { + sources = ComicSource.all().where((e) => e.searchPageData != null).toList(); + _keyword = widget.keyword; + controller = SearchBarController( + currentText: widget.keyword, + onSearch: (text) { + setState(() { + _keyword = text; + }); + }, + ); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return SmoothCustomScrollView(slivers: [ + SliverSearchBar(controller: controller), + SliverList( + key: ValueKey(_keyword), + delegate: SliverChildBuilderDelegate( + (context, index) { + final source = sources[index]; + return _SliverSearchResult(source: source, keyword: widget.keyword); + }, + childCount: sources.length, + ), + ), + ]); + } +} + +class _SliverSearchResult extends StatefulWidget { + const _SliverSearchResult({required this.source, required this.keyword}); + + final ComicSource source; + + final String keyword; + + @override + State<_SliverSearchResult> createState() => _SliverSearchResultState(); +} + +class _SliverSearchResultState extends State<_SliverSearchResult> + with AutomaticKeepAliveClientMixin { + bool isLoading = true; + + static const _kComicHeight = 144.0; + + get _comicWidth => _kComicHeight * 0.72; + + static const _kLeftPadding = 16.0; + + List? comics; + + void load() async { + final data = widget.source.searchPageData!; + var options = + (data.searchOptions ?? []).map((e) => e.defaultValue).toList(); + if (data.loadPage != null) { + var res = await data.loadPage!(widget.keyword, 1, options); + if (!res.error) { + setState(() { + comics = res.data; + isLoading = false; + }); + } + } else if (data.loadNext != null) { + var res = await data.loadNext!(widget.keyword, null, options); + if (!res.error) { + setState(() { + comics = res.data; + isLoading = false; + }); + } + } + } + + @override + void initState() { + super.initState(); + load(); + } + + Widget buildPlaceHolder() { + return Container( + height: _kComicHeight, + width: _comicWidth, + margin: const EdgeInsets.only(left: _kLeftPadding), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerLow, + borderRadius: BorderRadius.circular(8), + ), + ); + } + + Widget buildComic(Comic c) { + return AnimatedTapRegion( + borderRadius: 8, + onTap: () { + context.to(() => ComicPage( + id: c.id, + sourceKey: c.sourceKey, + )); + }, + child: Container( + height: _kComicHeight, + width: _comicWidth, + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainerLow, + ), + child: AnimatedImage( + width: _comicWidth, + height: _kComicHeight, + fit: BoxFit.cover, + image: CachedImageProvider(c.cover), + ), + ), + ).paddingLeft(_kLeftPadding); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return InkWell( + onTap: () { + context.to( + () => SearchResultPage( + text: widget.keyword, + sourceKey: widget.source.key, + ), + ); + }, + child: Column( + children: [ + ListTile( + mouseCursor: SystemMouseCursors.click, + title: Text(widget.source.name), + ), + if (isLoading) + SizedBox( + height: _kComicHeight, + width: double.infinity, + child: Shimmer.fromColors( + baseColor: context.colorScheme.surfaceContainerLow, + highlightColor: context.colorScheme.surfaceContainer, + direction: ShimmerDirection.ltr, + child: LayoutBuilder(builder: (context, constrains) { + var itemWidth = _comicWidth + _kLeftPadding; + var items = (constrains.maxWidth / itemWidth).ceil(); + return Stack( + children: [ + Positioned( + left: 0, + top: 0, + bottom: 0, + child: Row( + children: List.generate( + items, + (index) => buildPlaceHolder(), + ), + ), + ) + ], + ); + }), + ), + ) + else + SizedBox( + height: _kComicHeight, + child: ListView( + scrollDirection: Axis.horizontal, + children: [ + for (var c in comics!) buildComic(c), + ], + ), + ), + ], + ).paddingBottom(16), + ); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 735c7e5..44bf020 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -7,6 +7,7 @@ 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/state_controller.dart'; +import 'package:venera/pages/aggregated_search_page.dart'; import 'package:venera/pages/search_result_page.dart'; import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/ext.dart'; @@ -27,6 +28,8 @@ class _SearchPageState extends State { String searchTarget = ""; + bool aggregatedSearch = false; + var focusNode = FocusNode(); var options = []; @@ -36,15 +39,21 @@ class _SearchPageState extends State { } void search([String? text]) { - context - .to( - () => SearchResultPage( - text: text ?? controller.text, - sourceKey: searchTarget, - options: options, - ), - ) - .then((_) => update()); + if (aggregatedSearch) { + context + .to(() => AggregatedSearchPage(keyword: text ?? controller.text)) + .then((_) => update()); + } else { + context + .to( + () => SearchResultPage( + text: text ?? controller.text, + sourceKey: searchTarget, + options: options, + ), + ) + .then((_) => update()); + } } var suggestions = >[]; @@ -189,6 +198,7 @@ class _SearchPageState extends State { children: [ ListTile( contentPadding: EdgeInsets.zero, + leading: const Icon(Icons.search), title: Text("Search in".tl), ), Wrap( @@ -197,8 +207,9 @@ class _SearchPageState extends State { children: sources.map((e) { return OptionChip( text: e.name, - isSelected: searchTarget == e.key, + isSelected: searchTarget == e.key || aggregatedSearch, onTap: () { + if (aggregatedSearch) return; setState(() { searchTarget = e.key; useDefaultOptions(); @@ -207,6 +218,18 @@ class _SearchPageState extends State { ); }).toList(), ), + ListTile( + contentPadding: EdgeInsets.zero, + title: Text("Aggregated Search".tl), + leading: Checkbox( + value: aggregatedSearch, + onChanged: (value) { + setState(() { + aggregatedSearch = value ?? false; + }); + }, + ), + ), ], ), ), @@ -221,6 +244,10 @@ class _SearchPageState extends State { } Widget buildSearchOptions() { + if (aggregatedSearch) { + return const SliverToBoxAdapter(child: SizedBox()); + } + var children = []; final searchOptions = @@ -262,9 +289,9 @@ class _SearchPageState extends State { delegate: SliverChildBuilderDelegate( (context, index) { if (index == 0) { - return const Divider( - thickness: 0.6, - ).paddingTop(16); + return const SizedBox( + height: 16, + ); } if (index == 1) { return ListTile( diff --git a/lib/pages/search_result_page.dart b/lib/pages/search_result_page.dart index 1d67e5f..9543c0b 100644 --- a/lib/pages/search_result_page.dart +++ b/lib/pages/search_result_page.dart @@ -14,14 +14,14 @@ class SearchResultPage extends StatefulWidget { super.key, required this.text, required this.sourceKey, - required this.options, + this.options, }); final String text; final String sourceKey; - final List options; + final List? options; @override State createState() => _SearchResultPageState(); @@ -99,7 +99,7 @@ class _SearchResultPageState extends State { onSearch: search, ); sourceKey = widget.sourceKey; - options = widget.options; + options = widget.options ?? const []; validateOptions(); text = widget.text; appdata.addSearchHistory(text); diff --git a/pubspec.lock b/pubspec.lock index e85ba84..4d57838 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -852,6 +852,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.1" + shimmer: + dependency: "direct main" + description: + name: shimmer + sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" + url: "https://pub.dev" + source: hosted + version: "3.0.0" sky_engine: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7b4097e..e4da6a9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,6 +71,7 @@ dependencies: ref: 3315082b9f7055655610e4f6f136b69e48228c05 pdf: ^3.11.1 dynamic_color: ^1.7.0 + shimmer: ^3.0.0 dev_dependencies: flutter_test: From 14fe9011443f1e7041256ccd4d7bb67c8970b21b Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 9 Dec 2024 18:06:35 +0800 Subject: [PATCH 27/50] improve ui --- assets/translation.json | 6 ++++-- lib/pages/aggregated_search_page.dart | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index c10221f..4cd82c7 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -248,7 +248,8 @@ "No new version available": "没有新版本可用", "Export as pdf": "导出为pdf", "Export as epub": "导出为epub", - "Aggregated Search": "聚合搜索" + "Aggregated Search": "聚合搜索", + "No search results found": "未找到搜索结果" }, "zh_TW": { "Home": "首頁", @@ -499,6 +500,7 @@ "No new version available": "沒有新版本可用", "Export as pdf": "匯出為pdf", "Export as epub": "匯出為epub", - "Aggregated Search": "聚合搜索" + "Aggregated Search": "聚合搜索", + "No search results found": "未找到搜索結果" } } \ No newline at end of file diff --git a/lib/pages/aggregated_search_page.dart b/lib/pages/aggregated_search_page.dart index 10883ad..16007cd 100644 --- a/lib/pages/aggregated_search_page.dart +++ b/lib/pages/aggregated_search_page.dart @@ -5,6 +5,7 @@ import "package:venera/foundation/app.dart"; import "package:venera/foundation/comic_source/comic_source.dart"; import "package:venera/foundation/image_provider/cached_image.dart"; import "package:venera/pages/search_result_page.dart"; +import "package:venera/utils/translations.dart"; import "comic_page.dart"; @@ -48,7 +49,7 @@ class _AggregatedSearchPageState extends State { delegate: SliverChildBuilderDelegate( (context, index) { final source = sources[index]; - return _SliverSearchResult(source: source, keyword: widget.keyword); + return _SliverSearchResult(source: source, keyword: _keyword); }, childCount: sources.length, ), @@ -193,6 +194,22 @@ class _SliverSearchResultState extends State<_SliverSearchResult> }), ), ) + else if (comics == null || comics!.isEmpty) + SizedBox( + height: _kComicHeight, + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.error_outline), + const SizedBox(width: 8), + Text("No search results found".tl), + ], + ), + const Spacer(), + ], + ).paddingHorizontal(16), + ) else SizedBox( height: _kComicHeight, From 4e121748cd4ca52905cc7b659d0409263f3bd208 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 9 Dec 2024 19:56:43 +0800 Subject: [PATCH 28/50] Use PageStorage to store state --- lib/components/appbar.dart | 13 ++- lib/components/comic.dart | 35 ++++++ lib/pages/categories_page.dart | 1 + lib/pages/explore_page.dart | 202 +++++++++++++++++++++------------ lib/pages/main_page.dart | 16 ++- 5 files changed, 188 insertions(+), 79 deletions(-) diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 0550e60..825dc14 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -281,7 +281,9 @@ class _FilledTabBarState extends State { _IndicatorPainter? painter; - var scrollController = ScrollController(); + var scrollController = ScrollController( + keepScrollOffset: false + ); var tabBarKey = GlobalKey(); @@ -298,12 +300,20 @@ class _FilledTabBarState extends State { super.dispose(); } + PageStorageBucket get bucket => PageStorage.of(context); + @override void didChangeDependencies() { _controller = widget.controller ?? DefaultTabController.of(context); _controller.animation!.addListener(onTabChanged); initPainter(); super.didChangeDependencies(); + var prevIndex = bucket.readState(context) as int?; + if (prevIndex != null && prevIndex != _controller.index) { + Future.microtask(() { + _controller.index = prevIndex; + }); + } } @override @@ -387,6 +397,7 @@ class _FilledTabBarState extends State { } updateScrollOffset(i); previousIndex = i; + bucket.writeState(context, i); } void updateScrollOffset(int i) { diff --git a/lib/components/comic.dart b/lib/components/comic.dart index b8e5061..2c6b457 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -865,6 +865,39 @@ class ComicListState extends State { String? _nextUrl; + Map get state => { + 'maxPage': _maxPage, + 'data': _data, + 'page': _page, + 'error': _error, + 'loading': _loading, + 'nextUrl': _nextUrl, + }; + + void restoreState(Map? state) { + if (state == null) { + return; + } + _maxPage = state['maxPage']; + _data.clear(); + _data.addAll(state['data']); + _page = state['page']; + _error = state['error']; + _loading.clear(); + _loading.addAll(state['loading']); + _nextUrl = state['nextUrl']; + } + + void storeState() { + PageStorage.of(context).writeState(context, state); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + restoreState(PageStorage.of(context).readState(context)); + } + void remove(Comic c) { if (_data[_page] == null || !_data[_page]!.remove(c)) { for (var page in _data.values) { @@ -1025,6 +1058,7 @@ class ComicListState extends State { } } finally { _loading[page] = false; + storeState(); } } @@ -1073,6 +1107,7 @@ class ComicListState extends State { ); } return SmoothCustomScrollView( + key: const PageStorageKey('scroll'), controller: widget.controller, slivers: [ if (widget.leadingSliver != null) widget.leadingSliver!, diff --git a/lib/pages/categories_page.dart b/lib/pages/categories_page.dart index fe2bb45..6ea4e93 100644 --- a/lib/pages/categories_page.dart +++ b/lib/pages/categories_page.dart @@ -53,6 +53,7 @@ class CategoriesPage extends StatelessWidget { child: Column( children: [ FilledTabBar( + key: PageStorageKey(categories.toString()), tabs: categories.map((e) { String title = e; try { diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index abd7618..5528aa3 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -110,7 +110,7 @@ class _ExplorePageState extends State return Tab(text: i.ts(comicSource.key), key: Key(i)); } - Widget buildBody(String i) => _SingleExplorePage(i, key: Key(i)); + Widget buildBody(String i) => _SingleExplorePage(i, key: PageStorageKey(i)); Widget buildEmpty() { var msg = "No Explore Pages".tl; @@ -147,7 +147,7 @@ class _ExplorePageState extends State Widget tabBar = Material( child: FilledTabBar( - key: Key(pages.toString()), + key: PageStorageKey(pages.toString()), tabs: pages.map((e) => buildTab(e)).toList(), controller: controller, ), @@ -240,12 +240,6 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> with AutomaticKeepAliveClientMixin<_SingleExplorePage> { late final ExplorePageData data; - bool loading = true; - - String? message; - - List? parts; - late final String comicSourceKey; int key = 0; @@ -288,9 +282,19 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> Widget build(BuildContext context) { super.build(context); if (data.loadMultiPart != null) { - return buildMultiPart(); + return _MultiPartExplorePage( + key: PageStorageKey(key), + data: data, + controller: scrollController, + comicSourceKey: comicSourceKey, + ); } else if (data.loadPage != null || data.loadNext != null) { - return buildComicList(); + return ComicList( + loadPage: data.loadPage, + loadNext: data.loadNext, + key: PageStorageKey(key), + controller: scrollController, + ); } else if (data.loadMixed != null) { return _MixedExplorePage( data, @@ -305,74 +309,14 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> } } - Widget buildComicList() { - return ComicList( - loadPage: data.loadPage, - loadNext: data.loadNext, - key: ValueKey(key), - controller: scrollController, - ); - } - - void load() async { - var res = await data.loadMultiPart!(); - loading = false; - if (mounted) { - setState(() { - if (res.error) { - message = res.errorMessage; - } else { - parts = res.data; - } - }); - } - } - - Widget buildMultiPart() { - if (loading) { - load(); - return const Center( - child: CircularProgressIndicator(), - ); - } else if (message != null) { - return NetworkError( - message: message!, - retry: refresh, - withAppbar: false, - ); - } else { - return buildPage(); - } - } - - Widget buildPage() { - return SmoothCustomScrollView( - controller: scrollController, - slivers: _buildPage().toList(), - ); - } - - Iterable _buildPage() sync* { - for (var part in parts!) { - yield* _buildExplorePagePart(part, comicSourceKey); - } - } - @override Object? get tag => widget.title; @override void refresh() { - message = null; - if (data.loadMultiPart != null) { - setState(() { - loading = true; - }); - } else { - setState(() { - key++; - }); - } + setState(() { + key++; + }); } @override @@ -393,7 +337,8 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> } class _MixedExplorePage extends StatefulWidget { - const _MixedExplorePage(this.data, this.sourceKey, {super.key, this.controller}); + const _MixedExplorePage(this.data, this.sourceKey, + {super.key, this.controller}); final ExplorePageData data; @@ -518,3 +463,112 @@ Iterable _buildExplorePagePart( yield buildTitle(part); yield buildComics(part); } + +class _MultiPartExplorePage extends StatefulWidget { + const _MultiPartExplorePage({ + super.key, + required this.data, + required this.controller, + required this.comicSourceKey, + }); + + final ExplorePageData data; + + final ScrollController controller; + + final String comicSourceKey; + + @override + State<_MultiPartExplorePage> createState() => _MultiPartExplorePageState(); +} + +class _MultiPartExplorePageState extends State<_MultiPartExplorePage> { + late final ExplorePageData data; + + List? parts; + + bool loading = true; + + String? message; + + Map get state => { + "loading": loading, + "message": message, + "parts": parts, + }; + + void restoreState(dynamic state) { + if (state == null) return; + loading = state["loading"]; + message = state["message"]; + parts = state["parts"]; + } + + void storeState() { + PageStorage.of(context).writeState(context, state); + } + + @override + void initState() { + super.initState(); + data = widget.data; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + restoreState(PageStorage.of(context).readState(context)); + } + + void load() async { + var res = await data.loadMultiPart!(); + loading = false; + if (mounted) { + setState(() { + if (res.error) { + message = res.errorMessage; + } else { + parts = res.data; + } + }); + storeState(); + } + } + + @override + Widget build(BuildContext context) { + if (loading) { + load(); + return const Center( + child: CircularProgressIndicator(), + ); + } else if (message != null) { + return NetworkError( + message: message!, + retry: () { + setState(() { + loading = true; + message = null; + }); + }, + withAppbar: false, + ); + } else { + return buildPage(); + } + } + + Widget buildPage() { + return SmoothCustomScrollView( + key: const PageStorageKey('scroll'), + controller: widget.controller, + slivers: _buildPage().toList(), + ); + } + + Iterable _buildPage() sync* { + for (var part in parts!) { + yield* _buildExplorePagePart(part, widget.comicSourceKey); + } + } +} diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index 3cba828..2801e55 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -62,10 +62,18 @@ class _MainPageState extends State { } final _pages = [ - const HomePage(), - const FavoritesPage(), - const ExplorePage(), - const CategoriesPage(), + const HomePage( + key: PageStorageKey('home'), + ), + const FavoritesPage( + key: PageStorageKey('favorites'), + ), + const ExplorePage( + key: PageStorageKey('explore'), + ), + const CategoriesPage( + key: PageStorageKey('categories'), + ), ]; var index = 0; From 659b211038fbb2055390821d38099e48d7c94146 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 9 Dec 2024 20:07:08 +0800 Subject: [PATCH 29/50] Improve TabBar --- lib/components/appbar.dart | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 825dc14..1d11a14 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -189,20 +189,19 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate { leading ?? (Navigator.of(context).canPop() ? Tooltip( - message: "Back".tl, - child: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.maybePop(context), - ), - ) + message: "Back".tl, + child: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => Navigator.maybePop(context), + ), + ) : const SizedBox()), const SizedBox( width: 16, ), Expanded( child: DefaultTextStyle( - style: - DefaultTextStyle.of(context).style.copyWith(fontSize: 20), + style: DefaultTextStyle.of(context).style.copyWith(fontSize: 20), maxLines: 1, overflow: TextOverflow.ellipsis, child: title, @@ -215,7 +214,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate { ], ).paddingTop(topPadding); - if(style == AppbarStyle.blur) { + if (style == AppbarStyle.blur) { return SizedBox.expand( child: BlurEffect( blur: 15, @@ -281,9 +280,7 @@ class _FilledTabBarState extends State { _IndicatorPainter? painter; - var scrollController = ScrollController( - keepScrollOffset: false - ); + var scrollController = ScrollController(); var tabBarKey = GlobalKey(); @@ -305,15 +302,16 @@ class _FilledTabBarState extends State { @override void didChangeDependencies() { _controller = widget.controller ?? DefaultTabController.of(context); - _controller.animation!.addListener(onTabChanged); initPainter(); super.didChangeDependencies(); var prevIndex = bucket.readState(context) as int?; - if (prevIndex != null && prevIndex != _controller.index) { - Future.microtask(() { - _controller.index = prevIndex; - }); + if (prevIndex != null && + prevIndex != _controller.index && + prevIndex >= 0 && + prevIndex < widget.tabs.length) { + _controller.index = prevIndex; } + _controller.animation!.addListener(onTabChanged); } @override @@ -357,6 +355,7 @@ class _FilledTabBarState extends State { controller: scrollController, builder: (context, controller, physics) { return SingleChildScrollView( + key: const PageStorageKey('scroll'), scrollDirection: Axis.horizontal, padding: EdgeInsets.zero, controller: controller, From 07f8cd2455fda3d05282abe60e57661af20ece1c Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 10 Dec 2024 14:45:48 +0800 Subject: [PATCH 30/50] improve explore page loading --- lib/components/comic.dart | 15 +++++++++++ lib/pages/explore_page.dart | 50 ++++++++++++++++++++++++++++++------- 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 2c6b457..ddccd21 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -832,6 +832,7 @@ class ComicList extends StatefulWidget { this.errorLeading, this.menuBuilder, this.controller, + this.refreshHandlerCallback, }); final Future>> Function(int page)? loadPage; @@ -848,6 +849,8 @@ class ComicList extends StatefulWidget { final ScrollController? controller; + final void Function(VoidCallback c)? refreshHandlerCallback; + @override State createState() => ComicListState(); } @@ -892,10 +895,22 @@ class ComicListState extends State { PageStorage.of(context).writeState(context, state); } + void refresh() { + _data.clear(); + _page = 1; + _maxPage = null; + _error = null; + _nextUrl = null; + _loading.clear(); + storeState(); + setState(() {}); + } + @override void didChangeDependencies() { super.didChangeDependencies(); restoreState(PageStorage.of(context).readState(context)); + widget.refreshHandlerCallback?.call(refresh); } void remove(Comic c) { diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 5528aa3..519f8c5 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -242,12 +242,12 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> late final String comicSourceKey; - int key = 0; - bool _wantKeepAlive = true; var scrollController = ScrollController(); + VoidCallback? refreshHandler; + void onSettingsChanged() { var explorePages = appdata.settings["explore_pages"]; if (!explorePages.contains(widget.title)) { @@ -283,24 +283,33 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> super.build(context); if (data.loadMultiPart != null) { return _MultiPartExplorePage( - key: PageStorageKey(key), + key: const PageStorageKey("comic_list"), data: data, controller: scrollController, comicSourceKey: comicSourceKey, + refreshHandlerCallback: (c) { + refreshHandler = c; + }, ); } else if (data.loadPage != null || data.loadNext != null) { return ComicList( loadPage: data.loadPage, loadNext: data.loadNext, - key: PageStorageKey(key), + key: const PageStorageKey("comic_list"), controller: scrollController, + refreshHandlerCallback: (c) { + refreshHandler = c; + }, ); } else if (data.loadMixed != null) { return _MixedExplorePage( data, comicSourceKey, - key: ValueKey(key), + key: const PageStorageKey("comic_list"), controller: scrollController, + refreshHandlerCallback: (c) { + refreshHandler = c; + }, ); } else { return const Center( @@ -314,9 +323,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> @override void refresh() { - setState(() { - key++; - }); + refreshHandler?.call(); } @override @@ -338,7 +345,7 @@ class _SingleExplorePageState extends StateWithController<_SingleExplorePage> class _MixedExplorePage extends StatefulWidget { const _MixedExplorePage(this.data, this.sourceKey, - {super.key, this.controller}); + {super.key, this.controller, required this.refreshHandlerCallback}); final ExplorePageData data; @@ -346,12 +353,24 @@ class _MixedExplorePage extends StatefulWidget { final ScrollController? controller; + final void Function(VoidCallback c) refreshHandlerCallback; + @override State<_MixedExplorePage> createState() => _MixedExplorePageState(); } class _MixedExplorePageState extends MultiPageLoadingState<_MixedExplorePage, Object> { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + widget.refreshHandlerCallback(refresh); + } + + void refresh() { + reset(); + } + Iterable buildSlivers(BuildContext context, List data) sync* { List cache = []; for (var part in data) { @@ -470,6 +489,7 @@ class _MultiPartExplorePage extends StatefulWidget { required this.data, required this.controller, required this.comicSourceKey, + required this.refreshHandlerCallback, }); final ExplorePageData data; @@ -478,6 +498,8 @@ class _MultiPartExplorePage extends StatefulWidget { final String comicSourceKey; + final void Function(VoidCallback c) refreshHandlerCallback; + @override State<_MultiPartExplorePage> createState() => _MultiPartExplorePageState(); } @@ -508,6 +530,15 @@ class _MultiPartExplorePageState extends State<_MultiPartExplorePage> { PageStorage.of(context).writeState(context, state); } + void refresh() { + setState(() { + loading = true; + message = null; + parts = null; + }); + storeState(); + } + @override void initState() { super.initState(); @@ -518,6 +549,7 @@ class _MultiPartExplorePageState extends State<_MultiPartExplorePage> { void didChangeDependencies() { super.didChangeDependencies(); restoreState(PageStorage.of(context).readState(context)); + widget.refreshHandlerCallback(refresh); } void load() async { From 3cf9228e2ae4a239d0d7bfb0ad78b2fe74284e63 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 10 Dec 2024 16:01:06 +0800 Subject: [PATCH 31/50] improve performance & ui --- lib/components/comic.dart | 2 +- lib/components/loading.dart | 18 +++++++++++++++--- lib/components/menu.dart | 9 ++++++--- lib/pages/comic_page.dart | 26 +++++++++++--------------- lib/pages/explore_page.dart | 15 +++++++++------ 5 files changed, 42 insertions(+), 28 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index ddccd21..55d0838 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -43,7 +43,7 @@ class ComicTile extends StatelessWidget { var renderBox = context.findRenderObject() as RenderBox; var size = renderBox.size; var location = renderBox.localToGlobal( - Offset(size.width / 2, size.height / 2), + Offset((size.width - 242) / 2, size.height / 2), ); showMenu(location, context); } diff --git a/lib/components/loading.dart b/lib/components/loading.dart index e03ba40..161bf8f 100644 --- a/lib/components/loading.dart +++ b/lib/components/loading.dart @@ -96,6 +96,20 @@ class ListLoadingIndicator extends StatelessWidget { } } +class SliverListLoadingIndicator extends StatelessWidget { + const SliverListLoadingIndicator({super.key}); + + @override + Widget build(BuildContext context) { + // SliverToBoxAdapter can not been lazy loaded. + // Use SliverList to make sure the animation can be lazy loaded. + return SliverList.list(children: const [ + SizedBox(), + ListLoadingIndicator(), + ]); + } +} + abstract class LoadingState extends State { bool isLoading = false; @@ -299,9 +313,7 @@ abstract class MultiPageLoadingState Widget buildLoading(BuildContext context) { return Center( - child: const CircularProgressIndicator( - strokeWidth: 2, - ).fixWidth(32).fixHeight(32), + child: const CircularProgressIndicator().fixWidth(32).fixHeight(32), ); } diff --git a/lib/components/menu.dart b/lib/components/menu.dart index 7ba17f0..411295f 100644 --- a/lib/components/menu.dart +++ b/lib/components/menu.dart @@ -42,6 +42,9 @@ class _MenuRoute extends PopupRoute { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), + border: context.brightness == Brightness.dark + ? Border.all(color: context.colorScheme.outlineVariant) + : null, boxShadow: [ BoxShadow( color: context.colorScheme.shadow.withOpacity(0.2), @@ -51,10 +54,10 @@ class _MenuRoute extends PopupRoute { ], ), child: BlurEffect( - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(4), child: Material( - color: context.colorScheme.surface.withOpacity(0.82), - borderRadius: BorderRadius.circular(8), + color: context.colorScheme.surface.withOpacity(0.78), + borderRadius: BorderRadius.circular(4), child: Container( width: width, padding: diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 12d84f5..58c46f0 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -223,7 +223,8 @@ class _ComicPageState extends LoadingState children: [ SelectableText(comic.title, style: ts.s18), if (comic.subTitle != null) - SelectableText(comic.subTitle!, style: ts.s14).paddingVertical(4), + SelectableText(comic.subTitle!, style: ts.s14) + .paddingVertical(4), Text( (ComicSource.find(comic.sourceKey)?.name) ?? '', style: ts.s12, @@ -1115,14 +1116,12 @@ class _ComicChaptersState extends State<_ComicChapters> { (state.history?.readEpisode ?? const {}).contains(i + 1); return Padding( padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), - child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(16)), - child: Material( - elevation: 5, - color: context.colorScheme.surface, - surfaceTintColor: context.colorScheme.surfaceTint, + child: Material( + color: context.colorScheme.surfaceContainer, + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: InkWell( + onTap: () => state.read(i + 1), borderRadius: const BorderRadius.all(Radius.circular(12)), - shadowColor: Colors.transparent, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), @@ -1133,19 +1132,18 @@ class _ComicChaptersState extends State<_ComicChapters> { textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: TextStyle( - color: - visited ? context.colorScheme.outline : null), + color: visited ? context.colorScheme.outline : null, + ), ), ), ), ), - onTap: () => state.read(i + 1), ), ); }), gridDelegate: const SliverGridDelegateWithFixedHeight( maxCrossAxisExtent: 200, itemHeight: 48), - ), + ).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)), if (eps.length > 20 && !showAll) SliverToBoxAdapter( child: Align( @@ -1328,9 +1326,7 @@ class _ComicThumbnailsState extends State<_ComicThumbnails> { ), ) else if (isLoading) - const SliverToBoxAdapter( - child: ListLoadingIndicator(), - ), + const SliverListLoadingIndicator(), const SliverToBoxAdapter( child: Divider(), ), diff --git a/lib/pages/explore_page.dart b/lib/pages/explore_page.dart index 519f8c5..2c10f52 100644 --- a/lib/pages/explore_page.dart +++ b/lib/pages/explore_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:sliver_tools/sliver_tools.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; @@ -110,7 +111,9 @@ class _ExplorePageState extends State return Tab(text: i.ts(comicSource.key), key: Key(i)); } - Widget buildBody(String i) => _SingleExplorePage(i, key: PageStorageKey(i)); + Widget buildBody(String i) => Material( + child: _SingleExplorePage(i, key: PageStorageKey(i)), + ); Widget buildEmpty() { var msg = "No Explore Pages".tl; @@ -401,7 +404,7 @@ class _MixedExplorePageState controller: widget.controller, slivers: [ ...buildSlivers(context, data), - if (haveNextPage) const ListLoadingIndicator().toSliver() + const SliverListLoadingIndicator(), ], ); } @@ -514,10 +517,10 @@ class _MultiPartExplorePageState extends State<_MultiPartExplorePage> { String? message; Map get state => { - "loading": loading, - "message": message, - "parts": parts, - }; + "loading": loading, + "message": message, + "parts": parts, + }; void restoreState(dynamic state) { if (state == null) return; From 0c9f7126a240160b7893355464acf0143465a762 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 11 Dec 2024 13:41:34 +0800 Subject: [PATCH 32/50] fix #91 --- .../image_provider/base_image_provider.dart | 80 ++++++++++--------- .../image_provider/reader_image.dart | 9 +++ lib/pages/reader/images.dart | 40 ++++------ lib/pages/reader/reader.dart | 21 +++++ pubspec.lock | 13 ++- pubspec.yaml | 3 + 6 files changed, 101 insertions(+), 65 deletions(-) diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index f7c981b..15f41ab 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -1,5 +1,6 @@ import 'dart:async' show Future, StreamController, scheduleMicrotask; import 'dart:convert'; +import 'dart:math'; import 'dart:ui' as ui show Codec; import 'dart:ui'; import 'package:flutter/foundation.dart'; @@ -10,6 +11,39 @@ abstract class BaseImageProvider> extends ImageProvider { const BaseImageProvider(); + static double? _effectiveScreenWidth; + + static const double _normalComicImageRatio = 0.72; + + static const double _minComicImageWidth = 1920 * _normalComicImageRatio; + + static TargetImageSize _getTargetSize(width, height) { + if (_effectiveScreenWidth == null) { + final screens = PlatformDispatcher.instance.displays; + for (var screen in screens) { + if (screen.size.width > screen.size.height) { + _effectiveScreenWidth = max( + _effectiveScreenWidth ?? 0, + screen.size.height * _normalComicImageRatio, + ); + } else { + _effectiveScreenWidth = max( + _effectiveScreenWidth ?? 0, + screen.size.width + ); + } + } + if (_effectiveScreenWidth! < _minComicImageWidth) { + _effectiveScreenWidth = _minComicImageWidth; + } + } + if (width > _effectiveScreenWidth!) { + height = (height * _effectiveScreenWidth! / width).round(); + width = _effectiveScreenWidth!.round(); + } + return TargetImageSize(width: width, height: height); + } + @override ImageStreamCompleter loadImage(T key, ImageDecoderCallback decode) { final chunkEvents = StreamController(); @@ -45,19 +79,12 @@ abstract class BaseImageProvider> while (data == null && !stop) { try { - if(_cache.containsKey(key.key)){ - data = _cache[key.key]; - } else { - data = await load(chunkEvents); - _checkCacheSize(); - _cache[key.key] = data; - _cacheSize += data.length; - } + data = await load(chunkEvents); } catch (e) { - if(e.toString().contains("Invalid Status Code: 404")) { + if (e.toString().contains("Invalid Status Code: 404")) { rethrow; } - if(e.toString().contains("Invalid Status Code: 403")) { + if (e.toString().contains("Invalid Status Code: 403")) { rethrow; } if (e.toString().contains("handshake")) { @@ -73,23 +100,24 @@ abstract class BaseImageProvider> } } - if(stop) { + if (stop) { throw Exception("Image loading is stopped"); } - if(data!.isEmpty) { + if (data!.isEmpty) { throw Exception("Empty image data"); } try { final buffer = await ImmutableBuffer.fromUint8List(data); - return await decode(buffer); + return await decode(buffer, getTargetSize: _getTargetSize); } catch (e) { await CacheManager().delete(this.key); if (data.length < 2 * 1024) { // data is too short, it's likely that the data is text, not image try { - var text = const Utf8Codec(allowMalformed: false).decoder.convert(data); + var text = + const Utf8Codec(allowMalformed: false).decoder.convert(data); throw Exception("Expected image data, but got text: $text"); } catch (e) { // ignore @@ -107,30 +135,6 @@ abstract class BaseImageProvider> } } - static final _cache = {}; - - static var _cacheSize = 0; - - static var _cacheSizeLimit = 50 * 1024 * 1024; - - static void _checkCacheSize(){ - while (_cacheSize > _cacheSizeLimit){ - var firstKey = _cache.keys.first; - _cacheSize -= _cache[firstKey]!.length; - _cache.remove(firstKey); - } - } - - static void clearCache(){ - _cache.clear(); - _cacheSize = 0; - } - - static void setCacheSizeLimit(int size){ - _cacheSizeLimit = size; - _checkCacheSize(); - } - Future load(StreamController chunkEvents); String get key; diff --git a/lib/foundation/image_provider/reader_image.dart b/lib/foundation/image_provider/reader_image.dart index 6734acf..1686040 100644 --- a/lib/foundation/image_provider/reader_image.dart +++ b/lib/foundation/image_provider/reader_image.dart @@ -2,6 +2,7 @@ import 'dart:async' show Future, StreamController; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/network/images.dart'; +import 'package:venera/utils/io.dart'; import 'base_image_provider.dart'; import 'reader_image.dart' as image_provider; @@ -20,6 +21,14 @@ class ReaderImageProvider @override Future load(StreamController chunkEvents) async { + if (imageKey.startsWith('file://')) { + var file = File(imageKey); + if (await file.exists()) { + return file.readAsBytes(); + } + throw "Error: File not found."; + } + await for (var event in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { chunkEvents.add(ImageChunkEvent( diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index f3eb839..9998775 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -162,19 +162,22 @@ class _GalleryModeState extends State<_GalleryMode> } else { int pageIndex = index - 1; int startIndex = pageIndex * reader.imagesPerPage; - int endIndex = math.min(startIndex + reader.imagesPerPage, reader.images!.length); - List pageImages = reader.images!.sublist(startIndex, endIndex); + int endIndex = math.min( + startIndex + reader.imagesPerPage, reader.images!.length); + List pageImages = + reader.images!.sublist(startIndex, endIndex); cached[index] = true; cache(index); photoViewControllers[index] = PhotoViewController(); - if(reader.imagesPerPage == 1) { + if (reader.imagesPerPage == 1) { return PhotoViewGalleryPageOptions( filterQuality: FilterQuality.medium, controller: photoViewControllers[index], - imageProvider: _createImageProviderFromKey(pageImages[0], context), + imageProvider: + _createImageProviderFromKey(pageImages[0], context), fit: BoxFit.contain, errorBuilder: (_, error, s, retry) { return NetworkError(message: error.toString(), retry: retry); @@ -645,32 +648,19 @@ class _ContinuousModeState extends State<_ContinuousMode> ImageProvider _createImageProviderFromKey( String imageKey, BuildContext context) { - if (imageKey.startsWith('file://')) { - return FileImage(File(imageKey.replaceFirst("file://", ''))); - } else { - var reader = context.reader; - return ReaderImageProvider( - imageKey, - reader.type.comicSource!.key, - reader.cid, - reader.eid, - ); - } + var reader = context.reader; + return ReaderImageProvider( + imageKey, + reader.type.comicSource!.key, + reader.cid, + reader.eid, + ); } ImageProvider _createImageProvider(int page, BuildContext context) { var reader = context.reader; var imageKey = reader.images![page - 1]; - if (imageKey.startsWith('file://')) { - return FileImage(File(imageKey.replaceFirst("file://", ''))); - } else { - return ReaderImageProvider( - imageKey, - reader.type.comicSource!.key, - reader.cid, - reader.eid, - ); - } + return _createImageProviderFromKey(imageKey, context); } void _precacheImage(int page, BuildContext context) { diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 8f91306..636cd8b 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -9,6 +9,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_memory_info/flutter_memory_info.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -21,6 +22,7 @@ import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/local.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/file_type.dart'; @@ -142,9 +144,27 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { if(appdata.settings['enableTurnPageByVolumeKey']) { handleVolumeEvent(); } + setImageCacheSize(); super.initState(); } + void setImageCacheSize() async { + var availableRAM = await MemoryInfo.getFreePhysicalMemorySize(); + if (availableRAM == null) return; + int maxImageCacheSize; + if (availableRAM < 1 << 30) { + maxImageCacheSize = 100 << 20; + } else if (availableRAM < 2 << 30) { + maxImageCacheSize = 200 << 20; + } else if (availableRAM < 4 << 30) { + maxImageCacheSize = 300 << 20; + } else { + maxImageCacheSize = 500 << 20; + } + Log.info("Reader", "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize"); + PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize; + } + @override void dispose() { autoPageTurningTimer?.cancel(); @@ -154,6 +174,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { Future.microtask(() { DataSync().onDataChanged(); }); + PaintingBinding.instance.imageCache.maximumSizeBytes = 100 << 20; super.dispose(); } diff --git a/pubspec.lock b/pubspec.lock index 4d57838..12b1e64 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -388,6 +388,15 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_memory_info: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: a401e3f96dca6ab0ab07b6b2ec0b649833f04c14 + url: "https://github.com/wgh136/flutter_memory_info" + source: git + version: "0.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1070,10 +1079,10 @@ packages: dependency: transitive description: name: win32 - sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.5.4" + version: "5.9.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index e4da6a9..2027b20 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,6 +72,9 @@ dependencies: pdf: ^3.11.1 dynamic_color: ^1.7.0 shimmer: ^3.0.0 + flutter_memory_info: + git: + url: https://github.com/wgh136/flutter_memory_info dev_dependencies: flutter_test: From 4801457e0e7367f82162e85b966e2bb3381c8323 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 12 Dec 2024 14:24:11 +0800 Subject: [PATCH 33/50] update flutter_memory_info --- pubspec.lock | 9 ++++----- pubspec.yaml | 4 +--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 12b1e64..f11ab68 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -391,11 +391,10 @@ packages: flutter_memory_info: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: a401e3f96dca6ab0ab07b6b2ec0b649833f04c14 - url: "https://github.com/wgh136/flutter_memory_info" - source: git + name: flutter_memory_info + sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74" + url: "https://pub.dev" + source: hosted version: "0.0.1" flutter_plugin_android_lifecycle: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 2027b20..347437d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -72,9 +72,7 @@ dependencies: pdf: ^3.11.1 dynamic_color: ^1.7.0 shimmer: ^3.0.0 - flutter_memory_info: - git: - url: https://github.com/wgh136/flutter_memory_info + flutter_memory_info: ^0.0.1 dev_dependencies: flutter_test: From af9835eb8f373d2ee23a1d774c0533c4da433702 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 12 Dec 2024 16:41:42 +0800 Subject: [PATCH 34/50] update flutter to 3.27.0 & update packages --- lib/components/appbar.dart | 5 +- lib/components/button.dart | 10 +- lib/components/comic.dart | 8 +- lib/components/components.dart | 2 - lib/components/custom_slider.dart | 25 +-- lib/components/flyout.dart | 115 +------------ lib/components/menu.dart | 10 +- lib/components/window_frame.dart | 2 +- lib/foundation/comic_source/category.dart | 2 +- lib/foundation/comic_source/comic_source.dart | 2 +- lib/foundation/comic_source/models.dart | 4 +- lib/foundation/comic_source/parser.dart | 5 +- lib/foundation/history.dart | 1 + lib/foundation/widget_utils.dart | 6 + lib/main.dart | 2 - lib/network/app_dio.dart | 8 +- lib/pages/categories_page.dart | 2 +- lib/pages/explore_page.dart | 1 - lib/pages/favorites/favorites_page.dart | 2 +- lib/pages/favorites/local_favorites_page.dart | 18 +- lib/pages/favorites/side_bar.dart | 4 +- lib/pages/history_page.dart | 2 - lib/pages/home_page.dart | 6 +- lib/pages/reader/gesture.dart | 2 +- lib/pages/reader/reader.dart | 4 +- lib/pages/reader/scaffold.dart | 10 +- lib/pages/settings/explore_settings.dart | 4 +- lib/pages/settings/setting_components.dart | 4 +- lib/pages/settings/settings_page.dart | 2 +- pubspec.lock | 154 +++++++++--------- pubspec.yaml | 14 +- 31 files changed, 171 insertions(+), 265 deletions(-) diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 1d11a14..841d5fe 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -76,7 +76,7 @@ class _AppbarState extends State { var content = Container( decoration: BoxDecoration( color: widget.backgroundColor ?? - context.colorScheme.surface.withOpacity(0.72), + context.colorScheme.surface.toOpacity(0.72), ), height: _kAppBarHeight + context.padding.top, child: Row( @@ -219,7 +219,7 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate { child: BlurEffect( blur: 15, child: Material( - color: context.colorScheme.surface.withOpacity(0.72), + color: context.colorScheme.surface.toOpacity(0.72), elevation: 0, borderRadius: BorderRadius.circular(radius), child: body, @@ -734,6 +734,7 @@ class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate { icon: const Icon(Icons.clear), onPressed: () { editingController.clear(); + onChanged?.call(""); }, ); }, diff --git a/lib/components/button.dart b/lib/components/button.dart index 8a16de0..e3cc116 100644 --- a/lib/components/button.dart +++ b/lib/components/button.dart @@ -214,7 +214,7 @@ class _ButtonState extends State