From b92eca177cad9c20027882dfb116b8b7ff2dda53 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 8 Dec 2024 18:20:04 +0800 Subject: [PATCH 01/36] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=94=B6=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/foundation/history.dart | 16 +- lib/foundation/image_favorites.dart | 94 ++++++++ lib/pages/favorites/favorites_page.dart | 9 +- lib/pages/home_page.dart | 125 ++++++----- lib/pages/home_page/image_favorites.dart | 57 +++++ .../image_favorites_group.dart | 118 ++++++++++ .../image_favorites_page.dart | 210 ++++++++++++++++++ lib/pages/reader/scaffold.dart | 46 +++- lib/utils/data.dart | 33 ++- "\344\273\273\345\212\241.md" | 19 ++ 10 files changed, 632 insertions(+), 95 deletions(-) create mode 100644 lib/foundation/image_favorites.dart create mode 100644 lib/pages/home_page/image_favorites.dart create mode 100644 lib/pages/image_favorites_page/image_favorites_group.dart create mode 100644 lib/pages/image_favorites_page/image_favorites_page.dart create mode 100644 "\344\273\273\345\212\241.md" diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index d5def70..f42c82a 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'dart:convert'; 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/foundation/state_controller.dart'; import 'package:venera/utils/translations.dart'; import 'app.dart'; +part "image_favorites.dart"; typedef HistoryType = ComicType; @@ -37,7 +40,7 @@ class History implements Comic { @override String cover; - + int ep; int page; @@ -200,8 +203,6 @@ class HistoryManager with ChangeNotifier { Map? _cachedHistory; - static const _kMaxHistoryLength = 200; - Future init() async { _db = sqlite3.open("${App.dataPath}/history.db"); @@ -221,18 +222,13 @@ class HistoryManager with ChangeNotifier { """); notifyListeners(); + ImageFavoriteManager.init(); } /// add history. if exists, update time. /// /// This function would be called when user start reading. Future addHistory(History newItem) async { - while(count() >= _kMaxHistoryLength) { - _db.execute(""" - delete from history - where time == (select min(time) from history); - """); - } _db.execute(""" insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); @@ -282,7 +278,7 @@ class HistoryManager with ChangeNotifier { } History? findSync(String id, ComicType type) { - if(_cachedHistory == null) { + if (_cachedHistory == null) { updateCache(); } if (!_cachedHistory!.containsKey(id)) { diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart new file mode 100644 index 0000000..19c509e --- /dev/null +++ b/lib/foundation/image_favorites.dart @@ -0,0 +1,94 @@ +part of "history.dart"; + +class ImageFavorite { + /// unique id for the comic + final String id; + + final String imagePath; + + final DateTime time; + + final String title; + + final int ep; + + final int page; + + final Map otherInfo; + + const ImageFavorite( + this.id, + this.imagePath, + this.title, + this.time, + this.ep, + this.page, + this.otherInfo, + ); + + ImageFavorite.fromMap(Map map) + : time = map["time"] ?? DateTime.now(), // 兜底从 picacomic 引入时没有 time 的情况 + title = map["title"], + imagePath = map["imagePath"], + ep = map["ep"], + page = map["page"], + id = map["id"], + otherInfo = map["otherInfo"] ?? {}; +} + +class ImageFavoriteManager { + static Database get _db => HistoryManager()._db; + + /// 检查表image_favorites是否存在, 不存在则创建 + static void init() { + _db.execute("CREATE TABLE IF NOT EXISTS image_favorites (" + "id TEXT," + "title TEXT NOT NULL," + "time int," + "cover TEXT NOT NULL," + "ep INTEGER NOT NULL," + "page INTEGER NOT NULL," + "other TEXT NOT NULL," + "PRIMARY KEY (id, ep, page)" + ");"); + } + + static void add(ImageFavorite favorite) { + _db.execute(""" + insert into image_favorites(id, title, time, cover, ep, page, other) + values(?, ?, ?, ?, ?, ?, ?); + """, [ + favorite.id, + favorite.title, + favorite.time.millisecondsSinceEpoch, + favorite.imagePath, + favorite.ep, + favorite.page, + jsonEncode(favorite.otherInfo) + ]); + Future.microtask( + () => StateController.findOrNull(tag: "home_page")?.update()); + } + + static List getAll() { + var res = _db.select("select * from image_favorites;"); + return res + .map((e) => ImageFavorite(e["id"], e["cover"], e["title"], e["time"], + e["ep"], e["page"], jsonDecode(e["other"]))) + .toList(); + } + + static void delete(ImageFavorite favorite) { + _db.execute(""" + delete from image_favorites + where id = ? and ep = ? and page = ?; + """, [favorite.id, favorite.ep, favorite.page]); + Future.microtask( + () => StateController.findOrNull(tag: "home_page")?.update()); + } + + static int get length { + var res = _db.select("select count(*) from image_favorites;"); + return res.first.values.first! as int; + } +} diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 76542fd..8199c52 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'dart:math'; import 'package:flutter/material.dart'; @@ -32,7 +33,7 @@ class FavoritesPage extends StatefulWidget { State createState() => _FavoritesPageState(); } -class _FavoritesPageState extends State { +class _FavoritesPageState extends State { String? folder; bool isNetwork = false; @@ -55,7 +56,7 @@ class _FavoritesPageState extends State { @override void initState() { var data = appdata.implicitData['favoriteFolder']; - if(data != null){ + if (data != null) { folder = data['name']; isNetwork = data['isNetwork'] ?? false; } @@ -98,7 +99,7 @@ class _FavoritesPageState extends State { alignment: Alignment.centerLeft, child: Material( child: SizedBox( - width: min(300, context.width-16), + width: min(300, context.width - 16), child: _LeftBar( withAppbar: true, favPage: this, @@ -167,4 +168,4 @@ abstract interface class FolderList { void update(); void updateFolders(); -} \ No newline at end of file +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index d4e0ee8..a91c453 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -15,6 +15,7 @@ import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_source_page.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/pages/history_page.dart'; +import 'package:venera/pages/image_favorites_page/image_favorites_page.dart'; import 'package:venera/pages/search_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; @@ -22,6 +23,7 @@ import 'package:venera/utils/import_comic.dart'; import 'package:venera/utils/translations.dart'; import 'local_comics_page.dart'; +part "./home_page/image_favorites.dart"; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -37,6 +39,7 @@ class HomePage extends StatelessWidget { const _Local(), const _ComicSourceWidget(), const _AccountsWidget(), + const ImageFavorites(), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], ); @@ -85,7 +88,8 @@ class _SyncDataWidget extends StatefulWidget { State<_SyncDataWidget> createState() => _SyncDataWidgetState(); } -class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObserver { +class _SyncDataWidgetState extends State<_SyncDataWidget> + with WidgetsBindingObserver { @override void initState() { super.initState(); @@ -95,7 +99,7 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs } void update() { - if(mounted) { + if (mounted) { setState(() {}); } } @@ -112,8 +116,8 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs @override void didChangeAppLifecycleState(AppLifecycleState state) { super.didChangeAppLifecycleState(state); - if(state == AppLifecycleState.resumed) { - if(DateTime.now().difference(lastCheck) > const Duration(minutes: 10)) { + if (state == AppLifecycleState.resumed) { + if (DateTime.now().difference(lastCheck) > const Duration(minutes: 10)) { lastCheck = DateTime.now(); DataSync().downloadData(); } @@ -123,7 +127,7 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs @override Widget build(BuildContext context) { Widget child; - if(!DataSync().isEnabled) { + if (!DataSync().isEnabled) { child = const SliverPadding(padding: EdgeInsets.zero); } else if (DataSync().isUploading || DataSync().isDownloading) { child = SliverToBoxAdapter( @@ -161,17 +165,15 @@ class _SyncDataWidgetState extends State<_SyncDataWidget> with WidgetsBindingObs mainAxisSize: MainAxisSize.min, children: [ IconButton( - icon: const Icon(Icons.cloud_upload_outlined), - onPressed: () async { - DataSync().uploadData(); - } - ), + icon: const Icon(Icons.cloud_upload_outlined), + onPressed: () async { + DataSync().uploadData(); + }), IconButton( - icon: const Icon(Icons.cloud_download_outlined), - onPressed: () async { - DataSync().downloadData(); - } - ), + icon: const Icon(Icons.cloud_download_outlined), + onPressed: () async { + DataSync().downloadData(); + }), ], ), ), @@ -520,50 +522,50 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { ), ) : Column( - key: key, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(width: 600), - ...List.generate(importMethods.length, (index) { - return RadioListTile( - title: Text(importMethods[index]), - value: index, - groupValue: type, - onChanged: (value) { - setState(() { - type = value as int; - }); - }, - ); - }), - if(type != 3) - ListTile( - title: Text("Add to favorites".tl), - trailing: Select( - current: selectedFolder, - values: folders, - minWidth: 112, - onTap: (v) { + key: key, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 600), + ...List.generate(importMethods.length, (index) { + return RadioListTile( + title: Text(importMethods[index]), + value: index, + groupValue: type, + onChanged: (value) { setState(() { - selectedFolder = folders[v]; + type = value as int; }); }, - ), - ).paddingHorizontal(8), - if(!App.isIOS && !App.isMacOS) - CheckboxListTile( - enabled: true, - title: Text("Copy to app local path".tl), - value: copyToLocalFolder, - onChanged:(v) { - setState(() { - copyToLocalFolder = !copyToLocalFolder; - }); - }).paddingHorizontal(8), - const SizedBox(height: 8), - Text(info).paddingHorizontal(24), - ], - ), + ); + }), + if (type != 3) + ListTile( + title: Text("Add to favorites".tl), + trailing: Select( + current: selectedFolder, + values: folders, + minWidth: 112, + onTap: (v) { + setState(() { + selectedFolder = folders[v]; + }); + }, + ), + ).paddingHorizontal(8), + if (!App.isIOS && !App.isMacOS) + CheckboxListTile( + enabled: true, + title: Text("Copy to app local path".tl), + value: copyToLocalFolder, + onChanged: (v) { + setState(() { + copyToLocalFolder = !copyToLocalFolder; + }); + }).paddingHorizontal(8), + const SizedBox(height: 8), + Text(info).paddingHorizontal(24), + ], + ), actions: [ Button.text( child: Row( @@ -593,7 +595,9 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { help += "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" .tl; - help +="If you import an EhViewer's database, program will automatically create folders according to the download label in that database.".tl; + help += + "If you import an EhViewer's database, program will automatically create folders according to the download label in that database." + .tl; return ContentDialog( title: "Help".tl, content: Text(help).paddingHorizontal(16), @@ -626,16 +630,15 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> { loading = true; }); var importer = ImportComic( - selectedFolder: selectedFolder, - copyToLocal: copyToLocalFolder); - var result = switch(type) { + selectedFolder: selectedFolder, copyToLocal: copyToLocalFolder); + var result = switch (type) { 0 => await importer.directory(true), 1 => await importer.directory(false), 2 => await importer.cbz(), 3 => await importer.ehViewer(), int() => true, }; - if(result) { + if (result) { context.pop(); } else { setState(() { diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart new file mode 100644 index 0000000..abd7596 --- /dev/null +++ b/lib/pages/home_page/image_favorites.dart @@ -0,0 +1,57 @@ +part of '../home_page.dart'; + +class ImageFavorites extends StatefulWidget { + const ImageFavorites({super.key}); + + @override + State createState() => ImageFavoritesState(); +} + +class ImageFavoritesState extends State { + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + width: 0.6, + ), + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + context.to(() => const ImageFavoritesPage()); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 56, + child: Row( + children: [ + Center( + child: Text('Image Favorites'.tl, style: ts.s18), + ), + const Spacer(), + const Icon(Icons.arrow_right), + ], + ), + ).paddingHorizontal(16), + SizedBox( + width: double.infinity, + child: Text( + "@a image favorites" + .tlParams({"a": ImageFavoriteManager.length.toString()}), + style: const TextStyle(fontSize: 15), + ).paddingHorizontal(16).paddingBottom(16), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/image_favorites_page/image_favorites_group.dart b/lib/pages/image_favorites_page/image_favorites_group.dart new file mode 100644 index 0000000..0c49a64 --- /dev/null +++ b/lib/pages/image_favorites_page/image_favorites_group.dart @@ -0,0 +1,118 @@ +part of 'image_favorites_page.dart'; + +class ImageFavoritesGroup extends StatefulWidget { + const ImageFavoritesGroup({super.key}); + + @override + State createState() => ImageFavoritesGroupState(); +} + +class ImageFavoritesGroupState extends State { + late List history; + late int count; + + void onHistoryChange() { + setState(() { + history = HistoryManager().getRecent(); + count = HistoryManager().count(); + }); + } + + @override + void initState() { + history = HistoryManager().getRecent(); + count = HistoryManager().count(); + HistoryManager().addListener(onHistoryChange); + super.initState(); + } + + @override + void dispose() { + HistoryManager().removeListener(onHistoryChange); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + width: 0.6, + ), + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + context.to(() => const HistoryPage()); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 56, + child: Row( + children: [ + Center( + child: Text('History'.tl, style: ts.s18), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text(count.toString(), style: ts.s12), + ), + const Spacer(), + const Icon(Icons.arrow_right), + ], + ), + ).paddingHorizontal(16), + if (history.isNotEmpty) + SizedBox( + height: 128, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: history.length, + itemBuilder: (context, index) { + return InkWell( + onTap: () { + // App.rootNavigatotKey.to(() => Reader()) + }, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 92, + height: 114, + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .secondaryContainer, + ), + clipBehavior: Clip.antiAlias, + child: AnimatedImage( + image: HistoryImageProvider(history[index]), + width: 96, + height: 128, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ), + ), + ); + }, + ), + ).paddingHorizontal(8).paddingBottom(16), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart new file mode 100644 index 0000000..2dc734d --- /dev/null +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:venera/components/components.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/consts.dart'; +import 'package:venera/foundation/history.dart'; +import 'package:venera/pages/history_page.dart'; +import 'package:venera/pages/reader/reader.dart'; +import 'package:venera/utils/translations.dart'; +part "./image_favorites_group.dart"; + +enum ImageFavoriteSortType { + name("name"), + timeAsc("time_asc"), + timeDesc("time_desc"), + favoriteNumDesc("favorite_num_desc"); + + final String value; + + const ImageFavoriteSortType(this.value); + + static ImageFavoriteSortType fromString(String value) { + for (var type in values) { + if (type.value == value) { + return type; + } + } + return name; + } +} + +class ImageFavoritesPage extends StatefulWidget { + const ImageFavoritesPage({super.key}); + + @override + State createState() => ImageFavoritesPageState(); +} + +class ImageFavoritesPageState extends State { + late ImageFavoriteSortType sortType; + + String keyword = ""; + + bool searchMode = false; + + bool multiSelectMode = false; + + Map selectedComics = {}; + + void update() { + if (keyword.isEmpty) { + setState(() { + comics = LocalManager().getComics(sortType); + }); + } else { + setState(() { + comics = LocalManager().search(keyword); + }); + } + } + + @override + Widget build(BuildContext context) { + var widget = SmoothCustomScrollView( + slivers: [ + if (!searchMode && !multiSelectMode) + SliverAppbar( + title: Text("Local".tl), + actions: [ + Tooltip( + message: "Search".tl, + child: IconButton( + icon: const Icon(Icons.search), + onPressed: () { + setState(() { + searchMode = true; + }); + }, + ), + ), + Tooltip( + message: "Sort".tl, + child: IconButton( + icon: const Icon(Icons.sort), + onPressed: sort, + ), + ), + Tooltip( + message: multiSelectMode + ? "Exit Multi-Select".tl + : "Multi-Select".tl, + child: IconButton( + icon: const Icon(Icons.checklist), + onPressed: () { + setState(() { + multiSelectMode = !multiSelectMode; + }); + }, + ), + ), + ], + ) + else if (multiSelectMode) + SliverAppbar( + leading: Tooltip( + message: "Cancel".tl, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + }, + ), + ), + title: Text( + "Selected @c comics".tlParams({"c": selectedComics.length})), + actions: selectActions, + ) + else if (searchMode) + SliverAppbar( + leading: Tooltip( + message: "Cancel".tl, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + searchMode = false; + keyword = ""; + update(); + }); + }, + ), + ), + title: TextField( + autofocus: true, + decoration: InputDecoration( + hintText: "Search".tl, + border: InputBorder.none, + ), + onChanged: (v) { + keyword = v; + update(); + }, + ), + ), + SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), + ], + ); + return context.width > changePoint ? widget.paddingHorizontal(8) : widget; + } + + void sort() { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder(builder: (context, setState) { + return ContentDialog( + title: "Sort".tl, + content: Column( + children: [ + RadioListTile( + title: Text("Name".tl), + value: LocalSortType.name, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + RadioListTile( + title: Text("Date".tl), + value: LocalSortType.timeAsc, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + RadioListTile( + title: Text("Date Desc".tl), + value: LocalSortType.timeDesc, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + ], + ), + actions: [ + FilledButton( + onPressed: () { + appdata.implicitData["local_sort"] = sortType.value; + appdata.writeImplicitData(); + Navigator.pop(context); + update(); + }, + child: Text("Confirm".tl), + ), + ], + ); + }); + }, + ); + } +} diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 95043c3..ddafb95 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -18,8 +18,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { bool get isOpen => _isOpen; - bool get isReversed => context.reader.mode == ReaderMode.galleryRightToLeft || - context.reader.mode == ReaderMode.continuousRightToLeft; + bool get isReversed => + context.reader.mode == ReaderMode.galleryRightToLeft || + context.reader.mode == ReaderMode.continuousRightToLeft; int showFloatingButtonValue = 0; @@ -233,13 +234,13 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { child: buildSlider(), ), IconButton.filledTonal( - onPressed: () => !isReversed - ? context.reader.chapter < context.reader.maxChapter - ? context.reader.toNextChapter() - : context.reader.toPage(context.reader.maxPage) - : context.reader.chapter > 1 - ? context.reader.toPrevChapter() - : context.reader.toPage(1), + onPressed: () => !isReversed + ? context.reader.chapter < context.reader.maxChapter + ? context.reader.toNextChapter() + : context.reader.toPage(context.reader.maxPage) + : context.reader.chapter > 1 + ? context.reader.toPrevChapter() + : context.reader.toPage(1), icon: const Icon(Icons.last_page)), const SizedBox( width: 8, @@ -263,6 +264,33 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ), ), const Spacer(), + Tooltip( + message: "收藏图片".tl, + child: IconButton( + icon: const Icon(Icons.favorite), + onPressed: () async { + try { + final id = "1"; + var image = ' await _persistentCurrentImage()'; + if (image != null) { + image = image.split("/").last; + var otherInfo = {}; + ImageFavoriteManager.add(ImageFavorite( + id, + image, + context.reader.widget.name, + DateTime.now(), + 1, + context.reader.page, + otherInfo)); + showToast(message: "成功收藏图片".tl, context: context); + } + } catch (e) { + showToast(message: e.toString(), context: context); + } + }, + ), + ), if (App.isWindows) Tooltip( message: "${"Full Screen".tl}(F12)", diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 788191d..5873028 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:developer'; import 'dart:isolate'; import 'package:sqlite3/sqlite3.dart'; @@ -134,7 +135,8 @@ Future importPicaData(File file) async { .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"); + folderNames + .removeWhere((e) => e == "folder_order" || e == "folder_sync"); for (var folderName in folderNames) { if (!LocalFavoritesManager().existsFolder(folderName)) { LocalFavoritesManager().createFolder(folderName); @@ -147,7 +149,7 @@ Future importPicaData(File file) async { name: comic['name'], coverPath: comic['cover_path'], author: comic['author'], - type: ComicType(switch(comic['type']) { + type: ComicType(switch (comic['type']) { 0 => 'picacg'.hashCode, 1 => 'ehentai'.hashCode, 2 => 'jm'.hashCode, @@ -161,11 +163,9 @@ Future importPicaData(File file) async { ); } } - } - catch(e) { + } catch (e) { Log.error("Import Data", "Failed to import local favorite: $e"); - } - finally { + } finally { db.dispose(); } } @@ -176,7 +176,7 @@ Future importPicaData(File file) async { for (var comic in db.select("SELECT * FROM history;")) { HistoryManager().addHistory( History.fromMap({ - "type": switch(comic['type']) { + "type": switch (comic['type']) { 0 => 'picacg'.hashCode, 1 => 'ehentai'.hashCode, 2 => 'jm'.hashCode, @@ -196,11 +196,22 @@ Future importPicaData(File file) async { }), ); } - } - catch(e) { + for (var comic in db.select("SELECT * FROM image_favorites;")) { + ImageFavoriteManager.add( + ImageFavorite.fromMap({ + "id": comic['id'], + "title": comic["title"], + "time": comic["time"], + "imagePath": comic["cover"], + "ep": comic["ep"], + "page": comic["page"], + "otherInfo": jsonDecode(comic["other"]), + }), + ); + } + } catch (e) { Log.error("Import Data", "Failed to import history: $e"); - } - finally { + } finally { db.dispose(); } } diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" new file mode 100644 index 0000000..d3c9091 --- /dev/null +++ "b/\344\273\273\345\212\241.md" @@ -0,0 +1,19 @@ +1. 建好 image_favorite 表 ok + 1. 导入图片收藏 ok +2. 做好页面 + 1. 阅读时, 下方的收藏 + 2. 单独的图片收藏页 + 1. 时间筛选, 按照近七天, 一个月, 三个月, 半年, 2024年, 2023年 + 2. 单本收藏数最多排序 + 3. 抄一下本地的那个, 然后用探索页的tab + 4. 支持删除 + 3. 主页的图片收藏显示 ok +3. 打通联动 +4. 自测 +5. 有些旧有的功能没有补齐 + 1. 点击收藏直接进行阅读 + 2. sync 网络收藏到本地的时候没有拉取页数的能力 + 1. sync 的时候也卡卡的, 一直拉不下来 + 2. lib\pages\favorites\favorite_actions.dart 应该手动拉一下最大的页数, 然后从拉取倒数几页来判断重复比较好 + 1. getMaxPageNum getPageSize + 2. 修改widget, 让用户可以填下拉几页, 并在这里提示大概一页有多少 pageNum \ No newline at end of file From 93a53f19c02ea7202880ed3a3d95e4f292e9113c Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Fri, 20 Dec 2024 22:29:00 +0800 Subject: [PATCH 02/36] =?UTF-8?q?feat:=20=E4=B8=BB=E4=BD=93=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E6=94=B6=E8=97=8F=E9=A1=B5=E9=9D=A2=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/image.dart | 5 +- lib/foundation/history.dart | 2 + lib/foundation/image_favorites.dart | 30 ++- .../image_favorites_provider.dart | 49 +++++ .../image_provider/local_favorite_image.dart | 7 +- .../image_favorites_group.dart | 118 ----------- .../image_favorites_item.dart | 197 ++++++++++++++++++ .../image_favorites_list.dart | 79 +++++++ .../image_favorites_page.dart | 171 +++++++++++++-- lib/pages/image_favorites_page/type.dart | 61 +++++- lib/utils/data.dart | 5 +- "\344\273\273\345\212\241.md" | 29 ++- 12 files changed, 596 insertions(+), 157 deletions(-) create mode 100644 lib/foundation/image_provider/image_favorites_provider.dart delete mode 100644 lib/pages/image_favorites_page/image_favorites_group.dart create mode 100644 lib/pages/image_favorites_page/image_favorites_item.dart create mode 100644 lib/pages/image_favorites_page/image_favorites_list.dart diff --git a/lib/components/image.dart b/lib/components/image.dart index 0ec5edf..5e6210d 100644 --- a/lib/components/image.dart +++ b/lib/components/image.dart @@ -22,6 +22,7 @@ class AnimatedImage extends StatefulWidget { this.filterQuality = FilterQuality.medium, this.isAntiAlias = false, this.part, + this.onError, Map? headers, int? cacheWidth, int? cacheHeight, @@ -63,6 +64,8 @@ class AnimatedImage extends StatefulWidget { final ImagePart? part; + final Function? onError; + static void clear() => _AnimatedImageState.clear(); @override @@ -271,7 +274,7 @@ class _AnimatedImageState extends State Widget result; if (_imageInfo != null) { - if(widget.part != null) { + if (widget.part != null) { return CustomPaint( painter: ImagePainter( image: _imageInfo!.image, diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index f42c82a..f6d16d7 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -1,7 +1,9 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; import 'package:flutter/widgets.dart' show ChangeNotifier; +import 'package:intl/intl.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 19c509e..5ab2d00 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -55,7 +55,7 @@ class ImageFavoriteManager { static void add(ImageFavorite favorite) { _db.execute(""" - insert into image_favorites(id, title, time, cover, ep, page, other) + insert or replace into image_favorites(id, title, time, cover, ep, page, other) values(?, ?, ?, ?, ?, ?, ?); """, [ favorite.id, @@ -66,15 +66,19 @@ class ImageFavoriteManager { favorite.page, jsonEncode(favorite.otherInfo) ]); - Future.microtask( - () => StateController.findOrNull(tag: "home_page")?.update()); } static List getAll() { var res = _db.select("select * from image_favorites;"); return res - .map((e) => ImageFavorite(e["id"], e["cover"], e["title"], e["time"], - e["ep"], e["page"], jsonDecode(e["other"]))) + .map((e) => ImageFavorite( + e["id"], + e["cover"], + e["title"], + DateTime.fromMillisecondsSinceEpoch(e["time"]), + e["ep"], + e["page"], + jsonDecode(e["other"]))) .toList(); } @@ -83,8 +87,20 @@ class ImageFavoriteManager { delete from image_favorites where id = ? and ep = ? and page = ?; """, [favorite.id, favorite.ep, favorite.page]); - Future.microtask( - () => StateController.findOrNull(tag: "home_page")?.update()); + } + + static List get earliestTimeToNow { + var res = _db.select("select MIN(time) from image_favorites;"); + int earliestYear = + DateTime.fromMillisecondsSinceEpoch(res.first.values.first! as int) + .year; + DateTime now = DateTime.now(); + int currentYear = now.year; + List yearsList = []; + for (int year = earliestYear; year <= currentYear; year++) { + yearsList.add(year.toString()); + } + return yearsList; } static int get length { diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart new file mode 100644 index 0000000..ae57392 --- /dev/null +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -0,0 +1,49 @@ +import 'dart:async' show Future, StreamController; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:venera/network/images.dart'; +import '../history.dart'; +import 'base_image_provider.dart'; +import 'image_favorites_provider.dart' as image_provider; + +class ImageFavoritesProvider + extends BaseImageProvider { + /// Image provider for normal image. + /// + /// [url] is the url of the image. Local file path is also supported. + const ImageFavoritesProvider(this.imageFavorite); + + final ImageFavorite imageFavorite; + + @override + Future load(StreamController chunkEvents) async { + String? imageKey = imageFavorite.otherInfo["imageKey"]; + List ids = imageFavorite.id.split('-'); + String sourceKey = ids[0]; + String cid = ids[1]; + String eid = imageFavorite.ep.toString(); + if (imageKey == null) { + throw "Error: imageFavorits no imageKey"; + } + await for (var progress + in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { + 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 => + "imageFavorite${imageFavorite.id}${imageFavorite.ep}${imageFavorite.page}"; +} diff --git a/lib/foundation/image_provider/local_favorite_image.dart b/lib/foundation/image_provider/local_favorite_image.dart index ade4a7b..feae47f 100644 --- a/lib/foundation/image_provider/local_favorite_image.dart +++ b/lib/foundation/image_provider/local_favorite_image.dart @@ -22,7 +22,7 @@ class LocalFavoriteImageProvider static void delete(String id, int intKey) { var fileName = (id + intKey.toString()).hashCode.toString(); var file = File(FilePath.join(App.dataPath, 'favorite_cover', fileName)); - if(file.existsSync()) { + if (file.existsSync()) { file.delete(); } } @@ -42,7 +42,7 @@ class LocalFavoriteImageProvider cumulativeBytesLoaded: progress.currentBytes, expectedTotalBytes: progress.totalBytes, )); - if(progress.imageBytes != null) { + if (progress.imageBytes != null) { var data = progress.imageBytes!; await file.writeAsBytes(data); return data; @@ -52,7 +52,8 @@ class LocalFavoriteImageProvider } @override - Future obtainKey(ImageConfiguration configuration) { + Future obtainKey( + ImageConfiguration configuration) { return SynchronousFuture(this); } diff --git a/lib/pages/image_favorites_page/image_favorites_group.dart b/lib/pages/image_favorites_page/image_favorites_group.dart deleted file mode 100644 index 1c2e994..0000000 --- a/lib/pages/image_favorites_page/image_favorites_group.dart +++ /dev/null @@ -1,118 +0,0 @@ -part of 'image_favorites_page.dart'; - -class ImageFavoritesGroup extends StatefulWidget { - const ImageFavoritesGroup({super.key}); - - @override - State createState() => ImageFavoritesGroupState(); -} - -class ImageFavoritesGroupState extends State { - late List history; - late int count; - - void onHistoryChange() { - setState(() { - history = HistoryManager().getRecent(); - count = HistoryManager().count(); - }); - } - - @override - void initState() { - history = HistoryManager().getRecent(); - count = HistoryManager().count(); - HistoryManager().addListener(onHistoryChange); - super.initState(); - } - - @override - void dispose() { - HistoryManager().removeListener(onHistoryChange); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - width: 0.6, - ), - borderRadius: BorderRadius.circular(8), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - context.to(() => const HistoryPage()); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 56, - child: Row( - children: [ - Center( - child: Text('History'.tl, style: ts.s18), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(count.toString(), style: ts.s12), - ), - const Spacer(), - const Icon(Icons.arrow_right), - ], - ), - ).paddingHorizontal(16), - if (history.isNotEmpty) - SizedBox( - height: 128, - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: history.length, - itemBuilder: (context, index) { - return InkWell( - onTap: () { - // App.rootNavigatotKey.to(() => Reader()) - }, - borderRadius: BorderRadius.circular(8), - child: Container( - width: 92, - height: 114, - margin: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context) - .colorScheme - .secondaryContainer, - ), - clipBehavior: Clip.antiAlias, - // child: AnimatedImage( - // image: HistoryImageProvider(history[index]), - // width: 96, - // height: 128, - // fit: BoxFit.cover, - // filterQuality: FilterQuality.medium, - // ), - ), - ); - }, - ), - ).paddingHorizontal(8).paddingBottom(16), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart new file mode 100644 index 0000000..46e44ec --- /dev/null +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -0,0 +1,197 @@ +part of 'image_favorites_page.dart'; + +class ImageFavoritesGroup { + final String id; + List imageFavorites; + final String eid; + ImageFavoritesGroup(this.id, this.imageFavorites, this.eid); + + // 避免后边收藏图片的时候标题有更新 + String get title { + return imageFavorites.last.title; + } + + String get sourceKey { + return id.split('-')[0]; + } + + String get cid { + return id.split('-')[1]; + } + + // 最早的那张图片收藏的时间 + DateTime get firstTime { + return imageFavorites + .fold( + imageFavorites[0], + (prev, current) => + prev.time.isBefore(current.time) ? prev : current) + .time; + } + + // 是否都有imageKey + bool get isAllHasImageKey { + return imageFavorites.every((e) => e.otherInfo["imageKey"] != null); + } + + // 是否都有封面 + bool get isHasFirstPage { + return imageFavorites[0].page == 0; + } +} + +class ImageFavoritesItem extends StatefulWidget { + const ImageFavoritesItem({super.key, required this.imageFavoritesGroup}); + final ImageFavoritesGroup imageFavoritesGroup; + @override + State createState() => ImageFavoritesItemState(); +} + +class ImageFavoritesItemState extends State { + bool isImageKeyLoading = false; + // 刷新 imageKey 失败的场景再刷新一次, 再次失败了就不重试了 + bool hasRefreshImageKeyOnErr = false; + // 如果 imageKey 失效了, 或者刚从pica导入, 没有就走这个逻辑 + void refreshImageKey() async { + if (isImageKeyLoading || hasRefreshImageKeyOnErr) return; + isImageKeyLoading = true; + ComicSource? comicSource = + ComicSource.find(widget.imageFavoritesGroup.sourceKey); + var res = await comicSource!.loadComicPages!( + widget.imageFavoritesGroup.cid, + widget.imageFavoritesGroup.eid, + ); + if (!res.error) { + List images = res.data; + // 塞个封面进去 + if (!widget.imageFavoritesGroup.isHasFirstPage) { + ImageFavorite copyObj = widget.imageFavoritesGroup.imageFavorites[0]; + ImageFavorite temp = ImageFavorite(copyObj.id, '', copyObj.title, + copyObj.time, copyObj.ep, 0, {'imageKey': images[0]}); + ImageFavoriteManager.add(temp); + } + for (var ele in widget.imageFavoritesGroup.imageFavorites) { + ele.otherInfo["imageKey"] = images[ele.page]; + ImageFavoriteManager.add(ele); + } + setState(() {}); + } + isImageKeyLoading = false; + } + + @override + Widget build(BuildContext context) { + int count = widget.imageFavoritesGroup.imageFavorites.length; + if (!widget.imageFavoritesGroup.isAllHasImageKey || + !widget.imageFavoritesGroup.isHasFirstPage) { + refreshImageKey(); + } + String time = DateFormat('yyyy-MM-dd HH:mm:ss') + .format(widget.imageFavoritesGroup.firstTime); + return Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + width: 0.6, + ), + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + context.to(() => const HistoryPage()); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 56, + child: Row( + children: [ + Expanded( + child: Text( + widget.imageFavoritesGroup.title, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text(count.toString(), style: ts.s12), + ), + ], + ), + ).paddingHorizontal(16), + SizedBox( + height: 145, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: count, + itemBuilder: (context, index) { + ImageProvider image = ImageFavoritesProvider( + widget.imageFavoritesGroup.imageFavorites[index]); + Widget imageWidget = AnimatedImage( + image: image, + width: 96, + height: 128, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + onError: () { + refreshImageKey(); + hasRefreshImageKeyOnErr = true; + }, + ); + int curPage = + widget.imageFavoritesGroup.imageFavorites[index].page; + String pageText = + curPage == 0 ? 'cover'.tl : (curPage + 1).toString(); + return InkWell( + onTap: () { + // App.rootNavigatorKey + }, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 92, + height: 131, + margin: const EdgeInsets.symmetric(horizontal: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.antiAlias, + child: Column( + children: [imageWidget, Text(pageText, style: ts.s12)], + ), + ), + ); + }, + ), + ).paddingHorizontal(8), + Row( + children: [ + Text( + "最早收藏时间: $time | ${widget.imageFavoritesGroup.sourceKey}", + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 12.0, + ), + ) + ], + ).paddingHorizontal(16).paddingBottom(8), + ], + ), + ), + ); + } +} diff --git a/lib/pages/image_favorites_page/image_favorites_list.dart b/lib/pages/image_favorites_page/image_favorites_list.dart new file mode 100644 index 0000000..d095fe7 --- /dev/null +++ b/lib/pages/image_favorites_page/image_favorites_list.dart @@ -0,0 +1,79 @@ +part of 'image_favorites_page.dart'; + +class ImageFavoritesList extends StatefulWidget { + const ImageFavoritesList( + {super.key, + required this.sortType, + required this.timeFilterSelect, + required this.keyword}); + final ImageFavoriteSortType sortType; + final String timeFilterSelect; + final String keyword; + + @override + State createState() => ImageFavoritesListState(); +} + +class ImageFavoritesListState extends State { + // 所有的图片收藏 + List imageFavoritesGroup = []; + late List curImageFavoritesGroup; + late List timeFilter; + @override + void initState() { + List imageFavorites = ImageFavoriteManager.getAll(); + + for (var ele in imageFavorites) { + try { + ImageFavoritesGroup tempGroup = imageFavoritesGroup + .where( + (i) => i.id == ele.id && i.eid == ele.ep.toString(), + ) + .first; + tempGroup.imageFavorites.add(ele); + } catch (e) { + imageFavoritesGroup + .add(ImageFavoritesGroup(ele.id, [ele], ele.ep.toString())); + } + } + super.initState(); + } + + void getImageFavorites() { + timeFilter = getDateTimeRangeFromFilter(widget.timeFilterSelect); + // 筛选到最终列表 + curImageFavoritesGroup = imageFavoritesGroup.where((ele) { + DateTime start = timeFilter[0]; + DateTime end = timeFilter[1]; + DateTime dateTimeToCheck = ele.firstTime; + return dateTimeToCheck.isAfter(start) && dateTimeToCheck.isBefore(end) || + dateTimeToCheck == start || + dateTimeToCheck == end; + }).toList(); + // 给列表排序 + switch (widget.sortType) { + case ImageFavoriteSortType.name: + curImageFavoritesGroup.sort((a, b) => a.title.compareTo(b.title)); + break; + case ImageFavoriteSortType.timeAsc: + curImageFavoritesGroup + .sort((a, b) => a.firstTime.compareTo(b.firstTime)); + break; + case ImageFavoriteSortType.timeDesc: + curImageFavoritesGroup + .sort((a, b) => b.firstTime.compareTo(a.firstTime)); + break; + default: + } + } + + @override + Widget build(BuildContext context) { + getImageFavorites(); + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return ImageFavoritesItem( + imageFavoritesGroup: curImageFavoritesGroup[index]); + }, childCount: 10)); + } +} diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index ebd5efc..a6e2ad8 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,14 +1,19 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/history.dart'; +import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; import 'package:venera/pages/history_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; -import 'package:venera/pages/reader/reader.dart'; import 'package:venera/utils/translations.dart'; -part "./image_favorites_group.dart"; +part "image_favorites_list.dart"; +part "image_favorites_item.dart"; class ImageFavoritesPage extends StatefulWidget { const ImageFavoritesPage({super.key}); @@ -20,17 +25,25 @@ class ImageFavoritesPage extends StatefulWidget { class ImageFavoritesPageState extends State with TickerProviderStateMixin { late ImageFavoriteSortType sortType; - + late String timeFilterSelect; + late List finalTimeList; + // 所有的图片收藏 + List imageFavoritesGroup = []; + late List curImageFavoritesGroup; + late List timeFilter; String keyword = ""; + // 进入关键词搜索模式 bool searchMode = false; + List optionTypes = ['Sort', 'Filter']; + bool multiSelectMode = false; late TabController controller = TabController( length: 2, vsync: this, ); - + int tabIndex = 0; Map selectedComics = {}; void update() { @@ -45,17 +58,84 @@ class ImageFavoritesPageState extends State } } + void getInitImageFavorites() { + List imageFavorites = ImageFavoriteManager.getAll(); + + for (var ele in imageFavorites) { + try { + ImageFavoritesGroup tempGroup = imageFavoritesGroup + .where( + (i) => i.id == ele.id && i.eid == ele.ep.toString(), + ) + .first; + tempGroup.imageFavorites.add(ele); + } catch (e) { + imageFavoritesGroup + .add(ImageFavoritesGroup(ele.id, [ele], ele.ep.toString())); + } + } + } + + void getCurImageFavorites() { + if (timeFilterSelect != "") { + timeFilter = getDateTimeRangeFromFilter(timeFilterSelect); + // 筛选到最终列表 + curImageFavoritesGroup = imageFavoritesGroup.where((ele) { + DateTime start = timeFilter[0]; + DateTime end = timeFilter[1]; + DateTime dateTimeToCheck = ele.firstTime; + return dateTimeToCheck.isAfter(start) && + dateTimeToCheck.isBefore(end) || + dateTimeToCheck == start || + dateTimeToCheck == end; + }).toList(); + } else { + curImageFavoritesGroup = imageFavoritesGroup; + } + // 给每个 group 的收藏图片排一个序 + for (var ele in curImageFavoritesGroup) { + ele.imageFavorites.sort((a, b) => a.page.compareTo(b.page)); + } + // 给列表排序 + switch (sortType) { + case ImageFavoriteSortType.name: + curImageFavoritesGroup.sort((a, b) => a.title.compareTo(b.title)); + break; + case ImageFavoriteSortType.timeAsc: + curImageFavoritesGroup + .sort((a, b) => a.firstTime.compareTo(b.firstTime)); + break; + case ImageFavoriteSortType.timeDesc: + curImageFavoritesGroup + .sort((a, b) => b.firstTime.compareTo(a.firstTime)); + break; + default: + } + } + @override void initState() { - var sort = appdata.implicitData["local_sort"] ?? "name"; + var sort = appdata.implicitData["image_favorites_sort"] ?? "name"; sortType = ImageFavoriteSortType.fromString(sort); - // comics = LocalManager().getComics(sortType); - // LocalManager().addListener(update); + timeFilterSelect = + appdata.implicitData["image_favorites_time_filter"] ?? ""; + finalTimeList = List.from([ + ...timeFilterList.map((e) => e.toString()), + ...ImageFavoriteManager.earliestTimeToNow + ]); + getInitImageFavorites(); super.initState(); } + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { + getCurImageFavorites(); void selectAll() { setState(() { // selectedComics = []; @@ -198,6 +278,11 @@ class ImageFavoritesPageState extends State }, ), ), + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return ImageFavoritesItem( + imageFavoritesGroup: curImageFavoritesGroup[index]); + }, childCount: 2)), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], ); @@ -207,9 +292,8 @@ class ImageFavoritesPageState extends State void sort() { Widget tabBar = Material( child: FilledTabBar( - tabs: ['Sort', 'Filter'] - .map((e) => Tab(text: e.tl, key: Key(e))) - .toList(), + key: PageStorageKey(optionTypes), + tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), controller: controller, ), ).paddingTop(context.padding.top); @@ -217,21 +301,72 @@ class ImageFavoritesPageState extends State context: context, builder: (context) { return StatefulBuilder(builder: (context, setState) { + void handleTabIndex() { + setState(() { + tabIndex = controller.index; + }); + } + + controller.addListener(handleTabIndex); + return ContentDialog( content: Container( - // 向上移动一点, 减少 Column 顶部的 padding, 避免观感太差 - transform: Matrix4.translationValues(0, -20, 0), - child: Column( - children: [ + // 向上移动一点, 减少 Column 顶部的 padding, 避免观感太差 + transform: Matrix4.translationValues(0, -20, 0), + child: Column(mainAxisSize: MainAxisSize.min, children: [ tabBar, - ], - ), - ), + tabIndex == 0 + ? Column( + children: [ + CustomListItem('Name', ImageFavoriteSortType.name), + CustomListItem( + 'timeAsc', ImageFavoriteSortType.timeAsc), + CustomListItem( + 'timeDesc', ImageFavoriteSortType.timeDesc), + CustomListItem('favorite Num Desc', + ImageFavoriteSortType.maxFavorites), + CustomListItem( + 'favoritesCompareComicPages', + ImageFavoriteSortType + .favoritesCompareComicPages), + ] + .map( + (e) => RadioListTile( + title: Text(e.title.tl), + value: e.value, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + ) + .toList(), + ) + : ListTile( + title: Text("时间筛选".tl), + trailing: Select( + current: timeFilterSelect, + values: finalTimeList, + minWidth: 64, + onTap: (index) { + setState(() { + timeFilterSelect = finalTimeList[index]; + }); + }, + ), + ) + ])), actions: [ FilledButton( onPressed: () { - appdata.implicitData["image_favorite_sort"] = sortType.value; + appdata.implicitData["image_favorites_sort"] = sortType.value; + appdata.implicitData["image_favorites_time_filter"] = + timeFilterSelect; appdata.writeImplicitData(); + controller.removeListener(handleTabIndex); + Navigator.pop(context); update(); }, diff --git a/lib/pages/image_favorites_page/type.dart b/lib/pages/image_favorites_page/type.dart index beb1a30..85ac229 100644 --- a/lib/pages/image_favorites_page/type.dart +++ b/lib/pages/image_favorites_page/type.dart @@ -2,7 +2,8 @@ enum ImageFavoriteSortType { name("name"), timeAsc("time_asc"), timeDesc("time_desc"), - favoriteNumDesc("favorite_num_desc"); + maxFavorites("max_favorites"), // 单本收藏数最多排序 + favoritesCompareComicPages("favorites_compare_comic_pages"); // 单本收藏数比上总页数 final String value; @@ -18,9 +19,61 @@ enum ImageFavoriteSortType { } } -class ImageFavoriteSortItem { +class CustomListItem { final String title; - final ImageFavoriteSortType value; + final T value; - ImageFavoriteSortItem(this.title, this.value); + CustomListItem(this.title, this.value); +} + +enum TimeFilterEnum { + lastWeek("lastWeek"), + lastMonth("lastMonth"), + lastHalfYear("lastHalfYear"), + lastYear("lastYear"); // 单本收藏数最多排序 + + final String value; + const TimeFilterEnum(this.value); + static TimeFilterEnum fromString(String value) { + for (var type in values) { + if (type.value == value) { + return type; + } + } + return lastWeek; + } +} + +const timeFilterList = [ + TimeFilterEnum.lastWeek, + TimeFilterEnum.lastMonth, + TimeFilterEnum.lastHalfYear, + TimeFilterEnum.lastYear, +]; + +getDateTimeRangeFromFilter(String timeFilter) { + DateTime now = DateTime.now(); + DateTime start = now; + DateTime end = now; + switch (timeFilter) { + case TimeFilterEnum.lastWeek: + start = now.subtract(const Duration(days: 7)); + break; + case TimeFilterEnum.lastMonth: + start = now.subtract(const Duration(days: 30)); + break; + case TimeFilterEnum.lastHalfYear: + start = now.subtract(const Duration(days: 180)); + break; + case TimeFilterEnum.lastYear: + start = now.subtract(const Duration(days: 365)); + break; + default: + // 会是 2024, 2025 之类的 + int year = int.parse(timeFilter); + start = DateTime(year, 1, 1); + end = DateTime(year, 12, 31, 23, 59, 59); + } + List ranges = [start, end]; + return ranges; } diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 5873028..c7be269 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -197,6 +197,9 @@ Future importPicaData(File file) async { ); } for (var comic in db.select("SELECT * FROM image_favorites;")) { + var otherInfo = jsonDecode(comic["other"]); + // 就洗一个较为通用的 url 的数据, 别的就不洗了 + otherInfo["imageKey"] = otherInfo["url"]; ImageFavoriteManager.add( ImageFavorite.fromMap({ "id": comic['id'], @@ -205,7 +208,7 @@ Future importPicaData(File file) async { "imagePath": comic["cover"], "ep": comic["ep"], "page": comic["page"], - "otherInfo": jsonDecode(comic["other"]), + "otherInfo": otherInfo, }), ); } diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" index b71cd72..4dbf2fa 100644 --- "a/\344\273\273\345\212\241.md" +++ "b/\344\273\273\345\212\241.md" @@ -3,16 +3,35 @@ 2. 做好页面 1. 阅读时, 下方的收藏 2. 单独的图片收藏页 - 1. 时间筛选, 按照近七天, 一个月, 三个月, 半年, 2024年, 2023年 - 2. 单本收藏数最多排序, 单本收藏数比上总页数最多排序, 并且将小于10页的权重降低 - 3. 抄一下本地的那个, 然后用探索页的tab + 1. 时间筛选, 按照近七天, 一个月, 三个月, 半年, 2024年, 2023年 ok + 2. 单本收藏数最多排序, 单本收藏数比上总页数最多排序, 并且将小于10页的权重降低 ok + 3. 图片收藏group + 1. 看下History 怎么缓存图片的 ok + 1. 他用的封面图, 我这边应该是更加类似预览才行, 得看看comic_page ok + 2. 好像也不行, 还是直接显示原图吧 + 1. 用存的url可以拉取图片,然后缓存在 cacheManager + 2. 有没有办法根据存的漫画源和ep还有page, 拉取图片呢 + 3. 获取images, 用 type.comicSource!.loadComicPages, 用 loadImage 获取图片数据 + 1. comicSource = ComicSource.find(widget.sourceKey); + 2. 显示图片, 支持图片的缓存, 支持用本地图片 + 1. 方案, 检查一下, group中是否所有的图片都有 imageKey ok + 2. 如果有, 就用imageKey 在 imageFavoritesProvider 中调用 ok + 3. 如果检测到本地有这图片, 就用本地的展示, 待学习 + 4. 如果imageKey获取图片失败, 或者存在没有imageKey的图片收藏, 就拿一下loadComicPages, 用最新的来查, 如果还是失败, 就放弃, 如果成功, 就将最新的imageKey更新进去 + 3. list 渐进式加载, silver 本身就支持 + 4. 显示封面, 显示页面page + 5. 响应筛选和排序 4. 支持删除 + 5. 支持导出 + 1. 导出成文本 + 2. 导出成pdf, 不做 + 6. 支持从文本导入漫画, 不做 3. 主页的图片收藏显示 ok 3. 打通联动 4. 自测 5. 有些旧有的功能没有补齐 - 1. 点击收藏直接进行阅读 - 2. sync 网络收藏到本地的时候没有拉取页数的能力 + 1. 点击收藏直接进行阅读, 让作者做 + 2. sync 网络收藏到本地的时候没有拉取页数的能力, 我来优化一下 1. sync 的时候也卡卡的, 一直拉不下来 2. lib\pages\favorites\favorite_actions.dart 应该手动拉一下最大的页数, 然后从拉取倒数几页来判断重复比较好 1. getMaxPageNum getPageSize From ca7fe1115357d1a093365ef31f2b7fb7a4e8e203 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Thu, 26 Dec 2024 22:33:27 +0800 Subject: [PATCH 03/36] =?UTF-8?q?feat:=20=E7=82=B9=E5=87=BB=E6=89=93?= =?UTF-8?q?=E5=BC=80=E5=A4=A7=E5=9B=BE=E6=B5=8F=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image_favorites_item.dart | 17 +- .../image_favorites_page.dart | 134 +++++++------ .../image_favorites_photo_view.dart | 188 ++++++++++++++++++ "\344\273\273\345\212\241.md" | 33 ++- 4 files changed, 304 insertions(+), 68 deletions(-) create mode 100644 lib/pages/image_favorites_page/image_favorites_photo_view.dart diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 4451116..3280cf8 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -46,10 +46,12 @@ class ImageFavoritesItem extends StatefulWidget { required this.imageFavoritesGroup, required this.selectedImageFavorites, required this.addSelected, - required this.multiSelectMode}); + required this.multiSelectMode, + required this.curImageFavoritesGroup}); final ImageFavoritesGroup imageFavoritesGroup; final Function(ImageFavorite) addSelected; final Map selectedImageFavorites; + final List curImageFavoritesGroup; final bool multiSelectMode; @override State createState() => ImageFavoritesItemState(); @@ -113,6 +115,7 @@ class ImageFavoritesItemState extends State { widget.addSelected(ele); } } else { + // 单击跳转漫画详情 App.mainNavigatorKey?.currentContext?.to(() => ComicPage( id: widget.imageFavoritesGroup.id, sourceKey: widget.imageFavoritesGroup.sourceKey, @@ -183,8 +186,18 @@ class ImageFavoritesItemState extends State { String pageText = curPage == 0 ? 'cover'.tl : (curPage + 1).toString(); return InkWell( - onDoubleTap: () {}, + onDoubleTap: () { + // 双击浏览大图 + App.mainNavigatorKey?.currentContext?.to( + () => ImageFavoritesPhotoView( + imageFavoritesGroup: widget.imageFavoritesGroup, + page: curImageFavorite.page, + curImageFavoritesGroup: widget.curImageFavoritesGroup, + ), + ); + }, onTap: () { + // 单击去阅读页面, 跳转到当前点击的page if (widget.multiSelectMode) { widget.addSelected(curImageFavorite); } else { diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 6a6660c..a935614 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,7 +1,10 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; @@ -14,6 +17,7 @@ import 'package:venera/pages/image_favorites_page/type.dart'; import 'package:venera/pages/reader/reader.dart'; import 'package:venera/utils/translations.dart'; part "image_favorites_item.dart"; +part "image_favorites_photo_view.dart"; class ImageFavoritesPage extends StatefulWidget { const ImageFavoritesPage({super.key}); @@ -44,6 +48,7 @@ class ImageFavoritesPageState extends State vsync: this, ); int tabIndex = 0; + // 多选的时候选中的图片 Map selectedImageFavorites = {}; late List imageFavorites; @@ -242,6 +247,18 @@ class ImageFavoritesPageState extends State }, ), ), + MenuButton( + entries: [ + MenuEntry( + icon: Icons.upload_file, + text: "Export current to clipboard".tl, + onClick: () async { + await Clipboard.setData( + ClipboardData(text: "要复制到剪贴板的内容")); + showToast(message: "成功赋值到剪贴板".tl, context: context); + }), + ], + ), ], ) else if (multiSelectMode) @@ -292,10 +309,12 @@ class ImageFavoritesPageState extends State SliverList( delegate: SliverChildBuilderDelegate((context, index) { return ImageFavoritesItem( - imageFavoritesGroup: curImageFavoritesGroup[index], - selectedImageFavorites: selectedImageFavorites, - addSelected: addSelected, - multiSelectMode: multiSelectMode); + imageFavoritesGroup: curImageFavoritesGroup[index], + selectedImageFavorites: selectedImageFavorites, + addSelected: addSelected, + multiSelectMode: multiSelectMode, + curImageFavoritesGroup: curImageFavoritesGroup, + ); }, childCount: 10)), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], @@ -305,10 +324,17 @@ class ImageFavoritesPageState extends State void sort() { Widget tabBar = Material( - child: FilledTabBar( - key: PageStorageKey(optionTypes), - tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), - controller: controller, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + ), + // 向上移动一点, 减少 Column 顶部的 padding, 避免观感太差 + transform: Matrix4.translationValues(0, -24, 0), + child: FilledTabBar( + key: PageStorageKey(optionTypes), + tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), + controller: controller, + ), ), ).paddingTop(context.padding.top); showDialog( @@ -322,56 +348,50 @@ class ImageFavoritesPageState extends State } controller.addListener(handleTabIndex); - return ContentDialog( - content: Container( - // 向上移动一点, 减少 Column 顶部的 padding, 避免观感太差 - transform: Matrix4.translationValues(0, -20, 0), - child: Column(mainAxisSize: MainAxisSize.min, children: [ - tabBar, - tabIndex == 0 - ? Column( - children: [ - CustomListItem('Name', ImageFavoriteSortType.name), - CustomListItem( - 'timeAsc', ImageFavoriteSortType.timeAsc), - CustomListItem( - 'timeDesc', ImageFavoriteSortType.timeDesc), - CustomListItem('favorite Num Desc', - ImageFavoriteSortType.maxFavorites), - CustomListItem( - 'favoritesCompareComicPages', - ImageFavoriteSortType - .favoritesCompareComicPages), - ] - .map( - (e) => RadioListTile( - title: Text(e.title.tl), - value: e.value, - groupValue: sortType, - onChanged: (v) { - setState(() { - sortType = v!; - }); - }, - ), - ) - .toList(), - ) - : ListTile( - title: Text("时间筛选".tl), - trailing: Select( - current: timeFilterSelect, - values: finalTimeList, - minWidth: 64, - onTap: (index) { - setState(() { - timeFilterSelect = finalTimeList[index]; - }); - }, - ), - ) - ])), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + tabBar, + tabIndex == 0 + ? Column( + children: [ + CustomListItem('Name', ImageFavoriteSortType.name), + CustomListItem( + 'timeAsc', ImageFavoriteSortType.timeAsc), + CustomListItem( + 'timeDesc', ImageFavoriteSortType.timeDesc), + CustomListItem('favorite Num Desc', + ImageFavoriteSortType.maxFavorites), + CustomListItem('favoritesCompareComicPages', + ImageFavoriteSortType.favoritesCompareComicPages), + ] + .map( + (e) => RadioListTile( + title: Text(e.title.tl), + value: e.value, + groupValue: sortType, + onChanged: (v) { + setState(() { + sortType = v!; + }); + }, + ), + ) + .toList(), + ) + : ListTile( + title: Text("时间筛选".tl), + trailing: Select( + current: timeFilterSelect, + values: finalTimeList, + minWidth: 64, + onTap: (index) { + setState(() { + timeFilterSelect = finalTimeList[index]; + }); + }, + ), + ) + ]), actions: [ FilledButton( onPressed: () { diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart new file mode 100644 index 0000000..e552170 --- /dev/null +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -0,0 +1,188 @@ +part of 'image_favorites_page.dart'; + +class ImageFavoritesPhotoView extends StatefulWidget { + const ImageFavoritesPhotoView( + {super.key, + required this.imageFavoritesGroup, + required this.page, + required this.curImageFavoritesGroup}); + final ImageFavoritesGroup imageFavoritesGroup; + final int page; + final List curImageFavoritesGroup; + @override + State createState() => + ImageFavoritesPhotoViewState(); +} + +class ImageFavoritesPhotoViewState extends State { + late PageController controller; + Map cancelImageFavorites = {}; + // 图片当前的 index + late int curIndex; + late int curImageFavoritesGroupIndex; + @override + void initState() { + curIndex = widget.imageFavoritesGroup.imageFavorites.indexWhere((ele) { + return ele.page == widget.page; + }); + controller = PageController(initialPage: curIndex); + curImageFavoritesGroupIndex = + widget.curImageFavoritesGroup.indexWhere((ele) { + return ele.id == widget.imageFavoritesGroup.id; + }); + super.initState(); + } + + void onPop() {} + PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { + final ImageFavorite curImageFavorite = widget + .curImageFavoritesGroup[curImageFavoritesGroupIndex] + .imageFavorites[index]; + return PhotoViewGalleryPageOptions( + // 图片加载器 支持本地、网络 + imageProvider: ImageFavoritesProvider(curImageFavorite), + // 初始化大小 全部展示 + minScale: PhotoViewComputedScale.contained * 1.0, + maxScale: PhotoViewComputedScale.covered * 10.0, + onTapUp: (context, details, controllerValue) { + Navigator.pop(context); + onPop(); + }); + } + + @override + Widget build(BuildContext context) { + ImageFavoritesGroup curGroup = + widget.curImageFavoritesGroup[curImageFavoritesGroupIndex]; + ImageFavorite curImageFavorite = curGroup.imageFavorites[curIndex]; + int curPage = curImageFavorite.page; + String pageText = curPage == 0 ? 'cover'.tl : (curPage + 1).toString(); + return PopScope( + onPopInvokedWithResult: (bool didPop, Object? result) async { + if (didPop) { + onPop(); + } + }, + child: Stack(children: [ + PhotoViewGallery.builder( + backgroundDecoration: BoxDecoration( + color: context.colorScheme.surface, + ), + builder: _buildItem, + itemCount: curGroup.imageFavorites.length, + loadingBuilder: (context, event) => Center( + child: SizedBox( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + backgroundColor: context.colorScheme.surfaceContainerHigh, + value: event == null || event.expectedTotalBytes == null + ? null + : event.cumulativeBytesLoaded / event.expectedTotalBytes!, + ), + ), + ), + enableRotation: true, + customSize: MediaQuery.of(context) + .size, //定义图片默认缩放基础的大小,默认全屏 MediaQuery.of(context).size + pageController: controller, + onPageChanged: (index) { + setState(() { + curIndex = index; + }); + }, + ), + Positioned( + top: 20, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + curGroup.title, + style: ts.s18, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).paddingTop(20), + ), + ), + Positioned( + bottom: 20, + left: 0, + right: 0, + child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + IconButton( + icon: Icon(Icons.arrow_circle_left), + onPressed: () { + if (curImageFavoritesGroupIndex == 0) { + curImageFavoritesGroupIndex = + widget.curImageFavoritesGroup.length - 1; + } else { + curImageFavoritesGroupIndex -= 1; + } + curIndex = 0; + controller.jumpToPage(0); + setState(() {}); + }, + ), + IconButton( + icon: cancelImageFavorites[curImageFavorite] == true + ? Icon(Icons.favorite_border) + : Icon(Icons.favorite), + onPressed: () { + if (cancelImageFavorites[curImageFavorite] == true) { + cancelImageFavorites[curImageFavorite] = false; + } else { + cancelImageFavorites[curImageFavorite] = true; + } + + setState(() {}); + }, + ), + Text( + pageText, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + IconButton( + icon: Icon(Icons.play_arrow), + onPressed: () {}, + ), + IconButton( + icon: Icon(Icons.arrow_circle_right), + onPressed: () { + if (curImageFavoritesGroupIndex == + widget.curImageFavoritesGroup.length - 1) { + curImageFavoritesGroupIndex = 0; + } else { + curImageFavoritesGroupIndex += 1; + } + curIndex = 0; + controller.jumpToPage(0); + setState(() {}); + }, + ), + ]), + ), + Positioned( + bottom: 33, + right: 20, + child: IntrinsicWidth( + stepWidth: 0, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text("${curIndex + 1}/${curGroup.imageFavorites.length}", + style: ts.s12), + ), + )) + ]), + ); + } +} diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" index d1bbd30..ba14fb2 100644 --- "a/\344\273\273\345\212\241.md" +++ "b/\344\273\273\345\212\241.md" @@ -23,15 +23,30 @@ 3. list 渐进式加载, silver 本身就支持 ok 4. 显示封面, 显示页面page ok 5. 响应筛选和排序 ok - 4. 支持选中删除 - 5. 支持导出 - 1. 导出成文本 - 2. 导出成pdf, 不做 - 6. 支持从文本导入漫画, 不做 - 7. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int - 1. ep 和 page 都不从0开始 - 2. eid 存在 otherInfo 里 - 8. 中英互译 + 4. 支持选中删除 ok + 5. 双击大图浏览 ok + 1. 显示标题 ok + 3. 支持取消收藏, 退出大图浏览时更新数据库 + 4. 点击进行阅读, 点击进入详情 + 5. 点击进入上一本, 下一本 ok + 6. 显示 tags + 6. 筛选 + 1. tag 筛选 + 2. 页面数量范围 + 7. 支持导出 + 1. 导出成文本, 包含标题, 链接, 总页数, 收藏数, 收藏占比, 阅读时间, tag 数等 + 2. 生成总结页面, 包含最喜欢作者, 本子, 共收藏本子数, 共收藏图片数, 最喜欢tag, 最喜欢本子, 按总页数, 收藏占比 + 8. bug解决 + 1. 进入的时候都是一张图, 都是cover + 2. 并行请求太多 timeout 风险较高 + 9. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int + 1. imagePath 不需要 + 2. ep 和 page 都不从0开始 + 3. eid 存在 otherInfo 里 + 4. 是否做一个大的改动, 将数据库改成一本漫画的一个章节对应一条记录的形式 + 10. 中英互译 + 11. 实际大数据量测试 + 12. 实机测试 3. 主页的图片收藏显示 ok 3. 打通联动 4. 自测 From 99db9746c4cb184b5ec34886ad55c06239ca510b Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Mon, 30 Dec 2024 23:26:56 +0800 Subject: [PATCH 04/36] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=8F=98=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/message.dart | 3 +- lib/foundation/history.dart | 4 + lib/foundation/image_favorites.dart | 440 +++++++++++++++--- .../image_favorites_provider.dart | 21 +- lib/foundation/local.dart | 9 +- lib/pages/comic_page.dart | 2 + lib/pages/home_page/image_favorites.dart | 26 +- .../image_favorites_item.dart | 194 ++++---- .../image_favorites_page.dart | 132 +++--- .../image_favorites_photo_view.dart | 107 +++-- lib/pages/reader/loading.dart | 58 ++- lib/pages/reader/reader.dart | 29 +- lib/pages/reader/scaffold.dart | 100 ++-- lib/utils/data.dart | 50 +- "\344\273\273\345\212\241.md" | 19 +- 15 files changed, 826 insertions(+), 368 deletions(-) diff --git a/lib/components/message.dart b/lib/components/message.dart index be86415..cfa07ff 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -5,6 +5,7 @@ void showToast({ required BuildContext context, Widget? icon, Widget? trailing, + int? seconds, }) { var newEntry = OverlayEntry( builder: (context) => _ToastOverlay( @@ -17,7 +18,7 @@ void showToast({ state?.addOverlay(newEntry); - Timer(const Duration(seconds: 2), () => state?.remove(newEntry)); + Timer(Duration(seconds: seconds ?? 2), () => state?.remove(newEntry)); } class _ToastOverlay extends StatelessWidget { diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 409ef07..d69d605 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -1,13 +1,17 @@ import 'dart:async'; import 'dart:convert'; import 'dart:developer'; +import 'dart:math'; import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:intl/intl.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/state_controller.dart'; +import 'package:venera/utils/ext.dart'; +import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'app.dart'; diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 20037ae..4e9148a 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -1,49 +1,205 @@ part of "history.dart"; class ImageFavorite { - /// unique id for the comic - final String id; + String imageKey; - final String imagePath; + int page; - final DateTime time; + // 是否是自动收藏的, 仅用于第一页 + bool? isAutoFavorite; - final String title; + ImageFavorite( + this.page, + this.imageKey, + this.isAutoFavorite, + ); +} +class ImageFavoritePro extends ImageFavorite { + final String eid; + final String id; // 漫画id final int ep; - - final int page; - - final Map otherInfo; - - const ImageFavorite( + final String epName; + final String sourceKey; + ImageFavoritePro( + super.page, + super.imageKey, + super.isAutoFavorite, + this.eid, this.id, - this.imagePath, - this.title, - this.time, this.ep, - this.page, - this.otherInfo, + this.sourceKey, + this.epName, ); + Map toJson() { + return { + 'page': page, + 'imageKey': imageKey, + 'isAutoFavorite': isAutoFavorite, + 'eid': eid, + 'id': id, + 'ep': ep, + 'sourceKey': sourceKey, + }; + } + + // 复制构造函数 + ImageFavoritePro.copy(ImageFavoritePro other) + : this(other.page, other.imageKey, other.isAutoFavorite, other.eid, + other.id, other.ep, other.sourceKey, other.epName); +} + +class ImageFavoritesEp { + final String eid; + final int ep; + int maxPage; + String epName; + List imageFavorites; + + // 避免使用常量 + static int firstPage = 1; + + ImageFavoritesEp( + this.eid, this.ep, this.imageFavorites, this.epName, this.maxPage); + Map toJson() { + return { + 'eid': eid, + 'ep': ep, + 'imageFavorites': imageFavorites, + 'epName': epName, + 'maxPage': maxPage, + }; + } + + // 是否有封面 + bool get isHasFirstPage { + return imageFavorites[0].page == firstPage; + } + + // 是否都有imageKey + bool get isHasImageKey { + return imageFavorites.every((e) => e.imageKey != ""); + } +} + +class ImageFavoritesSomething { + String author; + String subTitle; + List tags; + List translatedTags; + String epName; + ImageFavoritesSomething( + this.author, this.tags, this.translatedTags, this.epName, this.subTitle); +} + +class ImageFavoritesComic { + final String id; + final String title; + String subTitle; + String author; + final String sourceKey; + // 不一定是真的这本漫画的所有页数, 如果是多章节的时候 + int maxPage; + List tags; + List translatedTags; + final DateTime time; + List imageFavoritesEp; + final Map other; + + ImageFavoritesComic( + this.id, + this.imageFavoritesEp, + this.title, + this.sourceKey, + this.tags, + this.translatedTags, + this.time, + this.author, + this.other, + this.subTitle, + this.maxPage); + + // 是否都有imageKey + bool get isAllHasImageKey { + return imageFavoritesEp + .every((e) => e.imageFavorites.every((j) => j.imageKey != "")); + } + + int get maxPageFromEp { + int temp = 0; + for (var e in imageFavoritesEp) { + temp += e.maxPage; + } + return temp; + } + + // 是否都有封面 + bool get isAllHasFirstPage { + return imageFavoritesEp.every((e) => e.isHasFirstPage); + } + + List get sortedImageFavoritePros { + List temp = []; + for (var e in imageFavoritesEp) { + for (var i in e.imageFavorites) { + temp.add(i); + } + } + return temp; + } + + static List tagsToTranslated(List tags) { + var translatedTags = []; + for (var tag in tags) { + var translated = tag.translateTagsToCN; + if (translated != tag) { + translatedTags.add(translated); + } + } + return translatedTags; + } - ImageFavorite.fromMap(Map map) - : time = map["time"] ?? DateTime.now(), // 兜底从 picacomic 引入时没有 time 的情况 - title = map["title"], - imagePath = map["imagePath"], - ep = map["ep"], - page = map["page"], - id = map["id"], - otherInfo = map["otherInfo"] ?? {}; + static ImageFavoritesSomething getSomethingFromComicDetails( + ComicDetails comicDetails, int ep) { + List tags = []; + String author = ""; + try { + if (comicDetails.tags['Artists'] != null) { + author = comicDetails.tags['Artists']!.first; + } + if (comicDetails.tags['artist'] != null) { + author = comicDetails.tags['artist']!.first; + } + if (comicDetails.tags['作者'] != null) { + author = comicDetails.tags['作者']!.first; + } + if (comicDetails.tags['Author'] != null) { + author = comicDetails.tags['Author']!.first; + } + // ignore: empty_catches + } catch (e) {} + String epName = + comicDetails.chapters?.values.elementAtOrNull(ep - 1) ?? "E$ep"; + tags = comicDetails.tags.values.fold( + [], (previousValue, element) => [...previousValue, ...element]); + var translatedTags = tagsToTranslated(tags); + String subTitle = comicDetails.subTitle ?? ""; + return ImageFavoritesSomething( + author, tags, translatedTags, epName, subTitle); + } } class ImageFavoriteManager with ChangeNotifier { static Database get _db => HistoryManager()._db; static ImageFavoriteManager? cache; + static List imageFavoritesComicList = getAll(null); ImageFavoriteManager.create(); + static bool hasInit = false; factory ImageFavoriteManager() { return cache == null ? (cache = ImageFavoriteManager.create()) : cache!; } void updateValue() { + imageFavoritesComicList = getAll(null); notifyListeners(); } @@ -52,69 +208,204 @@ class ImageFavoriteManager with ChangeNotifier { _db.execute("CREATE TABLE IF NOT EXISTS image_favorites (" "id TEXT," "title TEXT NOT NULL," + "sub_title TEXT," + "author TEXT," + "tags TEXT," + "translated_tags TEXT," "time int," - "cover TEXT NOT NULL," - "ep INTEGER NOT NULL," - "page INTEGER NOT NULL," + "max_page int," + "source_key TEXT NOT NULL," + "image_favorites_ep TEXT NOT NULL," "other TEXT NOT NULL," - "PRIMARY KEY (id, ep, page)" + "PRIMARY KEY (id,source_key)" ");"); + hasInit = true; } - static void add(ImageFavorite favorite) { - _db.execute(""" - insert or replace into image_favorites(id, title, time, cover, ep, page, other) - values(?, ?, ?, ?, ?, ?, ?); + // 做排序和去重的操作 + static void addOrUpdateOrDelete(ImageFavoritesComic favorite) { + // 没有章节了就删掉 + if (favorite.imageFavoritesEp.isEmpty) { + _db.execute(""" + delete from image_favorites + where id == ? and source_key == ?; + """, [favorite.id, favorite.sourceKey]); + } else { + List tempImageFavoritesEp = []; + for (var e in favorite.imageFavoritesEp) { + int index = tempImageFavoritesEp.indexWhere((i) => i.ep == e.ep); + if (index == -1) { + tempImageFavoritesEp.add(e); + } + } + tempImageFavoritesEp.sort((a, b) => a.ep.compareTo(b.ep)); + List finalImageFavoritesEp = + jsonDecode(jsonEncode(tempImageFavoritesEp)); + for (var e in tempImageFavoritesEp) { + List finalImageFavorites = []; + int epIndex = tempImageFavoritesEp.indexOf(e); + for (ImageFavoritePro j in e.imageFavorites) { + int index = + finalImageFavorites.indexWhere((i) => i["page"] == j.page); + if (index == -1) { + // isAutoFavorite 为 null 不写入数据库, 同时只保留需要的属性, 避免增加太多重复字段在数据库里 + if (j.isAutoFavorite != null) { + finalImageFavorites.add({ + "page": j.page, + "imageKey": j.imageKey, + "isAutoFavorite": j.isAutoFavorite + }); + } else { + finalImageFavorites.add({"page": j.page, "imageKey": j.imageKey}); + } + } + } + finalImageFavorites.sort((a, b) => a["page"].compareTo(b["page"])); + finalImageFavoritesEp[epIndex]["imageFavorites"] = finalImageFavorites; + } + _db.execute(""" + insert or replace into image_favorites(id, title, sub_title, author, tags, translated_tags, time, max_page, source_key, image_favorites_ep, other) + values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """, [ - favorite.id, - favorite.title, - favorite.time.millisecondsSinceEpoch, - favorite.imagePath, - favorite.ep, - favorite.page, - jsonEncode(favorite.otherInfo) - ]); + favorite.id, + favorite.title, + favorite.subTitle, + favorite.author, + favorite.tags.join(","), + favorite.translatedTags.join(","), + favorite.time.millisecondsSinceEpoch, + favorite.maxPage, + favorite.sourceKey, + jsonEncode(finalImageFavoritesEp), + jsonEncode(favorite.other) + ]); + } ImageFavoriteManager().updateValue(); } - static void remove(String id, int ep, int page) async { - _db.execute(""" - delete from image_favorites - where id == ? and ep == ? and page == ?; - """, [id, ep, page]); - ImageFavoriteManager().updateValue(); + static ImageFavoritesComic? findFromComicList( + List tempList, + String id, + String sourceKey, + dynamic eid, + int page) { + ImageFavoritesComic? temp = tempList + .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); + if (temp == null) { + return null; + } else { + ImageFavoritesEp? tempEp = temp.imageFavoritesEp.firstWhereOrNull((e) { + if (eid is int) { + return e.ep == eid; + } + return e.eid == eid; + }); + if (tempEp == null) { + return null; + } else { + ImageFavorite? tempFavorite = + tempEp.imageFavorites.firstWhereOrNull((e) => e.page == page); + if (tempFavorite != null) { + return temp; + } + return null; + } + } } - static bool isHas(String id, int ep, int page) { - var res = _db.select(""" - select * from image_favorites - where id == ? and ep == ? and page == ?; - """, [id, ep, page]); - if (res.isEmpty) { - return false; - } - return true; + static bool isHas(String id, String sourceKey, String eid, int page) { + return findFromComicList( + imageFavoritesComicList, id, sourceKey, eid, page) != + null; } - static List getAll() { - var res = _db.select("select * from image_favorites;"); - return res - .map((e) => ImageFavorite( - e["id"], - e["cover"], - e["title"], - DateTime.fromMillisecondsSinceEpoch(e["time"]), - e["ep"], - e["page"], - jsonDecode(e["other"]))) - .toList(); + static List getAll(String? keyword) { + if (!hasInit) return []; + var res = []; + if (keyword == null) { + res = _db.select("select * from image_favorites;"); + } else { + _db.select( + """ + select * from image_favorites + WHERE LOWER(title) LIKE LOWER(?) + OR LOWER(sub_title) LIKE LOWER(?) + OR LOWER(tags) LIKE LOWER(?) + OR LOWER(translated_tags) LIKE LOWER(?) + OR LOWER(author) LIKE LOWER(?); + """, + ['%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%'], + ); + } + try { + return res.map((e) { + dynamic tempImageFavoritesEp = jsonDecode(e["image_favorites_ep"]); + List finalImageFavoritesEp = []; + tempImageFavoritesEp.forEach((i) { + List temp = []; + i["imageFavorites"].forEach((j) { + temp.add(ImageFavoritePro( + j["page"], + j["imageKey"], + j["isAutoFavorite"], + i["eid"], + e["id"], + i["ep"], + e["source_key"], + i["epName"])); + }); + finalImageFavoritesEp.add(ImageFavoritesEp( + i["eid"], i["ep"], temp, i["epName"], i["maxPage"] ?? 1)); + }); + return ImageFavoritesComic( + e["id"], + finalImageFavoritesEp, + e["title"], + e["source_key"], + e["tags"].split(","), + e["translated_tags"].split(","), + DateTime.fromMillisecondsSinceEpoch(e["time"]), + e["author"], + jsonDecode(e["other"]), + e["sub_title"], + e["max_page"], + ); + }).toList(); + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); + return []; + } } - static void delete(ImageFavorite favorite) { - _db.execute(""" - delete from image_favorites - where id = ? and ep = ? and page = ?; - """, [favorite.id, favorite.ep, favorite.page]); + static void deleteImageFavoritePro( + List imageFavoriteProList) { + for (var e in imageFavoritesComicList) { + // 找到需要删除的具体图片 + List filterImageFavoritesPro = + imageFavoriteProList.where((i) { + return i.id == e.id && i.sourceKey == e.sourceKey; + }).toList(); + if (filterImageFavoritesPro.isNotEmpty) { + e.imageFavoritesEp = e.imageFavoritesEp.where((i) { + // 去掉匹配到的具体图片 + i.imageFavorites = i.imageFavorites.where((j) { + ImageFavoritePro? temp = + filterImageFavoritesPro.firstWhereOrNull((k) { + return k.eid == j.eid && k.page == j.page; + }); + // 如果没有匹配到, 说明不是这个章节和page, 就留着 + return temp == null; + }).toList(); + // 如果一张图片都没了, 或者只有一张自动收藏的firstPage, 就去掉这个章节 + if (i.imageFavorites.length == 1 && + i.imageFavorites.first.isAutoFavorite == true) { + return false; + } + return i.imageFavorites.isNotEmpty; + }).toList(); + addOrUpdateOrDelete(e); + } + } ImageFavoriteManager().updateValue(); } @@ -133,7 +424,12 @@ class ImageFavoriteManager with ChangeNotifier { } static int get length { + if (!hasInit) return 0; var res = _db.select("select count(*) from image_favorites;"); return res.first.values.first! as int; } + + static List search(String keyword) { + return getAll(keyword); + } } diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart index ae57392..5a40fbb 100644 --- a/lib/foundation/image_provider/image_favorites_provider.dart +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -13,16 +13,15 @@ class ImageFavoritesProvider /// [url] is the url of the image. Local file path is also supported. const ImageFavoritesProvider(this.imageFavorite); - final ImageFavorite imageFavorite; + final ImageFavoritePro imageFavorite; @override Future load(StreamController chunkEvents) async { - String? imageKey = imageFavorite.otherInfo["imageKey"]; - List ids = imageFavorite.id.split('-'); - String sourceKey = ids[0]; - String cid = ids[1]; - String eid = imageFavorite.ep.toString(); - if (imageKey == null) { + String? imageKey = imageFavorite.imageKey; + String sourceKey = imageFavorite.sourceKey; + String cid = imageFavorite.id; + String eid = imageFavorite.eid; + if (imageKey == "") { throw "Error: imageFavorits no imageKey"; } await for (var progress @@ -43,7 +42,11 @@ class ImageFavoritesProvider return SynchronousFuture(this); } + static String getImageKey(ImageFavoritePro temp) { + return "${temp.imageKey}@${temp.sourceKey}@${temp.id}@${temp.eid}"; + } + + // 和 reader_image 的一样 @override - String get key => - "imageFavorite${imageFavorite.id}${imageFavorite.ep}${imageFavorite.page}"; + String get key => getImageKey(imageFavorite); } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 5d00c64..60859d4 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -119,6 +119,7 @@ class LocalComic with HistoryMixin implements Comic { ep: 0, page: 0, ), + comicDetails: this, ), ); } @@ -266,7 +267,7 @@ class LocalManager with ChangeNotifier { String findValidId(ComicType type) { final res = _db.select( ''' - SELECT id FROM comics WHERE comic_type = ? + SELECT id FROM comics WHERE comic_type = ? ORDER BY CAST(id AS INTEGER) DESC LIMIT 1; ''', @@ -318,8 +319,8 @@ class LocalManager with ChangeNotifier { List getComics(LocalSortType sortType) { var res = _db.select(''' SELECT * FROM comics - ORDER BY - ${sortType.value == 'name' ? 'title' : 'created_at'} + ORDER BY + ${sortType.value == 'name' ? 'title' : 'created_at'} ${sortType.value == 'time_asc' ? 'ASC' : 'DESC'} ; '''); @@ -361,7 +362,7 @@ class LocalManager with ChangeNotifier { LocalComic? findByName(String name) { final res = _db.select(''' - SELECT * FROM comics + SELECT * FROM comics WHERE title = ? OR directory = ?; ''', [name, name]); if (res.isEmpty) { diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index d78c893..a005e0f 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -145,6 +145,7 @@ class _ComicPageState extends LoadingState ep: 0, page: 0, ), + comicDetails: localComic, ); }); App.mainNavigatorKey!.currentContext!.pop(); @@ -663,6 +664,7 @@ abstract mixin class _ComicPageActions { initialChapter: ep, initialPage: page, history: History.fromModel(model: comic, ep: 0, page: 0), + comicDetails: comic, ), ); } diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart index abd7596..195fe7a 100644 --- a/lib/pages/home_page/image_favorites.dart +++ b/lib/pages/home_page/image_favorites.dart @@ -8,8 +8,28 @@ class ImageFavorites extends StatefulWidget { } class ImageFavoritesState extends State { + void refreshImageFavorites() { + setState(() {}); + } + + @override + void initState() { + ImageFavoriteManager().addListener(refreshImageFavorites); + super.initState(); + } + + @override + void dispose() { + ImageFavoriteManager().removeListener(refreshImageFavorites); + super.dispose(); + } + @override Widget build(BuildContext context) { + List allImageFavoritePros = []; + for (var comic in ImageFavoriteManager.imageFavoritesComicList) { + allImageFavoritePros.addAll(comic.sortedImageFavoritePros); + } return SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), @@ -43,8 +63,10 @@ class ImageFavoritesState extends State { SizedBox( width: double.infinity, child: Text( - "@a image favorites" - .tlParams({"a": ImageFavoriteManager.length.toString()}), + "@a Comic, @b image favorites".tlParams({ + "a": ImageFavoriteManager.length.toString(), + "b": allImageFavoritePros.length + }), style: const TextStyle(fontSize: 15), ).paddingHorizontal(16).paddingBottom(16), ), diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 3280cf8..0de3c9d 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -1,57 +1,17 @@ part of 'image_favorites_page.dart'; -class ImageFavoritesGroup { - final String id; - List imageFavorites; - final String eid; - ImageFavoritesGroup(this.id, this.imageFavorites, this.eid); - - // 避免后边收藏图片的时候标题有更新 - String get title { - return imageFavorites.last.title; - } - - String get sourceKey { - return id.split('-')[0]; - } - - String get cid { - return id.split('-')[1]; - } - - // 最早的那张图片收藏的时间 - DateTime get firstTime { - return imageFavorites - .fold( - imageFavorites[0], - (prev, current) => - prev.time.isBefore(current.time) ? prev : current) - .time; - } - - // 是否都有imageKey - bool get isAllHasImageKey { - return imageFavorites.every((e) => e.otherInfo["imageKey"] != null); - } - - // 是否都有封面 - bool get isHasFirstPage { - return imageFavorites[0].page == 0; - } -} - class ImageFavoritesItem extends StatefulWidget { const ImageFavoritesItem( {super.key, - required this.imageFavoritesGroup, + required this.imageFavoritesComic, required this.selectedImageFavorites, required this.addSelected, required this.multiSelectMode, - required this.curImageFavoritesGroup}); - final ImageFavoritesGroup imageFavoritesGroup; - final Function(ImageFavorite) addSelected; + required this.finalImageFavoritesComicList}); + final ImageFavoritesComic imageFavoritesComic; + final Function(ImageFavoritePro) addSelected; final Map selectedImageFavorites; - final List curImageFavoritesGroup; + final List finalImageFavoritesComicList; final bool multiSelectMode; @override State createState() => ImageFavoritesItemState(); @@ -61,43 +21,83 @@ class ImageFavoritesItemState extends State { bool isImageKeyLoading = false; // 刷新 imageKey 失败的场景再刷新一次, 再次失败了就不重试了 bool hasRefreshImageKeyOnErr = false; - // 如果 imageKey 失效了, 或者刚从pica导入, 没有就走这个逻辑 - void refreshImageKey() async { + // 如果 imageKey 失效了, 或者刚从pica导入(没有imageKey) + void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { if (isImageKeyLoading || hasRefreshImageKeyOnErr) return; isImageKeyLoading = true; ComicSource? comicSource = - ComicSource.find(widget.imageFavoritesGroup.sourceKey); - var res = await comicSource!.loadComicPages!( - widget.imageFavoritesGroup.cid, - widget.imageFavoritesGroup.eid, - ); - if (!res.error) { - List images = res.data; - // 塞个封面进去 - if (!widget.imageFavoritesGroup.isHasFirstPage) { - ImageFavorite copyObj = widget.imageFavoritesGroup.imageFavorites[0]; - ImageFavorite temp = ImageFavorite(copyObj.id, '', copyObj.title, - copyObj.time, copyObj.ep, 0, {'imageKey': images[0]}); - ImageFavoriteManager.add(temp); + ComicSource.find(widget.imageFavoritesComic.sourceKey); + var resArr = await Future.wait([ + comicSource!.loadComicPages!( + widget.imageFavoritesComic.id, + imageFavoritesEp.eid, + ), + comicSource.loadComicInfo!( + widget.imageFavoritesComic.id, + ) + ]); + Res> comicPagesRes = resArr[0] as Res>; + Res comicInfoRes = resArr[1] as Res; + if (!comicPagesRes.error && !comicInfoRes.error) { + List images = comicPagesRes.data; + ImageFavoritesSomething something = + ImageFavoritesComic.getSomethingFromComicDetails( + comicInfoRes.data, imageFavoritesEp.ep); + // 刷新一下值, 保存最新的 + widget.imageFavoritesComic.author = something.author; + widget.imageFavoritesComic.maxPage = images.length; + widget.imageFavoritesComic.subTitle = something.subTitle; + widget.imageFavoritesComic.tags = something.tags; + widget.imageFavoritesComic.translatedTags = something.translatedTags; + imageFavoritesEp.maxPage = images.length; + imageFavoritesEp.epName = something.epName; + // 塞一个封面进去 + if (!imageFavoritesEp.isHasFirstPage) { + ImageFavoritePro copy = + ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); + copy.page = ImageFavoritesEp.firstPage; + copy.imageKey = images[0]; + copy.isAutoFavorite = true; + imageFavoritesEp.imageFavorites.insert(0, copy); } - for (var ele in widget.imageFavoritesGroup.imageFavorites) { - ele.otherInfo["imageKey"] = images[ele.page]; - ImageFavoriteManager.add(ele); + for (var ele in imageFavoritesEp.imageFavorites) { + ele.imageKey = images[ele.page - 1]; } + ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); setState(() {}); } isImageKeyLoading = false; } + void goComicInfo(ImageFavoritesComic comic) { + App.mainNavigatorKey?.currentContext?.to(() => ComicPage( + id: comic.id, + sourceKey: comic.sourceKey, + )); + } + + void goReaderPage(ImageFavoritesComic comic, int ep, int page) { + App.mainNavigatorKey?.currentContext?.to( + () => ReaderWithLoading( + id: comic.id, + sourceKey: comic.sourceKey, + initialEp: ep, + initialPage: page, + ), + ); + } + @override Widget build(BuildContext context) { - int count = widget.imageFavoritesGroup.imageFavorites.length; - if (!widget.imageFavoritesGroup.isAllHasImageKey || - !widget.imageFavoritesGroup.isHasFirstPage) { - refreshImageKey(); + int count = widget.imageFavoritesComic.sortedImageFavoritePros.length; + if (!widget.imageFavoritesComic.isAllHasImageKey || + !widget.imageFavoritesComic.isAllHasFirstPage) { + for (var e in widget.imageFavoritesComic.imageFavoritesEp) { + refreshImageKey(e); + } } String time = DateFormat('yyyy-MM-dd HH:mm:ss') - .format(widget.imageFavoritesGroup.firstTime); + .format(widget.imageFavoritesComic.time); return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( @@ -111,19 +111,17 @@ class ImageFavoritesItemState extends State { borderRadius: BorderRadius.circular(8), onTap: () { if (widget.multiSelectMode) { - for (var ele in widget.imageFavoritesGroup.imageFavorites) { + for (var ele + in widget.imageFavoritesComic.sortedImageFavoritePros) { widget.addSelected(ele); } } else { // 单击跳转漫画详情 - App.mainNavigatorKey?.currentContext?.to(() => ComicPage( - id: widget.imageFavoritesGroup.id, - sourceKey: widget.imageFavoritesGroup.sourceKey, - )); + goComicInfo(widget.imageFavoritesComic); } }, onLongPress: () { - for (var ele in widget.imageFavoritesGroup.imageFavorites) { + for (var ele in widget.imageFavoritesComic.sortedImageFavoritePros) { widget.addSelected(ele); } }, @@ -136,7 +134,7 @@ class ImageFavoritesItemState extends State { children: [ Expanded( child: Text( - widget.imageFavoritesGroup.title, + widget.imageFavoritesComic.title, style: const TextStyle( fontWeight: FontWeight.w500, fontSize: 14.0, @@ -154,7 +152,9 @@ class ImageFavoritesItemState extends State { color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(8), ), - child: Text(count.toString(), style: ts.s12), + child: Text( + "$count/${widget.imageFavoritesComic.maxPageFromEp}", + style: ts.s12), ), ], ), @@ -165,8 +165,13 @@ class ImageFavoritesItemState extends State { scrollDirection: Axis.horizontal, itemCount: count, itemBuilder: (context, index) { - ImageFavorite curImageFavorite = - widget.imageFavoritesGroup.imageFavorites[index]; + ImageFavoritePro curImageFavorite = + widget.imageFavoritesComic.sortedImageFavoritePros[index]; + ImageFavoritesEp curImageFavoritesEp = widget + .imageFavoritesComic.imageFavoritesEp + .firstWhere((e) { + return e.eid == curImageFavorite.eid; + }); ImageProvider image = ImageFavoritesProvider(curImageFavorite); bool isSelected = @@ -178,21 +183,25 @@ class ImageFavoritesItemState extends State { fit: BoxFit.cover, filterQuality: FilterQuality.medium, onError: () { - refreshImageKey(); + refreshImageKey(curImageFavoritesEp); hasRefreshImageKeyOnErr = true; }, ); int curPage = curImageFavorite.page; - String pageText = - curPage == 0 ? 'cover'.tl : (curPage + 1).toString(); + String pageText = curPage == ImageFavoritesEp.firstPage + ? '@a cover'.tlParams({"a": curImageFavorite.epName}) + : curPage.toString(); return InkWell( onDoubleTap: () { // 双击浏览大图 App.mainNavigatorKey?.currentContext?.to( () => ImageFavoritesPhotoView( - imageFavoritesGroup: widget.imageFavoritesGroup, - page: curImageFavorite.page, - curImageFavoritesGroup: widget.curImageFavoritesGroup, + imageFavoritesComic: widget.imageFavoritesComic, + imageFavoritePro: curImageFavorite, + finalImageFavoritesComicList: + widget.finalImageFavoritesComicList, + goComicInfo: goComicInfo, + goReaderPage: goReaderPage, ), ); }, @@ -201,12 +210,8 @@ class ImageFavoritesItemState extends State { if (widget.multiSelectMode) { widget.addSelected(curImageFavorite); } else { - App.mainNavigatorKey?.currentContext?.to( - () => ReaderWithLoading( - id: widget.imageFavoritesGroup.id, - sourceKey: widget.imageFavoritesGroup.sourceKey, - ), - ); + goReaderPage(widget.imageFavoritesComic, + curImageFavorite.ep, curPage); } }, onLongPress: () { @@ -235,7 +240,12 @@ class ImageFavoritesItemState extends State { ), clipBehavior: Clip.antiAlias, child: imageWidget), - Text(pageText, style: ts.s12) + Text( + pageText, + style: ts.s10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) ], )), ); @@ -245,7 +255,7 @@ class ImageFavoritesItemState extends State { Row( children: [ Text( - "最早收藏时间: $time | ${widget.imageFavoritesGroup.sourceKey}", + "最早收藏时间: $time | ${widget.imageFavoritesComic.sourceKey}", textAlign: TextAlign.left, style: const TextStyle( fontSize: 12.0, diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index a935614..46024cc 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,4 +1,5 @@ import 'dart:developer'; +import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,13 +9,17 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; +import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; import 'package:venera/pages/reader/reader.dart'; +import 'package:venera/utils/file_type.dart'; +import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; part "image_favorites_item.dart"; part "image_favorites_photo_view.dart"; @@ -32,8 +37,8 @@ class ImageFavoritesPageState extends State late String timeFilterSelect; late List finalTimeList; // 所有的图片收藏 - List imageFavoritesGroup = []; - late List curImageFavoritesGroup; + List imageFavoritesComicList = []; + late List curImageFavoritesComicList; late List timeFilter; String keyword = ""; @@ -49,29 +54,19 @@ class ImageFavoritesPageState extends State ); int tabIndex = 0; // 多选的时候选中的图片 - Map selectedImageFavorites = {}; - late List imageFavorites; + Map selectedImageFavorites = {}; + late List imageFavoritePros; void update() { setState(() {}); } void getInitImageFavorites() { - imageFavorites = ImageFavoriteManager.getAll(); - imageFavoritesGroup = []; - for (var ele in imageFavorites) { - try { - ImageFavoritesGroup tempGroup = imageFavoritesGroup - .where( - (i) => i.id == ele.id && i.eid == ele.ep.toString(), - ) - .first; - tempGroup.imageFavorites.add(ele); - } catch (e) { - imageFavoritesGroup - .add(ImageFavoritesGroup(ele.id, [ele], ele.ep.toString())); - } + imageFavoritePros = []; + for (var e in ImageFavoriteManager.imageFavoritesComicList) { + imageFavoritePros.addAll(e.sortedImageFavoritePros); } + imageFavoritesComicList = ImageFavoriteManager.imageFavoritesComicList; } void refreshImageFavorites() { @@ -80,17 +75,18 @@ class ImageFavoritesPageState extends State } void getCurImageFavorites() { + List tempList = imageFavoritesComicList; + if (keyword != "") { + tempList = ImageFavoriteManager.search(keyword); + } // 筛选到最终列表 - curImageFavoritesGroup = imageFavoritesGroup.where((ele) { + curImageFavoritesComicList = tempList.where((ele) { bool isFilter = true; - if (keyword != "") { - isFilter = ele.title.contains(keyword); - } if (timeFilterSelect != "") { timeFilter = getDateTimeRangeFromFilter(timeFilterSelect); DateTime start = timeFilter[0]; DateTime end = timeFilter[1]; - DateTime dateTimeToCheck = ele.firstTime; + DateTime dateTimeToCheck = ele.time; isFilter = dateTimeToCheck.isAfter(start) && dateTimeToCheck.isBefore(end) || dateTimeToCheck == start || @@ -98,22 +94,28 @@ class ImageFavoritesPageState extends State } return isFilter; }).toList(); - // 给每个 group 的收藏图片排一个序 - for (var ele in curImageFavoritesGroup) { - ele.imageFavorites.sort((a, b) => a.page.compareTo(b.page)); - } // 给列表排序 switch (sortType) { case ImageFavoriteSortType.name: - curImageFavoritesGroup.sort((a, b) => a.title.compareTo(b.title)); + curImageFavoritesComicList.sort((a, b) => a.title.compareTo(b.title)); break; case ImageFavoriteSortType.timeAsc: - curImageFavoritesGroup - .sort((a, b) => a.firstTime.compareTo(b.firstTime)); + curImageFavoritesComicList.sort((a, b) => a.time.compareTo(b.time)); break; case ImageFavoriteSortType.timeDesc: - curImageFavoritesGroup - .sort((a, b) => b.firstTime.compareTo(a.firstTime)); + curImageFavoritesComicList.sort((a, b) => b.time.compareTo(a.time)); + break; + case ImageFavoriteSortType.maxFavorites: + curImageFavoritesComicList.sort((a, b) => b + .sortedImageFavoritePros.length + .compareTo(a.sortedImageFavoritePros.length)); + break; + case ImageFavoriteSortType.favoritesCompareComicPages: + curImageFavoritesComicList.sort((a, b) { + double tempA = a.sortedImageFavoritePros.length / a.maxPageFromEp; + double tempB = b.sortedImageFavoritePros.length / b.maxPageFromEp; + return tempB.compareTo(tempA); + }); break; default: } @@ -148,7 +150,7 @@ class ImageFavoritesPageState extends State text: "Delete".tl, onClick: () { selectedImageFavorites.keys.toList().forEach((ele) { - ImageFavoriteManager.delete(ele); + ImageFavoriteManager.deleteImageFavoritePro([ele]); }); setState(() { multiSelectMode = false; @@ -159,11 +161,13 @@ class ImageFavoritesPageState extends State ]); } + var scrollController = ScrollController(); + @override Widget build(BuildContext context) { getCurImageFavorites(); void selectAll() { - for (var ele in imageFavorites) { + for (var ele in imageFavoritePros) { selectedImageFavorites[ele] = true; } update(); @@ -175,13 +179,7 @@ class ImageFavoritesPageState extends State }); } - void invertSelection() { - setState(() { - selectedImageFavorites.removeWhere((k, v) => !v); - }); - } - - void addSelected(ImageFavorite i) { + void addSelected(ImageFavoritePro i) { if (selectedImageFavorites[i] == null) { selectedImageFavorites[i] = true; } else { @@ -204,13 +202,10 @@ class ImageFavoritesPageState extends State icon: const Icon(Icons.deselect), tooltip: "Deselect".tl, onPressed: deSelect), - IconButton( - icon: const Icon(Icons.flip), - tooltip: "Invert Selection".tl, - onPressed: invertSelection), buildMultiSelectMenu(), ]; var widget = SmoothCustomScrollView( + controller: scrollController, slivers: [ if (!searchMode && !multiSelectMode) SliverAppbar( @@ -309,17 +304,28 @@ class ImageFavoritesPageState extends State SliverList( delegate: SliverChildBuilderDelegate((context, index) { return ImageFavoritesItem( - imageFavoritesGroup: curImageFavoritesGroup[index], + imageFavoritesComic: curImageFavoritesComicList[index], selectedImageFavorites: selectedImageFavorites, addSelected: addSelected, multiSelectMode: multiSelectMode, - curImageFavoritesGroup: curImageFavoritesGroup, + finalImageFavoritesComicList: curImageFavoritesComicList, ); - }, childCount: 10)), + }, childCount: curImageFavoritesComicList.length)), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], ); - return context.width > changePoint ? widget.paddingHorizontal(8) : widget; + Widget body = Scrollbar( + controller: scrollController, + thickness: App.isDesktop ? 8 : 12, + radius: const Radius.circular(8), + interactive: true, + child: ScrollConfiguration( + behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), + child: + context.width > changePoint ? widget.paddingHorizontal(8) : widget, + ), + ); + return body; } void sort() { @@ -378,18 +384,22 @@ class ImageFavoritesPageState extends State ) .toList(), ) - : ListTile( - title: Text("时间筛选".tl), - trailing: Select( - current: timeFilterSelect, - values: finalTimeList, - minWidth: 64, - onTap: (index) { - setState(() { - timeFilterSelect = finalTimeList[index]; - }); - }, - ), + : Column( + children: [ + ListTile( + title: Text("时间筛选".tl), + trailing: Select( + current: timeFilterSelect, + values: finalTimeList, + minWidth: 64, + onTap: (index) { + setState(() { + timeFilterSelect = finalTimeList[index]; + }); + }, + ), + ) + ], ) ]), actions: [ diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index e552170..2734ef7 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -3,12 +3,16 @@ part of 'image_favorites_page.dart'; class ImageFavoritesPhotoView extends StatefulWidget { const ImageFavoritesPhotoView( {super.key, - required this.imageFavoritesGroup, - required this.page, - required this.curImageFavoritesGroup}); - final ImageFavoritesGroup imageFavoritesGroup; - final int page; - final List curImageFavoritesGroup; + required this.imageFavoritesComic, + required this.imageFavoritePro, + required this.finalImageFavoritesComicList, + required this.goComicInfo, + required this.goReaderPage}); + final ImageFavoritesComic imageFavoritesComic; + final ImageFavoritePro imageFavoritePro; + final List finalImageFavoritesComicList; + final Function(ImageFavoritesComic) goComicInfo; + final Function(ImageFavoritesComic, int, int) goReaderPage; @override State createState() => ImageFavoritesPhotoViewState(); @@ -19,25 +23,27 @@ class ImageFavoritesPhotoViewState extends State { Map cancelImageFavorites = {}; // 图片当前的 index late int curIndex; - late int curImageFavoritesGroupIndex; + late int curImageFavoritesComicIndex; @override void initState() { - curIndex = widget.imageFavoritesGroup.imageFavorites.indexWhere((ele) { - return ele.page == widget.page; + curIndex = + widget.imageFavoritesComic.sortedImageFavoritePros.indexWhere((ele) { + return ele.page == widget.imageFavoritePro.page && + ele.eid == widget.imageFavoritePro.eid; }); controller = PageController(initialPage: curIndex); - curImageFavoritesGroupIndex = - widget.curImageFavoritesGroup.indexWhere((ele) { - return ele.id == widget.imageFavoritesGroup.id; + curImageFavoritesComicIndex = + widget.finalImageFavoritesComicList.indexWhere((ele) { + return ele.id == widget.imageFavoritesComic.id; }); super.initState(); } void onPop() {} PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { - final ImageFavorite curImageFavorite = widget - .curImageFavoritesGroup[curImageFavoritesGroupIndex] - .imageFavorites[index]; + final ImageFavoritePro curImageFavorite = widget + .finalImageFavoritesComicList[curImageFavoritesComicIndex] + .sortedImageFavoritePros[index]; return PhotoViewGalleryPageOptions( // 图片加载器 支持本地、网络 imageProvider: ImageFavoritesProvider(curImageFavorite), @@ -50,13 +56,21 @@ class ImageFavoritesPhotoViewState extends State { }); } + Future _getCurrentImageData(ImageFavoritePro temp) async { + return (await CacheManager() + .findCache(ImageFavoritesProvider.getImageKey(temp)))! + .readAsBytes(); + } + @override Widget build(BuildContext context) { - ImageFavoritesGroup curGroup = - widget.curImageFavoritesGroup[curImageFavoritesGroupIndex]; - ImageFavorite curImageFavorite = curGroup.imageFavorites[curIndex]; + ImageFavoritesComic curComic = + widget.finalImageFavoritesComicList[curImageFavoritesComicIndex]; + ImageFavoritePro curImageFavorite = + curComic.sortedImageFavoritePros[curIndex]; int curPage = curImageFavorite.page; - String pageText = curPage == 0 ? 'cover'.tl : (curPage + 1).toString(); + String pageText = + curPage == ImageFavoritesEp.firstPage ? 'cover'.tl : curPage.toString(); return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { @@ -69,7 +83,7 @@ class ImageFavoritesPhotoViewState extends State { color: context.colorScheme.surface, ), builder: _buildItem, - itemCount: curGroup.imageFavorites.length, + itemCount: curComic.sortedImageFavoritePros.length, loadingBuilder: (context, event) => Center( child: SizedBox( width: 20.0, @@ -99,7 +113,7 @@ class ImageFavoritesPhotoViewState extends State { child: Padding( padding: const EdgeInsets.all(8.0), child: Text( - curGroup.title, + curComic.title, style: ts.s18, maxLines: 1, overflow: TextOverflow.ellipsis, @@ -114,11 +128,11 @@ class ImageFavoritesPhotoViewState extends State { IconButton( icon: Icon(Icons.arrow_circle_left), onPressed: () { - if (curImageFavoritesGroupIndex == 0) { - curImageFavoritesGroupIndex = - widget.curImageFavoritesGroup.length - 1; + if (curImageFavoritesComicIndex == 0) { + curImageFavoritesComicIndex = + widget.finalImageFavoritesComicList.length - 1; } else { - curImageFavoritesGroupIndex -= 1; + curImageFavoritesComicIndex -= 1; } curIndex = 0; controller.jumpToPage(0); @@ -139,25 +153,38 @@ class ImageFavoritesPhotoViewState extends State { setState(() {}); }, ), - Text( - pageText, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - ), - ), IconButton( icon: Icon(Icons.play_arrow), - onPressed: () {}, + onPressed: () { + widget.goReaderPage(curComic, curImageFavorite.ep, curPage); + }, + ), + IconButton( + icon: Icon(Icons.menu_book), + onPressed: () { + widget.goComicInfo(curComic); + }, + ), + IconButton( + icon: const Icon(Icons.download), + onPressed: () async { + var data = await _getCurrentImageData(curImageFavorite); + if (data == null) { + return; + } + var fileType = detectFileType(data); + var filename = "${curImageFavorite.page}${fileType.ext}"; + saveFile(data: data, filename: filename); + }, ), IconButton( icon: Icon(Icons.arrow_circle_right), onPressed: () { - if (curImageFavoritesGroupIndex == - widget.curImageFavoritesGroup.length - 1) { - curImageFavoritesGroupIndex = 0; + if (curImageFavoritesComicIndex == + widget.finalImageFavoritesComicList.length - 1) { + curImageFavoritesComicIndex = 0; } else { - curImageFavoritesGroupIndex += 1; + curImageFavoritesComicIndex += 1; } curIndex = 0; controller.jumpToPage(0); @@ -167,8 +194,7 @@ class ImageFavoritesPhotoViewState extends State { ]), ), Positioned( - bottom: 33, - right: 20, + top: 70, child: IntrinsicWidth( stepWidth: 0, child: Container( @@ -178,7 +204,8 @@ class ImageFavoritesPhotoViewState extends State { color: Theme.of(context).colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(8), ), - child: Text("${curIndex + 1}/${curGroup.imageFavorites.length}", + child: Text( + "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavoritePros.length}", style: ts.s12), ), )) diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index c0c7e27..1139272 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -5,12 +5,18 @@ class ReaderWithLoading extends StatefulWidget { super.key, required this.id, required this.sourceKey, + this.initialEp, + this.initialPage, }); final String id; final String sourceKey; + final int? initialEp; + + final int? initialPage; + @override State createState() => _ReaderWithLoadingState(); } @@ -25,8 +31,9 @@ class _ReaderWithLoadingState name: data.name, chapters: data.chapters, history: data.history, - initialChapter: data.history.ep, - initialPage: data.history.page, + initialChapter: widget.initialEp ?? data.history.ep, + initialPage: widget.initialPage ?? data.history.page, + comicDetails: data.comicDetails, ); } @@ -47,17 +54,17 @@ class _ReaderWithLoadingState } return Res( ReaderProps( - type: ComicType.fromKey(widget.sourceKey), - cid: widget.id, - name: localComic.title, - chapters: localComic.chapters, - history: history ?? - History.fromModel( - model: localComic, - ep: 0, - page: 0, - ), - ), + type: ComicType.fromKey(widget.sourceKey), + cid: widget.id, + name: localComic.title, + chapters: localComic.chapters, + history: history ?? + History.fromModel( + model: localComic, + ep: 0, + page: 0, + ), + comicDetails: localComic), ); } else { var comic = await comicSource.loadComicInfo!(widget.id); @@ -66,17 +73,17 @@ class _ReaderWithLoadingState } return Res( ReaderProps( - type: ComicType.fromKey(widget.sourceKey), - cid: widget.id, - name: comic.data.title, - chapters: comic.data.chapters, - history: history ?? - History.fromModel( - model: comic.data, - ep: 0, - page: 0, - ), - ), + type: ComicType.fromKey(widget.sourceKey), + cid: widget.id, + name: comic.data.title, + chapters: comic.data.chapters, + history: history ?? + History.fromModel( + model: comic.data, + ep: 0, + page: 0, + ), + comicDetails: comic.data), ); } } @@ -92,6 +99,8 @@ class ReaderProps { final Map? chapters; final History history; + // 缺少作者, 现在的作者都是上传者 + final Object comicDetails; const ReaderProps({ required this.type, @@ -99,5 +108,6 @@ class ReaderProps { required this.name, required this.chapters, required this.history, + required this.comicDetails, }); } diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index d3221cc..41cf504 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -27,8 +27,10 @@ import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/data_sync.dart'; +import 'package:venera/utils/ext.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; +import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'package:venera/utils/volume.dart'; import 'package:window_manager/window_manager.dart'; @@ -57,10 +59,13 @@ class Reader extends StatefulWidget { required this.history, this.initialPage, this.initialChapter, + required this.comicDetails, }); final ComicType type; + final Object comicDetails; + final String cid; final String name; @@ -95,6 +100,8 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { String get cid => widget.cid; + Object get comicDetails => widget.comicDetails; + String get eid => widget.chapters?.keys.elementAt(chapter - 1) ?? '0'; List? images; @@ -114,12 +121,14 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { void _checkImagesPerPageChange() { int currentImagesPerPage = imagesPerPage; if (_lastImagesPerPage != currentImagesPerPage) { - _adjustPageForImagesPerPageChange(_lastImagesPerPage, currentImagesPerPage); + _adjustPageForImagesPerPageChange( + _lastImagesPerPage, currentImagesPerPage); _lastImagesPerPage = currentImagesPerPage; } } - void _adjustPageForImagesPerPageChange(int oldImagesPerPage, int newImagesPerPage) { + void _adjustPageForImagesPerPageChange( + int oldImagesPerPage, int newImagesPerPage) { int previousImageIndex = (page - 1) * oldImagesPerPage; int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; page = newPage; @@ -144,7 +153,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { updateHistory(); }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); - if(appdata.settings['enableTurnPageByVolumeKey']) { + if (appdata.settings['enableTurnPageByVolumeKey']) { handleVolumeEvent(); } setImageCacheSize(); @@ -164,7 +173,8 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } else { maxImageCacheSize = 500 << 20; } - Log.info("Reader", "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize"); + Log.info("Reader", + "Detect available RAM: $availableRAM, set image cache size to $maxImageCacheSize"); PaintingBinding.instance.imageCache.maximumSizeBytes = maxImageCacheSize; } @@ -209,7 +219,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } void updateHistory() { - if(history != null) { + if (history != null) { history!.page = page; history!.ep = chapter; if (maxPage > 1) { @@ -222,11 +232,11 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } void handleVolumeEvent() { - if(!App.isAndroid) { + if (!App.isAndroid) { // Currently only support Android return; } - if(volumeListener != null) { + if (volumeListener != null) { volumeListener?.cancel(); } volumeListener = VolumeListener( @@ -240,7 +250,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } void stopVolumeEvent() { - if(volumeListener != null) { + if (volumeListener != null) { volumeListener?.cancel(); volumeListener = null; } @@ -300,7 +310,8 @@ abstract mixin class _ReaderLocation { bool toPage(int page) { if (_validatePage(page)) { if (page == this.page) { - if(!(chapter == 1 && page == 1) && !(chapter == maxChapter && page == maxPage)) { + if (!(chapter == 1 && page == 1) && + !(chapter == maxChapter && page == maxPage)) { return false; } } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index bf56f2b..fe078b3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -237,52 +237,90 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { bool isLiked() { String cid = context.reader.cid; - int chapter = context.reader.chapter; + String eid = context.reader.eid; int page = context.reader.page; String sourceKey = context.reader.type.sourceKey; - String id = "$sourceKey-$cid"; - return ImageFavoriteManager.isHas(id, chapter, page); + return ImageFavoriteManager.isHas(cid, sourceKey, eid, page); } void imageFavoritesAction() { try { - String cid = context.reader.cid; + String id = context.reader.cid; String eid = context.reader.eid; - int page = context.reader.page - 1; + String title = context.reader.history!.title; + String subTitle = context.reader.history!.subtitle; + int maxPage = context.reader.history!.maxPage ?? 1; + int ep = context.reader.chapter; + int page = context.reader.page; String sourceKey = context.reader.type.sourceKey; - String imageKey = context.reader.images![page]; - String id = "$sourceKey-$cid"; + String imageKey = context.reader.images![page - 1]; + List tags = []; + + String author = ""; + var epName = context.reader.widget.chapters?.values + .elementAtOrNull(context.reader.chapter - 1) ?? + "E${context.reader.chapter}"; + Object comicDetails = context.reader.comicDetails; + if (comicDetails is ComicDetails) { + ImageFavoritesSomething something = + ImageFavoritesComic.getSomethingFromComicDetails(comicDetails, ep); + tags = something.tags; + author = something.author; + } else if (comicDetails is LocalComic) { + tags = comicDetails.tags; + } + var translatedTags = ImageFavoritesComic.tagsToTranslated(tags); + if (isLiked()) { - ImageFavoriteManager.remove(id, int.parse(eid), page); - showToast(message: "成功取消收藏图片".tl, context: context); - } else { - var otherInfo = {"imageKey": imageKey}; - // 如果是手动收藏的就标记一下, 避免把它误删除了 - if (page == 0) { - otherInfo["notAuto"] = true; - } else { - ImageFavoriteManager.add(ImageFavorite( - id, - "", - context.reader.widget.name, - DateTime.now(), - int.parse(eid), - page, - otherInfo)); + if (page == ImageFavoritesEp.firstPage) { + showToast(message: "封面不能在此取消收藏".tl, context: context); + return; } - ImageFavoriteManager.add(ImageFavorite( + ImageFavoriteManager.deleteImageFavoritePro([ + ImageFavoritePro(page, imageKey, null, eid, id, ep, sourceKey, epName) + ]); + showToast(message: "取消收藏图片".tl, context: context); + } else { + ImageFavoritesComic? imageFavoritesComic = ImageFavoriteManager + .imageFavoritesComicList + .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); + imageFavoritesComic ??= ImageFavoritesComic( id, - "", - context.reader.widget.name, + [], + title, + sourceKey, + tags, + translatedTags, DateTime.now(), - int.parse(eid), - page, - otherInfo)); + author, + {}, + subTitle, + maxPage); + ImageFavoritePro imageFavoritePro = ImageFavoritePro( + page, imageKey, null, eid, id, ep, sourceKey, epName); + ImageFavoritesEp? imageFavoritesEp = imageFavoritesComic + .imageFavoritesEp + .firstWhereOrNull((e) => e.eid == eid); + if (imageFavoritesEp == null) { + ImageFavoritePro copy = ImageFavoritePro.copy(imageFavoritePro); + copy.page = ImageFavoritesEp.firstPage; + copy.imageKey = context.reader.images![0]; + copy.isAutoFavorite = true; + // 塞一个封面进去 + imageFavoritesEp = ImageFavoritesEp( + eid, ep, [copy, imageFavoritePro], epName, maxPage); + imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp); + } else { + imageFavoritesEp.imageFavorites.add(imageFavoritePro); + } + + ImageFavoriteManager.addOrUpdateOrDelete(imageFavoritesComic); showToast(message: "成功收藏图片".tl, context: context); } update(); - } catch (e) { - showToast(message: e.toString(), context: context); + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); + showToast(message: e.toString(), context: context, seconds: 1); } } diff --git a/lib/utils/data.dart b/lib/utils/data.dart index c7be269..4a7ee77 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -11,6 +11,7 @@ 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:venera/utils/ext.dart'; import 'package:zip_flutter/zip_flutter.dart'; import 'io.dart'; @@ -196,21 +197,42 @@ Future importPicaData(File file) async { }), ); } + List imageFavoritesComicList = []; for (var comic in db.select("SELECT * FROM image_favorites;")) { - var otherInfo = jsonDecode(comic["other"]); - // 就洗一个较为通用的 url 的数据, 别的就不洗了 - otherInfo["imageKey"] = otherInfo["url"]; - ImageFavoriteManager.add( - ImageFavorite.fromMap({ - "id": comic['id'], - "title": comic["title"], - "time": comic["time"], - "imagePath": comic["cover"], - "ep": comic["ep"], - "page": comic["page"], - "otherInfo": otherInfo, - }), - ); + String sourceKey = comic["id"].split("-")[0]; + if (sourceKey == "htManga") { + sourceKey = "wnacg"; + } + if (ComicSource.find(sourceKey) == null) { + continue; + } + String id = comic["id"].split("-")[1]; + int page = comic["page"]; + int ep = comic["ep"]; + String title = comic["title"]; + String epName = comic["epName"]; + ImageFavoritesComic? tempComic = + ImageFavoriteManager.findFromComicList( + imageFavoritesComicList, id, sourceKey, ep, page); + ImageFavoritePro curImageFavorite = + ImageFavoritePro(page, "", null, "", id, ep, sourceKey, epName); + if (tempComic == null) { + tempComic = ImageFavoritesComic(id, [], title, sourceKey, [], [], + DateTime.now(), "", {}, "", 1); + tempComic.imageFavoritesEp = [ + ImageFavoritesEp("", ep, [curImageFavorite], epName, 1) + ]; + } else { + ImageFavoritesEp? tempEp = + tempComic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == ep); + if (tempEp == null) { + tempComic.imageFavoritesEp + .add(ImageFavoritesEp("", ep, [curImageFavorite], epName, 1)); + } else { + tempEp.imageFavorites.add(curImageFavorite); + } + } + ImageFavoriteManager.addOrUpdateOrDelete(tempComic); } } catch (e) { Log.error("Import Data", "Failed to import history: $e"); diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" index ba14fb2..45d7d8b 100644 --- "a/\344\273\273\345\212\241.md" +++ "b/\344\273\273\345\212\241.md" @@ -27,26 +27,27 @@ 5. 双击大图浏览 ok 1. 显示标题 ok 3. 支持取消收藏, 退出大图浏览时更新数据库 - 4. 点击进行阅读, 点击进入详情 + 4. 点击进行阅读, 点击进入详情 ok 5. 点击进入上一本, 下一本 ok 6. 显示 tags - 6. 筛选 + 6. 首页显示最喜欢作者, 最多收藏tag, 最喜欢的本子(按照收藏数以及按照收藏比) + 7. 筛选 1. tag 筛选 - 2. 页面数量范围 - 7. 支持导出 + 2. 收藏数量范围 + 8. 支持导出 1. 导出成文本, 包含标题, 链接, 总页数, 收藏数, 收藏占比, 阅读时间, tag 数等 2. 生成总结页面, 包含最喜欢作者, 本子, 共收藏本子数, 共收藏图片数, 最喜欢tag, 最喜欢本子, 按总页数, 收藏占比 - 8. bug解决 + 9. bug解决 1. 进入的时候都是一张图, 都是cover 2. 并行请求太多 timeout 风险较高 - 9. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int + 10. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int ok 1. imagePath 不需要 2. ep 和 page 都不从0开始 3. eid 存在 otherInfo 里 4. 是否做一个大的改动, 将数据库改成一本漫画的一个章节对应一条记录的形式 - 10. 中英互译 - 11. 实际大数据量测试 - 12. 实机测试 + 11. 中英互译 + 12. 实际大数据量测试 + 13. 实机测试 3. 主页的图片收藏显示 ok 3. 打通联动 4. 自测 From c0c5c17e9b4b8c7388c5bd61ecf5ce3bad3c7e30 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Wed, 1 Jan 2025 18:14:13 +0800 Subject: [PATCH 05/36] =?UTF-8?q?feat:=20=E5=9F=BA=E6=9C=AC=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/image.dart | 2 + lib/foundation/image_favorites.dart | 50 +++- lib/pages/home_page.dart | 5 + lib/pages/home_page/image_favorites.dart | 275 ++++++++++++++++- .../image_favorites_item.dart | 6 +- .../image_favorites_page.dart | 73 +++-- .../image_favorites_photo_view.dart | 167 +++++------ lib/pages/image_favorites_page/type.dart | 9 +- lib/pages/reader/scaffold.dart | 6 +- lib/pages/search_page.dart | 5 +- pubspec.lock | 276 +++++++++--------- "\344\273\273\345\212\241.md" | 29 +- 12 files changed, 616 insertions(+), 287 deletions(-) diff --git a/lib/components/image.dart b/lib/components/image.dart index 5e6210d..4a95eb8 100644 --- a/lib/components/image.dart +++ b/lib/components/image.dart @@ -172,6 +172,8 @@ class _AnimatedImageState extends State _handleImageFrame, onChunk: _handleImageChunk, onError: (Object error, StackTrace? stackTrace) { + // 图片加错错误回调 + widget.onError?.call(error, stackTrace); setState(() { _lastException = error; }); diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 4e9148a..5b7392a 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -40,9 +40,20 @@ class ImageFavoritePro extends ImageFavorite { 'id': id, 'ep': ep, 'sourceKey': sourceKey, + 'epName': epName, }; } + ImageFavoritePro.fromJson(Map json) + : this( + json['page'], + json['imageKey'], + json['isAutoFavorite'], + json['eid'], + json['id'], + json['ep'], + json['sourceKey'], + json['epName']); // 复制构造函数 ImageFavoritePro.copy(ImageFavoritePro other) : this(other.page, other.imageKey, other.isAutoFavorite, other.eid, @@ -71,6 +82,13 @@ class ImageFavoritesEp { }; } + ImageFavoritesEp.fromJson(Map json) + : eid = json['eid'], + ep = json['ep'], + imageFavorites = List.from( + json['imageFavorites'].map((e) => ImageFavoritePro.fromJson(e))), + epName = json['epName'], + maxPage = json['maxPage']; // 是否有封面 bool get isHasFirstPage { return imageFavorites[0].page == firstPage; @@ -118,7 +136,35 @@ class ImageFavoritesComic { this.other, this.subTitle, this.maxPage); + Map toJson() { + return { + 'id': id, + 'title': title, + 'subTitle': subTitle, + 'author': author, + 'tags': tags, + 'translatedTags': translatedTags, + 'time': time.millisecondsSinceEpoch, + 'maxPage': maxPage, + 'sourceKey': sourceKey, + 'imageFavoritesEp': imageFavoritesEp, + 'other': other, + }; + } + ImageFavoritesComic.fromJson(Map json) + : id = json['id'], + title = json['title'], + subTitle = json['subTitle'], + author = json['author'], + tags = List.from(json['tags']), + translatedTags = List.from(json['translatedTags']), + time = DateTime.fromMillisecondsSinceEpoch(json['time']), + maxPage = json['maxPage'], + sourceKey = json['sourceKey'], + imageFavoritesEp = List.from( + json['imageFavoritesEp'].map((e) => ImageFavoritesEp.fromJson(e))), + other = json['other']; // 是否都有imageKey bool get isAllHasImageKey { return imageFavoritesEp @@ -322,10 +368,10 @@ class ImageFavoriteManager with ChangeNotifier { static List getAll(String? keyword) { if (!hasInit) return []; var res = []; - if (keyword == null) { + if (keyword == null || keyword == "") { res = _db.select("select * from image_favorites;"); } else { - _db.select( + res = _db.select( """ select * from image_favorites WHERE LOWER(title) LIKE LOWER(?) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 98b75cb..fe9498d 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,3 +1,7 @@ +import 'dart:convert'; +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:venera/components/components.dart'; @@ -18,6 +22,7 @@ import 'package:venera/pages/image_favorites_page/image_favorites_page.dart'; import 'package:venera/pages/search_page.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/import_comic.dart'; +import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'local_comics_page.dart'; diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart index 195fe7a..9e2a16d 100644 --- a/lib/pages/home_page/image_favorites.dart +++ b/lib/pages/home_page/image_favorites.dart @@ -1,5 +1,33 @@ part of '../home_page.dart'; +class ImageFavoritesFindComic { + final String id; + final String title; + final String sourceKey; + + const ImageFavoritesFindComic(this.id, this.title, this.sourceKey); +} + +// 算出最喜欢的 +class ImageFavoritesCompute { + final List tags; + final List authors; + // 喜欢的图片数 + final List comicByNum; + // 图片数比上总页数 + final List comicByPercentage; + + const ImageFavoritesCompute( + this.tags, this.authors, this.comicByNum, this.comicByPercentage); +} + +enum ImageFavoritesComputeType { + tags, + authors, + comicByNum, + comicByPercentage, +} + class ImageFavorites extends StatefulWidget { const ImageFavorites({super.key}); @@ -7,13 +35,133 @@ class ImageFavorites extends StatefulWidget { State createState() => ImageFavoritesState(); } +List exceptTags = ['連載中']; + class ImageFavoritesState extends State { - void refreshImageFavorites() { + ImageFavoritesCompute? imageFavoritesCompute; + List allImageFavoritePros = []; + static String separator = "*venera*"; + static var enableTranslate = App.locale.languageCode == 'zh'; + static Color getColor(Color baseColor, double depth) { + // 将 RGB 颜色转换为 HSV 颜色 + HSVColor hsvColor = HSVColor.fromColor(baseColor); + // 根据深度调整明度 + HSVColor adjustedColor = hsvColor.withValue((1 - depth) * hsvColor.value); + // 将调整后的 HSV 颜色转换回 RGB 颜色 + Color finalColor = adjustedColor.toColor(); + return finalColor; + } + + static ImageFavoritesFindComic fromStringToImageFavoritesFindComic( + String str, String suffix, List tempComicsList) { + List temp = str.split(separator); + String sourceKey = temp[0]; + String id = temp[1]; + ImageFavoritesComic comic = tempComicsList.firstWhere((e) { + return e.sourceKey == sourceKey && e.id == id; + }); + return ImageFavoritesFindComic( + id, + '${comic.title.length > 10 ? comic.title.substring(0, 10) : comic.title}... $suffix', + sourceKey); + } + + // compute 需要传入字符串, 复杂对象无法传入 + static ImageFavoritesCompute computeImageFavorites(String temp) { + List tempComics = List.from( + jsonDecode(temp).map((e) => ImageFavoritesComic.fromJson(e))); + Map tagCount = {}; + Map authorCount = {}; + Map comicImageCount = {}; + Map comicMaxPages = {}; + + for (ImageFavoritesComic imageFavoritesComic in tempComics) { + // 统计标签 + for (var tag in imageFavoritesComic.tags) { + String finalTag = enableTranslate ? tag.translateTagsToCN : tag; + tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1; + } + + // 统计作者下的图片数 + if (imageFavoritesComic.author != "") { + authorCount[imageFavoritesComic.author] = + (authorCount[imageFavoritesComic.author] ?? 0) + + imageFavoritesComic.sortedImageFavoritePros.length; + } + + // 统计漫画图片数和总页数 + String comicId = + '${imageFavoritesComic.sourceKey}$separator${imageFavoritesComic.id}'; + comicImageCount[comicId] = (comicImageCount[comicId] ?? 0) + + imageFavoritesComic.sortedImageFavoritePros.length; + comicMaxPages[comicId] = + (comicMaxPages[comicId] ?? 0) + imageFavoritesComic.maxPageFromEp; + } + + // 按数量排序标签 + List sortedTags = tagCount.keys.toList() + ..sort((a, b) => tagCount[b]!.compareTo(tagCount[a]!)); + + // 按数量排序作者 + List sortedAuthors = authorCount.keys.toList() + ..sort((a, b) => authorCount[b]!.compareTo(authorCount[a]!)); + + // 按收藏数量排序漫画 + List> sortedComicsByNum = comicImageCount.entries + .toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + // 按收藏比例排序漫画 + List> sortedComicsByPercentage = comicImageCount + .entries + .toList() + ..sort((a, b) { + double percentageA = comicImageCount[a.key]! / comicMaxPages[a.key]!; + double percentageB = comicImageCount[b.key]! / comicMaxPages[b.key]!; + return percentageB.compareTo(percentageA); + }); + + // 只返回前10个结果 + return ImageFavoritesCompute( + sortedTags + .where((tag) => !exceptTags.contains(tag)) + .take(10) + .map((tag) => '$tag (${tagCount[tag]})') + .toList(), + sortedAuthors + .take(10) + .map((author) => '$author (${authorCount[author]})') + .toList(), + sortedComicsByNum + .take(10) + .map((comic) => fromStringToImageFavoritesFindComic( + comic.key, '(${comic.value})', tempComics)) + .toList(), + sortedComicsByPercentage + .take(10) + .map((comic) => fromStringToImageFavoritesFindComic( + comic.key, + '(${(comicImageCount[comic.key]! / comicMaxPages[comic.key]! * 100).toStringAsFixed(1)}%)', + tempComics)) + .toList()); + } + + void refreshImageFavorites() async { + imageFavoritesCompute = null; + allImageFavoritePros = []; + for (var comic in ImageFavoriteManager.imageFavoritesComicList) { + allImageFavoritePros.addAll(comic.sortedImageFavoritePros); + } + setState(() {}); + // 避免性能开销, 开一个线程计算 + imageFavoritesCompute = await compute(computeImageFavorites, + jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); setState(() {}); } @override void initState() { + refreshImageFavorites(); ImageFavoriteManager().addListener(refreshImageFavorites); super.initState(); } @@ -24,12 +172,61 @@ class ImageFavoritesState extends State { super.dispose(); } + Widget roundBtn( + Object text, + ImageFavoritesComputeType type, + ) { + bool isString = text is String; + return InkWell( + onTap: () { + RegExp regExp = RegExp(r" \(\d+\)"); + if (type == ImageFavoritesComputeType.tags) { + // 跳转到标签搜索页面 + context.to(() => ImageFavoritesPage( + initialKeyword: (text as String).replaceAll(regExp, ''))); + } + if (type == ImageFavoritesComputeType.authors) { + context.to(() => ImageFavoritesPage( + initialKeyword: (text as String).replaceAll(regExp, ''))); + } + if (type == ImageFavoritesComputeType.comicByNum || + type == ImageFavoritesComputeType.comicByPercentage) { + context.to(() => ComicPage( + id: (text as ImageFavoritesFindComic).id, + sourceKey: text.sourceKey, + )); + } + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + isString ? text : (text as ImageFavoritesFindComic).title, + ), + ), + ); + } + + Widget listRoundBtn(List list, ImageFavoritesComputeType type) { + return Expanded( + child: SizedBox( + height: 24, + child: ListView.separated( + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 4); + }, + scrollDirection: Axis.horizontal, + itemCount: list.length, + itemBuilder: (context, index) { + return roundBtn(list[index], type); + }))); + } + @override Widget build(BuildContext context) { - List allImageFavoritePros = []; - for (var comic in ImageFavoriteManager.imageFavoritesComicList) { - allImageFavoritePros.addAll(comic.sortedImageFavoritePros); - } return SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), @@ -60,16 +257,64 @@ class ImageFavoritesState extends State { ], ), ).paddingHorizontal(16), - SizedBox( - width: double.infinity, - child: Text( - "@a Comic, @b image favorites".tlParams({ - "a": ImageFavoriteManager.length.toString(), - "b": allImageFavoritePros.length - }), - style: const TextStyle(fontSize: 15), - ).paddingHorizontal(16).paddingBottom(16), - ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "@a Comic, @b image favorites".tlParams({ + "a": ImageFavoriteManager.length.toString(), + "b": allImageFavoritePros.length + }), + style: const TextStyle(fontSize: 15), + ), + if (imageFavoritesCompute != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + Text( + "作者:".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.authors, + ImageFavoritesComputeType.authors) + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "标签:".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.tags, + ImageFavoritesComputeType.tags) + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "漫画(数量):".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.comicByNum, + ImageFavoritesComputeType.comicByNum) + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "漫画(比例):".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.comicByPercentage, + ImageFavoritesComputeType.comicByPercentage) + ], + ), + ], + ], + ).paddingHorizontal(16).paddingBottom(16), ], ), ), diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 0de3c9d..7c66ec3 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -64,7 +64,9 @@ class ImageFavoritesItemState extends State { ele.imageKey = images[ele.page - 1]; } ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); - setState(() {}); + if (mounted) { + setState(() {}); + } } isImageKeyLoading = false; } @@ -182,7 +184,7 @@ class ImageFavoritesItemState extends State { height: 128, fit: BoxFit.cover, filterQuality: FilterQuality.medium, - onError: () { + onError: (Object error, StackTrace? stackTrace) { refreshImageKey(curImageFavoritesEp); hasRefreshImageKeyOnErr = true; }, diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 46024cc..56a7e1a 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -25,8 +25,8 @@ part "image_favorites_item.dart"; part "image_favorites_photo_view.dart"; class ImageFavoritesPage extends StatefulWidget { - const ImageFavoritesPage({super.key}); - + const ImageFavoritesPage({super.key, this.initialKeyword}); + final String? initialKeyword; @override State createState() => ImageFavoritesPageState(); } @@ -35,6 +35,7 @@ class ImageFavoritesPageState extends State with TickerProviderStateMixin { late ImageFavoriteSortType sortType; late String timeFilterSelect; + late String numFilterSelect; late List finalTimeList; // 所有的图片收藏 List imageFavoritesComicList = []; @@ -48,10 +49,7 @@ class ImageFavoritesPageState extends State List optionTypes = ['Sort', 'Filter']; bool multiSelectMode = false; - late TabController controller = TabController( - length: 2, - vsync: this, - ); + late TabController controller; int tabIndex = 0; // 多选的时候选中的图片 Map selectedImageFavorites = {}; @@ -70,8 +68,10 @@ class ImageFavoritesPageState extends State } void refreshImageFavorites() { - getInitImageFavorites(); - update(); + if (mounted) { + getInitImageFavorites(); + update(); + } } void getCurImageFavorites() { @@ -82,7 +82,7 @@ class ImageFavoritesPageState extends State // 筛选到最终列表 curImageFavoritesComicList = tempList.where((ele) { bool isFilter = true; - if (timeFilterSelect != "") { + if (timeFilterSelect != TimeFilterEnum.all.name) { timeFilter = getDateTimeRangeFromFilter(timeFilterSelect); DateTime start = timeFilter[0]; DateTime end = timeFilter[1]; @@ -92,6 +92,10 @@ class ImageFavoritesPageState extends State dateTimeToCheck == start || dateTimeToCheck == end; } + if (numFilterSelect != numFilterList[0]) { + isFilter = + ele.sortedImageFavoritePros.length > int.parse(numFilterSelect); + } return isFilter; }).toList(); // 给列表排序 @@ -123,10 +127,20 @@ class ImageFavoritesPageState extends State @override void initState() { + controller = TabController( + length: 2, + vsync: this, + ); + if (widget.initialKeyword != null) { + keyword = widget.initialKeyword!; + searchMode = true; + } var sort = appdata.implicitData["image_favorites_sort"] ?? "name"; sortType = ImageFavoriteSortType.fromString(sort); - timeFilterSelect = - appdata.implicitData["image_favorites_time_filter"] ?? ""; + timeFilterSelect = appdata.implicitData["image_favorites_time_filter"] ?? + TimeFilterEnum.all.name; + numFilterSelect = + appdata.implicitData["image_favorites_num_filter"] ?? numFilterList[0]; finalTimeList = List.from([ ...timeFilterList.map((e) => e.name), ...ImageFavoriteManager.earliestTimeToNow @@ -226,6 +240,10 @@ class ImageFavoritesPageState extends State message: "Sort".tl, child: IconButton( icon: const Icon(Icons.sort), + color: timeFilterSelect != TimeFilterEnum.all.name || + numFilterSelect != numFilterList[0] + ? Theme.of(context).colorScheme.primary + : null, onPressed: sort, ), ), @@ -242,18 +260,6 @@ class ImageFavoritesPageState extends State }, ), ), - MenuButton( - entries: [ - MenuEntry( - icon: Icons.upload_file, - text: "Export current to clipboard".tl, - onClick: () async { - await Clipboard.setData( - ClipboardData(text: "要复制到剪贴板的内容")); - showToast(message: "成功赋值到剪贴板".tl, context: context); - }), - ], - ), ], ) else if (multiSelectMode) @@ -295,6 +301,7 @@ class ImageFavoritesPageState extends State hintText: "Search".tl, border: InputBorder.none, ), + controller: TextEditingController(text: keyword), onChanged: (v) { keyword = v; update(); @@ -398,6 +405,19 @@ class ImageFavoritesPageState extends State }); }, ), + ), + ListTile( + title: Text("图片收藏数大于".tl), + trailing: Select( + current: numFilterSelect, + values: numFilterList, + minWidth: 64, + onTap: (index) { + setState(() { + numFilterSelect = numFilterList[index]; + }); + }, + ), ) ], ) @@ -410,9 +430,10 @@ class ImageFavoritesPageState extends State timeFilterSelect; appdata.writeImplicitData(); controller.removeListener(handleTabIndex); - - Navigator.pop(context); - update(); + if (mounted) { + Navigator.pop(context); + update(); + } }, child: Text("Confirm".tl), ), diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index 2734ef7..9ca07f9 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -20,7 +20,7 @@ class ImageFavoritesPhotoView extends StatefulWidget { class ImageFavoritesPhotoViewState extends State { late PageController controller; - Map cancelImageFavorites = {}; + Map cancelImageFavorites = {}; // 图片当前的 index late int curIndex; late int curImageFavoritesComicIndex; @@ -39,7 +39,11 @@ class ImageFavoritesPhotoViewState extends State { super.initState(); } - void onPop() {} + void onPop() { + ImageFavoriteManager.deleteImageFavoritePro( + cancelImageFavorites.keys.toList()); + } + PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { final ImageFavoritePro curImageFavorite = widget .finalImageFavoritesComicList[curImageFavoritesComicIndex] @@ -70,7 +74,7 @@ class ImageFavoritesPhotoViewState extends State { curComic.sortedImageFavoritePros[curIndex]; int curPage = curImageFavorite.page; String pageText = - curPage == ImageFavoritesEp.firstPage ? 'cover'.tl : curPage.toString(); + curPage == ImageFavoritesEp.firstPage ? 'cover'.tl : "第$curPage页"; return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { @@ -124,91 +128,82 @@ class ImageFavoritesPhotoViewState extends State { bottom: 20, left: 0, right: 0, - child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - IconButton( - icon: Icon(Icons.arrow_circle_left), - onPressed: () { - if (curImageFavoritesComicIndex == 0) { - curImageFavoritesComicIndex = - widget.finalImageFavoritesComicList.length - 1; - } else { - curImageFavoritesComicIndex -= 1; - } - curIndex = 0; - controller.jumpToPage(0); - setState(() {}); - }, - ), - IconButton( - icon: cancelImageFavorites[curImageFavorite] == true - ? Icon(Icons.favorite_border) - : Icon(Icons.favorite), - onPressed: () { - if (cancelImageFavorites[curImageFavorite] == true) { - cancelImageFavorites[curImageFavorite] = false; - } else { - cancelImageFavorites[curImageFavorite] = true; - } + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row(children: [ + Text( + "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavoritePros.length}", + style: ts.s12), + Spacer(), + IconButton( + icon: Icon(Icons.arrow_circle_left), + onPressed: () { + if (curImageFavoritesComicIndex == 0) { + curImageFavoritesComicIndex = + widget.finalImageFavoritesComicList.length - 1; + } else { + curImageFavoritesComicIndex -= 1; + } + curIndex = 0; + controller.jumpToPage(0); + setState(() {}); + }, + ), + IconButton( + icon: cancelImageFavorites[curImageFavorite] == true + ? Icon(Icons.favorite_border) + : Icon(Icons.favorite), + onPressed: () { + if (cancelImageFavorites[curImageFavorite] == true) { + cancelImageFavorites[curImageFavorite] = false; + } else { + cancelImageFavorites[curImageFavorite] = true; + } - setState(() {}); - }, - ), - IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () { - widget.goReaderPage(curComic, curImageFavorite.ep, curPage); - }, - ), - IconButton( - icon: Icon(Icons.menu_book), - onPressed: () { - widget.goComicInfo(curComic); - }, - ), - IconButton( - icon: const Icon(Icons.download), - onPressed: () async { - var data = await _getCurrentImageData(curImageFavorite); - if (data == null) { - return; - } - var fileType = detectFileType(data); - var filename = "${curImageFavorite.page}${fileType.ext}"; - saveFile(data: data, filename: filename); - }, - ), - IconButton( - icon: Icon(Icons.arrow_circle_right), - onPressed: () { - if (curImageFavoritesComicIndex == - widget.finalImageFavoritesComicList.length - 1) { - curImageFavoritesComicIndex = 0; - } else { - curImageFavoritesComicIndex += 1; - } - curIndex = 0; - controller.jumpToPage(0); - setState(() {}); - }, - ), - ]), - ), - Positioned( - top: 70, - child: IntrinsicWidth( - stepWidth: 0, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavoritePros.length}", - style: ts.s12), + setState(() {}); + }, + ), + IconButton( + icon: Icon(Icons.play_arrow), + onPressed: () { + widget.goReaderPage(curComic, curImageFavorite.ep, curPage); + }, + ), + IconButton( + icon: Icon(Icons.menu_book), + onPressed: () { + widget.goComicInfo(curComic); + }, ), - )) + IconButton( + icon: const Icon(Icons.download), + onPressed: () async { + var data = await _getCurrentImageData(curImageFavorite); + if (data == null) { + return; + } + var fileType = detectFileType(data); + var filename = "${curImageFavorite.page}${fileType.ext}"; + saveFile(data: data, filename: filename); + }, + ), + IconButton( + icon: Icon(Icons.arrow_circle_right), + onPressed: () { + if (curImageFavoritesComicIndex == + widget.finalImageFavoritesComicList.length - 1) { + curImageFavoritesComicIndex = 0; + } else { + curImageFavoritesComicIndex += 1; + } + curIndex = 0; + controller.jumpToPage(0); + setState(() {}); + }, + ), + ]), + ), + ), ]), ); } diff --git a/lib/pages/image_favorites_page/type.dart b/lib/pages/image_favorites_page/type.dart index 10a5ff9..f5605fc 100644 --- a/lib/pages/image_favorites_page/type.dart +++ b/lib/pages/image_favorites_page/type.dart @@ -29,6 +29,7 @@ class CustomListItem { } enum TimeFilterEnum { + all("all"), lastWeek("lastWeek"), lastMonth("lastMonth"), lastHalfYear("lastHalfYear"), @@ -47,18 +48,22 @@ enum TimeFilterEnum { } const timeFilterList = [ + TimeFilterEnum.all, TimeFilterEnum.lastWeek, TimeFilterEnum.lastMonth, TimeFilterEnum.lastHalfYear, TimeFilterEnum.lastYear, ]; - +const numFilterList = ['0', '1', '2', '5', '10', '20', '50', '100']; getDateTimeRangeFromFilter(String timeFilter) { DateTime now = DateTime.now(); DateTime start = now; DateTime end = now; try { - if (timeFilter == TimeFilterEnum.lastWeek.name) { + if (timeFilter == TimeFilterEnum.all.name) { + start = DateTime(2025, 1, 1); + end = DateTime(2099, 12, 31); + } else if (timeFilter == TimeFilterEnum.lastWeek.name) { start = now.subtract(const Duration(days: 7)); } else if (timeFilter == TimeFilterEnum.lastMonth.name) { start = now.subtract(const Duration(days: 30)); diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index fe078b3..881b2b2 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -245,11 +245,15 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { void imageFavoritesAction() { try { + if (context.reader.images![0].contains('file:///')) { + showToast(message: "本地收藏暂不支持".tl, context: context); + return; + } String id = context.reader.cid; String eid = context.reader.eid; String title = context.reader.history!.title; String subTitle = context.reader.history!.subtitle; - int maxPage = context.reader.history!.maxPage ?? 1; + int maxPage = context.reader.images!.length; int ep = context.reader.chapter; int page = context.reader.page; String sourceKey = context.reader.type.sourceKey; diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 44bf020..3e22088 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -17,7 +17,9 @@ import 'package:venera/utils/translations.dart'; import 'comic_page.dart'; class SearchPage extends StatefulWidget { - const SearchPage({super.key}); + final List? initialOptions; + + const SearchPage({super.key, this.initialOptions}); @override State createState() => _SearchPageState(); @@ -138,6 +140,7 @@ class _SearchPageState extends State { @override void initState() { + options = widget.initialOptions ?? []; var defaultSearchTarget = appdata.settings['defaultSearchTarget']; if (defaultSearchTarget != null && ComicSource.find(defaultSearchTarget) != null) { diff --git a/pubspec.lock b/pubspec.lock index b5b6a9c..0190a06 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: app_links sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.3" app_links_linux: @@ -14,7 +14,7 @@ packages: description: name: app_links_linux sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" app_links_platform_interface: @@ -22,7 +22,7 @@ packages: description: name: app_links_platform_interface sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" app_links_web: @@ -30,7 +30,7 @@ packages: description: name: app_links_web sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.4" archive: @@ -38,7 +38,7 @@ packages: description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.6.1" args: @@ -46,7 +46,7 @@ packages: description: name: args sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.0" async: @@ -54,7 +54,7 @@ packages: description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.11.0" barcode: @@ -62,7 +62,7 @@ packages: description: name: barcode sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.8" battery_plus: @@ -70,7 +70,7 @@ packages: description: name: battery_plus sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.2.1" battery_plus_platform_interface: @@ -78,7 +78,7 @@ packages: description: name: battery_plus_platform_interface sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.1" bidi: @@ -86,7 +86,7 @@ packages: description: name: bidi sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.12" boolean_selector: @@ -94,7 +94,7 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.1" build_cli_annotations: @@ -102,7 +102,7 @@ packages: description: name: build_cli_annotations sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" characters: @@ -110,7 +110,7 @@ packages: description: name: characters sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" clock: @@ -118,7 +118,7 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" collection: @@ -126,7 +126,7 @@ packages: description: name: collection sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.19.0" convert: @@ -134,7 +134,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.2" cross_file: @@ -142,7 +142,7 @@ packages: description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.3.4+2" crypto: @@ -150,7 +150,7 @@ packages: description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.6" csslib: @@ -158,7 +158,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" dbus: @@ -166,7 +166,7 @@ packages: description: name: dbus sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.10" desktop_webview_window: @@ -183,7 +183,7 @@ packages: description: name: dio sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.7.0" dio_web_adapter: @@ -191,7 +191,7 @@ packages: description: name: dio_web_adapter sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" dynamic_color: @@ -199,7 +199,7 @@ packages: description: name: dynamic_color sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.7.0" fake_async: @@ -207,7 +207,7 @@ packages: description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.1" ffi: @@ -215,7 +215,7 @@ packages: description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.3" file: @@ -223,7 +223,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.1" file_selector: @@ -231,7 +231,7 @@ packages: description: name: file_selector sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.3" file_selector_android: @@ -239,7 +239,7 @@ packages: description: name: file_selector_android sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.1+12" file_selector_ios: @@ -247,7 +247,7 @@ packages: description: name: file_selector_ios sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.3+1" file_selector_linux: @@ -255,7 +255,7 @@ packages: description: name: file_selector_linux sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+2" file_selector_macos: @@ -263,7 +263,7 @@ packages: description: name: file_selector_macos sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.4+2" file_selector_platform_interface: @@ -271,7 +271,7 @@ packages: description: name: file_selector_platform_interface sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.6.2" file_selector_web: @@ -279,7 +279,7 @@ packages: description: name: file_selector_web sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.4+2" file_selector_windows: @@ -287,7 +287,7 @@ packages: description: name: file_selector_windows sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.3+3" fixnum: @@ -295,7 +295,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.1" flutter: @@ -308,7 +308,7 @@ packages: description: name: flutter_file_dialog sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" flutter_inappwebview: @@ -316,7 +316,7 @@ packages: description: name: flutter_inappwebview sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.1.5" flutter_inappwebview_android: @@ -324,7 +324,7 @@ packages: description: name: flutter_inappwebview_android sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.3" flutter_inappwebview_internal_annotations: @@ -332,7 +332,7 @@ packages: description: name: flutter_inappwebview_internal_annotations sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.0" flutter_inappwebview_ios: @@ -340,7 +340,7 @@ packages: description: name: flutter_inappwebview_ios sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" flutter_inappwebview_macos: @@ -348,7 +348,7 @@ packages: description: name: flutter_inappwebview_macos sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" flutter_inappwebview_platform_interface: @@ -356,7 +356,7 @@ packages: description: name: flutter_inappwebview_platform_interface sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0+1" flutter_inappwebview_web: @@ -364,7 +364,7 @@ packages: description: name: flutter_inappwebview_web sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.2" flutter_inappwebview_windows: @@ -372,7 +372,7 @@ packages: description: name: flutter_inappwebview_windows sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.6.0" flutter_lints: @@ -380,7 +380,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.0" flutter_localizations: @@ -393,17 +393,17 @@ packages: description: name: flutter_memory_info sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" - url: "https://pub.dev" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + url: "https://pub.flutter-io.cn" source: hosted - version: "2.0.23" + version: "2.0.24" flutter_qjs: dependency: "direct main" description: @@ -418,7 +418,7 @@ packages: description: name: flutter_reorderable_grid_view sha256: "732bcb1b29d5130c11a70e6acec512941fafe241f0e80bffd93ca6e415819915" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.4.0" flutter_rust_bridge: @@ -426,7 +426,7 @@ packages: description: name: flutter_rust_bridge sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.7.0" flutter_saf: @@ -448,7 +448,7 @@ packages: description: name: flutter_to_arch sha256: b68b2757a89a517ae2141cbc672acdd1f69721dd686cacad03876b6f436ff040 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" flutter_to_debian: @@ -456,7 +456,7 @@ packages: description: name: flutter_to_debian sha256: d23534407334b331ce20fbaa8395b9ecc255d0c047136b8998715f36933ee696 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.2" flutter_web_plugins: @@ -469,7 +469,7 @@ packages: description: name: freezed_annotation sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.4" gtk: @@ -477,7 +477,7 @@ packages: description: name: gtk sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.0" html: @@ -485,7 +485,7 @@ packages: description: name: html sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.15.5" http: @@ -493,7 +493,7 @@ packages: description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.2" http_parser: @@ -501,7 +501,7 @@ packages: description: name: http_parser sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.1.1" http_profile: @@ -509,7 +509,7 @@ packages: description: name: http_profile sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.1.0" image: @@ -517,7 +517,7 @@ packages: description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.3.0" intl: @@ -525,7 +525,7 @@ packages: description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.19.0" io: @@ -533,7 +533,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.5" js: @@ -541,7 +541,7 @@ packages: description: name: js sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.1" json_annotation: @@ -549,7 +549,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.9.0" leak_tracker: @@ -557,7 +557,7 @@ packages: description: name: leak_tracker sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.0.7" leak_tracker_flutter_testing: @@ -565,7 +565,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.8" leak_tracker_testing: @@ -573,23 +573,23 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" - url: "https://pub.dev" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.flutter-io.cn" source: hosted - version: "5.1.0" + version: "5.1.1" local_auth: dependency: "direct main" description: name: local_auth sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" local_auth_android: @@ -597,23 +597,23 @@ packages: description: name: local_auth_android sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.46" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" - url: "https://pub.dev" + sha256: "5c5127061107278ab4cafa1ac51b3b6760282bf1a2abf011270908a429d1634b" + url: "https://pub.flutter-io.cn" source: hosted - version: "1.4.1" + version: "1.4.2" local_auth_platform_interface: dependency: transitive description: name: local_auth_platform_interface sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.10" local_auth_windows: @@ -621,7 +621,7 @@ packages: description: name: local_auth_windows sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.11" lodepng_flutter: @@ -638,7 +638,7 @@ packages: description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.12.16+1" material_color_utilities: @@ -646,7 +646,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.11.1" meta: @@ -654,7 +654,7 @@ packages: description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.15.0" mime: @@ -662,7 +662,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" mime_type: @@ -670,7 +670,7 @@ packages: description: name: mime_type sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.0.1" path: @@ -678,7 +678,7 @@ packages: description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.9.0" path_parsing: @@ -686,7 +686,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" path_provider: @@ -694,7 +694,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.5" path_provider_android: @@ -702,7 +702,7 @@ packages: description: name: path_provider_android sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.15" path_provider_foundation: @@ -710,7 +710,7 @@ packages: description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.4.1" path_provider_linux: @@ -718,7 +718,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -726,7 +726,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" path_provider_windows: @@ -734,7 +734,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.0" pdf: @@ -742,7 +742,7 @@ packages: description: name: pdf sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.11.1" petitparser: @@ -750,7 +750,7 @@ packages: description: name: petitparser sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.0.2" photo_view: @@ -767,7 +767,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.6" plugin_platform_interface: @@ -775,7 +775,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.8" pointycastle: @@ -783,7 +783,7 @@ packages: description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.9.1" qr: @@ -791,7 +791,7 @@ packages: description: name: qr sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.2" rhttp: @@ -799,7 +799,7 @@ packages: description: name: rhttp sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.9.8" screen_retriever: @@ -807,7 +807,7 @@ packages: description: name: screen_retriever sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_linux: @@ -815,7 +815,7 @@ packages: description: name: screen_retriever_linux sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_macos: @@ -823,7 +823,7 @@ packages: description: name: screen_retriever_macos sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_platform_interface: @@ -831,7 +831,7 @@ packages: description: name: screen_retriever_platform_interface sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" screen_retriever_windows: @@ -839,7 +839,7 @@ packages: description: name: screen_retriever_windows sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.0" scrollable_positioned_list: @@ -856,7 +856,7 @@ packages: description: name: share_plus sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "10.1.3" share_plus_platform_interface: @@ -864,7 +864,7 @@ packages: description: name: share_plus_platform_interface sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.0.2" shimmer: @@ -872,7 +872,7 @@ packages: description: name: shimmer sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.0.0" sky_engine: @@ -885,7 +885,7 @@ packages: description: name: sliver_tools sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.2.12" source_span: @@ -893,7 +893,7 @@ packages: description: name: source_span sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.10.0" sprintf: @@ -901,7 +901,7 @@ packages: description: name: sprintf sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "7.0.0" sqlite3: @@ -909,7 +909,7 @@ packages: description: name: sqlite3 sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.5.0" sqlite3_flutter_libs: @@ -917,7 +917,7 @@ packages: description: name: sqlite3_flutter_libs sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.5.28" stack_trace: @@ -925,7 +925,7 @@ packages: description: name: stack_trace sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.12.0" stream_channel: @@ -933,7 +933,7 @@ packages: description: name: stream_channel sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.2" string_scanner: @@ -941,7 +941,7 @@ packages: description: name: string_scanner sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.3.0" term_glyph: @@ -949,7 +949,7 @@ packages: description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.2.1" test_api: @@ -957,7 +957,7 @@ packages: description: name: test_api sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.3" typed_data: @@ -965,7 +965,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.4.0" upower: @@ -973,7 +973,7 @@ packages: description: name: upower sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.7.0" url_launcher: @@ -981,7 +981,7 @@ packages: description: name: url_launcher sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.1" url_launcher_android: @@ -989,7 +989,7 @@ packages: description: name: url_launcher_android sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.14" url_launcher_ios: @@ -997,7 +997,7 @@ packages: description: name: url_launcher_ios sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.3.2" url_launcher_linux: @@ -1005,7 +1005,7 @@ packages: description: name: url_launcher_linux sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.1" url_launcher_macos: @@ -1013,7 +1013,7 @@ packages: description: name: url_launcher_macos sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.2.2" url_launcher_platform_interface: @@ -1021,7 +1021,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.2" url_launcher_web: @@ -1029,7 +1029,7 @@ packages: description: name: url_launcher_web sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.3.3" url_launcher_windows: @@ -1037,7 +1037,7 @@ packages: description: name: url_launcher_windows sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "3.1.3" uuid: @@ -1045,7 +1045,7 @@ packages: description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "4.5.1" vector_math: @@ -1053,7 +1053,7 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "2.1.4" vm_service: @@ -1061,7 +1061,7 @@ packages: description: name: vm_service sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "14.3.0" web: @@ -1069,7 +1069,7 @@ packages: description: name: web sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" webdav_client: @@ -1086,7 +1086,7 @@ packages: description: name: win32 sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "5.9.0" window_manager: @@ -1094,7 +1094,7 @@ packages: description: name: window_manager sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.4.3" xdg_directories: @@ -1102,7 +1102,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "1.1.0" xml: @@ -1110,23 +1110,23 @@ packages: description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "6.5.0" yaml: dependency: "direct main" description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" - url: "https://pub.dev" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.flutter-io.cn" source: hosted - version: "3.1.2" + version: "3.1.3" zip_flutter: dependency: "direct main" description: name: zip_flutter sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646 - url: "https://pub.dev" + url: "https://pub.flutter-io.cn" source: hosted version: "0.0.6" sdks: diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" index 45d7d8b..8f798dd 100644 --- "a/\344\273\273\345\212\241.md" +++ "b/\344\273\273\345\212\241.md" @@ -26,28 +26,29 @@ 4. 支持选中删除 ok 5. 双击大图浏览 ok 1. 显示标题 ok - 3. 支持取消收藏, 退出大图浏览时更新数据库 + 3. 支持取消收藏, 退出大图浏览时更新数据库 ok 4. 点击进行阅读, 点击进入详情 ok 5. 点击进入上一本, 下一本 ok - 6. 显示 tags - 6. 首页显示最喜欢作者, 最多收藏tag, 最喜欢的本子(按照收藏数以及按照收藏比) - 7. 筛选 - 1. tag 筛选 - 2. 收藏数量范围 - 8. 支持导出 - 1. 导出成文本, 包含标题, 链接, 总页数, 收藏数, 收藏占比, 阅读时间, tag 数等 - 2. 生成总结页面, 包含最喜欢作者, 本子, 共收藏本子数, 共收藏图片数, 最喜欢tag, 最喜欢本子, 按总页数, 收藏占比 - 9. bug解决 + 6. 显示 tags no + 6. 打通本地收藏的图片收藏 after + 7. 首页显示最喜欢作者, 最多收藏tag, 最喜欢的本子(按照收藏数以及按照收藏比) ok + 8. 筛选 + 1. tag 搜索 ok + 2. 收藏数量范围 ok + 9. 支持导出 + 1. 导出成文本, 包含标题, 链接, 总页数, 收藏数, 收藏占比, 阅读时间, tag 数等 no + 2. 生成总结页面, 包含最喜欢作者, 本子, 共收藏本子数, 共收藏图片数, 最喜欢tag, 最喜欢本子, 按总页数, 收藏占比 no + 10. bug解决 1. 进入的时候都是一张图, 都是cover 2. 并行请求太多 timeout 风险较高 - 10. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int ok + 11. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int ok 1. imagePath 不需要 2. ep 和 page 都不从0开始 3. eid 存在 otherInfo 里 4. 是否做一个大的改动, 将数据库改成一本漫画的一个章节对应一条记录的形式 - 11. 中英互译 - 12. 实际大数据量测试 - 13. 实机测试 + 12. 中英互译 + 13. 实际大数据量测试 + 14. 实机测试 3. 主页的图片收藏显示 ok 3. 打通联动 4. 自测 From a875c033de01f0e81e125b84339739362d9385d4 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sat, 4 Jan 2025 12:15:34 +0800 Subject: [PATCH 06/36] =?UTF-8?q?feat:=20=E7=BF=BB=E8=AF=91=E4=B8=8Ebug?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 72 +++- lib/components/select.dart | 4 +- lib/foundation/appdata.dart | 1 + lib/foundation/comic_source/favorites.dart | 5 + lib/foundation/comic_source/parser.dart | 10 +- lib/foundation/history.dart | 1 + lib/foundation/image_favorites.dart | 9 +- lib/pages/favorites/favorite_actions.dart | 44 ++- lib/pages/favorites/local_favorites_page.dart | 93 ++++- .../favorites/network_favorites_page.dart | 20 +- lib/pages/home_page/image_favorites.dart | 31 +- .../image_favorites_item.dart | 78 +++- .../image_favorites_page.dart | 336 +++++++++++------- .../image_favorites_photo_view.dart | 164 +++++---- lib/pages/image_favorites_page/type.dart | 41 +-- lib/pages/reader/scaffold.dart | 60 ++-- lib/pages/settings/local_favorites.dart | 18 +- lib/utils/data.dart | 45 ++- lib/utils/utils.dart | 22 ++ "\344\273\273\345\212\241.md" | 2 +- 20 files changed, 717 insertions(+), 339 deletions(-) create mode 100644 lib/utils/utils.dart diff --git a/assets/translation.json b/assets/translation.json index ebb586a..be053d5 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -18,7 +18,7 @@ "help": "帮助", "Select": "选择", "Selected @a comics": "已选择 @a 部漫画", - "Imported @a comics": "已导入 @a 部漫画", + "Imported @a comics, loaded @b pages, received @c comics": "已导入 @a 部漫画, 加载 @b 页, 接收到 @c 部漫画", "Downloading": "下载中", "Back": "后退", "Delete": "删除", @@ -249,6 +249,40 @@ "Export as pdf": "导出为pdf", "Export as epub": "导出为epub", "Aggregated Search": "聚合搜索", + "Local comic collection is not supported at present": "本地收藏暂不支持", + "The cover cannot be uncollected here": "封面不能在此取消收藏", + "Uncollect the image": "取消收藏图片", + "Successfully collected": "收藏成功", + "Collect the image": "收藏图片", + "Support sliding to collect images": "支持滑动收藏图片", + "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在图片浏览页面, 你可以根据你的阅读模式横滑或者竖滑快速收藏图片", + "Calculate your favorite from @a comics and @b images": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的", + "Author: ": "作者: ", + "Tags: ": "标签: ", + "Comics(number): ": "漫画(数量): ", + "Comics(percentage): ": "漫画(比例): ", + "Time Filter": "时间筛选", + "Image Favorites Greater Than": "图片收藏数大于", + "Earliest collection time": "最早收藏时间", + "favoritesCompareComicPages": "收藏数与漫画页数比较", + "Cover": "封面", + "Page @a": "第 @a 页", + "Time Asc": "时间升序", + "Time Desc": "时间降序", + "Favorite Num": "收藏数", + "Favorite Num Compare Comic Pages": "收藏数比漫画页数", + "All": "全部", + "Last Week": "上周", + "Last Month": "上月", + "Last Half Year": "半年", + "Last Year": "一年", + "Filter": "筛选", + "Image Favorites": "图片收藏", + "Title": "标题", + "@a Cover": "@a 封面", + "Update the page number by the latest collection": "按最新收藏更新页数", + "Copy the title successfully": "复制标题成功", + "The comic is invalid, please long press to delete, you can double click the title to copy": "该漫画已失效, 请长按删除, 可以双击标题进行复制", "No search results found": "未找到搜索结果", "Added @c comics to download queue." : "已添加 @c 本漫画到下载队列", "Download started": "下载已开始", @@ -282,7 +316,7 @@ "help": "幫助", "Select": "選擇", "Selected @a comics": "已選擇 @a 部漫畫", - "Imported @a comics": "已匯入 @a 部漫畫", + "Imported @a comics, loaded @b pages, received @c comics": "已匯入 @a 部漫畫, 加載 @b 頁, 接收到 @c 部漫畫", "Downloading": "下載中", "Back": "後退", "Delete": "刪除", @@ -516,6 +550,40 @@ "Added @c comics to download queue." : "已添加 @c 本漫畫到下載隊列", "Download started": "下載已開始", "Click favorite": "點擊收藏", + "Local comic collection is not supported at present": "本地收藏暫不支持", + "The cover cannot be uncollected here": "封面不能在此取消收藏", + "Uncollect the image": "取消收藏圖片", + "Successfully collected": "收藏成功", + "Collect the image": "收藏圖片", + "Support sliding to collect images": "支持滑動收藏圖片", + "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在圖片瀏覽頁面, 你可以根據你的閱讀模式橫向或者縱向滑動快速收藏圖片", + "Calculate your favorite from @a comics and @b images": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的", + "Author: ": "作者: ", + "Tags: ": "標籤: ", + "Comics(number): ": "漫畫(數量): ", + "Comics(percentage): ": "漫畫(比例): ", + "Time Filter": "時間篩選", + "Image Favorites Greater Than": "圖片收藏數大於", + "Earliest collection time": "最早收藏時間", + "favoritesCompareComicPages": "收藏數與漫畫頁數比較", + "Cover": "封面", + "Page @a": "第 @a 頁", + "Time Asc": "時間升序", + "Time Desc": "時間降序", + "Favorite Num": "收藏數", + "Favorite Num Compare Comic Pages": "收藏數比漫畫頁數", + "All": "全部", + "Last Week": "上周", + "Last Month": "上月", + "Last Half Year": "半年", + "Last Year": "一年", + "Filter": "篩選", + "Image Favorites": "圖片收藏", + "Title": "標題", + "@a Cover": "@a 封面", + "Update the page number by the latest collection": "按最新收藏更新頁數", + "Copy the title successfully": "複製標題成功", + "The comic is invalid, please long press to delete, you can double click the title to copy": "該漫畫已失效, 請長按刪除, 可以雙擊標題進行複製", "End": "末尾", "None": "無", "View Detail": "查看詳情", diff --git a/lib/components/select.dart b/lib/components/select.dart index 201c78f..1e5b456 100644 --- a/lib/components/select.dart +++ b/lib/components/select.dart @@ -50,7 +50,7 @@ class Select extends StatelessWidget { .map((e) => PopupMenuItem( height: App.isMobile ? 46 : 40, value: e, - child: Text(e), + child: Text(e.tl), )) .toList(), ).then((value) { @@ -66,7 +66,7 @@ class Select extends StatelessWidget { constraints: BoxConstraints( minWidth: minWidth != null ? (minWidth! - 32) : 0, ), - child: Text(current ?? ' ', style: ts.s14), + child: Text((current ?? ' ').tl, style: ts.s14), ), const SizedBox(width: 8), Icon(Icons.arrow_drop_down, color: context.colorScheme.primary), diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 654de9b..f05307c 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -123,6 +123,7 @@ class _Settings with ChangeNotifier { 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, 'ignoreCertificateErrors': false, + 'supportSwipeToFavorite': 'yes', // yes, no 'authorizationRequired': false, 'onClickFavorite': 'viewDetail', // viewDetail, read 'enableDnsOverrides': false, diff --git a/lib/foundation/comic_source/favorites.dart b/lib/foundation/comic_source/favorites.dart index 8fe8651..76b2ec0 100644 --- a/lib/foundation/comic_source/favorites.dart +++ b/lib/foundation/comic_source/favorites.dart @@ -10,6 +10,10 @@ class FavoriteData { final bool multiFolder; + // 这个收藏时间新旧顺序, 是为了最小成本同步远端的收藏, 只拉取远程最新收藏的漫画, 就不需要全拉取一遍了 + // 如果为 null, 不做处理, 拉取全部 + final bool? isNewToOldSort; + final Future>> Function(int page, [String? folder])? loadComic; @@ -44,6 +48,7 @@ class FavoriteData { this.addFolder, this.allFavoritesId, this.addOrDelFavorite, + this.isNewToOldSort, }); } diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index 36218e0..efef8e5 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -193,7 +193,7 @@ class ComicSourceParser { login = (account, pwd) async { try { await JsEngine().runCode(""" - ComicSource.sources.$_key.account.login(${jsonEncode(account)}, + ComicSource.sources.$_key.account.login(${jsonEncode(account)}, ${jsonEncode(pwd)}) """); var source = ComicSource.find(_key!)!; @@ -502,9 +502,9 @@ class ComicSourceParser { try { var res = await JsEngine().runCode(""" ComicSource.sources.$_key.categoryComics.load( - ${jsonEncode(category)}, - ${jsonEncode(param)}, - ${jsonEncode(options)}, + ${jsonEncode(category)}, + ${jsonEncode(param)}, + ${jsonEncode(options)}, ${jsonEncode(page)} ) """); @@ -618,6 +618,7 @@ class ComicSourceParser { if (!_checkExists("favorites")) return null; final bool multiFolder = _getValue("favorites.multiFolder"); + final bool? isNewToOldSort = _getValue("favorites.isNewToOldSort"); Future> retryZone(Future> Function() func) async { if (!ComicSource.find(_key!)!.isLogged) { @@ -770,6 +771,7 @@ class ComicSourceParser { addFolder: addFolder, deleteFolder: deleteFolder, addOrDelFavorite: addOrDelFavFunc, + isNewToOldSort: isNewToOldSort, ); } diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index d69d605..b6ef165 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -13,6 +13,7 @@ import 'package:venera/foundation/state_controller.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; +import 'package:venera/utils/utils.dart'; import 'app.dart'; part "image_favorites.dart"; diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 5b7392a..c4ec6d9 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -100,6 +100,7 @@ class ImageFavoritesEp { } } +// 从漫画详情中获取到的信息 class ImageFavoritesSomething { String author; String subTitle; @@ -238,6 +239,7 @@ class ImageFavoritesComic { class ImageFavoriteManager with ChangeNotifier { static Database get _db => HistoryManager()._db; static ImageFavoriteManager? cache; + final Throttler _throttler = Throttler(duration: Duration(seconds: 2)); static List imageFavoritesComicList = getAll(null); ImageFavoriteManager.create(); static bool hasInit = false; @@ -245,8 +247,11 @@ class ImageFavoriteManager with ChangeNotifier { return cache == null ? (cache = ImageFavoriteManager.create()) : cache!; } void updateValue() { - imageFavoritesComicList = getAll(null); - notifyListeners(); + // 避免从pica导入的时候, 疯狂触发 + _throttler.run(() { + imageFavoritesComicList = getAll(null); + notifyListeners(); + }); } /// 检查表image_favorites是否存在, 不存在则创建 diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index b9512d7..63fdc59 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -147,13 +147,13 @@ Future> updateComicsInfo(String folder) async { var newInfo = (await comicSource.loadComicInfo!(c.id)).data; var newTags = []; - for(var entry in newInfo.tags.entries) { + for (var entry in newInfo.tags.entries) { const shouldIgnore = ['author', 'artist', 'time']; var namespace = entry.key; if (shouldIgnore.contains(namespace.toLowerCase())) { continue; } - for(var tag in entry.value) { + for (var tag in entry.value) { newTags.add("$namespace:$tag"); } } @@ -305,6 +305,7 @@ Future sortFolders() async { Future importNetworkFolder( String source, + int updatePageNum, String? folder, String? folderID, ) async { @@ -312,7 +313,7 @@ Future importNetworkFolder( if (comicSource == null) { return; } - if(folder != null && folder.isEmpty) { + if (folder != null && folder.isEmpty) { folder = null; } var resultName = folder ?? comicSource.name; @@ -324,7 +325,7 @@ Future importNetworkFolder( return; } } - if(!exists) { + if (!exists) { LocalFavoritesManager().createFolder(resultName); LocalFavoritesManager().linkFolderToNetwork( resultName, @@ -332,21 +333,28 @@ Future importNetworkFolder( folderID ?? "", ); } - + bool isNewToOldSort = comicSource.favoriteData?.isNewToOldSort ?? true; var current = 0; + int receivedComics = 0; + int requestCount = 0; var isFinished = false; + int maxPage = 1; String? next; - + // 如果是从旧到新, 先取一下maxPage + if (!isNewToOldSort) { + var res = await comicSource.favoriteData?.loadComic!(1, folderID); + maxPage = res?.subData ?? 1; + } Future fetchNext() async { var retry = 3; - - while (true) { + while (updatePageNum >= requestCount && !isFinished) { try { if (comicSource.favoriteData?.loadComic != null) { - next ??= '1'; + next ??= isNewToOldSort ? '1' : maxPage.toString(); var page = int.parse(next!); var res = await comicSource.favoriteData!.loadComic!(page, folderID); var count = 0; + receivedComics += res.data.length; for (var c in res.data) { var result = LocalFavoritesManager().addComic( resultName, @@ -363,16 +371,19 @@ Future importNetworkFolder( count++; } } + requestCount++; current += count; if (res.data.isEmpty || res.subData == page) { isFinished = true; next = null; } else { - next = (page + 1).toString(); + next = + isNewToOldSort ? (page + 1).toString() : (page - 1).toString(); } } else if (comicSource.favoriteData?.loadNext != null) { var res = await comicSource.favoriteData!.loadNext!(next, folderID); var count = 0; + receivedComics += res.data.length; for (var c in res.data) { var result = LocalFavoritesManager().addComic( resultName, @@ -389,6 +400,7 @@ Future importNetworkFolder( count++; } } + requestCount++; current += count; if (res.data.isEmpty || res.subData == null) { isFinished = true; @@ -408,6 +420,8 @@ Future importNetworkFolder( continue; } } + // 跳出循环, 表示已经完成, 强制为 true, 避免死循环 + isFinished = true; } bool isCanceled = false; @@ -415,6 +429,7 @@ Future importNetworkFolder( bool isErrored() => errorMsg != null; void Function()? updateDialog; + void Function()? closeDialog; showDialog( context: App.rootContext, @@ -422,6 +437,7 @@ Future importNetworkFolder( return StatefulBuilder( builder: (context, setState) { updateDialog = () => setState(() {}); + closeDialog = () => Navigator.pop(context); return ContentDialog( title: isFinished ? "Finished".tl @@ -437,8 +453,11 @@ Future importNetworkFolder( value: isFinished ? 1 : null, ), const SizedBox(height: 4), - Text("Imported @c comics".tlParams({ - "c": current, + Text("Imported @a comics, loaded @b pages, received @c comics" + .tlParams({ + "a": current, + "b": requestCount, + "c": receivedComics, })), const SizedBox(height: 4), if (isErrored()) Text("Error: $errorMsg"), @@ -476,4 +495,5 @@ Future importNetworkFolder( break; } } + closeDialog?.call(); } diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 2a8f344..c71aa77 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -1,5 +1,78 @@ part of 'favorites_page.dart'; +String allPageText = 'All'.tl; +List pageNumList = [1, 2, 3, 5, 10, 20, 50, 100, 200, allPageText] + .map((e) => e.toString()) + .toList(); + +class _SelectUpdatePageNum extends StatefulWidget { + const _SelectUpdatePageNum({ + required this.networkSource, + this.networkFolder, + super.key, + }); + + final String? networkFolder; + final String networkSource; + + @override + State<_SelectUpdatePageNum> createState() => _SelectUpdatePageNumState(); +} + +class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> { + int updatePageNum = 9999999; + @override + void initState() { + updatePageNum = + appdata.implicitData["local_favorites_update_page_num"] ?? 9999999; + super.initState(); + } + + @override + Widget build(BuildContext context) { + var source = ComicSource.find(widget.networkSource); + var sourceName = source?.name ?? widget.networkSource; + var text = "The folder is Linked to @source".tlParams({ + "source": sourceName, + }); + if (widget.networkFolder != null && widget.networkFolder!.isNotEmpty) { + text += "\n${"Source Folder".tl}: ${widget.networkFolder}"; + } + + return Column( + children: [ + Row( + children: [Text(text)], + ), + if (source?.favoriteData?.isNewToOldSort != null) + Row( + children: [ + Text("Update the page number by the latest collection".tl), + Spacer(), + Select( + current: updatePageNum.toString() == '9999999' + ? allPageText + : updatePageNum.toString(), + values: pageNumList, + minWidth: 64, + onTap: (index) { + setState(() { + updatePageNum = int.parse(pageNumList[index] == allPageText + ? '9999999' + : pageNumList[index]); + appdata.implicitData["local_favorites_update_page_num"] = + updatePageNum; + appdata.writeImplicitData(); + }); + }, + ) + ], + ), + ], + ); + } +} + class _LocalFavoritesPage extends StatefulWidget { const _LocalFavoritesPage({required this.folder, super.key}); @@ -136,17 +209,17 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { message: "Sync".tl, child: Flyout( flyoutBuilder: (context) { - var sourceName = ComicSource.find(networkSource!)?.name ?? - networkSource!; - var text = "The folder is Linked to @source".tlParams({ - "source": sourceName, - }); - if (networkFolder != null && networkFolder!.isNotEmpty) { - text += "\n${"Source Folder".tl}: $networkFolder"; - } + final GlobalKey<_SelectUpdatePageNumState> + selectUpdatePageNumKey = + GlobalKey<_SelectUpdatePageNumState>(); + var updatePageWidget = _SelectUpdatePageNum( + networkSource: networkSource!, + networkFolder: networkFolder, + key: selectUpdatePageNumKey, + ); return FlyoutContent( title: "Sync".tl, - content: Text(text), + content: updatePageWidget, actions: [ Button.filled( child: Text("Update".tl), @@ -154,6 +227,8 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> { context.pop(); importNetworkFolder( networkSource!, + selectUpdatePageNumKey + .currentState!.updatePageNum, widget.folder, networkFolder!, ).then( diff --git a/lib/pages/favorites/network_favorites_page.dart b/lib/pages/favorites/network_favorites_page.dart index 5cd0f53..8acc3b1 100644 --- a/lib/pages/favorites/network_favorites_page.dart +++ b/lib/pages/favorites/network_favorites_page.dart @@ -20,8 +20,7 @@ Future _deleteComic( return StatefulBuilder(builder: (context, setState) { return ContentDialog( title: "Remove".tl, - content: Text("Remove comic from favorite?".tl) - .paddingHorizontal(16), + content: Text("Remove comic from favorite?".tl).paddingHorizontal(16), actions: [ Button.filled( isLoading: loading, @@ -94,9 +93,8 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> { return ComicList( key: comicListKey, leadingSliver: SliverAppbar( - style: context.width < changePoint - ? AppbarStyle.shadow - : AppbarStyle.blur, + style: + context.width < changePoint ? AppbarStyle.shadow : AppbarStyle.blur, leading: Tooltip( message: "Folders".tl, child: context.width <= _kTwoPanelChangeWidth @@ -117,7 +115,7 @@ class _NormalFavoritePageState extends State<_NormalFavoritePage> { icon: Icons.sync, text: "Convert to local".tl, onClick: () { - importNetworkFolder(widget.data.key, null, null); + importNetworkFolder(widget.data.key, 9999999, null, null); }, ) ]), @@ -215,9 +213,8 @@ class _MultiFolderFavoritesPageState extends State<_MultiFolderFavoritesPage> { @override Widget build(BuildContext context) { var sliverAppBar = SliverAppbar( - style: context.width < changePoint - ? AppbarStyle.shadow - : AppbarStyle.blur, + style: + context.width < changePoint ? AppbarStyle.shadow : AppbarStyle.blur, leading: Tooltip( message: "Folders".tl, child: context.width <= _kTwoPanelChangeWidth @@ -431,8 +428,7 @@ class _FolderTile extends StatelessWidget { return StatefulBuilder(builder: (context, setState) { return ContentDialog( title: "Delete".tl, - content: Text("Delete folder?".tl) - .paddingHorizontal(16), + content: Text("Delete folder?".tl).paddingHorizontal(16), actions: [ Button.filled( isLoading: loading, @@ -558,7 +554,7 @@ class _FavoriteFolder extends StatelessWidget { icon: Icons.sync, text: "Convert to local".tl, onClick: () { - importNetworkFolder(data.key, title, folderID); + importNetworkFolder(data.key, 9999999, title, folderID); }, ) ]), diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart index 9e2a16d..cc5bb7f 100644 --- a/lib/pages/home_page/image_favorites.dart +++ b/lib/pages/home_page/image_favorites.dart @@ -147,16 +147,18 @@ class ImageFavoritesState extends State { } void refreshImageFavorites() async { - imageFavoritesCompute = null; - allImageFavoritePros = []; - for (var comic in ImageFavoriteManager.imageFavoritesComicList) { - allImageFavoritePros.addAll(comic.sortedImageFavoritePros); + if(mounted){ + imageFavoritesCompute = null; + allImageFavoritePros = []; + for (var comic in ImageFavoriteManager.imageFavoritesComicList) { + allImageFavoritePros.addAll(comic.sortedImageFavoritePros); + } + setState(() {}); + // 避免性能开销, 开一个线程计算 + imageFavoritesCompute = await compute(computeImageFavorites, + jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); + setState(() {}); } - setState(() {}); - // 避免性能开销, 开一个线程计算 - imageFavoritesCompute = await compute(computeImageFavorites, - jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); - setState(() {}); } @override @@ -261,7 +263,8 @@ class ImageFavoritesState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "@a Comic, @b image favorites".tlParams({ + "Calculate your favorite from @a comics and @b images" + .tlParams({ "a": ImageFavoriteManager.length.toString(), "b": allImageFavoritePros.length }), @@ -272,7 +275,7 @@ class ImageFavoritesState extends State { Row( children: [ Text( - "作者:".tl, + "Author: ".tl, style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.authors, @@ -283,7 +286,7 @@ class ImageFavoritesState extends State { Row( children: [ Text( - "标签:".tl, + "Tags: ".tl, style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.tags, @@ -294,7 +297,7 @@ class ImageFavoritesState extends State { Row( children: [ Text( - "漫画(数量):".tl, + "Comics(number): ".tl, style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.comicByNum, @@ -305,7 +308,7 @@ class ImageFavoritesState extends State { Row( children: [ Text( - "漫画(比例):".tl, + "Comics(percentage): ".tl, style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.comicByPercentage, diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 7c66ec3..3591aac 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -1,18 +1,21 @@ part of 'image_favorites_page.dart'; class ImageFavoritesItem extends StatefulWidget { - const ImageFavoritesItem( - {super.key, - required this.imageFavoritesComic, - required this.selectedImageFavorites, - required this.addSelected, - required this.multiSelectMode, - required this.finalImageFavoritesComicList}); + const ImageFavoritesItem({ + super.key, + required this.imageFavoritesComic, + required this.selectedImageFavorites, + required this.addSelected, + required this.multiSelectMode, + required this.finalImageFavoritesComicList, + required this.isRefreshEpMap, + }); final ImageFavoritesComic imageFavoritesComic; final Function(ImageFavoritePro) addSelected; final Map selectedImageFavorites; final List finalImageFavoritesComicList; final bool multiSelectMode; + final Map isRefreshEpMap; @override State createState() => ImageFavoritesItemState(); } @@ -23,7 +26,13 @@ class ImageFavoritesItemState extends State { bool hasRefreshImageKeyOnErr = false; // 如果 imageKey 失效了, 或者刚从pica导入(没有imageKey) void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { - if (isImageKeyLoading || hasRefreshImageKeyOnErr) return; + if (isImageKeyLoading || + hasRefreshImageKeyOnErr || + (widget.isRefreshEpMap[imageFavoritesEp]?.isLoaded ?? false)) { + return; + } + widget.isRefreshEpMap[imageFavoritesEp] = + LoadingImageFavoritesEpRes(isLoaded: true, isInvalid: false); isImageKeyLoading = true; ComicSource? comicSource = ComicSource.find(widget.imageFavoritesComic.sourceKey); @@ -38,6 +47,10 @@ class ImageFavoritesItemState extends State { ]); Res> comicPagesRes = resArr[0] as Res>; Res comicInfoRes = resArr[1] as Res; + if (comicInfoRes.errorMessage?.contains("404") ?? false) { + widget.isRefreshEpMap[imageFavoritesEp] = + LoadingImageFavoritesEpRes(isLoaded: true, isInvalid: true); + } if (!comicPagesRes.error && !comicInfoRes.error) { List images = comicPagesRes.data; ImageFavoritesSomething something = @@ -56,17 +69,17 @@ class ImageFavoritesItemState extends State { ImageFavoritePro copy = ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); copy.page = ImageFavoritesEp.firstPage; - copy.imageKey = images[0]; copy.isAutoFavorite = true; imageFavoritesEp.imageFavorites.insert(0, copy); } + // 统一刷一下最新的imageKey for (var ele in imageFavoritesEp.imageFavorites) { ele.imageKey = images[ele.page - 1]; } ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); - if (mounted) { - setState(() {}); - } + } + if (mounted) { + setState(() {}); } isImageKeyLoading = false; } @@ -89,6 +102,11 @@ class ImageFavoritesItemState extends State { ); } + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { int count = widget.imageFavoritesComic.sortedImageFavoritePros.length; @@ -111,6 +129,12 @@ class ImageFavoritesItemState extends State { ), child: InkWell( borderRadius: BorderRadius.circular(8), + onDoubleTap: () { + Clipboard.setData( + ClipboardData(text: widget.imageFavoritesComic.title)); + showToast( + message: "Copy the title successfully".tl, context: context); + }, onTap: () { if (widget.multiSelectMode) { for (var ele @@ -185,13 +209,18 @@ class ImageFavoritesItemState extends State { fit: BoxFit.cover, filterQuality: FilterQuality.medium, onError: (Object error, StackTrace? stackTrace) { + if (widget + .isRefreshEpMap[curImageFavoritesEp]?.isLoaded ?? + false) { + return; + } refreshImageKey(curImageFavoritesEp); hasRefreshImageKeyOnErr = true; }, ); int curPage = curImageFavorite.page; String pageText = curPage == ImageFavoritesEp.firstPage - ? '@a cover'.tlParams({"a": curImageFavorite.epName}) + ? '@a Cover'.tlParams({"a": curImageFavorite.epName}) : curPage.toString(); return InkWell( onDoubleTap: () { @@ -226,7 +255,7 @@ class ImageFavoritesItemState extends State { decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: isSelected - ? Theme.of(context).colorScheme.secondaryContainer + ? Theme.of(context).colorScheme.primaryContainer : null, ), padding: const EdgeInsets.symmetric(horizontal: 4), @@ -254,15 +283,32 @@ class ImageFavoritesItemState extends State { }, ), ).paddingHorizontal(8), + if (widget + .isRefreshEpMap[ + widget.imageFavoritesComic.imageFavoritesEp.first] + ?.isInvalid ?? + false) + Row( + children: [ + Text( + "The comic is invalid, please long press to delete, you can double click the title to copy" + .tl, + style: TextStyle( + color: Theme.of(context).colorScheme.error, // 设置为红色 + fontSize: 12, + )), + ], + ).paddingHorizontal(16), Row( children: [ Text( - "最早收藏时间: $time | ${widget.imageFavoritesComic.sourceKey}", + "Earliest collection time".tl + + ": $time | ${widget.imageFavoritesComic.sourceKey}", textAlign: TextAlign.left, style: const TextStyle( fontSize: 12.0, ), - ) + ), ], ).paddingHorizontal(16).paddingBottom(8), ], diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 56a7e1a..876abc6 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -18,12 +18,19 @@ import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; import 'package:venera/pages/reader/reader.dart'; +import 'package:venera/utils/ext.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; part "image_favorites_item.dart"; part "image_favorites_photo_view.dart"; +class LoadingImageFavoritesEpRes { + bool isLoaded; + bool isInvalid; + LoadingImageFavoritesEpRes({required this.isLoaded, required this.isInvalid}); +} + class ImageFavoritesPage extends StatefulWidget { const ImageFavoritesPage({super.key, this.initialKeyword}); final String? initialKeyword; @@ -31,9 +38,8 @@ class ImageFavoritesPage extends StatefulWidget { State createState() => ImageFavoritesPageState(); } -class ImageFavoritesPageState extends State - with TickerProviderStateMixin { - late ImageFavoriteSortType sortType; +class ImageFavoritesPageState extends State { + late String sortType; late String timeFilterSelect; late String numFilterSelect; late List finalTimeList; @@ -41,16 +47,13 @@ class ImageFavoritesPageState extends State List imageFavoritesComicList = []; late List curImageFavoritesComicList; late List timeFilter; + Map isRefreshEpMap = {}; String keyword = ""; // 进入关键词搜索模式 bool searchMode = false; - List optionTypes = ['Sort', 'Filter']; - bool multiSelectMode = false; - late TabController controller; - int tabIndex = 0; // 多选的时候选中的图片 Map selectedImageFavorites = {}; late List imageFavoritePros; @@ -59,6 +62,15 @@ class ImageFavoritesPageState extends State setState(() {}); } + void updateDialogConfig(String tempSortType, String tempTimeFilterSelect, + String tempNumFilterSelect) { + setState(() { + sortType = tempSortType; + timeFilterSelect = tempTimeFilterSelect; + numFilterSelect = tempNumFilterSelect; + }); + } + void getInitImageFavorites() { imageFavoritePros = []; for (var e in ImageFavoriteManager.imageFavoritesComicList) { @@ -82,7 +94,7 @@ class ImageFavoritesPageState extends State // 筛选到最终列表 curImageFavoritesComicList = tempList.where((ele) { bool isFilter = true; - if (timeFilterSelect != TimeFilterEnum.all.name) { + if (timeFilterSelect != TimeFilterEnum.all.value) { timeFilter = getDateTimeRangeFromFilter(timeFilterSelect); DateTime start = timeFilter[0]; DateTime end = timeFilter[1]; @@ -99,50 +111,55 @@ class ImageFavoritesPageState extends State return isFilter; }).toList(); // 给列表排序 - switch (sortType) { - case ImageFavoriteSortType.name: - curImageFavoritesComicList.sort((a, b) => a.title.compareTo(b.title)); - break; - case ImageFavoriteSortType.timeAsc: - curImageFavoritesComicList.sort((a, b) => a.time.compareTo(b.time)); - break; - case ImageFavoriteSortType.timeDesc: - curImageFavoritesComicList.sort((a, b) => b.time.compareTo(a.time)); - break; - case ImageFavoriteSortType.maxFavorites: - curImageFavoritesComicList.sort((a, b) => b - .sortedImageFavoritePros.length - .compareTo(a.sortedImageFavoritePros.length)); - break; - case ImageFavoriteSortType.favoritesCompareComicPages: - curImageFavoritesComicList.sort((a, b) { - double tempA = a.sortedImageFavoritePros.length / a.maxPageFromEp; - double tempB = b.sortedImageFavoritePros.length / b.maxPageFromEp; - return tempB.compareTo(tempA); - }); - break; - default: + if (sortType == ImageFavoriteSortType.title.value) { + curImageFavoritesComicList.sort((a, b) => a.title.compareTo(b.title)); + } else if (sortType == ImageFavoriteSortType.timeAsc.value) { + curImageFavoritesComicList.sort((a, b) => a.time.compareTo(b.time)); + } else if (sortType == ImageFavoriteSortType.timeDesc.value) { + curImageFavoritesComicList.sort((a, b) => b.time.compareTo(a.time)); + } else if (sortType == ImageFavoriteSortType.maxFavorites.value) { + curImageFavoritesComicList.sort((a, b) => b.sortedImageFavoritePros.length + .compareTo(a.sortedImageFavoritePros.length)); + } else if (sortType == + ImageFavoriteSortType.favoritesCompareComicPages.value) { + curImageFavoritesComicList.sort((a, b) { + double tempA = a.sortedImageFavoritePros.length / a.maxPageFromEp; + double tempB = b.sortedImageFavoritePros.length / b.maxPageFromEp; + return tempB.compareTo(tempA); + }); } } @override void initState() { - controller = TabController( - length: 2, - vsync: this, - ); if (widget.initialKeyword != null) { keyword = widget.initialKeyword!; searchMode = true; } - var sort = appdata.implicitData["image_favorites_sort"] ?? "name"; - sortType = ImageFavoriteSortType.fromString(sort); - timeFilterSelect = appdata.implicitData["image_favorites_time_filter"] ?? - TimeFilterEnum.all.name; + String initSortType = appdata.implicitData["image_favorites_sort"] ?? + ImageFavoriteSortType.title.value; + sortType = ImageFavoriteSortType.values + .firstWhereOrNull((e) => e.value == initSortType) + ?.value ?? + ImageFavoriteSortType.title.value; + String initTimeFilter = + appdata.implicitData["image_favorites_time_filter"] ?? + TimeFilterEnum.all.value; + // 可能是年份 + int yearNum = int.tryParse(initTimeFilter) ?? 2023; + bool isValideYearNum = yearNum >= 2023 && yearNum <= 2099; + if (!isValideYearNum) { + timeFilterSelect = timeFilterList + .firstWhereOrNull((e) => e.value == initTimeFilter) + ?.value ?? + TimeFilterEnum.all.value; + } else { + timeFilterSelect = initTimeFilter; + } numFilterSelect = appdata.implicitData["image_favorites_num_filter"] ?? numFilterList[0]; finalTimeList = List.from([ - ...timeFilterList.map((e) => e.name), + ...timeFilterList.map((e) => e.value), ...ImageFavoriteManager.earliestTimeToNow ]); getInitImageFavorites(); @@ -152,7 +169,6 @@ class ImageFavoritesPageState extends State @override void dispose() { - controller.dispose(); ImageFavoriteManager().removeListener(refreshImageFavorites); super.dispose(); } @@ -223,7 +239,7 @@ class ImageFavoritesPageState extends State slivers: [ if (!searchMode && !multiSelectMode) SliverAppbar( - title: Text("Local".tl), + title: Text("Image Favorites".tl), actions: [ Tooltip( message: "Search".tl, @@ -240,7 +256,7 @@ class ImageFavoritesPageState extends State message: "Sort".tl, child: IconButton( icon: const Icon(Icons.sort), - color: timeFilterSelect != TimeFilterEnum.all.name || + color: timeFilterSelect != TimeFilterEnum.all.value || numFilterSelect != numFilterList[0] ? Theme.of(context).colorScheme.primary : null, @@ -311,6 +327,7 @@ class ImageFavoritesPageState extends State SliverList( delegate: SliverChildBuilderDelegate((context, index) { return ImageFavoritesItem( + isRefreshEpMap: isRefreshEpMap, imageFavoritesComic: curImageFavoritesComicList[index], selectedImageFavorites: selectedImageFavorites, addSelected: addSelected, @@ -336,13 +353,76 @@ class ImageFavoritesPageState extends State } void sort() { + showDialog( + context: context, + builder: (context) { + return ImageFavoritesDialog( + initSortType: sortType, + initTimeFilterSelect: timeFilterSelect, + initNumFilterSelect: numFilterSelect, + finalTimeList: finalTimeList, + updateDialogConfig: updateDialogConfig, + ); + }, + ); + } +} + +class ImageFavoritesDialog extends StatefulWidget { + ImageFavoritesDialog({ + super.key, + required this.initSortType, + required this.initTimeFilterSelect, + required this.initNumFilterSelect, + required this.finalTimeList, + required this.updateDialogConfig, + }); + String initSortType; + String initTimeFilterSelect; + String initNumFilterSelect; + final List finalTimeList; + final Function updateDialogConfig; + @override + State createState() => ImageFavoritesDialogState(); +} + +class ImageFavoritesDialogState extends State + with TickerProviderStateMixin { + late TabController controller; + List optionTypes = ['Sort', 'Filter']; + int tabIndex = 0; + void handleTabIndex() { + if (mounted) { + setState(() { + tabIndex = controller.index; + }); + } + } + + @override + void initState() { + controller = TabController( + length: 2, + vsync: this, + ); + controller.addListener(handleTabIndex); + super.initState(); + } + + @override + void dispose() { + controller.removeListener(handleTabIndex); + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { Widget tabBar = Material( child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), ), - // 向上移动一点, 减少 Column 顶部的 padding, 避免观感太差 - transform: Matrix4.translationValues(0, -24, 0), child: FilledTabBar( key: PageStorageKey(optionTypes), tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), @@ -350,97 +430,83 @@ class ImageFavoritesPageState extends State ), ), ).paddingTop(context.padding.top); - showDialog( - context: context, - builder: (context) { - return StatefulBuilder(builder: (context, setState) { - void handleTabIndex() { - setState(() { - tabIndex = controller.index; - }); - } - - controller.addListener(handleTabIndex); - return ContentDialog( - content: Column(mainAxisSize: MainAxisSize.min, children: [ - tabBar, - tabIndex == 0 - ? Column( - children: [ - CustomListItem('Name', ImageFavoriteSortType.name), - CustomListItem( - 'timeAsc', ImageFavoriteSortType.timeAsc), - CustomListItem( - 'timeDesc', ImageFavoriteSortType.timeDesc), - CustomListItem('favorite Num Desc', - ImageFavoriteSortType.maxFavorites), - CustomListItem('favoritesCompareComicPages', - ImageFavoriteSortType.favoritesCompareComicPages), - ] - .map( - (e) => RadioListTile( - title: Text(e.title.tl), - value: e.value, - groupValue: sortType, - onChanged: (v) { - setState(() { - sortType = v!; - }); - }, - ), - ) - .toList(), - ) - : Column( - children: [ - ListTile( - title: Text("时间筛选".tl), - trailing: Select( - current: timeFilterSelect, - values: finalTimeList, - minWidth: 64, - onTap: (index) { - setState(() { - timeFilterSelect = finalTimeList[index]; - }); - }, - ), - ), - ListTile( - title: Text("图片收藏数大于".tl), - trailing: Select( - current: numFilterSelect, - values: numFilterList, - minWidth: 64, - onTap: (index) { - setState(() { - numFilterSelect = numFilterList[index]; - }); - }, - ), - ) - ], + return ContentDialog( + content: Column(mainAxisSize: MainAxisSize.min, children: [ + tabBar, + tabIndex == 0 + ? Column( + children: [ + ImageFavoriteSortType.title, + ImageFavoriteSortType.timeAsc, + ImageFavoriteSortType.timeDesc, + ImageFavoriteSortType.maxFavorites, + ImageFavoriteSortType.favoritesCompareComicPages, + ] + .map( + (e) => RadioListTile( + title: Text(e.value.tl), + value: e.value, + groupValue: widget.initSortType, + onChanged: (v) { + setState(() { + widget.initSortType = v!; + }); + }, + ), ) - ]), - actions: [ - FilledButton( - onPressed: () { - appdata.implicitData["image_favorites_sort"] = sortType.value; - appdata.implicitData["image_favorites_time_filter"] = - timeFilterSelect; - appdata.writeImplicitData(); - controller.removeListener(handleTabIndex); - if (mounted) { - Navigator.pop(context); - update(); - } - }, - child: Text("Confirm".tl), - ), - ], - ); - }); - }, + .toList(), + ) + : Column( + children: [ + ListTile( + title: Text("Time Filter".tl), + trailing: Select( + current: widget.initTimeFilterSelect, + values: widget.finalTimeList, + minWidth: 64, + onTap: (index) { + setState(() { + widget.initTimeFilterSelect = + widget.finalTimeList[index]; + }); + }, + ), + ), + ListTile( + title: Text("Image Favorites Greater Than".tl), + trailing: Select( + current: widget.initNumFilterSelect, + values: numFilterList, + minWidth: 64, + onTap: (index) { + setState(() { + widget.initNumFilterSelect = numFilterList[index]; + }); + }, + ), + ) + ], + ) + ]), + actions: [ + FilledButton( + onPressed: () { + appdata.implicitData["image_favorites_sort"] = widget.initSortType; + appdata.implicitData["image_favorites_time_filter"] = + widget.initTimeFilterSelect; + appdata.implicitData["image_favorites_num_filter"] = + widget.initNumFilterSelect; + appdata.writeImplicitData(); + controller.removeListener(handleTabIndex); + if (mounted) { + Navigator.pop(context); + widget.updateDialogConfig(widget.initSortType, + widget.initTimeFilterSelect, widget.initNumFilterSelect); + } + }, + child: Text("Confirm".tl), + ), + ], ); } } diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index 9ca07f9..2ddc8ef 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -73,8 +73,9 @@ class ImageFavoritesPhotoViewState extends State { ImageFavoritePro curImageFavorite = curComic.sortedImageFavoritePros[curIndex]; int curPage = curImageFavorite.page; - String pageText = - curPage == ImageFavoritesEp.firstPage ? 'cover'.tl : "第$curPage页"; + String pageText = curPage == ImageFavoritesEp.firstPage + ? 'Cover'.tl + : "Page @a".tlParams({'a': curPage}); return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { @@ -116,12 +117,23 @@ class ImageFavoritesPhotoViewState extends State { right: 0, child: Padding( padding: const EdgeInsets.all(8.0), - child: Text( - curComic.title, - style: ts.s18, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).paddingTop(20), + child: InkWell( + onTap: () { + widget.goComicInfo(curComic); + }, + onDoubleTap: () { + Clipboard.setData(ClipboardData(text: curComic.title)); + showToast( + message: "Copy the title successfully".tl, + context: context); + }, + child: Text( + curComic.title, + style: ts.s18, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).paddingTop(20), + ), ), ), Positioned( @@ -135,71 +147,89 @@ class ImageFavoritesPhotoViewState extends State { "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavoritePros.length}", style: ts.s12), Spacer(), - IconButton( - icon: Icon(Icons.arrow_circle_left), - onPressed: () { - if (curImageFavoritesComicIndex == 0) { - curImageFavoritesComicIndex = - widget.finalImageFavoritesComicList.length - 1; - } else { - curImageFavoritesComicIndex -= 1; - } - curIndex = 0; - controller.jumpToPage(0); - setState(() {}); - }, + Flexible( + flex: 1, + child: IconButton( + icon: Icon(Icons.arrow_circle_left), + onPressed: () { + if (curImageFavoritesComicIndex == 0) { + curImageFavoritesComicIndex = + widget.finalImageFavoritesComicList.length - 1; + } else { + curImageFavoritesComicIndex -= 1; + } + curIndex = 0; + controller.jumpToPage(0); + setState(() {}); + }, + ), ), - IconButton( - icon: cancelImageFavorites[curImageFavorite] == true - ? Icon(Icons.favorite_border) - : Icon(Icons.favorite), - onPressed: () { - if (cancelImageFavorites[curImageFavorite] == true) { - cancelImageFavorites[curImageFavorite] = false; - } else { - cancelImageFavorites[curImageFavorite] = true; - } + Flexible( + flex: 1, + child: IconButton( + icon: cancelImageFavorites[curImageFavorite] == true + ? Icon(Icons.favorite_border) + : Icon(Icons.favorite), + onPressed: () { + if (cancelImageFavorites[curImageFavorite] == true) { + cancelImageFavorites[curImageFavorite] = false; + } else { + cancelImageFavorites[curImageFavorite] = true; + } - setState(() {}); - }, + setState(() {}); + }, + ), ), - IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () { - widget.goReaderPage(curComic, curImageFavorite.ep, curPage); - }, + Flexible( + flex: 1, + child: IconButton( + icon: Icon(Icons.play_arrow), + onPressed: () { + widget.goReaderPage(curComic, curImageFavorite.ep, curPage); + }, + ), ), - IconButton( - icon: Icon(Icons.menu_book), - onPressed: () { - widget.goComicInfo(curComic); - }, + Flexible( + flex: 1, + child: IconButton( + icon: Icon(Icons.menu_book), + onPressed: () { + widget.goComicInfo(curComic); + }, + ), ), - IconButton( - icon: const Icon(Icons.download), - onPressed: () async { - var data = await _getCurrentImageData(curImageFavorite); - if (data == null) { - return; - } - var fileType = detectFileType(data); - var filename = "${curImageFavorite.page}${fileType.ext}"; - saveFile(data: data, filename: filename); - }, + Flexible( + flex: 1, + child: IconButton( + icon: const Icon(Icons.download), + onPressed: () async { + var data = await _getCurrentImageData(curImageFavorite); + if (data == null) { + return; + } + var fileType = detectFileType(data); + var filename = "${curImageFavorite.page}${fileType.ext}"; + saveFile(data: data, filename: filename); + }, + ), ), - IconButton( - icon: Icon(Icons.arrow_circle_right), - onPressed: () { - if (curImageFavoritesComicIndex == - widget.finalImageFavoritesComicList.length - 1) { - curImageFavoritesComicIndex = 0; - } else { - curImageFavoritesComicIndex += 1; - } - curIndex = 0; - controller.jumpToPage(0); - setState(() {}); - }, + Flexible( + flex: 1, + child: IconButton( + icon: Icon(Icons.arrow_circle_right), + onPressed: () { + if (curImageFavoritesComicIndex == + widget.finalImageFavoritesComicList.length - 1) { + curImageFavoritesComicIndex = 0; + } else { + curImageFavoritesComicIndex += 1; + } + curIndex = 0; + controller.jumpToPage(0); + setState(() {}); + }, + ), ), ]), ), diff --git a/lib/pages/image_favorites_page/type.dart b/lib/pages/image_favorites_page/type.dart index f5605fc..42359be 100644 --- a/lib/pages/image_favorites_page/type.dart +++ b/lib/pages/image_favorites_page/type.dart @@ -1,11 +1,11 @@ import 'package:venera/foundation/log.dart'; enum ImageFavoriteSortType { - name("name"), - timeAsc("time_asc"), - timeDesc("time_desc"), - maxFavorites("max_favorites"), // 单本收藏数最多排序 - favoritesCompareComicPages("favorites_compare_comic_pages"); // 单本收藏数比上总页数 + title("Title"), + timeAsc("Time Asc"), + timeDesc("Time Desc"), + maxFavorites("Favorite Num"), // 单本收藏数最多排序 + favoritesCompareComicPages("Favorite Num Compare Comic Pages"); // 单本收藏数比上总页数 final String value; @@ -17,23 +17,16 @@ enum ImageFavoriteSortType { return type; } } - return name; + return title; } } -class CustomListItem { - final String title; - final T value; - - CustomListItem(this.title, this.value); -} - enum TimeFilterEnum { - all("all"), - lastWeek("lastWeek"), - lastMonth("lastMonth"), - lastHalfYear("lastHalfYear"), - lastYear("lastYear"); // 单本收藏数最多排序 + all("All"), + lastWeek("Last Week"), + lastMonth("Last Month"), + lastHalfYear("Last Half Year"), + lastYear("Last Year"); // 单本收藏数最多排序 final String value; const TimeFilterEnum(this.value); @@ -43,7 +36,7 @@ enum TimeFilterEnum { return type; } } - return lastWeek; + return all; } } @@ -60,16 +53,16 @@ getDateTimeRangeFromFilter(String timeFilter) { DateTime start = now; DateTime end = now; try { - if (timeFilter == TimeFilterEnum.all.name) { + if (timeFilter == TimeFilterEnum.all.value) { start = DateTime(2025, 1, 1); end = DateTime(2099, 12, 31); - } else if (timeFilter == TimeFilterEnum.lastWeek.name) { + } else if (timeFilter == TimeFilterEnum.lastWeek.value) { start = now.subtract(const Duration(days: 7)); - } else if (timeFilter == TimeFilterEnum.lastMonth.name) { + } else if (timeFilter == TimeFilterEnum.lastMonth.value) { start = now.subtract(const Duration(days: 30)); - } else if (timeFilter == TimeFilterEnum.lastHalfYear.name) { + } else if (timeFilter == TimeFilterEnum.lastHalfYear.value) { start = now.subtract(const Duration(days: 180)); - } else if (timeFilter == TimeFilterEnum.lastYear.name) { + } else if (timeFilter == TimeFilterEnum.lastYear.value) { start = now.subtract(const Duration(days: 365)); } else { // 是 2024, 2025 之类的 diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 881b2b2..3311d32 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -96,28 +96,30 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { var readerMode = context.reader.mode; // 横向阅读的时候, 如果纵向滑就触发收藏, 纵向阅读的时候, 如果横向滑动就触发收藏 double imageFavoritesListenDistance = 0; - _gestureDetectorState!.dragListenerForImageFavorites = _DragListener( - onMove: (offset) { - switch (readerMode) { - case ReaderMode.continuousTopToBottom: - case ReaderMode.galleryTopToBottom: - imageFavoritesListenDistance += offset.dx; - break; - case ReaderMode.continuousLeftToRight: - case ReaderMode.galleryLeftToRight: - case ReaderMode.galleryRightToLeft: - case ReaderMode.continuousRightToLeft: - imageFavoritesListenDistance += offset.dy; - break; - } - }, - onEnd: () { - if (imageFavoritesListenDistance.abs() > 150) { - imageFavoritesAction(); - } - imageFavoritesListenDistance = 0; - }, - ); + if (appdata.settings['supportSwipeToFavorite'] == 'yes') { + _gestureDetectorState!.dragListenerForImageFavorites = _DragListener( + onMove: (offset) { + switch (readerMode) { + case ReaderMode.continuousTopToBottom: + case ReaderMode.galleryTopToBottom: + imageFavoritesListenDistance += offset.dx; + break; + case ReaderMode.continuousLeftToRight: + case ReaderMode.galleryLeftToRight: + case ReaderMode.galleryRightToLeft: + case ReaderMode.continuousRightToLeft: + imageFavoritesListenDistance += offset.dy; + break; + } + }, + onEnd: () { + if (imageFavoritesListenDistance.abs() > 150) { + imageFavoritesAction(); + } + imageFavoritesListenDistance = 0; + }, + ); + } } @override @@ -246,7 +248,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { void imageFavoritesAction() { try { if (context.reader.images![0].contains('file:///')) { - showToast(message: "本地收藏暂不支持".tl, context: context); + showToast( + message: "Local comic collection is not supported at present".tl, + context: context); return; } String id = context.reader.cid; @@ -277,13 +281,15 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { if (isLiked()) { if (page == ImageFavoritesEp.firstPage) { - showToast(message: "封面不能在此取消收藏".tl, context: context); + showToast( + message: "The cover cannot be uncollected here".tl, + context: context); return; } ImageFavoriteManager.deleteImageFavoritePro([ ImageFavoritePro(page, imageKey, null, eid, id, ep, sourceKey, epName) ]); - showToast(message: "取消收藏图片".tl, context: context); + showToast(message: "Uncollect the image".tl, context: context); } else { ImageFavoritesComic? imageFavoritesComic = ImageFavoriteManager .imageFavoritesComicList @@ -319,7 +325,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } ImageFavoriteManager.addOrUpdateOrDelete(imageFavoritesComic); - showToast(message: "成功收藏图片".tl, context: context); + showToast(message: "Successfully collected".tl, context: context); } update(); } catch (e, stackTrace) { @@ -389,7 +395,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ), const Spacer(), Tooltip( - message: "收藏图片".tl, + message: "Collect the image".tl, child: IconButton( icon: Icon( isLiked() ? Icons.favorite : Icons.favorite_border), diff --git a/lib/pages/settings/local_favorites.dart b/lib/pages/settings/local_favorites.dart index 04511fa..3541077 100644 --- a/lib/pages/settings/local_favorites.dart +++ b/lib/pages/settings/local_favorites.dart @@ -33,7 +33,9 @@ class _LocalFavoritesSettingsState extends State { SelectSetting( title: "Quick Favorite".tl, settingKey: "quickFavorite", - help: "Long press on the favorite button to quickly add to this folder".tl, + help: + "Long press on the favorite button to quickly add to this folder" + .tl, optionTranslation: { for (var e in LocalFavoritesManager().folderNames) e: e }, @@ -44,7 +46,8 @@ class _LocalFavoritesSettingsState extends State { var controller = showLoadingDialog(context); var count = await LocalFavoritesManager().removeInvalid(); controller.close(); - context.showMessage(message: "Deleted @a favorite items".tlParams({'a': count})); + context.showMessage( + message: "Deleted @a favorite items".tlParams({'a': count})); }, actionTitle: 'Delete'.tl, ).toSliver(), @@ -56,6 +59,17 @@ class _LocalFavoritesSettingsState extends State { "read": "Read".tl, }, ).toSliver(), + SelectSetting( + title: "Support sliding to collect images".tl, + settingKey: "supportSwipeToFavorite", + optionTranslation: { + "yes": "Yes".tl, + "no": "No".tl, + }, + help: + "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" + .tl, + ).toSliver(), ], ); } diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 4a7ee77..3af0a3d 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -138,6 +138,21 @@ Future importPicaData(File file) async { .toList(); folderNames .removeWhere((e) => e == "folder_order" || e == "folder_sync"); + for (var folderSyncValue in db.select("SELECT * FROM folder_sync;")) { + var folderName = folderSyncValue["folder_name"]; + var sourceKey = folderSyncValue["source_key"]; + sourceKey = sourceKey == "htManga" ? "wnacg" : sourceKey; + // 有值就跳过 + if (LocalFavoritesManager().findLinked(folderName).$1 != null) { + continue; + } + try { + LocalFavoritesManager().linkFolderToNetwork(folderName, sourceKey, + jsonDecode(folderSyncValue["sync_data"])["folderId"]); + } catch (e, stack) { + Log.error(e.toString(), stack); + } + } for (var folderName in folderNames) { if (!LocalFavoritesManager().existsFolder(folderName)) { LocalFavoritesManager().createFolder(folderName); @@ -197,9 +212,11 @@ Future importPicaData(File file) async { }), ); } - List imageFavoritesComicList = []; + List imageFavoritesComicList = + ImageFavoriteManager.imageFavoritesComicList; for (var comic in db.select("SELECT * FROM image_favorites;")) { String sourceKey = comic["id"].split("-")[0]; + // 换名字了, 绅士漫画 if (sourceKey == "htManga") { sourceKey = "wnacg"; } @@ -208,12 +225,12 @@ Future importPicaData(File file) async { } String id = comic["id"].split("-")[1]; int page = comic["page"]; - int ep = comic["ep"]; + // 章节和page是从1开始的, pica 可能有从 0 开始的, 得转一下 + int ep = comic["ep"] == 0 ? 1 : comic["ep"]; String title = comic["title"]; - String epName = comic["epName"]; - ImageFavoritesComic? tempComic = - ImageFavoriteManager.findFromComicList( - imageFavoritesComicList, id, sourceKey, ep, page); + String epName = ""; + ImageFavoritesComic? tempComic = imageFavoritesComicList + .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); ImageFavoritePro curImageFavorite = ImageFavoritePro(page, "", null, "", id, ep, sourceKey, epName); if (tempComic == null) { @@ -222,6 +239,7 @@ Future importPicaData(File file) async { tempComic.imageFavoritesEp = [ ImageFavoritesEp("", ep, [curImageFavorite], epName, 1) ]; + imageFavoritesComicList.add(tempComic); } else { ImageFavoritesEp? tempEp = tempComic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == ep); @@ -229,13 +247,20 @@ Future importPicaData(File file) async { tempComic.imageFavoritesEp .add(ImageFavoritesEp("", ep, [curImageFavorite], epName, 1)); } else { - tempEp.imageFavorites.add(curImageFavorite); + // 如果已经有这个page了, 就不添加了 + if (tempEp.imageFavorites + .firstWhereOrNull((e) => e.page == page) == + null) { + tempEp.imageFavorites.add(curImageFavorite); + } } } - ImageFavoriteManager.addOrUpdateOrDelete(tempComic); } - } catch (e) { - Log.error("Import Data", "Failed to import history: $e"); + for (var temp in imageFavoritesComicList) { + ImageFavoriteManager.addOrUpdateOrDelete(temp); + } + } catch (e, stack) { + Log.error("Import Data", "Failed to import history: $e", stack); } finally { db.dispose(); } diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart new file mode 100644 index 0000000..4877733 --- /dev/null +++ b/lib/utils/utils.dart @@ -0,0 +1,22 @@ +import 'dart:async'; + +// 节流 +class Throttler { + Timer? _timer; + final Duration duration; + + Throttler({required this.duration}); + + void run(void Function() callback) { + if (_timer == null || !_timer!.isActive) { + callback(); + _timer = Timer(duration, () { + _timer = null; + }); + } + } + + void dispose() { + _timer?.cancel(); + } +} diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" index 8f798dd..8377575 100644 --- "a/\344\273\273\345\212\241.md" +++ "b/\344\273\273\345\212\241.md" @@ -46,7 +46,7 @@ 2. ep 和 page 都不从0开始 3. eid 存在 otherInfo 里 4. 是否做一个大的改动, 将数据库改成一本漫画的一个章节对应一条记录的形式 - 12. 中英互译 + 12. 中英互译 ok 13. 实际大数据量测试 14. 实机测试 3. 主页的图片收藏显示 ok From aee64a355488f445af756bf72ff49521d2bbe53a Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 5 Jan 2025 11:48:43 +0800 Subject: [PATCH 07/36] =?UTF-8?q?feat:=20=E5=AE=9E=E6=9C=BA=E6=B5=8B?= =?UTF-8?q?=E8=AF=95=E5=92=8C=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 8 +- lib/foundation/image_favorites.dart | 26 +- lib/pages/home_page/image_favorites.dart | 116 +++++---- .../image_favorites_item.dart | 229 ++++++++++-------- .../image_favorites_page.dart | 63 ++++- lib/pages/search_page.dart | 5 +- lib/pages/settings/app.dart | 10 +- lib/utils/data.dart | 12 +- lib/utils/utils.dart | 17 +- 9 files changed, 283 insertions(+), 203 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index be053d5..6e9812f 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -164,7 +164,7 @@ "Date Desc": "日期降序", "Start": "开始", "Export App Data": "导出应用数据", - "Import App Data": "导入应用数据", + "Import App Data (Please restart after success)": "导入应用数据(成功后请手动重启)", "Export": "导出", "Download Threads": "下载线程数", "Update Time": "更新时间", @@ -256,7 +256,7 @@ "Collect the image": "收藏图片", "Support sliding to collect images": "支持滑动收藏图片", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在图片浏览页面, 你可以根据你的阅读模式横滑或者竖滑快速收藏图片", - "Calculate your favorite from @a comics and @b images": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的", + "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的, 括号后是图片数量或图片数比漫画页数", "Author: ": "作者: ", "Tags: ": "标签: ", "Comics(number): ": "漫画(数量): ", @@ -461,7 +461,7 @@ "Date Desc": "日期降序", "Start": "開始", "Export App Data": "匯出應用數據", - "Import App Data": "匯入應用數據", + "Import App Data (Please restart after success)": "匯入應用數據(成功后請手動重啟)", "Export": "匯出", "Download Threads": "下載線程數", "Update Time": "更新時間", @@ -557,7 +557,7 @@ "Collect the image": "收藏圖片", "Support sliding to collect images": "支持滑動收藏圖片", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在圖片瀏覽頁面, 你可以根據你的閱讀模式橫向或者縱向滑動快速收藏圖片", - "Calculate your favorite from @a comics and @b images": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的", + "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的, 括號後是圖片數量或圖片數比漫畫頁數", "Author: ": "作者: ", "Tags: ": "標籤: ", "Comics(number): ": "漫畫(數量): ", diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index c4ec6d9..f27d97f 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -58,6 +58,18 @@ class ImageFavoritePro extends ImageFavorite { ImageFavoritePro.copy(ImageFavoritePro other) : this(other.page, other.imageKey, other.isAutoFavorite, other.eid, other.id, other.ep, other.sourceKey, other.epName); + @override + bool operator ==(Object other) { + return other is ImageFavoritePro && + other.id == id && + other.sourceKey == sourceKey && + other.page == page && + other.eid == eid && + other.ep == ep; + } + + @override + int get hashCode => Object.hash(id, sourceKey, page, eid, ep); } class ImageFavoritesEp { @@ -239,7 +251,7 @@ class ImageFavoritesComic { class ImageFavoriteManager with ChangeNotifier { static Database get _db => HistoryManager()._db; static ImageFavoriteManager? cache; - final Throttler _throttler = Throttler(duration: Duration(seconds: 2)); + final Debouncer _debouncer = Debouncer(); static List imageFavoritesComicList = getAll(null); ImageFavoriteManager.create(); static bool hasInit = false; @@ -247,11 +259,11 @@ class ImageFavoriteManager with ChangeNotifier { return cache == null ? (cache = ImageFavoriteManager.create()) : cache!; } void updateValue() { - // 避免从pica导入的时候, 疯狂触发 - _throttler.run(() { + // 避免从pica导入的时候, 疯狂触发更新 + _debouncer.run(() { imageFavoritesComicList = getAll(null); notifyListeners(); - }); + }, Duration(seconds: 5)); } /// 检查表image_favorites是否存在, 不存在则创建 @@ -285,7 +297,8 @@ class ImageFavoriteManager with ChangeNotifier { List tempImageFavoritesEp = []; for (var e in favorite.imageFavoritesEp) { int index = tempImageFavoritesEp.indexWhere((i) => i.ep == e.ep); - if (index == -1) { + // 再做一层保险, 防止出现ep为0的脏数据 + if (index == -1 && e.ep > 0) { tempImageFavoritesEp.add(e); } } @@ -462,6 +475,9 @@ class ImageFavoriteManager with ChangeNotifier { static List get earliestTimeToNow { var res = _db.select("select MIN(time) from image_favorites;"); + if (res.first.values.first == null) { + return []; + } int earliestYear = DateTime.fromMillisecondsSinceEpoch(res.first.values.first! as int) .year; diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart index cc5bb7f..29ea77c 100644 --- a/lib/pages/home_page/image_favorites.dart +++ b/lib/pages/home_page/image_favorites.dart @@ -2,16 +2,24 @@ part of '../home_page.dart'; class ImageFavoritesFindComic { final String id; - final String title; + final String fullTitle; + final String showTitle; final String sourceKey; - const ImageFavoritesFindComic(this.id, this.title, this.sourceKey); + const ImageFavoritesFindComic( + this.id, this.fullTitle, this.showTitle, this.sourceKey); +} + +class ImageFavoritesTextWithCount { + final String text; + final int count; + const ImageFavoritesTextWithCount(this.text, this.count); } // 算出最喜欢的 class ImageFavoritesCompute { - final List tags; - final List authors; + final List tags; + final List authors; // 喜欢的图片数 final List comicByNum; // 图片数比上总页数 @@ -35,22 +43,29 @@ class ImageFavorites extends StatefulWidget { State createState() => ImageFavoritesState(); } -List exceptTags = ['連載中']; +// 去掉这些没有意义的标签 +List exceptTags = [ + '連載中', + '', + 'translated', + 'chinese', + 'sole male', + 'sole female', + 'original', + 'doujinshi', + 'manga', + 'multi-work series', + 'mosaic censorship', + 'dilf', + 'bbm', + 'uncensored', + 'full censorship' +].map((e) => e.toLowerCase()).toList(); class ImageFavoritesState extends State { ImageFavoritesCompute? imageFavoritesCompute; List allImageFavoritePros = []; static String separator = "*venera*"; - static var enableTranslate = App.locale.languageCode == 'zh'; - static Color getColor(Color baseColor, double depth) { - // 将 RGB 颜色转换为 HSV 颜色 - HSVColor hsvColor = HSVColor.fromColor(baseColor); - // 根据深度调整明度 - HSVColor adjustedColor = hsvColor.withValue((1 - depth) * hsvColor.value); - // 将调整后的 HSV 颜色转换回 RGB 颜色 - Color finalColor = adjustedColor.toColor(); - return finalColor; - } static ImageFavoritesFindComic fromStringToImageFavoritesFindComic( String str, String suffix, List tempComicsList) { @@ -62,7 +77,8 @@ class ImageFavoritesState extends State { }); return ImageFavoritesFindComic( id, - '${comic.title.length > 10 ? comic.title.substring(0, 10) : comic.title}... $suffix', + comic.title, + '${comic.title.length > 36 ? comic.title.substring(0, 36) : comic.title}... $suffix', sourceKey); } @@ -76,19 +92,20 @@ class ImageFavoritesState extends State { Map comicMaxPages = {}; for (ImageFavoritesComic imageFavoritesComic in tempComics) { - // 统计标签 for (var tag in imageFavoritesComic.tags) { - String finalTag = enableTranslate ? tag.translateTagsToCN : tag; + String finalTag = tag; tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1; } - // 统计作者下的图片数 if (imageFavoritesComic.author != "") { - authorCount[imageFavoritesComic.author] = - (authorCount[imageFavoritesComic.author] ?? 0) + - imageFavoritesComic.sortedImageFavoritePros.length; + String finalAuthor = imageFavoritesComic.author; + authorCount[finalAuthor] = (authorCount[finalAuthor] ?? 0) + + imageFavoritesComic.sortedImageFavoritePros.length; + } + // 小于10页的漫画不统计 + if (imageFavoritesComic.maxPageFromEp < 10) { + continue; } - // 统计漫画图片数和总页数 String comicId = '${imageFavoritesComic.sourceKey}$separator${imageFavoritesComic.id}'; @@ -121,24 +138,20 @@ class ImageFavoritesState extends State { return percentageB.compareTo(percentageA); }); - // 只返回前10个结果 return ImageFavoritesCompute( sortedTags - .where((tag) => !exceptTags.contains(tag)) - .take(10) - .map((tag) => '$tag (${tagCount[tag]})') + .where((tag) => !exceptTags.contains(tag.toLowerCase())) + .map((tag) => ImageFavoritesTextWithCount(tag, tagCount[tag]!)) .toList(), sortedAuthors - .take(10) - .map((author) => '$author (${authorCount[author]})') + .map((author) => + ImageFavoritesTextWithCount(author, authorCount[author]!)) .toList(), sortedComicsByNum - .take(10) .map((comic) => fromStringToImageFavoritesFindComic( comic.key, '(${comic.value})', tempComics)) .toList(), sortedComicsByPercentage - .take(10) .map((comic) => fromStringToImageFavoritesFindComic( comic.key, '(${(comicImageCount[comic.key]! / comicMaxPages[comic.key]! * 100).toStringAsFixed(1)}%)', @@ -147,7 +160,7 @@ class ImageFavoritesState extends State { } void refreshImageFavorites() async { - if(mounted){ + if (mounted) { imageFavoritesCompute = null; allImageFavoritePros = []; for (var comic in ImageFavoriteManager.imageFavoritesComicList) { @@ -178,26 +191,23 @@ class ImageFavoritesState extends State { Object text, ImageFavoritesComputeType type, ) { - bool isString = text is String; + bool textWithCount = text is ImageFavoritesTextWithCount; + bool isAuthor = type == ImageFavoritesComputeType.authors; + var enableTranslate = App.locale.languageCode == 'zh'; + String translateText = ''; + if (textWithCount) { + translateText = enableTranslate ? text.text.translateTagsToCN : text.text; + if (isAuthor) { + translateText = TagsTranslation.artistTags[text.text] ?? text.text; + } + } return InkWell( onTap: () { - RegExp regExp = RegExp(r" \(\d+\)"); - if (type == ImageFavoritesComputeType.tags) { - // 跳转到标签搜索页面 - context.to(() => ImageFavoritesPage( - initialKeyword: (text as String).replaceAll(regExp, ''))); - } - if (type == ImageFavoritesComputeType.authors) { - context.to(() => ImageFavoritesPage( - initialKeyword: (text as String).replaceAll(regExp, ''))); - } - if (type == ImageFavoritesComputeType.comicByNum || - type == ImageFavoritesComputeType.comicByPercentage) { - context.to(() => ComicPage( - id: (text as ImageFavoritesFindComic).id, - sourceKey: text.sourceKey, - )); - } + RegExp regExpForTag = RegExp(r" \(\d+\)$"); + context.to(() => ImageFavoritesPage( + initialKeyword: (textWithCount + ? text.text.replaceAll(regExpForTag, '') + : (text as ImageFavoritesFindComic).fullTitle))); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), @@ -206,7 +216,9 @@ class ImageFavoritesState extends State { borderRadius: BorderRadius.circular(8), ), child: Text( - isString ? text : (text as ImageFavoritesFindComic).title, + textWithCount + ? "${enableTranslate ? translateText : text.text} (${text.count})" + : (text as ImageFavoritesFindComic).showTitle, ), ), ); @@ -263,7 +275,7 @@ class ImageFavoritesState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Calculate your favorite from @a comics and @b images" + "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages" .tlParams({ "a": ImageFavoriteManager.length.toString(), "b": allImageFavoritePros.length diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 3591aac..7c7ec71 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -8,14 +8,16 @@ class ImageFavoritesItem extends StatefulWidget { required this.addSelected, required this.multiSelectMode, required this.finalImageFavoritesComicList, - required this.isRefreshEpMap, + required this.isRefreshComicList, + required this.setRefreshComicList, }); final ImageFavoritesComic imageFavoritesComic; final Function(ImageFavoritePro) addSelected; final Map selectedImageFavorites; final List finalImageFavoritesComicList; final bool multiSelectMode; - final Map isRefreshEpMap; + final List isRefreshComicList; + final Function(LoadingImageFavoritesComicRes) setRefreshComicList; @override State createState() => ImageFavoritesItemState(); } @@ -24,15 +26,17 @@ class ImageFavoritesItemState extends State { bool isImageKeyLoading = false; // 刷新 imageKey 失败的场景再刷新一次, 再次失败了就不重试了 bool hasRefreshImageKeyOnErr = false; + late LoadingImageFavoritesComicRes loadingImageFavoritesComicRes; + // 如果 imageKey 失效了, 或者刚从pica导入(没有imageKey) void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { if (isImageKeyLoading || hasRefreshImageKeyOnErr || - (widget.isRefreshEpMap[imageFavoritesEp]?.isLoaded ?? false)) { + loadingImageFavoritesComicRes.isLoaded) { return; } - widget.isRefreshEpMap[imageFavoritesEp] = - LoadingImageFavoritesEpRes(isLoaded: true, isInvalid: false); + loadingImageFavoritesComicRes.isLoaded = true; + widget.setRefreshComicList(loadingImageFavoritesComicRes); isImageKeyLoading = true; ComicSource? comicSource = ComicSource.find(widget.imageFavoritesComic.sourceKey); @@ -48,10 +52,9 @@ class ImageFavoritesItemState extends State { Res> comicPagesRes = resArr[0] as Res>; Res comicInfoRes = resArr[1] as Res; if (comicInfoRes.errorMessage?.contains("404") ?? false) { - widget.isRefreshEpMap[imageFavoritesEp] = - LoadingImageFavoritesEpRes(isLoaded: true, isInvalid: true); - } - if (!comicPagesRes.error && !comicInfoRes.error) { + loadingImageFavoritesComicRes.isInvalid = true; + widget.setRefreshComicList(loadingImageFavoritesComicRes); + } else if (!comicPagesRes.error && !comicInfoRes.error) { List images = comicPagesRes.data; ImageFavoritesSomething something = ImageFavoritesComic.getSomethingFromComicDetails( @@ -104,6 +107,15 @@ class ImageFavoritesItemState extends State { @override void initState() { + loadingImageFavoritesComicRes = widget.isRefreshComicList.firstWhereOrNull( + (e) => + e.id == widget.imageFavoritesComic.id && + e.sourceKey == widget.imageFavoritesComic.sourceKey) ?? + LoadingImageFavoritesComicRes( + isLoaded: false, + isInvalid: false, + id: widget.imageFavoritesComic.id, + sourceKey: widget.imageFavoritesComic.sourceKey); super.initState(); } @@ -187,107 +199,112 @@ class ImageFavoritesItemState extends State { ).paddingHorizontal(16), SizedBox( height: 145, - child: ListView.builder( + child: CustomScrollView( scrollDirection: Axis.horizontal, - itemCount: count, - itemBuilder: (context, index) { - ImageFavoritePro curImageFavorite = - widget.imageFavoritesComic.sortedImageFavoritePros[index]; - ImageFavoritesEp curImageFavoritesEp = widget - .imageFavoritesComic.imageFavoritesEp - .firstWhere((e) { - return e.eid == curImageFavorite.eid; - }); - ImageProvider image = - ImageFavoritesProvider(curImageFavorite); - bool isSelected = - widget.selectedImageFavorites[curImageFavorite] ?? false; - Widget imageWidget = AnimatedImage( - image: image, - width: 96, - height: 128, - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - onError: (Object error, StackTrace? stackTrace) { - if (widget - .isRefreshEpMap[curImageFavoritesEp]?.isLoaded ?? - false) { - return; - } - refreshImageKey(curImageFavoritesEp); - hasRefreshImageKeyOnErr = true; - }, - ); - int curPage = curImageFavorite.page; - String pageText = curPage == ImageFavoritesEp.firstPage - ? '@a Cover'.tlParams({"a": curImageFavorite.epName}) - : curPage.toString(); - return InkWell( - onDoubleTap: () { - // 双击浏览大图 - App.mainNavigatorKey?.currentContext?.to( - () => ImageFavoritesPhotoView( - imageFavoritesComic: widget.imageFavoritesComic, - imageFavoritePro: curImageFavorite, - finalImageFavoritesComicList: - widget.finalImageFavoritesComicList, - goComicInfo: goComicInfo, - goReaderPage: goReaderPage, - ), - ); - }, - onTap: () { - // 单击去阅读页面, 跳转到当前点击的page - if (widget.multiSelectMode) { - widget.addSelected(curImageFavorite); - } else { - goReaderPage(widget.imageFavoritesComic, - curImageFavorite.ep, curPage); - } - }, - onLongPress: () { - widget.addSelected(curImageFavorite); - }, - borderRadius: BorderRadius.circular(8), - child: Container( - width: 98, - height: 128, - decoration: BoxDecoration( + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + ImageFavoritePro curImageFavorite = widget + .imageFavoritesComic.sortedImageFavoritePros[index]; + ImageFavoritesEp curImageFavoritesEp = widget + .imageFavoritesComic.imageFavoritesEp + .firstWhere((e) { + return e.eid == curImageFavorite.eid; + }); + ImageProvider image = + ImageFavoritesProvider(curImageFavorite); + bool isSelected = + widget.selectedImageFavorites[curImageFavorite] ?? + false; + Widget imageWidget = AnimatedImage( + image: image, + width: 96, + height: 128, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + onError: (Object error, StackTrace? stackTrace) { + if (loadingImageFavoritesComicRes.isLoaded) { + return; + } + refreshImageKey(curImageFavoritesEp); + hasRefreshImageKeyOnErr = true; + }, + ); + int curPage = curImageFavorite.page; + String pageText = curPage == ImageFavoritesEp.firstPage + ? '@a Cover' + .tlParams({"a": curImageFavorite.epName}) + : curPage.toString(); + return InkWell( + onDoubleTap: () { + // 双击浏览大图 + App.mainNavigatorKey?.currentContext?.to( + () => ImageFavoritesPhotoView( + imageFavoritesComic: widget.imageFavoritesComic, + imageFavoritePro: curImageFavorite, + finalImageFavoritesComicList: + widget.finalImageFavoritesComicList, + goComicInfo: goComicInfo, + goReaderPage: goReaderPage, + ), + ); + }, + onTap: () { + // 单击去阅读页面, 跳转到当前点击的page + if (widget.multiSelectMode) { + widget.addSelected(curImageFavorite); + } else { + goReaderPage(widget.imageFavoritesComic, + curImageFavorite.ep, curPage); + } + }, + onLongPress: () { + widget.addSelected(curImageFavorite); + }, borderRadius: BorderRadius.circular(8), - color: isSelected - ? Theme.of(context).colorScheme.primaryContainer - : null, - ), - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - children: [ - Container( - height: 128, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context) - .colorScheme - .secondaryContainer, - ), - clipBehavior: Clip.antiAlias, - child: imageWidget), - Text( - pageText, - style: ts.s10, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - ], - )), - ); - }, + child: Container( + width: 98, + height: 128, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isSelected + ? Theme.of(context) + .colorScheme + .primaryContainer + : null, + ), + padding: + const EdgeInsets.symmetric(horizontal: 4), + child: Column( + children: [ + Container( + height: 128, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .secondaryContainer, + ), + clipBehavior: Clip.antiAlias, + child: imageWidget), + Text( + pageText, + style: ts.s10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + ], + )), + ); + }, + childCount: count, + ), + ), + ], ), ).paddingHorizontal(8), - if (widget - .isRefreshEpMap[ - widget.imageFavoritesComic.imageFavoritesEp.first] - ?.isInvalid ?? - false) + if (loadingImageFavoritesComicRes.isInvalid) Row( children: [ Text( diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 876abc6..1ebff21 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:developer'; import 'dart:math'; @@ -22,13 +23,20 @@ import 'package:venera/utils/ext.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; +import 'package:venera/utils/utils.dart'; part "image_favorites_item.dart"; part "image_favorites_photo_view.dart"; -class LoadingImageFavoritesEpRes { +class LoadingImageFavoritesComicRes { bool isLoaded; bool isInvalid; - LoadingImageFavoritesEpRes({required this.isLoaded, required this.isInvalid}); + String id; + String sourceKey; + LoadingImageFavoritesComicRes( + {required this.isLoaded, + required this.isInvalid, + required this.id, + required this.sourceKey}); } class ImageFavoritesPage extends StatefulWidget { @@ -47,7 +55,9 @@ class ImageFavoritesPageState extends State { List imageFavoritesComicList = []; late List curImageFavoritesComicList; late List timeFilter; - Map isRefreshEpMap = {}; + List isRefreshComicList = []; + late TextEditingController _textEditingController; + final Debouncer _debouncer = Debouncer(); String keyword = ""; // 进入关键词搜索模式 @@ -57,6 +67,18 @@ class ImageFavoritesPageState extends State { // 多选的时候选中的图片 Map selectedImageFavorites = {}; late List imageFavoritePros; + // 避免重复请求 + void setRefreshComicList(LoadingImageFavoritesComicRes res) { + LoadingImageFavoritesComicRes? tempRes = + isRefreshComicList.firstWhereOrNull( + (e) => e.id == res.id && e.sourceKey == res.sourceKey); + if (tempRes == null) { + isRefreshComicList.add(res); + } else { + tempRes.isLoaded = res.isLoaded; + tempRes.isInvalid = res.isInvalid; + } + } void update() { setState(() {}); @@ -136,6 +158,7 @@ class ImageFavoritesPageState extends State { keyword = widget.initialKeyword!; searchMode = true; } + _textEditingController = TextEditingController(text: keyword); String initSortType = appdata.implicitData["image_favorites_sort"] ?? ImageFavoriteSortType.title.value; sortType = ImageFavoriteSortType.values @@ -169,7 +192,9 @@ class ImageFavoritesPageState extends State { @override void dispose() { + _textEditingController.dispose(); ImageFavoriteManager().removeListener(refreshImageFavorites); + scrollController.dispose(); super.dispose(); } @@ -258,7 +283,7 @@ class ImageFavoritesPageState extends State { icon: const Icon(Icons.sort), color: timeFilterSelect != TimeFilterEnum.all.value || numFilterSelect != numFilterList[0] - ? Theme.of(context).colorScheme.primary + ? Theme.of(context).colorScheme.inversePrimary : null, onPressed: sort, ), @@ -313,26 +338,29 @@ class ImageFavoritesPageState extends State { ), title: TextField( autofocus: true, + controller: _textEditingController, decoration: InputDecoration( hintText: "Search".tl, border: InputBorder.none, ), - controller: TextEditingController(text: keyword), onChanged: (v) { - keyword = v; - update(); + _debouncer.run(() { + keyword = _textEditingController.text; + update(); + }, Duration(seconds: 1)); }, ), ), SliverList( delegate: SliverChildBuilderDelegate((context, index) { return ImageFavoritesItem( - isRefreshEpMap: isRefreshEpMap, + isRefreshComicList: isRefreshComicList, imageFavoritesComic: curImageFavoritesComicList[index], selectedImageFavorites: selectedImageFavorites, addSelected: addSelected, multiSelectMode: multiSelectMode, finalImageFavoritesComicList: curImageFavoritesComicList, + setRefreshComicList: setRefreshComicList, ); }, childCount: curImageFavoritesComicList.length)), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), @@ -349,7 +377,24 @@ class ImageFavoritesPageState extends State { context.width > changePoint ? widget.paddingHorizontal(8) : widget, ), ); - return body; + return PopScope( + canPop: !multiSelectMode && !searchMode, + onPopInvokedWithResult: (didPop, result) { + if (multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedImageFavorites.clear(); + }); + } else if (searchMode) { + setState(() { + searchMode = false; + keyword = ""; + update(); + }); + } + }, + child: body, + ); } void sort() { diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 3e22088..44bf020 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -17,9 +17,7 @@ import 'package:venera/utils/translations.dart'; import 'comic_page.dart'; class SearchPage extends StatefulWidget { - final List? initialOptions; - - const SearchPage({super.key, this.initialOptions}); + const SearchPage({super.key}); @override State createState() => _SearchPageState(); @@ -140,7 +138,6 @@ class _SearchPageState extends State { @override void initState() { - options = widget.initialOptions ?? []; var defaultSearchTarget = appdata.settings['defaultSearchTarget']; if (defaultSearchTarget != null && ComicSource.find(defaultSearchTarget) != null) { diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index c59e19e..368de45 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -107,15 +107,16 @@ class _AppSettingsState extends State { actionTitle: 'Export'.tl, ).toSliver(), _CallbackSetting( - title: "Import App Data".tl, + title: "Import App Data (Please restart after success)".tl, callback: () async { var controller = showLoadingDialog(context); var file = await selectFile(ext: ['venera', 'picadata']); if (file != null) { - var cacheFile = File(FilePath.join(App.cachePath, "import_data_temp")); + var cacheFile = + File(FilePath.join(App.cachePath, "import_data_temp")); await file.saveTo(cacheFile.path); try { - if(file.name.endsWith('picadata')) { + if (file.name.endsWith('picadata')) { await importPicaData(cacheFile); } else { await importAppData(cacheFile); @@ -123,8 +124,7 @@ class _AppSettingsState extends State { } catch (e, s) { Log.error("Import data", e.toString(), s); context.showMessage(message: "Failed to import data".tl); - } - finally { + } finally { cacheFile.deleteIgnoreError(); } } diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 3af0a3d..ca5687f 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -140,7 +140,7 @@ Future importPicaData(File file) async { .removeWhere((e) => e == "folder_order" || e == "folder_sync"); for (var folderSyncValue in db.select("SELECT * FROM folder_sync;")) { var folderName = folderSyncValue["folder_name"]; - var sourceKey = folderSyncValue["source_key"]; + var sourceKey = folderSyncValue["key"]; sourceKey = sourceKey == "htManga" ? "wnacg" : sourceKey; // 有值就跳过 if (LocalFavoritesManager().findLinked(folderName).$1 != null) { @@ -231,21 +231,21 @@ Future importPicaData(File file) async { String epName = ""; ImageFavoritesComic? tempComic = imageFavoritesComicList .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); - ImageFavoritePro curImageFavorite = - ImageFavoritePro(page, "", null, "", id, ep, sourceKey, epName); + ImageFavoritePro curImageFavorite = ImageFavoritePro( + page, "", null, ep.toString(), id, ep, sourceKey, epName); if (tempComic == null) { tempComic = ImageFavoritesComic(id, [], title, sourceKey, [], [], DateTime.now(), "", {}, "", 1); tempComic.imageFavoritesEp = [ - ImageFavoritesEp("", ep, [curImageFavorite], epName, 1) + ImageFavoritesEp(ep.toString(), ep, [curImageFavorite], epName, 1) ]; imageFavoritesComicList.add(tempComic); } else { ImageFavoritesEp? tempEp = tempComic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == ep); if (tempEp == null) { - tempComic.imageFavoritesEp - .add(ImageFavoritesEp("", ep, [curImageFavorite], epName, 1)); + tempComic.imageFavoritesEp.add(ImageFavoritesEp( + ep.toString(), ep, [curImageFavorite], epName, 1)); } else { // 如果已经有这个page了, 就不添加了 if (tempEp.imageFavorites diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 4877733..90d9bf1 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,19 +1,12 @@ import 'dart:async'; -// 节流 -class Throttler { +class Debouncer { Timer? _timer; - final Duration duration; - Throttler({required this.duration}); - - void run(void Function() callback) { - if (_timer == null || !_timer!.isActive) { - callback(); - _timer = Timer(duration, () { - _timer = null; - }); - } + void run(void Function() action, + [Duration delay = const Duration(milliseconds: 300)]) { + _timer?.cancel(); + _timer = Timer(delay, action); } void dispose() { From 7f5c2d0677c81843fae87c51b063b99702ff9438 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 5 Jan 2025 16:07:38 +0800 Subject: [PATCH 08/36] =?UTF-8?q?feat:=20jm=E5=AF=BC=E5=85=A5,=20pica?= =?UTF-8?q?=E5=8E=86=E5=8F=B2=E8=AE=B0=E5=BD=95nhentai=E6=9C=89=E9=97=AE?= =?UTF-8?q?=E9=A2=98,=20=E4=B8=80=E9=94=AE=E5=8F=8D=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/gradle.properties | 6 +- assets/translation.json | 2 + lib/foundation/comic_source/favorites.dart | 6 +- lib/foundation/comic_source/parser.dart | 4 +- lib/foundation/image_favorites.dart | 2 +- lib/pages/favorites/favorite_actions.dart | 8 +- lib/pages/favorites/local_favorites_page.dart | 58 ++-- .../image_favorites_item.dart | 96 ++++-- .../image_favorites_page.dart | 2 +- lib/utils/data.dart | 5 +- lib/utils/io.dart | 1 - pubspec.lock | 286 +++++++++--------- "\344\273\273\345\212\241.md" | 64 ---- 13 files changed, 261 insertions(+), 279 deletions(-) delete mode 100644 "\344\273\273\345\212\241.md" diff --git a/android/gradle.properties b/android/gradle.properties index f8197f1..85eda9d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -3,8 +3,4 @@ android.useAndroidX=true android.enableJetifier=true android.defaults.buildfeatures.buildconfig=true android.nonTransitiveRClass=false -android.nonFinalResIds=false -systemProp.http.proxyHost=127.0.0.1 -systemProp.http.proxyPort=7890 -systemProp.https.proxyHost=127.0.0.1 -systemProp.https.proxyPort=7890 \ No newline at end of file +android.nonFinalResIds=false \ No newline at end of file diff --git a/assets/translation.json b/assets/translation.json index 6e9812f..a0f1a8f 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -41,6 +41,7 @@ "Select a folder": "选择一个文件夹", "Folder": "文件夹", "Confirm": "确认", + "Reversed successfully": "反转成功", "Remove comic from favorite?": "从收藏中移除漫画?", "Move": "移动", "Move to folder": "移动到文件夹", @@ -460,6 +461,7 @@ "Date": "日期", "Date Desc": "日期降序", "Start": "開始", + "Reversed successfully": "反轉成功", "Export App Data": "匯出應用數據", "Import App Data (Please restart after success)": "匯入應用數據(成功后請手動重啟)", "Export": "匯出", diff --git a/lib/foundation/comic_source/favorites.dart b/lib/foundation/comic_source/favorites.dart index 76b2ec0..a6423f5 100644 --- a/lib/foundation/comic_source/favorites.dart +++ b/lib/foundation/comic_source/favorites.dart @@ -11,8 +11,8 @@ class FavoriteData { final bool multiFolder; // 这个收藏时间新旧顺序, 是为了最小成本同步远端的收藏, 只拉取远程最新收藏的漫画, 就不需要全拉取一遍了 - // 如果为 null, 不做处理, 拉取全部 - final bool? isNewToOldSort; + // 如果为 null, 当做从新到旧 + final bool? isOldToNewSort; final Future>> Function(int page, [String? folder])? loadComic; @@ -48,7 +48,7 @@ class FavoriteData { this.addFolder, this.allFavoritesId, this.addOrDelFavorite, - this.isNewToOldSort, + this.isOldToNewSort, }); } diff --git a/lib/foundation/comic_source/parser.dart b/lib/foundation/comic_source/parser.dart index efef8e5..ea1b3b1 100644 --- a/lib/foundation/comic_source/parser.dart +++ b/lib/foundation/comic_source/parser.dart @@ -618,7 +618,7 @@ class ComicSourceParser { if (!_checkExists("favorites")) return null; final bool multiFolder = _getValue("favorites.multiFolder"); - final bool? isNewToOldSort = _getValue("favorites.isNewToOldSort"); + final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); Future> retryZone(Future> Function() func) async { if (!ComicSource.find(_key!)!.isLogged) { @@ -771,7 +771,7 @@ class ComicSourceParser { addFolder: addFolder, deleteFolder: deleteFolder, addOrDelFavorite: addOrDelFavFunc, - isNewToOldSort: isNewToOldSort, + isOldToNewSort: isOldToNewSort, ); } diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index f27d97f..eb78ea7 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -73,7 +73,7 @@ class ImageFavoritePro extends ImageFavorite { } class ImageFavoritesEp { - final String eid; + String eid; final int ep; int maxPage; String epName; diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 63fdc59..98a858e 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -333,7 +333,7 @@ Future importNetworkFolder( folderID ?? "", ); } - bool isNewToOldSort = comicSource.favoriteData?.isNewToOldSort ?? true; + bool isOldToNewSort = comicSource.favoriteData?.isOldToNewSort ?? false; var current = 0; int receivedComics = 0; int requestCount = 0; @@ -341,7 +341,7 @@ Future importNetworkFolder( int maxPage = 1; String? next; // 如果是从旧到新, 先取一下maxPage - if (!isNewToOldSort) { + if (isOldToNewSort) { var res = await comicSource.favoriteData?.loadComic!(1, folderID); maxPage = res?.subData ?? 1; } @@ -350,7 +350,7 @@ Future importNetworkFolder( while (updatePageNum >= requestCount && !isFinished) { try { if (comicSource.favoriteData?.loadComic != null) { - next ??= isNewToOldSort ? '1' : maxPage.toString(); + next ??= isOldToNewSort ? maxPage.toString() : '1'; var page = int.parse(next!); var res = await comicSource.favoriteData!.loadComic!(page, folderID); var count = 0; @@ -378,7 +378,7 @@ Future importNetworkFolder( next = null; } else { next = - isNewToOldSort ? (page + 1).toString() : (page - 1).toString(); + isOldToNewSort ? (page - 1).toString() : (page + 1).toString(); } } else if (comicSource.favoriteData?.loadNext != null) { var res = await comicSource.favoriteData!.loadNext!(next, folderID); diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index c71aa77..5247d89 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -44,30 +44,29 @@ class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> { Row( children: [Text(text)], ), - if (source?.favoriteData?.isNewToOldSort != null) - Row( - children: [ - Text("Update the page number by the latest collection".tl), - Spacer(), - Select( - current: updatePageNum.toString() == '9999999' - ? allPageText - : updatePageNum.toString(), - values: pageNumList, - minWidth: 64, - onTap: (index) { - setState(() { - updatePageNum = int.parse(pageNumList[index] == allPageText - ? '9999999' - : pageNumList[index]); - appdata.implicitData["local_favorites_update_page_num"] = - updatePageNum; - appdata.writeImplicitData(); - }); - }, - ) - ], - ), + Row( + children: [ + Text("Update the page number by the latest collection".tl), + Spacer(), + Select( + current: updatePageNum.toString() == '9999999' + ? allPageText + : updatePageNum.toString(), + values: pageNumList, + minWidth: 64, + onTap: (index) { + setState(() { + updatePageNum = int.parse(pageNumList[index] == allPageText + ? '9999999' + : pageNumList[index]); + appdata.implicitData["local_favorites_update_page_num"] = + updatePageNum; + appdata.writeImplicitData(); + }); + }, + ) + ], + ), ], ); } @@ -816,6 +815,17 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> { ); }, ), + IconButton( + icon: const Icon(Icons.swap_vert), + onPressed: () { + setState(() { + comics = comics.reversed.toList(); + changed = true; + showToast( + message: "Reversed successfully".tl, context: context); + }); + }, + ), ], ), body: ReorderableBuilder( diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 7c7ec71..ca87269 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -28,7 +28,7 @@ class ImageFavoritesItemState extends State { bool hasRefreshImageKeyOnErr = false; late LoadingImageFavoritesComicRes loadingImageFavoritesComicRes; - // 如果 imageKey 失效了, 或者刚从pica导入(没有imageKey) + // 如果刚从pica导入(没有imageKey) 或者 imageKey 失效了, 刷新一下 void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { if (isImageKeyLoading || hasRefreshImageKeyOnErr || @@ -40,6 +40,7 @@ class ImageFavoritesItemState extends State { isImageKeyLoading = true; ComicSource? comicSource = ComicSource.find(widget.imageFavoritesComic.sourceKey); + // 拿一下漫画信息和对应章节的图片 var resArr = await Future.wait([ comicSource!.loadComicPages!( widget.imageFavoritesComic.id, @@ -54,33 +55,62 @@ class ImageFavoritesItemState extends State { if (comicInfoRes.errorMessage?.contains("404") ?? false) { loadingImageFavoritesComicRes.isInvalid = true; widget.setRefreshComicList(loadingImageFavoritesComicRes); - } else if (!comicPagesRes.error && !comicInfoRes.error) { - List images = comicPagesRes.data; + if (mounted) { + setState(() {}); + } + return; + } + if (!comicInfoRes.error) { ImageFavoritesSomething something = ImageFavoritesComic.getSomethingFromComicDetails( comicInfoRes.data, imageFavoritesEp.ep); // 刷新一下值, 保存最新的 widget.imageFavoritesComic.author = something.author; - widget.imageFavoritesComic.maxPage = images.length; widget.imageFavoritesComic.subTitle = something.subTitle; widget.imageFavoritesComic.tags = something.tags; widget.imageFavoritesComic.translatedTags = something.translatedTags; - imageFavoritesEp.maxPage = images.length; imageFavoritesEp.epName = something.epName; - // 塞一个封面进去 - if (!imageFavoritesEp.isHasFirstPage) { - ImageFavoritePro copy = - ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); - copy.page = ImageFavoritesEp.firstPage; - copy.isAutoFavorite = true; - imageFavoritesEp.imageFavorites.insert(0, copy); + } else { + return; + } + if (comicPagesRes.error) { + // 能加载漫画信息, 说明只是章节对不太上, 刷新一下章节 + var chapters = comicInfoRes.data.chapters; + // 兜底一下, 如果章节对不上return, 说明是调用接口更新过章节的, 比如拷贝, jm, 避免丢失最初正确的eid + // 拷贝, jm可能会更新章节顺序, 以eid为准比较好 + if (imageFavoritesEp.eid != imageFavoritesEp.ep.toString()) { + return; } - // 统一刷一下最新的imageKey - for (var ele in imageFavoritesEp.imageFavorites) { - ele.imageKey = images[ele.page - 1]; + var finalEid = chapters?.keys.elementAt(imageFavoritesEp.ep - 1) ?? '0'; + var resArr = await Future.wait([ + comicSource!.loadComicPages!( + widget.imageFavoritesComic.id, + finalEid, + ) + ]); + comicPagesRes = resArr[0]; + if (comicPagesRes.error) { + return; + } else { + imageFavoritesEp.eid = finalEid; } - ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); } + List images = comicPagesRes.data; + widget.imageFavoritesComic.maxPage = images.length; + imageFavoritesEp.maxPage = images.length; + // 塞一个封面进去 + if (!imageFavoritesEp.isHasFirstPage) { + ImageFavoritePro copy = + ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); + copy.page = ImageFavoritesEp.firstPage; + copy.isAutoFavorite = true; + imageFavoritesEp.imageFavorites.insert(0, copy); + } + // 统一刷一下最新的imageKey + for (var ele in imageFavoritesEp.imageFavorites) { + ele.imageKey = images[ele.page - 1]; + } + ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); if (mounted) { setState(() {}); } @@ -212,25 +242,9 @@ class ImageFavoritesItemState extends State { .firstWhere((e) { return e.eid == curImageFavorite.eid; }); - ImageProvider image = - ImageFavoritesProvider(curImageFavorite); bool isSelected = widget.selectedImageFavorites[curImageFavorite] ?? false; - Widget imageWidget = AnimatedImage( - image: image, - width: 96, - height: 128, - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - onError: (Object error, StackTrace? stackTrace) { - if (loadingImageFavoritesComicRes.isLoaded) { - return; - } - refreshImageKey(curImageFavoritesEp); - hasRefreshImageKeyOnErr = true; - }, - ); int curPage = curImageFavorite.page; String pageText = curPage == ImageFavoritesEp.firstPage ? '@a Cover' @@ -287,7 +301,23 @@ class ImageFavoritesItemState extends State { .secondaryContainer, ), clipBehavior: Clip.antiAlias, - child: imageWidget), + child: AnimatedImage( + image: ImageFavoritesProvider( + curImageFavorite), + width: 96, + height: 128, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + onError: (Object error, + StackTrace? stackTrace) { + if (loadingImageFavoritesComicRes + .isLoaded) { + return; + } + refreshImageKey(curImageFavoritesEp); + hasRefreshImageKeyOnErr = true; + }, + )), Text( pageText, style: ts.s10, diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 1ebff21..a8916ef 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -347,7 +347,7 @@ class ImageFavoritesPageState extends State { _debouncer.run(() { keyword = _textEditingController.text; update(); - }, Duration(seconds: 1)); + }, Duration(milliseconds: 500)); }, ), ), diff --git a/lib/utils/data.dart b/lib/utils/data.dart index ca5687f..13c0d1e 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -198,17 +198,18 @@ Future importPicaData(File file) async { 2 => 'jm'.hashCode, 3 => 'hitomi'.hashCode, 4 => 'wnacg'.hashCode, - 6 => 'nhentai'.hashCode, + 5 => 'nhentai'.hashCode, _ => comic['type'] }, "id": comic['target'], - "maxPage": comic["max_page"], + "max_page": comic["max_page"], "ep": comic["ep"], "page": comic["page"], "time": comic["time"], "title": comic["title"], "subtitle": comic["subtitle"], "cover": comic["cover"], + "readEpisode": [comic["ep"]], }), ); } diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 79020d4..dc6df55 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -376,7 +376,6 @@ class _IOOverrides extends IOOverrides { return super.createFile(path); } } - } T overrideIO(T Function() f) { diff --git a/pubspec.lock b/pubspec.lock index 0190a06..b7fde93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -6,7 +6,7 @@ packages: description: name: app_links sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.3" app_links_linux: @@ -14,7 +14,7 @@ packages: description: name: app_links_linux sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.3" app_links_platform_interface: @@ -22,7 +22,7 @@ packages: description: name: app_links_platform_interface sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" app_links_web: @@ -30,7 +30,7 @@ packages: description: name: app_links_web sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.4" archive: @@ -38,7 +38,7 @@ packages: description: name: archive sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.6.1" args: @@ -46,7 +46,7 @@ packages: description: name: args sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.6.0" async: @@ -54,7 +54,7 @@ packages: description: name: async sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.11.0" barcode: @@ -62,7 +62,7 @@ packages: description: name: barcode sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.8" battery_plus: @@ -70,7 +70,7 @@ packages: description: name: battery_plus sha256: a0409fe7d21905987eb1348ad57c634f913166f14f0c8936b73d3f5940fac551 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.2.1" battery_plus_platform_interface: @@ -78,7 +78,7 @@ packages: description: name: battery_plus_platform_interface sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.1" bidi: @@ -86,7 +86,7 @@ packages: description: name: bidi sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.12" boolean_selector: @@ -94,7 +94,7 @@ packages: description: name: boolean_selector sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.1" build_cli_annotations: @@ -102,7 +102,7 @@ packages: description: name: build_cli_annotations sha256: b59d2769769efd6c9ff6d4c4cede0be115a566afc591705c2040b707534b1172 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" characters: @@ -110,7 +110,7 @@ packages: description: name: characters sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" clock: @@ -118,7 +118,7 @@ packages: description: name: clock sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" collection: @@ -126,7 +126,7 @@ packages: description: name: collection sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.19.0" convert: @@ -134,7 +134,7 @@ packages: description: name: convert sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.2" cross_file: @@ -142,7 +142,7 @@ packages: description: name: cross_file sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.3.4+2" crypto: @@ -150,7 +150,7 @@ packages: description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.6" csslib: @@ -158,7 +158,7 @@ packages: description: name: csslib sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.2" dbus: @@ -166,7 +166,7 @@ packages: description: name: dbus sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.10" desktop_webview_window: @@ -183,7 +183,7 @@ packages: description: name: dio sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.7.0" dio_web_adapter: @@ -191,7 +191,7 @@ packages: description: name: dio_web_adapter sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" dynamic_color: @@ -199,7 +199,7 @@ packages: description: name: dynamic_color sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.7.0" fake_async: @@ -207,7 +207,7 @@ packages: description: name: fake_async sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.1" ffi: @@ -215,7 +215,7 @@ packages: description: name: ffi sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.3" file: @@ -223,7 +223,7 @@ packages: description: name: file sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.1" file_selector: @@ -231,7 +231,7 @@ packages: description: name: file_selector sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.3" file_selector_android: @@ -239,7 +239,7 @@ packages: description: name: file_selector_android sha256: "98ac58e878b05ea2fdb204e7f4fc4978d90406c9881874f901428e01d3b18fbc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.1+12" file_selector_ios: @@ -247,7 +247,7 @@ packages: description: name: file_selector_ios sha256: "94b98ad950b8d40d96fee8fa88640c2e4bd8afcdd4817993bd04e20310f45420" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.3+1" file_selector_linux: @@ -255,7 +255,7 @@ packages: description: name: file_selector_linux sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.3+2" file_selector_macos: @@ -263,7 +263,7 @@ packages: description: name: file_selector_macos sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.4+2" file_selector_platform_interface: @@ -271,7 +271,7 @@ packages: description: name: file_selector_platform_interface sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.6.2" file_selector_web: @@ -279,7 +279,7 @@ packages: description: name: file_selector_web sha256: c4c0ea4224d97a60a7067eca0c8fd419e708ff830e0c83b11a48faf566cec3e7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.4+2" file_selector_windows: @@ -287,7 +287,7 @@ packages: description: name: file_selector_windows sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.3+3" fixnum: @@ -295,7 +295,7 @@ packages: description: name: fixnum sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.1" flutter: @@ -308,7 +308,7 @@ packages: description: name: flutter_file_dialog sha256: "9344b8f07be6a1b6f9854b723fb0cf84a8094ba94761af1d213589d3cb087488" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" flutter_inappwebview: @@ -316,7 +316,7 @@ packages: description: name: flutter_inappwebview sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.1.5" flutter_inappwebview_android: @@ -324,7 +324,7 @@ packages: description: name: flutter_inappwebview_android sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.3" flutter_inappwebview_internal_annotations: @@ -332,7 +332,7 @@ packages: description: name: flutter_inappwebview_internal_annotations sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.0" flutter_inappwebview_ios: @@ -340,7 +340,7 @@ packages: description: name: flutter_inappwebview_ios sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" flutter_inappwebview_macos: @@ -348,7 +348,7 @@ packages: description: name: flutter_inappwebview_macos sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" flutter_inappwebview_platform_interface: @@ -356,7 +356,7 @@ packages: description: name: flutter_inappwebview_platform_interface sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0+1" flutter_inappwebview_web: @@ -364,7 +364,7 @@ packages: description: name: flutter_inappwebview_web sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.2" flutter_inappwebview_windows: @@ -372,7 +372,7 @@ packages: description: name: flutter_inappwebview_windows sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.6.0" flutter_lints: @@ -380,7 +380,7 @@ packages: description: name: flutter_lints sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.0" flutter_localizations: @@ -393,17 +393,17 @@ packages: description: name: flutter_memory_info sha256: "1f112f1d7503aa1681fc8e923f6cd0e847bb2fbeec3753ed021cf1e5f7e9cd74" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" - url: "https://pub.flutter-io.cn" + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" source: hosted - version: "2.0.24" + version: "2.0.23" flutter_qjs: dependency: "direct main" description: @@ -418,7 +418,7 @@ packages: description: name: flutter_reorderable_grid_view sha256: "732bcb1b29d5130c11a70e6acec512941fafe241f0e80bffd93ca6e415819915" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.4.0" flutter_rust_bridge: @@ -426,7 +426,7 @@ packages: description: name: flutter_rust_bridge sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.7.0" flutter_saf: @@ -448,7 +448,7 @@ packages: description: name: flutter_to_arch sha256: b68b2757a89a517ae2141cbc672acdd1f69721dd686cacad03876b6f436ff040 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" flutter_to_debian: @@ -456,7 +456,7 @@ packages: description: name: flutter_to_debian sha256: d23534407334b331ce20fbaa8395b9ecc255d0c047136b8998715f36933ee696 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.2" flutter_web_plugins: @@ -469,7 +469,7 @@ packages: description: name: freezed_annotation sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.4" gtk: @@ -477,7 +477,7 @@ packages: description: name: gtk sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.0" html: @@ -485,7 +485,7 @@ packages: description: name: html sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.15.5" http: @@ -493,7 +493,7 @@ packages: description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.2" http_parser: @@ -501,7 +501,7 @@ packages: description: name: http_parser sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.1.1" http_profile: @@ -509,7 +509,7 @@ packages: description: name: http_profile sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.1.0" image: @@ -517,7 +517,7 @@ packages: description: name: image sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.3.0" intl: @@ -525,7 +525,7 @@ packages: description: name: intl sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.19.0" io: @@ -533,7 +533,7 @@ packages: description: name: io sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.5" js: @@ -541,7 +541,7 @@ packages: description: name: js sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.1" json_annotation: @@ -549,7 +549,7 @@ packages: description: name: json_annotation sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.9.0" leak_tracker: @@ -557,7 +557,7 @@ packages: description: name: leak_tracker sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "10.0.7" leak_tracker_flutter_testing: @@ -565,7 +565,7 @@ packages: description: name: leak_tracker_flutter_testing sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.8" leak_tracker_testing: @@ -573,23 +573,23 @@ packages: description: name: leak_tracker_testing sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 - url: "https://pub.flutter-io.cn" + sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.1.0" local_auth: dependency: "direct main" description: name: local_auth sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" local_auth_android: @@ -597,23 +597,23 @@ packages: description: name: local_auth_android sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.46" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "5c5127061107278ab4cafa1ac51b3b6760282bf1a2abf011270908a429d1634b" - url: "https://pub.flutter-io.cn" + sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" local_auth_platform_interface: dependency: transitive description: name: local_auth_platform_interface sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.10" local_auth_windows: @@ -621,7 +621,7 @@ packages: description: name: local_auth_windows sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.11" lodepng_flutter: @@ -638,7 +638,7 @@ packages: description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.12.16+1" material_color_utilities: @@ -646,7 +646,7 @@ packages: description: name: material_color_utilities sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.11.1" meta: @@ -654,7 +654,7 @@ packages: description: name: meta sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.15.0" mime: @@ -662,7 +662,7 @@ packages: description: name: mime sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.0.0" mime_type: @@ -670,7 +670,7 @@ packages: description: name: mime_type sha256: d652b613e84dac1af28030a9fba82c0999be05b98163f9e18a0849c6e63838bb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.0.1" path: @@ -678,7 +678,7 @@ packages: description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.9.0" path_parsing: @@ -686,7 +686,7 @@ packages: description: name: path_parsing sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" path_provider: @@ -694,7 +694,7 @@ packages: description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.5" path_provider_android: @@ -702,7 +702,7 @@ packages: description: name: path_provider_android sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.15" path_provider_foundation: @@ -710,7 +710,7 @@ packages: description: name: path_provider_foundation sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.4.1" path_provider_linux: @@ -718,7 +718,7 @@ packages: description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.2.1" path_provider_platform_interface: @@ -726,7 +726,7 @@ packages: description: name: path_provider_platform_interface sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" path_provider_windows: @@ -734,7 +734,7 @@ packages: description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.0" pdf: @@ -742,7 +742,7 @@ packages: description: name: pdf sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.11.1" petitparser: @@ -750,7 +750,7 @@ packages: description: name: petitparser sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.0.2" photo_view: @@ -767,7 +767,7 @@ packages: description: name: platform sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.6" plugin_platform_interface: @@ -775,7 +775,7 @@ packages: description: name: plugin_platform_interface sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.8" pointycastle: @@ -783,7 +783,7 @@ packages: description: name: pointycastle sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.9.1" qr: @@ -791,7 +791,7 @@ packages: description: name: qr sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.2" rhttp: @@ -799,7 +799,7 @@ packages: description: name: rhttp sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.9.8" screen_retriever: @@ -807,7 +807,7 @@ packages: description: name: screen_retriever sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" screen_retriever_linux: @@ -815,7 +815,7 @@ packages: description: name: screen_retriever_linux sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" screen_retriever_macos: @@ -823,7 +823,7 @@ packages: description: name: screen_retriever_macos sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" screen_retriever_platform_interface: @@ -831,7 +831,7 @@ packages: description: name: screen_retriever_platform_interface sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" screen_retriever_windows: @@ -839,7 +839,7 @@ packages: description: name: screen_retriever_windows sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.0" scrollable_positioned_list: @@ -856,7 +856,7 @@ packages: description: name: share_plus sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "10.1.3" share_plus_platform_interface: @@ -864,7 +864,7 @@ packages: description: name: share_plus_platform_interface sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.0.2" shimmer: @@ -872,7 +872,7 @@ packages: description: name: shimmer sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.0.0" sky_engine: @@ -885,7 +885,7 @@ packages: description: name: sliver_tools sha256: eae28220badfb9d0559207badcbbc9ad5331aac829a88cb0964d330d2a4636a6 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.2.12" source_span: @@ -893,7 +893,7 @@ packages: description: name: source_span sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.10.0" sprintf: @@ -901,7 +901,7 @@ packages: description: name: sprintf sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "7.0.0" sqlite3: @@ -909,7 +909,7 @@ packages: description: name: sqlite3 sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.5.0" sqlite3_flutter_libs: @@ -917,7 +917,7 @@ packages: description: name: sqlite3_flutter_libs sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.5.28" stack_trace: @@ -925,7 +925,7 @@ packages: description: name: stack_trace sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.12.0" stream_channel: @@ -933,7 +933,7 @@ packages: description: name: stream_channel sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.2" string_scanner: @@ -941,15 +941,23 @@ packages: description: name: string_scanner sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.3.0" + syntax_highlight: + dependency: "direct main" + description: + name: syntax_highlight + sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997 + url: "https://pub.dev" + source: hosted + version: "0.4.0" term_glyph: dependency: transitive description: name: term_glyph sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.2.1" test_api: @@ -957,7 +965,7 @@ packages: description: name: test_api sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.3" typed_data: @@ -965,7 +973,7 @@ packages: description: name: typed_data sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.4.0" upower: @@ -973,7 +981,7 @@ packages: description: name: upower sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.7.0" url_launcher: @@ -981,7 +989,7 @@ packages: description: name: url_launcher sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.1" url_launcher_android: @@ -989,7 +997,7 @@ packages: description: name: url_launcher_android sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.14" url_launcher_ios: @@ -997,7 +1005,7 @@ packages: description: name: url_launcher_ios sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.3.2" url_launcher_linux: @@ -1005,7 +1013,7 @@ packages: description: name: url_launcher_linux sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.1" url_launcher_macos: @@ -1013,7 +1021,7 @@ packages: description: name: url_launcher_macos sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.2.2" url_launcher_platform_interface: @@ -1021,7 +1029,7 @@ packages: description: name: url_launcher_platform_interface sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.2" url_launcher_web: @@ -1029,7 +1037,7 @@ packages: description: name: url_launcher_web sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.3.3" url_launcher_windows: @@ -1037,7 +1045,7 @@ packages: description: name: url_launcher_windows sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "3.1.3" uuid: @@ -1045,7 +1053,7 @@ packages: description: name: uuid sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "4.5.1" vector_math: @@ -1053,7 +1061,7 @@ packages: description: name: vector_math sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "2.1.4" vm_service: @@ -1061,7 +1069,7 @@ packages: description: name: vm_service sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "14.3.0" web: @@ -1069,7 +1077,7 @@ packages: description: name: web sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" webdav_client: @@ -1086,7 +1094,7 @@ packages: description: name: win32 sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "5.9.0" window_manager: @@ -1094,7 +1102,7 @@ packages: description: name: window_manager sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.4.3" xdg_directories: @@ -1102,7 +1110,7 @@ packages: description: name: xdg_directories sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "1.1.0" xml: @@ -1110,25 +1118,25 @@ packages: description: name: xml sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "6.5.0" yaml: dependency: "direct main" description: name: yaml - sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce - url: "https://pub.flutter-io.cn" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.2" zip_flutter: dependency: "direct main" description: name: zip_flutter sha256: be21152c35fcb6d0ef4ce89fc3aed681f7adc0db5490ca3eb5893f23fd20e646 - url: "https://pub.flutter-io.cn" + url: "https://pub.dev" source: hosted version: "0.0.6" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.1" + flutter: ">=3.27.1" \ No newline at end of file diff --git "a/\344\273\273\345\212\241.md" "b/\344\273\273\345\212\241.md" deleted file mode 100644 index 8377575..0000000 --- "a/\344\273\273\345\212\241.md" +++ /dev/null @@ -1,64 +0,0 @@ -1. 建好 image_favorite 表 ok - 1. 导入图片收藏 ok -2. 做好页面 - 1. 阅读时, 下方的收藏 - 1. 收藏时, 收藏封面 ok - 2. 上划收藏, 左滑收藏 ok - 2. 单独的图片收藏页 - 1. 时间筛选, 按照近七天, 一个月, 三个月, 半年, 2024年, 2023年 ok - 2. 单本收藏数最多排序, 单本收藏数比上总页数最多排序, 并且将小于10页的权重降低 ok - 3. 图片收藏group - 1. 看下History 怎么缓存图片的 ok - 1. 他用的封面图, 我这边应该是更加类似预览才行, 得看看comic_page ok - 2. 好像也不行, 还是直接显示原图吧 - 1. 用存的url可以拉取图片,然后缓存在 cacheManager - 2. 有没有办法根据存的漫画源和ep还有page, 拉取图片呢 - 3. 获取images, 用 type.comicSource!.loadComicPages, 用 loadImage 获取图片数据 - 1. comicSource = ComicSource.find(widget.sourceKey); - 2. 显示图片, 支持图片的缓存, 支持用本地图片 - 1. 方案, 检查一下, group中是否所有的图片都有 imageKey ok - 2. 如果有, 就用imageKey 在 imageFavoritesProvider 中调用 ok - 3. 如果检测到本地有这图片, 就用本地的展示, 待学习 - 4. 如果imageKey获取图片失败, 或者存在没有imageKey的图片收藏, 就拿一下loadComicPages, 用最新的来查, 如果还是失败, 就放弃, 如果成功, 就将最新的imageKey更新进去 - 3. list 渐进式加载, silver 本身就支持 ok - 4. 显示封面, 显示页面page ok - 5. 响应筛选和排序 ok - 4. 支持选中删除 ok - 5. 双击大图浏览 ok - 1. 显示标题 ok - 3. 支持取消收藏, 退出大图浏览时更新数据库 ok - 4. 点击进行阅读, 点击进入详情 ok - 5. 点击进入上一本, 下一本 ok - 6. 显示 tags no - 6. 打通本地收藏的图片收藏 after - 7. 首页显示最喜欢作者, 最多收藏tag, 最喜欢的本子(按照收藏数以及按照收藏比) ok - 8. 筛选 - 1. tag 搜索 ok - 2. 收藏数量范围 ok - 9. 支持导出 - 1. 导出成文本, 包含标题, 链接, 总页数, 收藏数, 收藏占比, 阅读时间, tag 数等 no - 2. 生成总结页面, 包含最喜欢作者, 本子, 共收藏本子数, 共收藏图片数, 最喜欢tag, 最喜欢本子, 按总页数, 收藏占比 no - 10. bug解决 - 1. 进入的时候都是一张图, 都是cover - 2. 并行请求太多 timeout 风险较高 - 11. 最后完整看一下数据结构, imagePath 是否需要存在, 以及ep是string还是int ok - 1. imagePath 不需要 - 2. ep 和 page 都不从0开始 - 3. eid 存在 otherInfo 里 - 4. 是否做一个大的改动, 将数据库改成一本漫画的一个章节对应一条记录的形式 - 12. 中英互译 ok - 13. 实际大数据量测试 - 14. 实机测试 - 3. 主页的图片收藏显示 ok -3. 打通联动 -4. 自测 -5. 有些旧有的功能没有补齐 - 1. 点击收藏直接进行阅读, 让作者做 - 1. 好像没有跳转到页面的能力 - 2. 没有显示全部的页数 - 3. 没有滚动条 - 4. sync 网络收藏到本地的时候没有拉取页数的能力, 我来优化一下 - 1. sync 的时候也卡卡的, 一直拉不下来 - 2. lib\pages\favorites\favorite_actions.dart 应该手动拉一下最大的页数, 然后从拉取倒数几页来判断重复比较好 - 1. getMaxPageNum getPageSize - 2. 修改设置, 支持全部, 然后加上之前的一些设置, 并在同步的那个气泡提示大概一页有多少 pageNum, 本次会拉取多少页 \ No newline at end of file From 4b0fd1d00268484ef35e81a8e039aca5bdb8ace5 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 5 Jan 2025 17:22:12 +0800 Subject: [PATCH 09/36] =?UTF-8?q?fix:=20=E5=A4=A7=E5=B0=8F=E5=86=99?= =?UTF-8?q?=E4=B8=8D=E4=B8=80=E8=87=B4,=20=E4=B8=80=E4=B8=AAhtManga,=20?= =?UTF-8?q?=E4=B8=80=E4=B8=AAhtmanga?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/utils/data.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 387dc7c..c2636f0 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -133,8 +133,8 @@ Future importPicaData(File file) async { .removeWhere((e) => e == "folder_order" || e == "folder_sync"); for (var folderSyncValue in db.select("SELECT * FROM folder_sync;")) { var folderName = folderSyncValue["folder_name"]; - var sourceKey = folderSyncValue["key"]; - sourceKey = sourceKey == "htManga" ? "wnacg" : sourceKey; + String sourceKey = folderSyncValue["key"]; + sourceKey = sourceKey.toLowerCase() == "htmanga" ? "wnacg" : sourceKey; // 有值就跳过 if (LocalFavoritesManager().findLinked(folderName).$1 != null) { continue; @@ -211,7 +211,7 @@ Future importPicaData(File file) async { for (var comic in db.select("SELECT * FROM image_favorites;")) { String sourceKey = comic["id"].split("-")[0]; // 换名字了, 绅士漫画 - if (sourceKey == "htManga") { + if (sourceKey.toLowerCase() == "htmanga") { sourceKey = "wnacg"; } if (ComicSource.find(sourceKey) == null) { From bfa261b393cbe31ffee9151ab52bf705679c3645 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 5 Jan 2025 21:41:38 +0800 Subject: [PATCH 10/36] =?UTF-8?q?feat:=20=E6=8B=89=E5=8F=96=E6=94=B6?= =?UTF-8?q?=E8=97=8F=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + lib/pages/favorites/favorite_actions.dart | 6 +++++- lib/pages/favorites/local_favorites_page.dart | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index d0754d1..4b70806 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ migrate_working_dir/ *.ipr *.iws .idea/ +.vscode/ # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 98a858e..82e155b 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -347,7 +347,7 @@ Future importNetworkFolder( } Future fetchNext() async { var retry = 3; - while (updatePageNum >= requestCount && !isFinished) { + while (updatePageNum > requestCount && !isFinished) { try { if (comicSource.favoriteData?.loadComic != null) { next ??= isOldToNewSort ? maxPage.toString() : '1'; @@ -379,6 +379,10 @@ Future importNetworkFolder( } else { next = isOldToNewSort ? (page - 1).toString() : (page + 1).toString(); + // 兼容收藏顺序按时间从旧到新的漫画拉取 + if (next == '0') { + isFinished = true; + } } } else if (comicSource.favoriteData?.loadNext != null) { var res = await comicSource.favoriteData!.loadNext!(next, folderID); diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index 5247d89..bac1ebf 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -53,7 +53,7 @@ class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> { ? allPageText : updatePageNum.toString(), values: pageNumList, - minWidth: 64, + minWidth: 48, onTap: (index) { setState(() { updatePageNum = int.parse(pageNumList[index] == allPageText From acc0147670ec6a81fe13f5d558a00f45d25fd6d7 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Tue, 7 Jan 2025 22:27:22 +0800 Subject: [PATCH 11/36] =?UTF-8?q?feat:=20=E6=94=B9=E6=88=90=E4=BB=A5ep?= =?UTF-8?q?=E4=B8=BA=E5=87=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/components/comic.dart | 32 ++++++++++++------- lib/foundation/image_favorites.dart | 24 +++++++------- .../image_favorites_item.dart | 8 ++--- .../image_favorites_photo_view.dart | 2 +- lib/pages/reader/scaffold.dart | 22 ++++++++++--- lib/pages/settings/local_favorites.dart | 11 ------- lib/pages/settings/reader.dart | 11 +++++++ lib/utils/data.dart | 13 ++++---- 8 files changed, 74 insertions(+), 49 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 344cab7..e8cc388 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -727,9 +727,16 @@ class _SliverGridComicsState extends State { comics.add(comic); } } + HistoryManager().addListener(update); super.initState(); } + @override + void dispose() { + HistoryManager().removeListener(update); + super.dispose(); + } + void update() { setState(() { comics.clear(); @@ -807,7 +814,10 @@ class _SliverGridComics extends StatelessWidget { duration: const Duration(milliseconds: 150), decoration: BoxDecoration( color: isSelected - ? Theme.of(context).colorScheme.secondaryContainer.toOpacity(0.72) + ? Theme.of(context) + .colorScheme + .secondaryContainer + .toOpacity(0.72) : null, borderRadius: BorderRadius.circular(12), ), @@ -901,13 +911,13 @@ class ComicListState extends State { late bool enablePageStorage = widget.enablePageStorage; Map get state => { - 'maxPage': _maxPage, - 'data': _data, - 'page': _page, - 'error': _error, - 'loading': _loading, - 'nextUrl': _nextUrl, - }; + 'maxPage': _maxPage, + 'data': _data, + 'page': _page, + 'error': _error, + 'loading': _loading, + 'nextUrl': _nextUrl, + }; void restoreState(Map? state) { if (state == null || !enablePageStorage) { @@ -924,7 +934,7 @@ class ComicListState extends State { } void storeState() { - if(enablePageStorage) { + if (enablePageStorage) { PageStorage.of(context).writeState(context, state); } } @@ -1094,11 +1104,11 @@ class ComicListState extends State { while (_data[page] == null) { await _fetchNext(); } - if(mounted) { + if (mounted) { setState(() {}); } } catch (e) { - if(mounted) { + if (mounted) { setState(() { _error = e.toString(); }); diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index eb78ea7..878f0c6 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -73,6 +73,7 @@ class ImageFavoritePro extends ImageFavorite { } class ImageFavoritesEp { + // 小心拷贝等多章节的可能更新章节顺序 String eid; final int ep; int maxPage; @@ -294,9 +295,12 @@ class ImageFavoriteManager with ChangeNotifier { where id == ? and source_key == ?; """, [favorite.id, favorite.sourceKey]); } else { + // 去重章节 List tempImageFavoritesEp = []; for (var e in favorite.imageFavoritesEp) { - int index = tempImageFavoritesEp.indexWhere((i) => i.ep == e.ep); + int index = tempImageFavoritesEp.indexWhere((i) { + return i.ep == e.ep; + }); // 再做一层保险, 防止出现ep为0的脏数据 if (index == -1 && e.ep > 0) { tempImageFavoritesEp.add(e); @@ -351,18 +355,16 @@ class ImageFavoriteManager with ChangeNotifier { List tempList, String id, String sourceKey, - dynamic eid, - int page) { + String eid, + int page, + int ep) { ImageFavoritesComic? temp = tempList .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); if (temp == null) { return null; } else { ImageFavoritesEp? tempEp = temp.imageFavoritesEp.firstWhereOrNull((e) { - if (eid is int) { - return e.ep == eid; - } - return e.eid == eid; + return e.ep == ep; }); if (tempEp == null) { return null; @@ -377,9 +379,9 @@ class ImageFavoriteManager with ChangeNotifier { } } - static bool isHas(String id, String sourceKey, String eid, int page) { + static bool isHas(String id, String sourceKey, String eid, int page, int ep) { return findFromComicList( - imageFavoritesComicList, id, sourceKey, eid, page) != + imageFavoritesComicList, id, sourceKey, eid, page, ep) != null; } @@ -444,7 +446,7 @@ class ImageFavoriteManager with ChangeNotifier { static void deleteImageFavoritePro( List imageFavoriteProList) { for (var e in imageFavoritesComicList) { - // 找到需要删除的具体图片 + // 找到同一个漫画中的需要删除的具体图片 List filterImageFavoritesPro = imageFavoriteProList.where((i) { return i.id == e.id && i.sourceKey == e.sourceKey; @@ -455,7 +457,7 @@ class ImageFavoriteManager with ChangeNotifier { i.imageFavorites = i.imageFavorites.where((j) { ImageFavoritePro? temp = filterImageFavoritesPro.firstWhereOrNull((k) { - return k.eid == j.eid && k.page == j.page; + return k.page == j.page && k.ep == j.ep; }); // 如果没有匹配到, 说明不是这个章节和page, 就留着 return temp == null; diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index ecef0c2..bfb2209 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -76,9 +76,9 @@ class ImageFavoritesItemState extends State { if (comicPagesRes.error) { // 能加载漫画信息, 说明只是章节对不太上, 刷新一下章节 var chapters = comicInfoRes.data.chapters; - // 兜底一下, 如果章节对不上return, 说明是调用接口更新过章节的, 比如拷贝, jm, 避免丢失最初正确的eid - // 拷贝, jm可能会更新章节顺序, 以eid为准比较好 - if (imageFavoritesEp.eid != imageFavoritesEp.ep.toString()) { + // 兜底一下, 如果不是从pica导入的空字符串, 说明是调用接口更新过章节的, 比如jm, 避免丢失最初正确的eid + // 拷贝等多章节可能会更新章节顺序, 后续如果碰到这种情况多的话, 就在右上角出一个功能批量刷新一下 + if (imageFavoritesEp.eid != "") { return; } var finalEid = chapters?.keys.elementAt(imageFavoritesEp.ep - 1) ?? '0'; @@ -240,7 +240,7 @@ class ImageFavoritesItemState extends State { ImageFavoritesEp curImageFavoritesEp = widget .imageFavoritesComic.imageFavoritesEp .firstWhere((e) { - return e.eid == curImageFavorite.eid; + return e.ep == curImageFavorite.ep; }); bool isSelected = widget.selectedImageFavorites[curImageFavorite] ?? diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index 2ddc8ef..42d61de 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -29,7 +29,7 @@ class ImageFavoritesPhotoViewState extends State { curIndex = widget.imageFavoritesComic.sortedImageFavoritePros.indexWhere((ele) { return ele.page == widget.imageFavoritePro.page && - ele.eid == widget.imageFavoritePro.eid; + ele.ep == widget.imageFavoritePro.ep; }); controller = PageController(initialPage: curIndex); curImageFavoritesComicIndex = diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 3311d32..2b5b8c7 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -240,9 +240,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { bool isLiked() { String cid = context.reader.cid; String eid = context.reader.eid; + int ep = context.reader.chapter; int page = context.reader.page; String sourceKey = context.reader.type.sourceKey; - return ImageFavoriteManager.isHas(cid, sourceKey, eid, page); + return ImageFavoriteManager.isHas(cid, sourceKey, eid, page, ep); } void imageFavoritesAction() { @@ -254,11 +255,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { return; } String id = context.reader.cid; + int ep = context.reader.chapter; String eid = context.reader.eid; String title = context.reader.history!.title; String subTitle = context.reader.history!.subtitle; int maxPage = context.reader.images!.length; - int ep = context.reader.chapter; int page = context.reader.page; String sourceKey = context.reader.type.sourceKey; String imageKey = context.reader.images![page - 1]; @@ -308,9 +309,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { maxPage); ImageFavoritePro imageFavoritePro = ImageFavoritePro( page, imageKey, null, eid, id, ep, sourceKey, epName); - ImageFavoritesEp? imageFavoritesEp = imageFavoritesComic - .imageFavoritesEp - .firstWhereOrNull((e) => e.eid == eid); + ImageFavoritesEp? imageFavoritesEp = + imageFavoritesComic.imageFavoritesEp.firstWhereOrNull((e) { + return e.ep == ep; + }); if (imageFavoritesEp == null) { ImageFavoritePro copy = ImageFavoritePro.copy(imageFavoritePro); copy.page = ImageFavoritesEp.firstPage; @@ -321,6 +323,16 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { eid, ep, [copy, imageFavoritePro], epName, maxPage); imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp); } else { + if (imageFavoritesEp.eid != eid) { + // 空字符串说明是从pica导入的, 那我们就手动刷一遍保证一致 + if (imageFavoritesEp.eid == "") { + imageFavoritesEp.eid == eid; + } else { + showToast( + message: "漫画的章节顺序可能发生了变化, 暂不支持收藏此章节".tl, context: context); + return; + } + } imageFavoritesEp.imageFavorites.add(imageFavoritePro); } diff --git a/lib/pages/settings/local_favorites.dart b/lib/pages/settings/local_favorites.dart index 3541077..260450d 100644 --- a/lib/pages/settings/local_favorites.dart +++ b/lib/pages/settings/local_favorites.dart @@ -59,17 +59,6 @@ class _LocalFavoritesSettingsState extends State { "read": "Read".tl, }, ).toSliver(), - SelectSetting( - title: "Support sliding to collect images".tl, - settingKey: "supportSwipeToFavorite", - optionTranslation: { - "yes": "Yes".tl, - "no": "No".tl, - }, - help: - "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" - .tl, - ).toSliver(), ], ); } diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 1c6f05d..b49c53e 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -116,6 +116,17 @@ class _ReaderSettingsState extends State { widget.onChanged?.call("enableClockAndBatteryInfoInReader"); }, ).toSliver(), + SelectSetting( + title: "Support sliding to collect images".tl, + settingKey: "supportSwipeToFavorite", + optionTranslation: { + "yes": "Yes".tl, + "no": "No".tl, + }, + help: + "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" + .tl, + ).toSliver(), _PopupWindowSetting( title: "Custom Image Processing".tl, builder: () => _CustomImageProcessing(), diff --git a/lib/utils/data.dart b/lib/utils/data.dart index c2636f0..459a894 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -134,7 +134,8 @@ Future importPicaData(File file) async { for (var folderSyncValue in db.select("SELECT * FROM folder_sync;")) { var folderName = folderSyncValue["folder_name"]; String sourceKey = folderSyncValue["key"]; - sourceKey = sourceKey.toLowerCase() == "htmanga" ? "wnacg" : sourceKey; + sourceKey = + sourceKey.toLowerCase() == "htmanga" ? "wnacg" : sourceKey; // 有值就跳过 if (LocalFavoritesManager().findLinked(folderName).$1 != null) { continue; @@ -225,21 +226,21 @@ Future importPicaData(File file) async { String epName = ""; ImageFavoritesComic? tempComic = imageFavoritesComicList .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); - ImageFavoritePro curImageFavorite = ImageFavoritePro( - page, "", null, ep.toString(), id, ep, sourceKey, epName); + ImageFavoritePro curImageFavorite = + ImageFavoritePro(page, "", null, "", id, ep, sourceKey, epName); if (tempComic == null) { tempComic = ImageFavoritesComic(id, [], title, sourceKey, [], [], DateTime.now(), "", {}, "", 1); tempComic.imageFavoritesEp = [ - ImageFavoritesEp(ep.toString(), ep, [curImageFavorite], epName, 1) + ImageFavoritesEp("", ep, [curImageFavorite], epName, 1) ]; imageFavoritesComicList.add(tempComic); } else { ImageFavoritesEp? tempEp = tempComic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == ep); if (tempEp == null) { - tempComic.imageFavoritesEp.add(ImageFavoritesEp( - ep.toString(), ep, [curImageFavorite], epName, 1)); + tempComic.imageFavoritesEp + .add(ImageFavoritesEp("", ep, [curImageFavorite], epName, 1)); } else { // 如果已经有这个page了, 就不添加了 if (tempEp.imageFavorites From 9b5f1aa66842345823ed009d0001be6c1cfe8e40 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Wed, 8 Jan 2025 22:09:08 +0800 Subject: [PATCH 12/36] =?UTF-8?q?feat:=20=E5=85=9C=E5=BA=95=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E5=8F=AF=E8=83=BD=E6=8A=A5=E9=94=99=E5=9C=BA=E6=99=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 2 + lib/foundation/image_favorites.dart | 4 +- lib/pages/home_page.dart | 1 + lib/pages/home_page/image_favorites.dart | 24 +-- .../image_favorites_item.dart | 154 +++++++++--------- .../image_favorites_page.dart | 1 + lib/pages/reader/scaffold.dart | 37 +++-- 7 files changed, 123 insertions(+), 100 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index af55131..c0ce32a 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -257,6 +257,7 @@ "Support sliding to collect images": "支持滑动收藏图片", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在图片浏览页面, 你可以根据你的阅读模式横滑或者竖滑快速收藏图片", "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的, 括号后是图片数量或图片数比漫画页数", + "The chapter order of the comic may have changed, temporarily not supported for collection": "漫画的章节顺序可能发生了变化, 暂不支持收藏此章节", "Author: ": "作者: ", "Tags: ": "标签: ", "Comics(number): ": "漫画(数量): ", @@ -564,6 +565,7 @@ "Support sliding to collect images": "支持滑動收藏圖片", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在圖片瀏覽頁面, 你可以根據你的閱讀模式橫向或者縱向滑動快速收藏圖片", "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的, 括號後是圖片數量或圖片數比漫畫頁數", + "The chapter order of the comic may have changed, temporarily not supported for collection": "漫畫的章節順序可能發生了變化, 暫不支持收藏此章節", "Author: ": "作者: ", "Tags: ": "標籤: ", "Comics(number): ": "漫畫(數量): ", diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 71842f0..749e8ba 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -264,7 +264,7 @@ class ImageFavoriteManager with ChangeNotifier { _debouncer.run(() { imageFavoritesComicList = getAll(null); notifyListeners(); - }, Duration(seconds: 5)); + }, Duration(seconds: 4)); } /// 检查表image_favorites是否存在, 不存在则创建 @@ -315,7 +315,7 @@ class ImageFavoriteManager with ChangeNotifier { for (ImageFavoritePro j in e.imageFavorites) { int index = finalImageFavorites.indexWhere((i) => i["page"] == j.page); - if (index == -1) { + if (index == -1 && j.page > 0) { // isAutoFavorite 为 null 不写入数据库, 同时只保留需要的属性, 避免增加太多重复字段在数据库里 if (j.isAutoFavorite != null) { finalImageFavorites.add({ diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index fead29f..5c77e01 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -12,6 +12,7 @@ import 'package:venera/foundation/history.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/log.dart'; import 'package:venera/pages/accounts_page.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_source_page.dart'; diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart index 29ea77c..427324b 100644 --- a/lib/pages/home_page/image_favorites.dart +++ b/lib/pages/home_page/image_favorites.dart @@ -160,17 +160,21 @@ class ImageFavoritesState extends State { } void refreshImageFavorites() async { - if (mounted) { - imageFavoritesCompute = null; - allImageFavoritePros = []; - for (var comic in ImageFavoriteManager.imageFavoritesComicList) { - allImageFavoritePros.addAll(comic.sortedImageFavoritePros); + try { + if (mounted) { + imageFavoritesCompute = null; + allImageFavoritePros = []; + for (var comic in ImageFavoriteManager.imageFavoritesComicList) { + allImageFavoritePros.addAll(comic.sortedImageFavoritePros); + } + setState(() {}); + // 避免性能开销, 开一个线程计算 + imageFavoritesCompute = await compute(computeImageFavorites, + jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); + setState(() {}); } - setState(() {}); - // 避免性能开销, 开一个线程计算 - imageFavoritesCompute = await compute(computeImageFavorites, - jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); - setState(() {}); + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); } } diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index bfb2209..c51aab9 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -30,91 +30,95 @@ class ImageFavoritesItemState extends State { // 如果刚从pica导入(没有imageKey) 或者 imageKey 失效了, 刷新一下 void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { - if (isImageKeyLoading || - hasRefreshImageKeyOnErr || - loadingImageFavoritesComicRes.isLoaded) { - return; - } - loadingImageFavoritesComicRes.isLoaded = true; - widget.setRefreshComicList(loadingImageFavoritesComicRes); - isImageKeyLoading = true; - ComicSource? comicSource = - ComicSource.find(widget.imageFavoritesComic.sourceKey); - // 拿一下漫画信息和对应章节的图片 - var resArr = await Future.wait([ - comicSource!.loadComicPages!( - widget.imageFavoritesComic.id, - imageFavoritesEp.eid, - ), - comicSource.loadComicInfo!( - widget.imageFavoritesComic.id, - ) - ]); - Res> comicPagesRes = resArr[0] as Res>; - Res comicInfoRes = resArr[1] as Res; - if (comicInfoRes.errorMessage?.contains("404") ?? false) { - loadingImageFavoritesComicRes.isInvalid = true; - widget.setRefreshComicList(loadingImageFavoritesComicRes); - if (mounted) { - setState(() {}); - } - return; - } - if (!comicInfoRes.error) { - ImageFavoritesSomething something = - ImageFavoritesComic.getSomethingFromComicDetails( - comicInfoRes.data, imageFavoritesEp.ep); - // 刷新一下值, 保存最新的 - widget.imageFavoritesComic.author = something.author; - widget.imageFavoritesComic.subTitle = something.subTitle; - widget.imageFavoritesComic.tags = something.tags; - widget.imageFavoritesComic.translatedTags = something.translatedTags; - imageFavoritesEp.epName = something.epName; - } else { - return; - } - if (comicPagesRes.error) { - // 能加载漫画信息, 说明只是章节对不太上, 刷新一下章节 - var chapters = comicInfoRes.data.chapters; - // 兜底一下, 如果不是从pica导入的空字符串, 说明是调用接口更新过章节的, 比如jm, 避免丢失最初正确的eid - // 拷贝等多章节可能会更新章节顺序, 后续如果碰到这种情况多的话, 就在右上角出一个功能批量刷新一下 - if (imageFavoritesEp.eid != "") { + try { + if (isImageKeyLoading || + hasRefreshImageKeyOnErr || + loadingImageFavoritesComicRes.isLoaded) { return; } - var finalEid = chapters?.keys.elementAt(imageFavoritesEp.ep - 1) ?? '0'; + loadingImageFavoritesComicRes.isLoaded = true; + widget.setRefreshComicList(loadingImageFavoritesComicRes); + isImageKeyLoading = true; + ComicSource? comicSource = + ComicSource.find(widget.imageFavoritesComic.sourceKey); + // 拿一下漫画信息和对应章节的图片 var resArr = await Future.wait([ - comicSource.loadComicPages!( + comicSource!.loadComicPages!( + widget.imageFavoritesComic.id, + imageFavoritesEp.eid, + ), + comicSource.loadComicInfo!( widget.imageFavoritesComic.id, - finalEid, ) ]); - comicPagesRes = resArr[0]; - if (comicPagesRes.error) { + Res> comicPagesRes = resArr[0] as Res>; + Res comicInfoRes = resArr[1] as Res; + if (comicInfoRes.errorMessage?.contains("404") ?? false) { + loadingImageFavoritesComicRes.isInvalid = true; + widget.setRefreshComicList(loadingImageFavoritesComicRes); + if (mounted) { + setState(() {}); + } return; + } + if (!comicInfoRes.error) { + ImageFavoritesSomething something = + ImageFavoritesComic.getSomethingFromComicDetails( + comicInfoRes.data, imageFavoritesEp.ep); + // 刷新一下值, 保存最新的 + widget.imageFavoritesComic.author = something.author; + widget.imageFavoritesComic.subTitle = something.subTitle; + widget.imageFavoritesComic.tags = something.tags; + widget.imageFavoritesComic.translatedTags = something.translatedTags; + imageFavoritesEp.epName = something.epName; } else { - imageFavoritesEp.eid = finalEid; + return; } + if (comicPagesRes.error) { + // 能加载漫画信息, 说明只是章节对不太上, 刷新一下章节 + var chapters = comicInfoRes.data.chapters; + // 兜底一下, 如果不是从pica导入的空字符串, 说明是调用接口更新过章节的, 比如jm, 避免丢失最初正确的eid + // 拷贝等多章节可能会更新章节顺序, 后续如果碰到这种情况多的话, 就在右上角出一个功能批量刷新一下 + if (imageFavoritesEp.eid != "") { + return; + } + var finalEid = chapters?.keys.elementAt(imageFavoritesEp.ep - 1) ?? '0'; + var resArr = await Future.wait([ + comicSource.loadComicPages!( + widget.imageFavoritesComic.id, + finalEid, + ) + ]); + comicPagesRes = resArr[0]; + if (comicPagesRes.error) { + return; + } else { + imageFavoritesEp.eid = finalEid; + } + } + List images = comicPagesRes.data; + widget.imageFavoritesComic.maxPage = images.length; + imageFavoritesEp.maxPage = images.length; + // 塞一个封面进去 + if (!imageFavoritesEp.isHasFirstPage) { + ImageFavoritePro copy = + ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); + copy.page = ImageFavoritesEp.firstPage; + copy.isAutoFavorite = true; + imageFavoritesEp.imageFavorites.insert(0, copy); + } + // 统一刷一下最新的imageKey + for (var ele in imageFavoritesEp.imageFavorites) { + ele.imageKey = images[ele.page - 1]; + } + ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); + if (mounted) { + setState(() {}); + } + isImageKeyLoading = false; + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); } - List images = comicPagesRes.data; - widget.imageFavoritesComic.maxPage = images.length; - imageFavoritesEp.maxPage = images.length; - // 塞一个封面进去 - if (!imageFavoritesEp.isHasFirstPage) { - ImageFavoritePro copy = - ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); - copy.page = ImageFavoritesEp.firstPage; - copy.isAutoFavorite = true; - imageFavoritesEp.imageFavorites.insert(0, copy); - } - // 统一刷一下最新的imageKey - for (var ele in imageFavoritesEp.imageFavorites) { - ele.imageKey = images[ele.page - 1]; - } - ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); - if (mounted) { - setState(() {}); - } - isImageKeyLoading = false; } void goComicInfo(ImageFavoritesComic comic) { diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index d87468d..d212a2d 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -13,6 +13,7 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 2b5b8c7..3e9adc9 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -238,12 +238,17 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } bool isLiked() { - String cid = context.reader.cid; - String eid = context.reader.eid; - int ep = context.reader.chapter; - int page = context.reader.page; - String sourceKey = context.reader.type.sourceKey; - return ImageFavoriteManager.isHas(cid, sourceKey, eid, page, ep); + try { + String cid = context.reader.cid; + String eid = context.reader.eid; + int ep = context.reader.chapter; + int page = context.reader.page; + String sourceKey = context.reader.type.sourceKey; + return ImageFavoriteManager.isHas(cid, sourceKey, eid, page, ep); + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); + return false; + } } void imageFavoritesAction() { @@ -328,8 +333,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { if (imageFavoritesEp.eid == "") { imageFavoritesEp.eid == eid; } else { + // 避免多章节漫画源的章节顺序发生变化, 如果情况比较多, 做一个以eid为准更新ep的功能 showToast( - message: "漫画的章节顺序可能发生了变化, 暂不支持收藏此章节".tl, context: context); + message: + "The chapter order of the comic may have changed, temporarily not supported for collection" + .tl, + context: context); return; } } @@ -508,12 +517,14 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { child: Container( decoration: BoxDecoration( color: context.colorScheme.surface.toOpacity(0.82), - border: Border( - top: BorderSide( - color: Colors.grey.toOpacity(0.5), - width: 0.5, - ), - ), + border: isOpen + ? Border( + top: BorderSide( + color: Colors.grey.toOpacity(0.5), + width: 0.5, + ), + ) + : null, ), padding: EdgeInsets.only(bottom: context.padding.bottom), child: child, From c03c3f7ce4cb069a22e3a486dac1ab12df30b1ad Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Wed, 8 Jan 2025 22:16:16 +0800 Subject: [PATCH 13/36] =?UTF-8?q?chore:=20=E6=B2=A1=E6=9C=89=E7=94=A8?= =?UTF-8?q?=E5=88=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/foundation/appdata.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index ab0738b..7031dd7 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -143,7 +143,6 @@ class _Settings with ChangeNotifier { 'quickFavorite': null, 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, - 'ignoreCertificateErrors': false, 'supportSwipeToFavorite': 'yes', // yes, no 'authorizationRequired': false, 'onClickFavorite': 'viewDetail', // viewDetail, read From a245cc6134a549b6cb47d320473f1f68e061c155 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Thu, 9 Jan 2025 22:47:28 +0800 Subject: [PATCH 14/36] =?UTF-8?q?feat:=20=E5=B0=BD=E9=87=8F=E4=BF=9D?= =?UTF-8?q?=E8=AF=81=E5=92=8C=E7=BD=91=E7=BB=9C=E6=94=B6=E8=97=8F=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E4=B8=80=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/favorites/favorite_actions.dart | 49 +++++++++++++---------- lib/pages/favorites/favorites_page.dart | 1 + 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 82e155b..1fb5a9a 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -339,6 +339,7 @@ Future importNetworkFolder( int requestCount = 0; var isFinished = false; int maxPage = 1; + List comics = []; String? next; // 如果是从旧到新, 先取一下maxPage if (isOldToNewSort) { @@ -350,25 +351,25 @@ Future importNetworkFolder( while (updatePageNum > requestCount && !isFinished) { try { if (comicSource.favoriteData?.loadComic != null) { - next ??= isOldToNewSort ? maxPage.toString() : '1'; + // 从旧到新的情况下, 假设有10页, 更新3页, 则从第8页开始, 8, 9, 10 三页 + next ??= + isOldToNewSort ? (maxPage - updatePageNum + 1).toString() : '1'; var page = int.parse(next!); var res = await comicSource.favoriteData!.loadComic!(page, folderID); var count = 0; receivedComics += res.data.length; for (var c in res.data) { - var result = LocalFavoritesManager().addComic( - resultName, - FavoriteItem( + if (!LocalFavoritesManager() + .comicExists(resultName, c.id, ComicType(source.hashCode))) { + count++; + comics.add(FavoriteItem( id: c.id, name: c.title, coverPath: c.cover, type: ComicType(source.hashCode), author: c.subtitle ?? '', tags: c.tags ?? [], - ), - ); - if (result) { - count++; + )); } } requestCount++; @@ -377,31 +378,24 @@ Future importNetworkFolder( isFinished = true; next = null; } else { - next = - isOldToNewSort ? (page - 1).toString() : (page + 1).toString(); - // 兼容收藏顺序按时间从旧到新的漫画拉取 - if (next == '0') { - isFinished = true; - } + next = (page + 1).toString(); } } else if (comicSource.favoriteData?.loadNext != null) { var res = await comicSource.favoriteData!.loadNext!(next, folderID); var count = 0; receivedComics += res.data.length; for (var c in res.data) { - var result = LocalFavoritesManager().addComic( - resultName, - FavoriteItem( + if (!LocalFavoritesManager() + .comicExists(resultName, c.id, ComicType(source.hashCode))) { + count++; + comics.add(FavoriteItem( id: c.id, name: c.title, coverPath: c.cover, type: ComicType(source.hashCode), author: c.subtitle ?? '', tags: c.tags ?? [], - ), - ); - if (result) { - count++; + )); } } requestCount++; @@ -499,5 +493,16 @@ Future importNetworkFolder( break; } } - closeDialog?.call(); + try { + if (appdata.settings['newFavoriteAddTo'] == "start" && !isOldToNewSort) { + // 如果是插到最前, 并且是从新到旧, 反转一下 + comics = comics.reversed.toList(); + } + for (var c in comics) { + LocalFavoritesManager().addComic(resultName, c); + } + closeDialog?.call(); + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); + } } diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 3fe2d5f..80445b2 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -11,6 +11,7 @@ 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/log.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/network/download.dart'; import 'package:venera/pages/comic_page.dart'; From 22ae856c0b7c3e8ef0a5a8b9cd712528bb330e69 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Fri, 10 Jan 2025 20:14:44 +0800 Subject: [PATCH 15/36] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=E7=83=AD=E7=82=B9tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/home_page/image_favorites.dart | 4 ++- .../image_favorites_item.dart | 36 ++++++++++++++++--- .../image_favorites_page.dart | 21 ++++++++--- 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart index 427324b..6c815a0 100644 --- a/lib/pages/home_page/image_favorites.dart +++ b/lib/pages/home_page/image_favorites.dart @@ -140,7 +140,9 @@ class ImageFavoritesState extends State { return ImageFavoritesCompute( sortedTags - .where((tag) => !exceptTags.contains(tag.toLowerCase())) + .where((tag) => + !exceptTags.contains(tag.toLowerCase()) && + !RegExp(r"\d+").hasMatch(tag)) .map((tag) => ImageFavoritesTextWithCount(tag, tagCount[tag]!)) .toList(), sortedAuthors diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index c51aab9..894b375 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -10,6 +10,7 @@ class ImageFavoritesItem extends StatefulWidget { required this.finalImageFavoritesComicList, required this.isRefreshComicList, required this.setRefreshComicList, + this.imageFavoritesCompute, }); final ImageFavoritesComic imageFavoritesComic; final Function(ImageFavoritePro) addSelected; @@ -18,6 +19,7 @@ class ImageFavoritesItem extends StatefulWidget { final bool multiSelectMode; final List isRefreshComicList; final Function(LoadingImageFavoritesComicRes) setRefreshComicList; + final ImageFavoritesCompute? imageFavoritesCompute; @override State createState() => ImageFavoritesItemState(); } @@ -162,8 +164,19 @@ class ImageFavoritesItemState extends State { refreshImageKey(e); } } - String time = DateFormat('yyyy-MM-dd HH:mm:ss') - .format(widget.imageFavoritesComic.time); + var enableTranslate = App.locale.languageCode == 'zh'; + String time = + DateFormat('yyyy-MM-dd HH:mm').format(widget.imageFavoritesComic.time); + List hotTags = []; + for (ImageFavoritesTextWithCount e + in widget.imageFavoritesCompute?.tags ?? []) { + if (widget.imageFavoritesComic.tags.contains(e.text)) { + hotTags.add(e.text); + } + if (hotTags.length == 5) { + break; + } + } return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( @@ -353,14 +366,27 @@ class ImageFavoritesItemState extends State { Row( children: [ Text( - "${"Earliest collection time".tl}: $time | ${widget.imageFavoritesComic.sourceKey}", + "${"Collection time".tl}:$time | ${widget.imageFavoritesComic.sourceKey}", textAlign: TextAlign.left, style: const TextStyle( fontSize: 12.0, ), - ), + ).paddingRight(8), + if (hotTags.isNotEmpty) + Expanded( + child: Text( + hotTags + .map((e) => enableTranslate ? e.translateTagsToCN : e) + .join(" "), + textAlign: TextAlign.right, + style: const TextStyle( + fontSize: 12.0, + overflow: TextOverflow.ellipsis, + ), + ), + ) ], - ).paddingHorizontal(16).paddingBottom(8), + ).paddingHorizontal(8).paddingBottom(8), ], ), ), diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index d212a2d..dd9d782 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; import 'dart:math'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; @@ -16,11 +18,13 @@ import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_page.dart'; +import 'package:venera/pages/home_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; import 'package:venera/pages/reader/reader.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; +import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'package:venera/utils/utils.dart'; part "image_favorites_item.dart"; @@ -47,6 +51,7 @@ class ImageFavoritesPage extends StatefulWidget { class ImageFavoritesPageState extends State { late String sortType; + ImageFavoritesCompute? imageFavoritesCompute; late String timeFilterSelect; late String numFilterSelect; late List finalTimeList; @@ -92,15 +97,19 @@ class ImageFavoritesPageState extends State { }); } - void getInitImageFavorites() { + void getInitImageFavorites() async { imageFavoritePros = []; for (var e in ImageFavoriteManager.imageFavoritesComicList) { imageFavoritePros.addAll(e.sortedImageFavoritePros); } imageFavoritesComicList = ImageFavoriteManager.imageFavoritesComicList; + imageFavoritesCompute = await compute( + ImageFavoritesState.computeImageFavorites, + jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); + update(); } - void refreshImageFavorites() { + void refreshImageFavorites() async { if (mounted) { getInitImageFavorites(); update(); @@ -258,7 +267,7 @@ class ImageFavoritesPageState extends State { onPressed: deSelect), buildMultiSelectMenu(), ]; - var widget = SmoothCustomScrollView( + var scrollWidget = SmoothCustomScrollView( controller: scrollController, slivers: [ if (!searchMode && !multiSelectMode) @@ -360,6 +369,7 @@ class ImageFavoritesPageState extends State { multiSelectMode: multiSelectMode, finalImageFavoritesComicList: curImageFavoritesComicList, setRefreshComicList: setRefreshComicList, + imageFavoritesCompute: imageFavoritesCompute, ); }, childCount: curImageFavoritesComicList.length)), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), @@ -372,8 +382,9 @@ class ImageFavoritesPageState extends State { interactive: true, child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false), - child: - context.width > changePoint ? widget.paddingHorizontal(8) : widget, + child: context.width > changePoint + ? scrollWidget.paddingHorizontal(8) + : scrollWidget, ), ); return PopScope( From da6ed89d48dd89c7b5c7a9fc7ae88992d031eadd Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Fri, 10 Jan 2025 20:42:04 +0800 Subject: [PATCH 16/36] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=8F=8C?= =?UTF-8?q?=E5=87=BB=E6=94=B6=E8=97=8F,=20=E4=B8=8D=E8=BF=87=E6=AD=A4?= =?UTF-8?q?=E6=97=B6=E7=A6=81=E6=AD=A2=E6=94=BE=E5=A4=A7=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 14 ++++++++++---- lib/foundation/appdata.dart | 2 +- lib/foundation/image_favorites.dart | 3 ++- lib/pages/favorites/favorite_actions.dart | 2 ++ lib/pages/reader/images.dart | 12 ++++++++++-- lib/pages/reader/scaffold.dart | 8 +++++--- lib/pages/settings/reader.dart | 9 +++++---- 7 files changed, 35 insertions(+), 15 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index c0ce32a..cd15699 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -254,7 +254,10 @@ "Uncollect the image": "取消收藏图片", "Successfully collected": "收藏成功", "Collect the image": "收藏图片", - "Support sliding to collect images": "支持滑动收藏图片", + "Quick collect image": "快速收藏图片", + "Not enable": "不启用", + "Double Tap": "双击", + "Swipe": "滑动", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在图片浏览页面, 你可以根据你的阅读模式横滑或者竖滑快速收藏图片", "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的, 括号后是图片数量或图片数比漫画页数", "The chapter order of the comic may have changed, temporarily not supported for collection": "漫画的章节顺序可能发生了变化, 暂不支持收藏此章节", @@ -264,7 +267,7 @@ "Comics(percentage): ": "漫画(比例): ", "Time Filter": "时间筛选", "Image Favorites Greater Than": "图片收藏数大于", - "Earliest collection time": "最早收藏时间", + "Collection time": "收藏时间", "favoritesCompareComicPages": "收藏数与漫画页数比较", "Cover": "封面", "Page @a": "第 @a 页", @@ -562,7 +565,7 @@ "Uncollect the image": "取消收藏圖片", "Successfully collected": "收藏成功", "Collect the image": "收藏圖片", - "Support sliding to collect images": "支持滑動收藏圖片", + "Quick collect image": "快速收藏圖片", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在圖片瀏覽頁面, 你可以根據你的閱讀模式橫向或者縱向滑動快速收藏圖片", "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的, 括號後是圖片數量或圖片數比漫畫頁數", "The chapter order of the comic may have changed, temporarily not supported for collection": "漫畫的章節順序可能發生了變化, 暫不支持收藏此章節", @@ -572,7 +575,10 @@ "Comics(percentage): ": "漫畫(比例): ", "Time Filter": "時間篩選", "Image Favorites Greater Than": "圖片收藏數大於", - "Earliest collection time": "最早收藏時間", + "Collection time": "收藏時間", + "Not enable": "不启用", + "Double Tap": "雙擊", + "Swipe": "滑動", "favoritesCompareComicPages": "收藏數與漫畫頁數比較", "Cover": "封面", "Page @a": "第 @a 頁", diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 7031dd7..fc4cd69 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -143,7 +143,7 @@ class _Settings with ChangeNotifier { 'quickFavorite': null, 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, - 'supportSwipeToFavorite': 'yes', // yes, no + 'quickCollectImage': 'Swipe', // No, DoubleTap, Swipe 'authorizationRequired': false, 'onClickFavorite': 'viewDetail', // viewDetail, read 'enableDnsOverrides': false, diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 749e8ba..5c2254b 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -260,9 +260,10 @@ class ImageFavoriteManager with ChangeNotifier { return cache == null ? (cache = ImageFavoriteManager.create()) : cache!; } void updateValue() { + // 立刻触发, 让阅读界面可以看到图片收藏的图标状态更新了 + imageFavoritesComicList = getAll(null); // 避免从pica导入的时候, 疯狂触发更新 _debouncer.run(() { - imageFavoritesComicList = getAll(null); notifyListeners(); }, Duration(seconds: 4)); } diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index 1fb5a9a..4d466ea 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -501,6 +501,8 @@ Future importNetworkFolder( for (var c in comics) { LocalFavoritesManager().addComic(resultName, c); } + // 延迟一点, 让用户看清楚到底新增了多少 + await Future.delayed(const Duration(milliseconds: 500)); closeDialog?.call(); } catch (e, stackTrace) { Log.error("Unhandled Exception", e.toString(), stackTrace); diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 2d74801..a16a0d9 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -263,6 +263,10 @@ class _GalleryModeState extends State<_GalleryMode> @override void handleDoubleTap(Offset location) { + if (appdata.settings['quickCollectImage'] == 'DoubleTap') { + context.readerScaffold.imageFavoritesAction(); + return; + } var controller = photoViewControllers[reader.page]!; controller.onDoubleClick?.call(); } @@ -461,7 +465,7 @@ class _ContinuousModeState extends State<_ContinuousMode> widget = Listener( onPointerDown: (event) { fingers++; - if(fingers > 1 && !disableScroll) { + if (fingers > 1 && !disableScroll) { setState(() { disableScroll = true; }); @@ -475,7 +479,7 @@ class _ContinuousModeState extends State<_ContinuousMode> }, onPointerUp: (event) { fingers--; - if(fingers <= 1 && disableScroll) { + if (fingers <= 1 && disableScroll) { setState(() { disableScroll = false; }); @@ -564,6 +568,10 @@ class _ContinuousModeState extends State<_ContinuousMode> @override void handleDoubleTap(Offset location) { + if (appdata.settings['quickCollectImage'] == 'DoubleTap') { + context.readerScaffold.imageFavoritesAction(); + return; + } double target; if (photoViewController.scale != photoViewController.getInitialScale?.call()) { diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 3e9adc9..3ecf51d 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -93,10 +93,12 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { void dragListenerHandler() async { // 过一秒执行, 避免 _gestureDetectorState 还未初始化 await Future.delayed(Duration(milliseconds: 1000)); + if(!mounted) return; var readerMode = context.reader.mode; // 横向阅读的时候, 如果纵向滑就触发收藏, 纵向阅读的时候, 如果横向滑动就触发收藏 double imageFavoritesListenDistance = 0; - if (appdata.settings['supportSwipeToFavorite'] == 'yes') { + _gestureDetectorState!.dragListenerForImageFavorites = null; + if (appdata.settings['quickCollectImage'] == 'Swipe') { _gestureDetectorState!.dragListenerForImageFavorites = _DragListener( onMove: (offset) { switch (readerMode) { @@ -295,7 +297,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ImageFavoriteManager.deleteImageFavoritePro([ ImageFavoritePro(page, imageKey, null, eid, id, ep, sourceKey, epName) ]); - showToast(message: "Uncollect the image".tl, context: context); + showToast(message: "Uncollect the image".tl, context: context, seconds: 1); } else { ImageFavoritesComic? imageFavoritesComic = ImageFavoriteManager .imageFavoritesComicList @@ -346,7 +348,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } ImageFavoriteManager.addOrUpdateOrDelete(imageFavoritesComic); - showToast(message: "Successfully collected".tl, context: context); + showToast(message: "Successfully collected".tl, context: context, seconds: 1); } update(); } catch (e, stackTrace) { diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index b49c53e..c9b51cc 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -117,11 +117,12 @@ class _ReaderSettingsState extends State { }, ).toSliver(), SelectSetting( - title: "Support sliding to collect images".tl, - settingKey: "supportSwipeToFavorite", + title: "Quick collect image".tl, + settingKey: "quickCollectImage", optionTranslation: { - "yes": "Yes".tl, - "no": "No".tl, + "No": "Not enable".tl, + "DoubleTap": "Double Tap".tl, + "Swipe": "Swipe".tl, }, help: "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" From 1ffa6388f3ebf95e08063db4fb84f489b32025e3 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sat, 11 Jan 2025 11:43:18 +0800 Subject: [PATCH 17/36] =?UTF-8?q?fix:=20=E8=87=AA=E5=8A=A8=E5=A1=9E?= =?UTF-8?q?=E5=B0=81=E9=9D=A2=E9=80=BB=E8=BE=91=E5=AE=8C=E5=96=84,=20?= =?UTF-8?q?=E5=88=87=E6=8D=A2=E5=BF=AB=E9=80=9F=E6=94=B6=E8=97=8F=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=AB=8B=E5=88=BB=E7=94=9F=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/reader/scaffold.dart | 33 ++++++++++++++++++++++----------- lib/pages/settings/reader.dart | 3 +++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 3ecf51d..016c970 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -93,7 +93,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { void dragListenerHandler() async { // 过一秒执行, 避免 _gestureDetectorState 还未初始化 await Future.delayed(Duration(milliseconds: 1000)); - if(!mounted) return; + if (!mounted) return; var readerMode = context.reader.mode; // 横向阅读的时候, 如果纵向滑就触发收藏, 纵向阅读的时候, 如果横向滑动就触发收藏 double imageFavoritesListenDistance = 0; @@ -297,7 +297,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ImageFavoriteManager.deleteImageFavoritePro([ ImageFavoritePro(page, imageKey, null, eid, id, ep, sourceKey, epName) ]); - showToast(message: "Uncollect the image".tl, context: context, seconds: 1); + showToast( + message: "Uncollect the image".tl, context: context, seconds: 1); } else { ImageFavoritesComic? imageFavoritesComic = ImageFavoriteManager .imageFavoritesComicList @@ -321,13 +322,18 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { return e.ep == ep; }); if (imageFavoritesEp == null) { - ImageFavoritePro copy = ImageFavoritePro.copy(imageFavoritePro); - copy.page = ImageFavoritesEp.firstPage; - copy.imageKey = context.reader.images![0]; - copy.isAutoFavorite = true; - // 塞一个封面进去 - imageFavoritesEp = ImageFavoritesEp( - eid, ep, [copy, imageFavoritePro], epName, maxPage); + if (page != ImageFavoritesEp.firstPage) { + ImageFavoritePro copy = ImageFavoritePro.copy(imageFavoritePro); + copy.page = ImageFavoritesEp.firstPage; + copy.imageKey = context.reader.images![0]; + copy.isAutoFavorite = true; + // 不是第一页的话, 自动塞一个封面进去 + imageFavoritesEp = ImageFavoritesEp( + eid, ep, [copy, imageFavoritePro], epName, maxPage); + } else { + imageFavoritesEp = + ImageFavoritesEp(eid, ep, [imageFavoritePro], epName, maxPage); + } imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp); } else { if (imageFavoritesEp.eid != eid) { @@ -348,7 +354,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } ImageFavoriteManager.addOrUpdateOrDelete(imageFavoritesComic); - showToast(message: "Successfully collected".tl, context: context, seconds: 1); + showToast( + message: "Successfully collected".tl, context: context, seconds: 1); } update(); } catch (e, stackTrace) { @@ -722,7 +729,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { onChanged: (key) { if (key == "readerMode") { context.reader.mode = ReaderMode.fromKey(appdata.settings[key]); - App.rootContext.pop(); + // 这行代码似乎会导致页面白屏, 所有 widget 都不显示 + // App.rootContext.pop(); } if (key == "enableTurnPageByVolumeKey") { if (appdata.settings[key]) { @@ -731,6 +739,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { context.reader.stopVolumeEvent(); } } + if (key == "quickCollectImage") { + dragListenerHandler(); + } context.reader.update(); }, ), diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index c9b51cc..778a215 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -124,6 +124,9 @@ class _ReaderSettingsState extends State { "DoubleTap": "Double Tap".tl, "Swipe": "Swipe".tl, }, + onChanged: () { + widget.onChanged?.call("quickCollectImage"); + }, help: "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" .tl, From a313bd2221fc8defe70e3f2fc5f38ef08243d7e3 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 11 Jan 2025 17:51:49 +0800 Subject: [PATCH 18/36] Refactor --- lib/foundation/comic_source/models.dart | 33 +- lib/foundation/consts.dart | 13 +- lib/foundation/history.dart | 6 +- lib/foundation/image_favorites.dart | 417 ++++++++++-------- .../image_favorites_provider.dart | 12 +- lib/foundation/local.dart | 3 +- lib/pages/comic_page.dart | 6 +- lib/pages/favorites/local_favorites_page.dart | 145 +++--- lib/pages/home_page.dart | 204 ++++++++- lib/pages/home_page/image_favorites.dart | 345 --------------- .../image_favorites_item.dart | 78 ++-- .../image_favorites_page.dart | 256 +++++------ .../image_favorites_photo_view.dart | 32 +- lib/pages/image_favorites_page/type.dart | 65 +-- lib/pages/reader/loading.dart | 60 +-- lib/pages/reader/reader.dart | 11 +- lib/pages/reader/scaffold.dart | 116 +++-- lib/utils/data.dart | 8 +- lib/utils/utils.dart | 15 - 19 files changed, 851 insertions(+), 974 deletions(-) delete mode 100644 lib/pages/home_page/image_favorites.dart delete mode 100644 lib/utils/utils.dart diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 7a137d6..7f08c83 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -73,7 +73,8 @@ class Comic { this.sourceKey, this.maxPage, this.language, - ): favoriteId = null, stars = null; + ) : favoriteId = null, + stars = null; Map toJson() { return { @@ -231,6 +232,34 @@ class ComicDetails with HistoryMixin { String get id => comicId; ComicType get comicType => ComicType(sourceKey.hashCode); + + /// Convert tags map to plain list + List get plainTags { + var res = []; + tags.forEach((key, value) { + res.addAll(value.map((e) => "$key:$e")); + }); + return res; + } + + /// Find the first author tag + String? findAuthor() { + var authorNamespaces = [ + "author", + "authors", + "artist", + "artists", + "作者", + "画师" + ]; + for (var entry in tags.entries) { + if (authorNamespaces.contains(entry.key.toLowerCase()) && + entry.value.isNotEmpty) { + return entry.value.first; + } + } + return null; + } } class ArchiveInfo { @@ -242,4 +271,4 @@ class ArchiveInfo { : title = json["title"], description = json["description"], id = json["id"]; -} \ No newline at end of file +} diff --git a/lib/foundation/consts.dart b/lib/foundation/consts.dart index 4c9211c..1e1cdf2 100644 --- a/lib/foundation/consts.dart +++ b/lib/foundation/consts.dart @@ -1,6 +1,17 @@ +/// If window width is less than this value, it is considered as mobile. const changePoint = 600; +/// If window width is less than this value, it is considered as tablet. +/// +/// If it is more than this value, it is considered as desktop. const changePoint2 = 1300; +/// Default user agent for http requests. const webUA = - "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"; \ No newline at end of file + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"; + +/// Pages for all comics is started from this value. +const firstPage = 1; + +/// Chapters for all comics is started from this value. +const firstChapter = 1; \ No newline at end of file diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 83a4c2f..83e61e2 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -2,17 +2,17 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'package:flutter/foundation.dart'; 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/foundation/log.dart'; import 'package:venera/utils/ext.dart'; -import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; -import 'package:venera/utils/utils.dart'; import 'app.dart'; +import 'consts.dart'; part "image_favorites.dart"; typedef HistoryType = ComicType; @@ -227,7 +227,7 @@ class HistoryManager with ChangeNotifier { """); notifyListeners(); - ImageFavoriteManager.init(); + ImageFavoriteManager().init(); } /// add history. if exists, update time. diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 5c2254b..10fbd4c 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -1,36 +1,26 @@ part of "history.dart"; class ImageFavorite { + final String eid; + final String id; // 漫画id + final int ep; + final String epName; + final String sourceKey; String imageKey; - int page; - - // 是否是自动收藏的, 仅用于第一页 bool? isAutoFavorite; ImageFavorite( this.page, this.imageKey, this.isAutoFavorite, - ); -} - -class ImageFavoritePro extends ImageFavorite { - final String eid; - final String id; // 漫画id - final int ep; - final String epName; - final String sourceKey; - ImageFavoritePro( - super.page, - super.imageKey, - super.isAutoFavorite, this.eid, this.id, this.ep, this.sourceKey, this.epName, ); + Map toJson() { return { 'page': page, @@ -44,23 +34,41 @@ class ImageFavoritePro extends ImageFavorite { }; } - ImageFavoritePro.fromJson(Map json) - : this( - json['page'], - json['imageKey'], - json['isAutoFavorite'], - json['eid'], - json['id'], - json['ep'], - json['sourceKey'], - json['epName']); - // 复制构造函数 - ImageFavoritePro.copy(ImageFavoritePro other) - : this(other.page, other.imageKey, other.isAutoFavorite, other.eid, - other.id, other.ep, other.sourceKey, other.epName); + ImageFavorite.fromJson(Map json) + : page = json['page'], + imageKey = json['imageKey'], + isAutoFavorite = json['isAutoFavorite'], + eid = json['eid'], + id = json['id'], + ep = json['ep'], + sourceKey = json['sourceKey'], + epName = json['epName']; + + ImageFavorite copyWith({ + int? page, + String? imageKey, + bool? isAutoFavorite, + String? eid, + String? id, + int? ep, + String? sourceKey, + String? epName, + }) { + return ImageFavorite( + page ?? this.page, + imageKey ?? this.imageKey, + isAutoFavorite ?? this.isAutoFavorite, + eid ?? this.eid, + id ?? this.id, + ep ?? this.ep, + sourceKey ?? this.sourceKey, + epName ?? this.epName, + ); + } + @override bool operator ==(Object other) { - return other is ImageFavoritePro && + return other is ImageFavorite && other.id == id && other.sourceKey == sourceKey && other.page == page && @@ -78,30 +86,11 @@ class ImageFavoritesEp { final int ep; int maxPage; String epName; - List imageFavorites; - - // 避免使用常量 - static int firstPage = 1; + List imageFavorites; ImageFavoritesEp( this.eid, this.ep, this.imageFavorites, this.epName, this.maxPage); - Map toJson() { - return { - 'eid': eid, - 'ep': ep, - 'imageFavorites': imageFavorites, - 'epName': epName, - 'maxPage': maxPage, - }; - } - ImageFavoritesEp.fromJson(Map json) - : eid = json['eid'], - ep = json['ep'], - imageFavorites = List.from( - json['imageFavorites'].map((e) => ImageFavoritePro.fromJson(e))), - epName = json['epName'], - maxPage = json['maxPage']; // 是否有封面 bool get isHasFirstPage { return imageFavorites[0].page == firstPage; @@ -111,17 +100,16 @@ class ImageFavoritesEp { bool get isHasImageKey { return imageFavorites.every((e) => e.imageKey != ""); } -} -// 从漫画详情中获取到的信息 -class ImageFavoritesSomething { - String author; - String subTitle; - List tags; - List translatedTags; - String epName; - ImageFavoritesSomething( - this.author, this.tags, this.translatedTags, this.epName, this.subTitle); + Map toJson() { + return { + 'eid': eid, + 'ep': ep, + 'maxPage': maxPage, + 'epName': epName, + 'imageFavorites': imageFavorites.map((e) => e.toJson()).toList(), + }; + } } class ImageFavoritesComic { @@ -130,6 +118,7 @@ class ImageFavoritesComic { String subTitle; String author; final String sourceKey; + // 不一定是真的这本漫画的所有页数, 如果是多章节的时候 int maxPage; List tags; @@ -139,46 +128,19 @@ class ImageFavoritesComic { final Map other; ImageFavoritesComic( - this.id, - this.imageFavoritesEp, - this.title, - this.sourceKey, - this.tags, - this.translatedTags, - this.time, - this.author, - this.other, - this.subTitle, - this.maxPage); - Map toJson() { - return { - 'id': id, - 'title': title, - 'subTitle': subTitle, - 'author': author, - 'tags': tags, - 'translatedTags': translatedTags, - 'time': time.millisecondsSinceEpoch, - 'maxPage': maxPage, - 'sourceKey': sourceKey, - 'imageFavoritesEp': imageFavoritesEp, - 'other': other, - }; - } + this.id, + this.imageFavoritesEp, + this.title, + this.sourceKey, + this.tags, + this.translatedTags, + this.time, + this.author, + this.other, + this.subTitle, + this.maxPage, + ); - ImageFavoritesComic.fromJson(Map json) - : id = json['id'], - title = json['title'], - subTitle = json['subTitle'], - author = json['author'], - tags = List.from(json['tags']), - translatedTags = List.from(json['translatedTags']), - time = DateTime.fromMillisecondsSinceEpoch(json['time']), - maxPage = json['maxPage'], - sourceKey = json['sourceKey'], - imageFavoritesEp = List.from( - json['imageFavoritesEp'].map((e) => ImageFavoritesEp.fromJson(e))), - other = json['other']; // 是否都有imageKey bool get isAllHasImageKey { return imageFavoritesEp @@ -198,8 +160,8 @@ class ImageFavoritesComic { return imageFavoritesEp.every((e) => e.isHasFirstPage); } - List get sortedImageFavoritePros { - List temp = []; + List get sortedImageFavorites { + List temp = []; for (var e in imageFavoritesEp) { for (var i in e.imageFavorites) { temp.add(i); @@ -208,68 +170,38 @@ class ImageFavoritesComic { return temp; } - static List tagsToTranslated(List tags) { - var translatedTags = []; - for (var tag in tags) { - var translated = tag.translateTagsToCN; - if (translated != tag) { - translatedTags.add(translated); - } - } - return translatedTags; + @override + bool operator ==(Object other) { + return other is ImageFavoritesComic && + other.id == id && + other.sourceKey == sourceKey; } - static ImageFavoritesSomething getSomethingFromComicDetails( - ComicDetails comicDetails, int ep) { - List tags = []; - String author = ""; - try { - if (comicDetails.tags['Artists'] != null) { - author = comicDetails.tags['Artists']!.first; - } - if (comicDetails.tags['artist'] != null) { - author = comicDetails.tags['artist']!.first; - } - if (comicDetails.tags['作者'] != null) { - author = comicDetails.tags['作者']!.first; - } - if (comicDetails.tags['Author'] != null) { - author = comicDetails.tags['Author']!.first; - } - // ignore: empty_catches - } catch (e) {} - String epName = - comicDetails.chapters?.values.elementAtOrNull(ep - 1) ?? "E$ep"; - tags = comicDetails.tags.values.fold( - [], (previousValue, element) => [...previousValue, ...element]); - var translatedTags = tagsToTranslated(tags); - String subTitle = comicDetails.subTitle ?? ""; - return ImageFavoritesSomething( - author, tags, translatedTags, epName, subTitle); - } + @override + int get hashCode => Object.hash(id, sourceKey); } class ImageFavoriteManager with ChangeNotifier { - static Database get _db => HistoryManager()._db; - static ImageFavoriteManager? cache; - final Debouncer _debouncer = Debouncer(); - static List imageFavoritesComicList = getAll(null); - ImageFavoriteManager.create(); - static bool hasInit = false; - factory ImageFavoriteManager() { - return cache == null ? (cache = ImageFavoriteManager.create()) : cache!; - } + Database get _db => HistoryManager()._db; + late List imageFavoritesComicList = getAll(null); + + static ImageFavoriteManager? _cache; + + ImageFavoriteManager._(); + + factory ImageFavoriteManager() => (_cache ??= ImageFavoriteManager._()); + void updateValue() { // 立刻触发, 让阅读界面可以看到图片收藏的图标状态更新了 imageFavoritesComicList = getAll(null); // 避免从pica导入的时候, 疯狂触发更新 - _debouncer.run(() { + Future.delayed(const Duration(seconds: 4), () { notifyListeners(); - }, Duration(seconds: 4)); + }); } /// 检查表image_favorites是否存在, 不存在则创建 - static void init() { + void init() { _db.execute("CREATE TABLE IF NOT EXISTS image_favorites (" "id TEXT," "title TEXT NOT NULL," @@ -284,11 +216,10 @@ class ImageFavoriteManager with ChangeNotifier { "other TEXT NOT NULL," "PRIMARY KEY (id,source_key)" ");"); - hasInit = true; } // 做排序和去重的操作 - static void addOrUpdateOrDelete(ImageFavoritesComic favorite) { + void addOrUpdateOrDelete(ImageFavoritesComic favorite) { // 没有章节了就删掉 if (favorite.imageFavoritesEp.isEmpty) { _db.execute(""" @@ -313,7 +244,7 @@ class ImageFavoriteManager with ChangeNotifier { for (var e in tempImageFavoritesEp) { List finalImageFavorites = []; int epIndex = tempImageFavoritesEp.indexOf(e); - for (ImageFavoritePro j in e.imageFavorites) { + for (ImageFavorite j in e.imageFavorites) { int index = finalImageFavorites.indexWhere((i) => i["page"] == j.page); if (index == -1 && j.page > 0) { @@ -355,13 +286,8 @@ class ImageFavoriteManager with ChangeNotifier { ImageFavoriteManager().updateValue(); } - static ImageFavoritesComic? findFromComicList( - List tempList, - String id, - String sourceKey, - String eid, - int page, - int ep) { + ImageFavoritesComic? findFromComicList(List tempList, + String id, String sourceKey, String eid, int page, int ep) { ImageFavoritesComic? temp = tempList .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); if (temp == null) { @@ -383,14 +309,13 @@ class ImageFavoriteManager with ChangeNotifier { } } - static bool isHas(String id, String sourceKey, String eid, int page, int ep) { + bool has(String id, String sourceKey, String eid, int page, int ep) { return findFromComicList( imageFavoritesComicList, id, sourceKey, eid, page, ep) != null; } - static List getAll(String? keyword) { - if (!hasInit) return []; + List getAll([String? keyword]) { var res = []; if (keyword == null || keyword == "") { res = _db.select("select * from image_favorites;"); @@ -409,12 +334,12 @@ class ImageFavoriteManager with ChangeNotifier { } try { return res.map((e) { - dynamic tempImageFavoritesEp = jsonDecode(e["image_favorites_ep"]); + var tempImageFavoritesEp = jsonDecode(e["image_favorites_ep"]); List finalImageFavoritesEp = []; tempImageFavoritesEp.forEach((i) { - List temp = []; + List temp = []; i["imageFavorites"].forEach((j) { - temp.add(ImageFavoritePro( + temp.add(ImageFavorite( j["page"], j["imageKey"], j["isAutoFavorite"], @@ -447,20 +372,18 @@ class ImageFavoriteManager with ChangeNotifier { } } - static void deleteImageFavoritePro( - List imageFavoriteProList) { + void deleteImageFavorite(List imageFavoriteList) { for (var e in imageFavoritesComicList) { // 找到同一个漫画中的需要删除的具体图片 - List filterImageFavoritesPro = - imageFavoriteProList.where((i) { + List filterImageFavorites = + imageFavoriteList.where((i) { return i.id == e.id && i.sourceKey == e.sourceKey; }).toList(); - if (filterImageFavoritesPro.isNotEmpty) { + if (filterImageFavorites.isNotEmpty) { e.imageFavoritesEp = e.imageFavoritesEp.where((i) { // 去掉匹配到的具体图片 i.imageFavorites = i.imageFavorites.where((j) { - ImageFavoritePro? temp = - filterImageFavoritesPro.firstWhereOrNull((k) { + ImageFavorite? temp = filterImageFavorites.firstWhereOrNull((k) { return k.page == j.page && k.ep == j.ep; }); // 如果没有匹配到, 说明不是这个章节和page, 就留着 @@ -479,7 +402,7 @@ class ImageFavoriteManager with ChangeNotifier { ImageFavoriteManager().updateValue(); } - static List get earliestTimeToNow { + List get earliestTimeToNow { var res = _db.select("select MIN(time) from image_favorites;"); if (res.first.values.first == null) { return []; @@ -496,13 +419,151 @@ class ImageFavoriteManager with ChangeNotifier { return yearsList; } - static int get length { - if (!hasInit) return 0; + int get length { var res = _db.select("select count(*) from image_favorites;"); return res.first.values.first! as int; } - static List search(String keyword) { + List search(String keyword) { + if (keyword == "") { + return []; + } return getAll(keyword); } + + Future computeImageFavorites() { + return compute( + _computeImageFavorites, + imageFavoritesComicList, + ); + } + + static ImageFavoritesCompute _computeImageFavorites( + List comics) { + // 去掉这些没有意义的标签 + const List exceptTags = [ + '連載中', + '', + 'translated', + 'chinese', + 'sole male', + 'sole female', + 'original', + 'doujinshi', + 'manga', + 'multi-work series', + 'mosaic censorship', + 'dilf', + 'bbm', + 'uncensored', + 'full censorship' + ]; + + Map tagCount = {}; + Map authorCount = {}; + Map comicImageCount = {}; + Map comicMaxPages = {}; + + for (var comic in comics) { + for (var tag in comic.tags) { + String finalTag = tag; + tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1; + } + + if (comic.author != "") { + String finalAuthor = comic.author; + authorCount[finalAuthor] = (authorCount[finalAuthor] ?? 0) + + comic.sortedImageFavorites.length; + } + // 小于10页的漫画不统计 + if (comic.maxPageFromEp < 10) { + continue; + } + comicImageCount[comic] = + (comicImageCount[comic] ?? 0) + comic.sortedImageFavorites.length; + comicMaxPages[comic] = (comicMaxPages[comic] ?? 0) + comic.maxPageFromEp; + } + + // 按数量排序标签 + List sortedTags = tagCount.keys.toList() + ..sort((a, b) => tagCount[b]!.compareTo(tagCount[a]!)); + + // 按数量排序作者 + List sortedAuthors = authorCount.keys.toList() + ..sort((a, b) => authorCount[b]!.compareTo(authorCount[a]!)); + + // 按收藏数量排序漫画 + List> sortedComicsByNum = + comicImageCount.entries.toList() + ..sort((a, b) => b.value.compareTo(a.value)); + + // 按收藏比例排序漫画 + List> sortedComicsByPercentage = + comicImageCount.entries.toList() + ..sort((a, b) { + double percentageA = + comicImageCount[a.key]! / comicMaxPages[a.key]!; + double percentageB = + comicImageCount[b.key]! / comicMaxPages[b.key]!; + return percentageB.compareTo(percentageA); + }); + + validateTag(String tag) { + if (tag.startsWith("Category:")) { + return false; + } + return !exceptTags.contains(tag.toLowerCase()) && !tag.isNum; + } + + return ImageFavoritesCompute( + sortedTags + .where(validateTag) + .map((tag) => TextWithCount(tag, tagCount[tag]!)) + .toList(), + sortedAuthors + .map((author) => TextWithCount(author, authorCount[author]!)) + .toList(), + sortedComicsByNum + .map((comic) => TextWithCount(comic.key.title, comic.value)) + .toList(), + sortedComicsByPercentage + .map((comic) => TextWithCount(comic.key.title, comic.value)) + .toList(), + ); + } + + ImageFavoritesComic? find(String id, String sourceKey) { + return imageFavoritesComicList.firstWhereOrNull( + (comic) => comic.id == id && comic.sourceKey == sourceKey, + ); + } +} + +class TextWithCount { + final String text; + final int count; + + const TextWithCount(this.text, this.count); +} + +class ImageFavoritesCompute { + /// 基于收藏的标签数排序 + final List tags; + + /// 基于收藏的作者数排序 + final List authors; + + /// 基于喜欢的图片数排序 + final List comicByNum; + + // 基于图片数比上总页数排序 + final List comicByPercentage; + + /// 计算后的图片收藏数据 + const ImageFavoritesCompute( + this.tags, + this.authors, + this.comicByNum, + this.comicByPercentage, + ); } diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart index 5a40fbb..23acde0 100644 --- a/lib/foundation/image_provider/image_favorites_provider.dart +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -8,21 +8,19 @@ import 'image_favorites_provider.dart' as image_provider; class ImageFavoritesProvider extends BaseImageProvider { - /// Image provider for normal image. - /// - /// [url] is the url of the image. Local file path is also supported. + /// Image provider for imageFavorites const ImageFavoritesProvider(this.imageFavorite); - final ImageFavoritePro imageFavorite; + final ImageFavorite imageFavorite; @override Future load(StreamController chunkEvents) async { - String? imageKey = imageFavorite.imageKey; + String imageKey = imageFavorite.imageKey; String sourceKey = imageFavorite.sourceKey; String cid = imageFavorite.id; String eid = imageFavorite.eid; if (imageKey == "") { - throw "Error: imageFavorits no imageKey"; + throw "Error: imageFavorites no imageKey"; } await for (var progress in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { @@ -42,7 +40,7 @@ class ImageFavoritesProvider return SynchronousFuture(this); } - static String getImageKey(ImageFavoritePro temp) { + static String getImageKey(ImageFavorite temp) { return "${temp.imageKey}@${temp.sourceKey}@${temp.id}@${temp.eid}"; } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 60859d4..d185e32 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -119,7 +119,8 @@ class LocalComic with HistoryMixin implements Comic { ep: 0, page: 0, ), - comicDetails: this, + author: subtitle, + tags: tags, ), ); } diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index a005e0f..ed84ff4 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -145,7 +145,8 @@ class _ComicPageState extends LoadingState ep: 0, page: 0, ), - comicDetails: localComic, + author: localComic.subTitle ?? '', + tags: localComic.tags, ); }); App.mainNavigatorKey!.currentContext!.pop(); @@ -664,7 +665,8 @@ abstract mixin class _ComicPageActions { initialChapter: ep, initialPage: page, history: History.fromModel(model: comic, ep: 0, page: 0), - comicDetails: comic, + author: comic.findAuthor() ?? '', + tags: comic.plainTags, ), ); } diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart index bac1ebf..6b08e14 100644 --- a/lib/pages/favorites/local_favorites_page.dart +++ b/lib/pages/favorites/local_favorites_page.dart @@ -1,77 +1,5 @@ part of 'favorites_page.dart'; -String allPageText = 'All'.tl; -List pageNumList = [1, 2, 3, 5, 10, 20, 50, 100, 200, allPageText] - .map((e) => e.toString()) - .toList(); - -class _SelectUpdatePageNum extends StatefulWidget { - const _SelectUpdatePageNum({ - required this.networkSource, - this.networkFolder, - super.key, - }); - - final String? networkFolder; - final String networkSource; - - @override - State<_SelectUpdatePageNum> createState() => _SelectUpdatePageNumState(); -} - -class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> { - int updatePageNum = 9999999; - @override - void initState() { - updatePageNum = - appdata.implicitData["local_favorites_update_page_num"] ?? 9999999; - super.initState(); - } - - @override - Widget build(BuildContext context) { - var source = ComicSource.find(widget.networkSource); - var sourceName = source?.name ?? widget.networkSource; - var text = "The folder is Linked to @source".tlParams({ - "source": sourceName, - }); - if (widget.networkFolder != null && widget.networkFolder!.isNotEmpty) { - text += "\n${"Source Folder".tl}: ${widget.networkFolder}"; - } - - return Column( - children: [ - Row( - children: [Text(text)], - ), - Row( - children: [ - Text("Update the page number by the latest collection".tl), - Spacer(), - Select( - current: updatePageNum.toString() == '9999999' - ? allPageText - : updatePageNum.toString(), - values: pageNumList, - minWidth: 48, - onTap: (index) { - setState(() { - updatePageNum = int.parse(pageNumList[index] == allPageText - ? '9999999' - : pageNumList[index]); - appdata.implicitData["local_favorites_update_page_num"] = - updatePageNum; - appdata.writeImplicitData(); - }); - }, - ) - ], - ), - ], - ); - } -} - class _LocalFavoritesPage extends StatefulWidget { const _LocalFavoritesPage({required this.folder, super.key}); @@ -861,3 +789,76 @@ class _ReorderComicsPageState extends State<_ReorderComicsPage> { ); } } + +class _SelectUpdatePageNum extends StatefulWidget { + const _SelectUpdatePageNum({ + required this.networkSource, + this.networkFolder, + super.key, + }); + + final String? networkFolder; + final String networkSource; + + @override + State<_SelectUpdatePageNum> createState() => _SelectUpdatePageNumState(); +} + +class _SelectUpdatePageNumState extends State<_SelectUpdatePageNum> { + int updatePageNum = 9999999; + + String get _allPageText => 'All'.tl; + + List get pageNumList => + ['1', '2', '3', '5', '10', '20', '50', '100', '200', _allPageText]; + + @override + void initState() { + updatePageNum = + appdata.implicitData["local_favorites_update_page_num"] ?? 9999999; + super.initState(); + } + + @override + Widget build(BuildContext context) { + var source = ComicSource.find(widget.networkSource); + var sourceName = source?.name ?? widget.networkSource; + var text = "The folder is Linked to @source".tlParams({ + "source": sourceName, + }); + if (widget.networkFolder != null && widget.networkFolder!.isNotEmpty) { + text += "\n${"Source Folder".tl}: ${widget.networkFolder}"; + } + + return Column( + children: [ + Row( + children: [Text(text)], + ), + Row( + children: [ + Text("Update the page number by the latest collection".tl), + Spacer(), + Select( + current: updatePageNum.toString() == '9999999' + ? _allPageText + : updatePageNum.toString(), + values: pageNumList, + minWidth: 48, + onTap: (index) { + setState(() { + updatePageNum = int.parse(pageNumList[index] == _allPageText + ? '9999999' + : pageNumList[index]); + appdata.implicitData["local_favorites_update_page_num"] = + updatePageNum; + appdata.writeImplicitData(); + }); + }, + ) + ], + ), + ], + ); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 5c77e01..13a3957 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,6 +1,3 @@ -import 'dart:convert'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:venera/components/components.dart'; @@ -26,7 +23,6 @@ import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'local_comics_page.dart'; -part "./home_page/image_favorites.dart"; class HomePage extends StatelessWidget { const HomePage({super.key}); @@ -919,3 +915,203 @@ class __AnimatedDownloadingIconState extends State<_AnimatedDownloadingIcon> ); } } + +enum ImageFavoritesComputeType { + tags, + authors, + comicByNum, + comicByPercentage, +} + +class ImageFavorites extends StatefulWidget { + const ImageFavorites({super.key}); + + @override + State createState() => _ImageFavoritesState(); +} + +class _ImageFavoritesState extends State { + ImageFavoritesCompute? imageFavoritesCompute; + List allImageFavoritePros = []; + + void refreshImageFavorites() async { + try { + imageFavoritesCompute = null; + allImageFavoritePros = []; + for (var comic in ImageFavoriteManager().imageFavoritesComicList) { + allImageFavoritePros.addAll(comic.sortedImageFavorites); + } + setState(() {}); + imageFavoritesCompute = + await ImageFavoriteManager().computeImageFavorites(); + if (mounted) { + setState(() {}); + } + } catch (e, stackTrace) { + Log.error("Unhandled Exception", e.toString(), stackTrace); + } + } + + @override + void initState() { + refreshImageFavorites(); + ImageFavoriteManager().addListener(refreshImageFavorites); + super.initState(); + } + + @override + void dispose() { + ImageFavoriteManager().removeListener(refreshImageFavorites); + super.dispose(); + } + + Widget roundBtn( + TextWithCount textWithCount, + ImageFavoritesComputeType type, + ) { + var enableTranslate = App.locale.languageCode == 'zh'; + var text = enableTranslate + ? textWithCount.text.translateTagsToCN + : textWithCount.text; + if (type == ImageFavoritesComputeType.tags) { + if (text.contains(':')) { + text = text.split(':').last; + } + } + if (text.length > 20) { + text = '${text.substring(0, 20)}...'; + } + text += "(${textWithCount.count})"; + return InkWell( + onTap: () { + context + .to(() => ImageFavoritesPage(initialKeyword: textWithCount.text)); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text(text), + ), + ); + } + + Widget listRoundBtn( + List list, ImageFavoritesComputeType type) { + return Expanded( + child: SizedBox( + height: 24, + child: ListView.separated( + separatorBuilder: (BuildContext context, int index) { + return SizedBox(width: 4); + }, + scrollDirection: Axis.horizontal, + itemCount: list.length, + itemBuilder: (context, index) { + return roundBtn(list[index], type); + }, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + width: 0.6, + ), + borderRadius: BorderRadius.circular(8), + ), + child: InkWell( + borderRadius: BorderRadius.circular(8), + onTap: () { + context.to(() => const ImageFavoritesPage()); + }, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 56, + child: Row( + children: [ + Center( + child: Text('Image Favorites'.tl, style: ts.s18), + ), + const Spacer(), + const Icon(Icons.arrow_right), + ], + ), + ).paddingHorizontal(16), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages" + .tlParams({ + "a": ImageFavoriteManager().length.toString(), + "b": allImageFavoritePros.length + }), + style: const TextStyle(fontSize: 15), + ), + if (imageFavoritesCompute != null) ...[ + const SizedBox(height: 8), + Row( + children: [ + Text( + "Author: ".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.authors, + ImageFavoritesComputeType.authors) + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "Tags: ".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.tags, + ImageFavoritesComputeType.tags) + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "Comics(number): ".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.comicByNum, + ImageFavoritesComputeType.comicByNum) + ], + ), + const SizedBox(height: 4), + Row( + children: [ + Text( + "Comics(percentage): ".tl, + style: const TextStyle(fontSize: 13), + ), + listRoundBtn(imageFavoritesCompute!.comicByPercentage, + ImageFavoritesComputeType.comicByPercentage) + ], + ), + ], + ], + ).paddingHorizontal(16).paddingBottom(16), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/home_page/image_favorites.dart b/lib/pages/home_page/image_favorites.dart deleted file mode 100644 index 6c815a0..0000000 --- a/lib/pages/home_page/image_favorites.dart +++ /dev/null @@ -1,345 +0,0 @@ -part of '../home_page.dart'; - -class ImageFavoritesFindComic { - final String id; - final String fullTitle; - final String showTitle; - final String sourceKey; - - const ImageFavoritesFindComic( - this.id, this.fullTitle, this.showTitle, this.sourceKey); -} - -class ImageFavoritesTextWithCount { - final String text; - final int count; - const ImageFavoritesTextWithCount(this.text, this.count); -} - -// 算出最喜欢的 -class ImageFavoritesCompute { - final List tags; - final List authors; - // 喜欢的图片数 - final List comicByNum; - // 图片数比上总页数 - final List comicByPercentage; - - const ImageFavoritesCompute( - this.tags, this.authors, this.comicByNum, this.comicByPercentage); -} - -enum ImageFavoritesComputeType { - tags, - authors, - comicByNum, - comicByPercentage, -} - -class ImageFavorites extends StatefulWidget { - const ImageFavorites({super.key}); - - @override - State createState() => ImageFavoritesState(); -} - -// 去掉这些没有意义的标签 -List exceptTags = [ - '連載中', - '', - 'translated', - 'chinese', - 'sole male', - 'sole female', - 'original', - 'doujinshi', - 'manga', - 'multi-work series', - 'mosaic censorship', - 'dilf', - 'bbm', - 'uncensored', - 'full censorship' -].map((e) => e.toLowerCase()).toList(); - -class ImageFavoritesState extends State { - ImageFavoritesCompute? imageFavoritesCompute; - List allImageFavoritePros = []; - static String separator = "*venera*"; - - static ImageFavoritesFindComic fromStringToImageFavoritesFindComic( - String str, String suffix, List tempComicsList) { - List temp = str.split(separator); - String sourceKey = temp[0]; - String id = temp[1]; - ImageFavoritesComic comic = tempComicsList.firstWhere((e) { - return e.sourceKey == sourceKey && e.id == id; - }); - return ImageFavoritesFindComic( - id, - comic.title, - '${comic.title.length > 36 ? comic.title.substring(0, 36) : comic.title}... $suffix', - sourceKey); - } - - // compute 需要传入字符串, 复杂对象无法传入 - static ImageFavoritesCompute computeImageFavorites(String temp) { - List tempComics = List.from( - jsonDecode(temp).map((e) => ImageFavoritesComic.fromJson(e))); - Map tagCount = {}; - Map authorCount = {}; - Map comicImageCount = {}; - Map comicMaxPages = {}; - - for (ImageFavoritesComic imageFavoritesComic in tempComics) { - for (var tag in imageFavoritesComic.tags) { - String finalTag = tag; - tagCount[finalTag] = (tagCount[finalTag] ?? 0) + 1; - } - - if (imageFavoritesComic.author != "") { - String finalAuthor = imageFavoritesComic.author; - authorCount[finalAuthor] = (authorCount[finalAuthor] ?? 0) + - imageFavoritesComic.sortedImageFavoritePros.length; - } - // 小于10页的漫画不统计 - if (imageFavoritesComic.maxPageFromEp < 10) { - continue; - } - // 统计漫画图片数和总页数 - String comicId = - '${imageFavoritesComic.sourceKey}$separator${imageFavoritesComic.id}'; - comicImageCount[comicId] = (comicImageCount[comicId] ?? 0) + - imageFavoritesComic.sortedImageFavoritePros.length; - comicMaxPages[comicId] = - (comicMaxPages[comicId] ?? 0) + imageFavoritesComic.maxPageFromEp; - } - - // 按数量排序标签 - List sortedTags = tagCount.keys.toList() - ..sort((a, b) => tagCount[b]!.compareTo(tagCount[a]!)); - - // 按数量排序作者 - List sortedAuthors = authorCount.keys.toList() - ..sort((a, b) => authorCount[b]!.compareTo(authorCount[a]!)); - - // 按收藏数量排序漫画 - List> sortedComicsByNum = comicImageCount.entries - .toList() - ..sort((a, b) => b.value.compareTo(a.value)); - - // 按收藏比例排序漫画 - List> sortedComicsByPercentage = comicImageCount - .entries - .toList() - ..sort((a, b) { - double percentageA = comicImageCount[a.key]! / comicMaxPages[a.key]!; - double percentageB = comicImageCount[b.key]! / comicMaxPages[b.key]!; - return percentageB.compareTo(percentageA); - }); - - return ImageFavoritesCompute( - sortedTags - .where((tag) => - !exceptTags.contains(tag.toLowerCase()) && - !RegExp(r"\d+").hasMatch(tag)) - .map((tag) => ImageFavoritesTextWithCount(tag, tagCount[tag]!)) - .toList(), - sortedAuthors - .map((author) => - ImageFavoritesTextWithCount(author, authorCount[author]!)) - .toList(), - sortedComicsByNum - .map((comic) => fromStringToImageFavoritesFindComic( - comic.key, '(${comic.value})', tempComics)) - .toList(), - sortedComicsByPercentage - .map((comic) => fromStringToImageFavoritesFindComic( - comic.key, - '(${(comicImageCount[comic.key]! / comicMaxPages[comic.key]! * 100).toStringAsFixed(1)}%)', - tempComics)) - .toList()); - } - - void refreshImageFavorites() async { - try { - if (mounted) { - imageFavoritesCompute = null; - allImageFavoritePros = []; - for (var comic in ImageFavoriteManager.imageFavoritesComicList) { - allImageFavoritePros.addAll(comic.sortedImageFavoritePros); - } - setState(() {}); - // 避免性能开销, 开一个线程计算 - imageFavoritesCompute = await compute(computeImageFavorites, - jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); - setState(() {}); - } - } catch (e, stackTrace) { - Log.error("Unhandled Exception", e.toString(), stackTrace); - } - } - - @override - void initState() { - refreshImageFavorites(); - ImageFavoriteManager().addListener(refreshImageFavorites); - super.initState(); - } - - @override - void dispose() { - ImageFavoriteManager().removeListener(refreshImageFavorites); - super.dispose(); - } - - Widget roundBtn( - Object text, - ImageFavoritesComputeType type, - ) { - bool textWithCount = text is ImageFavoritesTextWithCount; - bool isAuthor = type == ImageFavoritesComputeType.authors; - var enableTranslate = App.locale.languageCode == 'zh'; - String translateText = ''; - if (textWithCount) { - translateText = enableTranslate ? text.text.translateTagsToCN : text.text; - if (isAuthor) { - translateText = TagsTranslation.artistTags[text.text] ?? text.text; - } - } - return InkWell( - onTap: () { - RegExp regExpForTag = RegExp(r" \(\d+\)$"); - context.to(() => ImageFavoritesPage( - initialKeyword: (textWithCount - ? text.text.replaceAll(regExpForTag, '') - : (text as ImageFavoritesFindComic).fullTitle))); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - textWithCount - ? "${enableTranslate ? translateText : text.text} (${text.count})" - : (text as ImageFavoritesFindComic).showTitle, - ), - ), - ); - } - - Widget listRoundBtn(List list, ImageFavoritesComputeType type) { - return Expanded( - child: SizedBox( - height: 24, - child: ListView.separated( - separatorBuilder: (BuildContext context, int index) { - return SizedBox(width: 4); - }, - scrollDirection: Axis.horizontal, - itemCount: list.length, - itemBuilder: (context, index) { - return roundBtn(list[index], type); - }))); - } - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - width: 0.6, - ), - borderRadius: BorderRadius.circular(8), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - context.to(() => const ImageFavoritesPage()); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 56, - child: Row( - children: [ - Center( - child: Text('Image Favorites'.tl, style: ts.s18), - ), - const Spacer(), - const Icon(Icons.arrow_right), - ], - ), - ).paddingHorizontal(16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages" - .tlParams({ - "a": ImageFavoriteManager.length.toString(), - "b": allImageFavoritePros.length - }), - style: const TextStyle(fontSize: 15), - ), - if (imageFavoritesCompute != null) ...[ - const SizedBox(height: 8), - Row( - children: [ - Text( - "Author: ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.authors, - ImageFavoritesComputeType.authors) - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "Tags: ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.tags, - ImageFavoritesComputeType.tags) - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "Comics(number): ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.comicByNum, - ImageFavoritesComputeType.comicByNum) - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "Comics(percentage): ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.comicByPercentage, - ImageFavoritesComputeType.comicByPercentage) - ], - ), - ], - ], - ).paddingHorizontal(16).paddingBottom(16), - ], - ), - ), - ), - ); - } -} diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 894b375..c6fbb54 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -1,8 +1,7 @@ part of 'image_favorites_page.dart'; -class ImageFavoritesItem extends StatefulWidget { - const ImageFavoritesItem({ - super.key, +class _ImageFavoritesItem extends StatefulWidget { + const _ImageFavoritesItem({ required this.imageFavoritesComic, required this.selectedImageFavorites, required this.addSelected, @@ -12,20 +11,23 @@ class ImageFavoritesItem extends StatefulWidget { required this.setRefreshComicList, this.imageFavoritesCompute, }); + final ImageFavoritesComic imageFavoritesComic; - final Function(ImageFavoritePro) addSelected; + final Function(ImageFavorite) addSelected; final Map selectedImageFavorites; final List finalImageFavoritesComicList; final bool multiSelectMode; final List isRefreshComicList; final Function(LoadingImageFavoritesComicRes) setRefreshComicList; final ImageFavoritesCompute? imageFavoritesCompute; + @override - State createState() => ImageFavoritesItemState(); + State<_ImageFavoritesItem> createState() => _ImageFavoritesItemState(); } -class ImageFavoritesItemState extends State { +class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { bool isImageKeyLoading = false; + // 刷新 imageKey 失败的场景再刷新一次, 再次失败了就不重试了 bool hasRefreshImageKeyOnErr = false; late LoadingImageFavoritesComicRes loadingImageFavoritesComicRes; @@ -64,15 +66,18 @@ class ImageFavoritesItemState extends State { return; } if (!comicInfoRes.error) { - ImageFavoritesSomething something = - ImageFavoritesComic.getSomethingFromComicDetails( - comicInfoRes.data, imageFavoritesEp.ep); // 刷新一下值, 保存最新的 - widget.imageFavoritesComic.author = something.author; - widget.imageFavoritesComic.subTitle = something.subTitle; - widget.imageFavoritesComic.tags = something.tags; - widget.imageFavoritesComic.translatedTags = something.translatedTags; - imageFavoritesEp.epName = something.epName; + widget.imageFavoritesComic.author = + comicInfoRes.data.findAuthor() ?? ""; + widget.imageFavoritesComic.subTitle = comicInfoRes.data.subTitle ?? ''; + widget.imageFavoritesComic.tags = comicInfoRes.data.plainTags; + widget.imageFavoritesComic.translatedTags = widget + .imageFavoritesComic.tags + .map((e) => e.translateTagsToCN) + .toList(); + imageFavoritesEp.epName = comicInfoRes.data.chapters?.values + .elementAtOrNull(imageFavoritesEp.ep - 1) ?? + ""; } else { return; } @@ -103,17 +108,17 @@ class ImageFavoritesItemState extends State { imageFavoritesEp.maxPage = images.length; // 塞一个封面进去 if (!imageFavoritesEp.isHasFirstPage) { - ImageFavoritePro copy = - ImageFavoritePro.copy(imageFavoritesEp.imageFavorites[0]); - copy.page = ImageFavoritesEp.firstPage; - copy.isAutoFavorite = true; + ImageFavorite copy = imageFavoritesEp.imageFavorites[0].copyWith( + page: firstPage, + isAutoFavorite: true, + ); imageFavoritesEp.imageFavorites.insert(0, copy); } // 统一刷一下最新的imageKey for (var ele in imageFavoritesEp.imageFavorites) { ele.imageKey = images[ele.page - 1]; } - ImageFavoriteManager.addOrUpdateOrDelete(widget.imageFavoritesComic); + ImageFavoriteManager().addOrUpdateOrDelete(widget.imageFavoritesComic); if (mounted) { setState(() {}); } @@ -148,16 +153,17 @@ class ImageFavoritesItemState extends State { e.id == widget.imageFavoritesComic.id && e.sourceKey == widget.imageFavoritesComic.sourceKey) ?? LoadingImageFavoritesComicRes( - isLoaded: false, - isInvalid: false, - id: widget.imageFavoritesComic.id, - sourceKey: widget.imageFavoritesComic.sourceKey); + isLoaded: false, + isInvalid: false, + id: widget.imageFavoritesComic.id, + sourceKey: widget.imageFavoritesComic.sourceKey, + ); super.initState(); } @override Widget build(BuildContext context) { - int count = widget.imageFavoritesComic.sortedImageFavoritePros.length; + int count = widget.imageFavoritesComic.sortedImageFavorites.length; if (!widget.imageFavoritesComic.isAllHasImageKey || !widget.imageFavoritesComic.isAllHasFirstPage) { for (var e in widget.imageFavoritesComic.imageFavoritesEp) { @@ -168,10 +174,16 @@ class ImageFavoritesItemState extends State { String time = DateFormat('yyyy-MM-dd HH:mm').format(widget.imageFavoritesComic.time); List hotTags = []; - for (ImageFavoritesTextWithCount e - in widget.imageFavoritesCompute?.tags ?? []) { - if (widget.imageFavoritesComic.tags.contains(e.text)) { - hotTags.add(e.text); + for (var textWithCount in widget.imageFavoritesCompute?.tags ?? []) { + if (widget.imageFavoritesComic.tags.contains(textWithCount.text)) { + var enableTranslate = App.locale.languageCode == 'zh'; + var text = enableTranslate + ? textWithCount.text.translateTagsToCN + : textWithCount.text; + if (text.contains(':')) { + text = text.split(':').last; + } + hotTags.add(text); } if (hotTags.length == 5) { break; @@ -197,7 +209,7 @@ class ImageFavoritesItemState extends State { onTap: () { if (widget.multiSelectMode) { for (var ele - in widget.imageFavoritesComic.sortedImageFavoritePros) { + in widget.imageFavoritesComic.sortedImageFavorites) { widget.addSelected(ele); } } else { @@ -206,7 +218,7 @@ class ImageFavoritesItemState extends State { } }, onLongPress: () { - for (var ele in widget.imageFavoritesComic.sortedImageFavoritePros) { + for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { widget.addSelected(ele); } }, @@ -252,8 +264,8 @@ class ImageFavoritesItemState extends State { SliverList( delegate: SliverChildBuilderDelegate( (context, index) { - ImageFavoritePro curImageFavorite = widget - .imageFavoritesComic.sortedImageFavoritePros[index]; + ImageFavorite curImageFavorite = widget + .imageFavoritesComic.sortedImageFavorites[index]; ImageFavoritesEp curImageFavoritesEp = widget .imageFavoritesComic.imageFavoritesEp .firstWhere((e) { @@ -263,7 +275,7 @@ class ImageFavoritesItemState extends State { widget.selectedImageFavorites[curImageFavorite] ?? false; int curPage = curImageFavorite.page; - String pageText = curPage == ImageFavoritesEp.firstPage + String pageText = curPage == firstPage ? '@a Cover' .tlParams({"a": curImageFavorite.epName}) : curPage.toString(); diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index dd9d782..ad22124 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:math'; import 'package:flutter/foundation.dart'; @@ -18,7 +17,6 @@ import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_page.dart'; -import 'package:venera/pages/home_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; import 'package:venera/pages/reader/reader.dart'; import 'package:venera/utils/ext.dart'; @@ -26,8 +24,9 @@ import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; -import 'package:venera/utils/utils.dart'; + part "image_favorites_item.dart"; + part "image_favorites_photo_view.dart"; class LoadingImageFavoritesComicRes { @@ -35,6 +34,7 @@ class LoadingImageFavoritesComicRes { bool isInvalid; String id; String sourceKey; + LoadingImageFavoritesComicRes( {required this.isLoaded, required this.isInvalid, @@ -44,33 +44,35 @@ class LoadingImageFavoritesComicRes { class ImageFavoritesPage extends StatefulWidget { const ImageFavoritesPage({super.key, this.initialKeyword}); + final String? initialKeyword; + @override State createState() => ImageFavoritesPageState(); } class ImageFavoritesPageState extends State { - late String sortType; + late ImageFavoriteSortType sortType; ImageFavoritesCompute? imageFavoritesCompute; - late String timeFilterSelect; - late String numFilterSelect; - late List finalTimeList; + late TimeFilterEnum timeFilterSelect; + late int numFilterSelect; + // 所有的图片收藏 - List imageFavoritesComicList = []; - late List curImageFavoritesComicList; + List comics = []; late List timeFilter; List isRefreshComicList = []; late TextEditingController _textEditingController; - final Debouncer _debouncer = Debouncer(); String keyword = ""; // 进入关键词搜索模式 bool searchMode = false; bool multiSelectMode = false; + // 多选的时候选中的图片 - Map selectedImageFavorites = {}; - late List imageFavoritePros; + Map selectedImageFavorites = {}; + late List imageFavoritePros; + // 避免重复请求 void setRefreshComicList(LoadingImageFavoritesComicRes res) { LoadingImageFavoritesComicRes? tempRes = @@ -85,27 +87,28 @@ class ImageFavoritesPageState extends State { } void update() { - setState(() {}); + if (mounted) { + setState(() {}); + } } - void updateDialogConfig(String tempSortType, String tempTimeFilterSelect, - String tempNumFilterSelect) { + void updateDialogConfig(ImageFavoriteSortType sortType, + TimeFilterEnum timeFilter, int numFilter) { setState(() { - sortType = tempSortType; - timeFilterSelect = tempTimeFilterSelect; - numFilterSelect = tempNumFilterSelect; + this.sortType = sortType; + timeFilterSelect = timeFilter; + numFilterSelect = numFilter; }); } void getInitImageFavorites() async { imageFavoritePros = []; - for (var e in ImageFavoriteManager.imageFavoritesComicList) { - imageFavoritePros.addAll(e.sortedImageFavoritePros); + for (var e in ImageFavoriteManager().imageFavoritesComicList) { + imageFavoritePros.addAll(e.sortedImageFavorites); } - imageFavoritesComicList = ImageFavoriteManager.imageFavoritesComicList; - imageFavoritesCompute = await compute( - ImageFavoritesState.computeImageFavorites, - jsonEncode(ImageFavoriteManager.imageFavoritesComicList)); + getCurImageFavorites(); + imageFavoritesCompute = + await ImageFavoriteManager().computeImageFavorites(); update(); } @@ -117,46 +120,38 @@ class ImageFavoritesPageState extends State { } void getCurImageFavorites() { - List tempList = imageFavoritesComicList; - if (keyword != "") { - tempList = ImageFavoriteManager.search(keyword); - } + comics = searchMode + ? ImageFavoriteManager().search(keyword) + : ImageFavoriteManager().getAll(); + var now = DateTime.now(); // 筛选到最终列表 - curImageFavoritesComicList = tempList.where((ele) { + comics = comics.where((ele) { bool isFilter = true; - if (timeFilterSelect != TimeFilterEnum.all.value) { - timeFilter = getDateTimeRangeFromFilter(timeFilterSelect); - DateTime start = timeFilter[0]; - DateTime end = timeFilter[1]; - DateTime dateTimeToCheck = ele.time; - isFilter = - dateTimeToCheck.isAfter(start) && dateTimeToCheck.isBefore(end) || - dateTimeToCheck == start || - dateTimeToCheck == end; + if (timeFilterSelect != TimeFilterEnum.all) { + isFilter = now.difference(ele.time) <= timeFilterSelect.duration; } if (numFilterSelect != numFilterList[0]) { - isFilter = - ele.sortedImageFavoritePros.length > int.parse(numFilterSelect); + isFilter = ele.sortedImageFavorites.length > numFilterSelect; } return isFilter; }).toList(); // 给列表排序 - if (sortType == ImageFavoriteSortType.title.value) { - curImageFavoritesComicList.sort((a, b) => a.title.compareTo(b.title)); - } else if (sortType == ImageFavoriteSortType.timeAsc.value) { - curImageFavoritesComicList.sort((a, b) => a.time.compareTo(b.time)); - } else if (sortType == ImageFavoriteSortType.timeDesc.value) { - curImageFavoritesComicList.sort((a, b) => b.time.compareTo(a.time)); - } else if (sortType == ImageFavoriteSortType.maxFavorites.value) { - curImageFavoritesComicList.sort((a, b) => b.sortedImageFavoritePros.length - .compareTo(a.sortedImageFavoritePros.length)); - } else if (sortType == - ImageFavoriteSortType.favoritesCompareComicPages.value) { - curImageFavoritesComicList.sort((a, b) { - double tempA = a.sortedImageFavoritePros.length / a.maxPageFromEp; - double tempB = b.sortedImageFavoritePros.length / b.maxPageFromEp; - return tempB.compareTo(tempA); - }); + switch (sortType) { + case ImageFavoriteSortType.title: + comics.sort((a, b) => a.title.compareTo(b.title)); + case ImageFavoriteSortType.timeAsc: + comics.sort((a, b) => a.time.compareTo(b.time)); + case ImageFavoriteSortType.timeDesc: + comics.sort((a, b) => b.time.compareTo(a.time)); + case ImageFavoriteSortType.maxFavorites: + comics.sort((a, b) => b.sortedImageFavorites.length + .compareTo(a.sortedImageFavorites.length)); + case ImageFavoriteSortType.favoritesCompareComicPages: + comics.sort((a, b) { + double tempA = a.sortedImageFavorites.length / a.maxPageFromEp; + double tempB = b.sortedImageFavorites.length / b.maxPageFromEp; + return tempB.compareTo(tempA); + }); } } @@ -167,32 +162,14 @@ class ImageFavoritesPageState extends State { searchMode = true; } _textEditingController = TextEditingController(text: keyword); - String initSortType = appdata.implicitData["image_favorites_sort"] ?? - ImageFavoriteSortType.title.value; - sortType = ImageFavoriteSortType.values - .firstWhereOrNull((e) => e.value == initSortType) - ?.value ?? - ImageFavoriteSortType.title.value; - String initTimeFilter = - appdata.implicitData["image_favorites_time_filter"] ?? - TimeFilterEnum.all.value; - // 可能是年份 - int yearNum = int.tryParse(initTimeFilter) ?? 2023; - bool isValideYearNum = yearNum >= 2023 && yearNum <= 2099; - if (!isValideYearNum) { - timeFilterSelect = timeFilterList - .firstWhereOrNull((e) => e.value == initTimeFilter) - ?.value ?? - TimeFilterEnum.all.value; - } else { - timeFilterSelect = initTimeFilter; - } - numFilterSelect = - appdata.implicitData["image_favorites_num_filter"] ?? numFilterList[0]; - finalTimeList = List.from([ - ...timeFilterList.map((e) => e.value), - ...ImageFavoriteManager.earliestTimeToNow - ]); + sortType = ImageFavoriteSortType.values.firstWhereOrNull( + (e) => e.value == appdata.implicitData["image_favorites_sort"]) ?? + ImageFavoriteSortType.title; + timeFilterSelect = TimeFilterEnum.values.firstWhereOrNull((e) => + e.value == appdata.implicitData["image_favorites_time_filter"]) ?? + TimeFilterEnum.all; + numFilterSelect = appdata.implicitData["image_favorites_number_filter"] ?? + numFilterList[0]; getInitImageFavorites(); ImageFavoriteManager().addListener(refreshImageFavorites); super.initState(); @@ -212,9 +189,8 @@ class ImageFavoritesPageState extends State { icon: Icons.delete_outline, text: "Delete".tl, onClick: () { - selectedImageFavorites.keys.toList().forEach((ele) { - ImageFavoriteManager.deleteImageFavoritePro([ele]); - }); + ImageFavoriteManager() + .deleteImageFavorite(selectedImageFavorites.keys.toList()); setState(() { multiSelectMode = false; selectedImageFavorites.clear(); @@ -242,7 +218,7 @@ class ImageFavoritesPageState extends State { }); } - void addSelected(ImageFavoritePro i) { + void addSelected(ImageFavorite i) { if (selectedImageFavorites[i] == null) { selectedImageFavorites[i] = true; } else { @@ -289,10 +265,6 @@ class ImageFavoritesPageState extends State { message: "Sort".tl, child: IconButton( icon: const Icon(Icons.sort), - color: timeFilterSelect != TimeFilterEnum.all.value || - numFilterSelect != numFilterList[0] - ? Theme.of(context).colorScheme.inversePrimary - : null, onPressed: sort, ), ), @@ -352,26 +324,30 @@ class ImageFavoritesPageState extends State { border: InputBorder.none, ), onChanged: (v) { - _debouncer.run(() { + Future.delayed(Duration(milliseconds: 500), () { keyword = _textEditingController.text; update(); - }, Duration(milliseconds: 500)); + }); }, ), ), SliverList( - delegate: SliverChildBuilderDelegate((context, index) { - return ImageFavoritesItem( - isRefreshComicList: isRefreshComicList, - imageFavoritesComic: curImageFavoritesComicList[index], - selectedImageFavorites: selectedImageFavorites, - addSelected: addSelected, - multiSelectMode: multiSelectMode, - finalImageFavoritesComicList: curImageFavoritesComicList, - setRefreshComicList: setRefreshComicList, - imageFavoritesCompute: imageFavoritesCompute, - ); - }, childCount: curImageFavoritesComicList.length)), + delegate: SliverChildBuilderDelegate( + (context, index) { + return _ImageFavoritesItem( + isRefreshComicList: isRefreshComicList, + imageFavoritesComic: comics[index], + selectedImageFavorites: selectedImageFavorites, + addSelected: addSelected, + multiSelectMode: multiSelectMode, + finalImageFavoritesComicList: comics, + setRefreshComicList: setRefreshComicList, + imageFavoritesCompute: imageFavoritesCompute, + ); + }, + childCount: comics.length, + ), + ), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], ); @@ -415,7 +391,6 @@ class ImageFavoritesPageState extends State { initSortType: sortType, initTimeFilterSelect: timeFilterSelect, initNumFilterSelect: numFilterSelect, - finalTimeList: finalTimeList, updateDialogConfig: updateDialogConfig, ); }, @@ -424,19 +399,19 @@ class ImageFavoritesPageState extends State { } class ImageFavoritesDialog extends StatefulWidget { - ImageFavoritesDialog({ + const ImageFavoritesDialog({ super.key, required this.initSortType, required this.initTimeFilterSelect, required this.initNumFilterSelect, - required this.finalTimeList, required this.updateDialogConfig, }); - String initSortType; - String initTimeFilterSelect; - String initNumFilterSelect; - final List finalTimeList; + + final ImageFavoriteSortType initSortType; + final TimeFilterEnum initTimeFilterSelect; + final int initNumFilterSelect; final Function updateDialogConfig; + @override State createState() => ImageFavoritesDialogState(); } @@ -446,6 +421,10 @@ class ImageFavoritesDialogState extends State late TabController controller; List optionTypes = ['Sort', 'Filter']; int tabIndex = 0; + late var sortType = widget.initSortType; + late var timeFilter = widget.initTimeFilterSelect; + late var numFilter = widget.initNumFilterSelect; + void handleTabIndex() { if (mounted) { setState(() { @@ -474,15 +453,11 @@ class ImageFavoritesDialogState extends State @override Widget build(BuildContext context) { Widget tabBar = Material( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - ), - child: FilledTabBar( - key: PageStorageKey(optionTypes), - tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), - controller: controller, - ), + borderRadius: BorderRadius.circular(8), + child: FilledTabBar( + key: PageStorageKey(optionTypes), + tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), + controller: controller, ), ).paddingTop(context.padding.top); return ContentDialog( @@ -490,21 +465,15 @@ class ImageFavoritesDialogState extends State tabBar, tabIndex == 0 ? Column( - children: [ - ImageFavoriteSortType.title, - ImageFavoriteSortType.timeAsc, - ImageFavoriteSortType.timeDesc, - ImageFavoriteSortType.maxFavorites, - ImageFavoriteSortType.favoritesCompareComicPages, - ] + children: ImageFavoriteSortType.values .map( - (e) => RadioListTile( + (e) => RadioListTile( title: Text(e.value.tl), - value: e.value, - groupValue: widget.initSortType, + value: e, + groupValue: sortType, onChanged: (v) { setState(() { - widget.initSortType = v!; + sortType = v!; }); }, ), @@ -516,13 +485,13 @@ class ImageFavoritesDialogState extends State ListTile( title: Text("Time Filter".tl), trailing: Select( - current: widget.initTimeFilterSelect, - values: widget.finalTimeList, + current: timeFilter.value, + values: + TimeFilterEnum.values.map((e) => e.value).toList(), minWidth: 64, onTap: (index) { setState(() { - widget.initTimeFilterSelect = - widget.finalTimeList[index]; + timeFilter = TimeFilterEnum.values[index]; }); }, ), @@ -530,12 +499,12 @@ class ImageFavoritesDialogState extends State ListTile( title: Text("Image Favorites Greater Than".tl), trailing: Select( - current: widget.initNumFilterSelect, - values: numFilterList, + current: numFilter.toString(), + values: numFilterList.map((e) => e.toString()).toList(), minWidth: 64, onTap: (index) { setState(() { - widget.initNumFilterSelect = numFilterList[index]; + numFilter = numFilterList[index]; }); }, ), @@ -546,17 +515,14 @@ class ImageFavoritesDialogState extends State actions: [ FilledButton( onPressed: () { - appdata.implicitData["image_favorites_sort"] = widget.initSortType; - appdata.implicitData["image_favorites_time_filter"] = - widget.initTimeFilterSelect; - appdata.implicitData["image_favorites_num_filter"] = - widget.initNumFilterSelect; + appdata.implicitData["image_favorites_sort"] = sortType.value; + appdata.implicitData["image_favorites_time_filter"] = timeFilter.value; + appdata.implicitData["image_favorites_number_filter"] = numFilter; appdata.writeImplicitData(); controller.removeListener(handleTabIndex); if (mounted) { Navigator.pop(context); - widget.updateDialogConfig(widget.initSortType, - widget.initTimeFilterSelect, widget.initNumFilterSelect); + widget.updateDialogConfig(sortType, timeFilter, numFilter); } }, child: Text("Confirm".tl), diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index 42d61de..a9312f4 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -8,11 +8,13 @@ class ImageFavoritesPhotoView extends StatefulWidget { required this.finalImageFavoritesComicList, required this.goComicInfo, required this.goReaderPage}); + final ImageFavoritesComic imageFavoritesComic; - final ImageFavoritePro imageFavoritePro; + final ImageFavorite imageFavoritePro; final List finalImageFavoritesComicList; final Function(ImageFavoritesComic) goComicInfo; final Function(ImageFavoritesComic, int, int) goReaderPage; + @override State createState() => ImageFavoritesPhotoViewState(); @@ -20,14 +22,16 @@ class ImageFavoritesPhotoView extends StatefulWidget { class ImageFavoritesPhotoViewState extends State { late PageController controller; - Map cancelImageFavorites = {}; + Map cancelImageFavorites = {}; + // 图片当前的 index late int curIndex; late int curImageFavoritesComicIndex; + @override void initState() { curIndex = - widget.imageFavoritesComic.sortedImageFavoritePros.indexWhere((ele) { + widget.imageFavoritesComic.sortedImageFavorites.indexWhere((ele) { return ele.page == widget.imageFavoritePro.page && ele.ep == widget.imageFavoritePro.ep; }); @@ -40,14 +44,14 @@ class ImageFavoritesPhotoViewState extends State { } void onPop() { - ImageFavoriteManager.deleteImageFavoritePro( + ImageFavoriteManager().deleteImageFavorite( cancelImageFavorites.keys.toList()); } PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { - final ImageFavoritePro curImageFavorite = widget + final ImageFavorite curImageFavorite = widget .finalImageFavoritesComicList[curImageFavoritesComicIndex] - .sortedImageFavoritePros[index]; + .sortedImageFavorites[index]; return PhotoViewGalleryPageOptions( // 图片加载器 支持本地、网络 imageProvider: ImageFavoritesProvider(curImageFavorite), @@ -60,7 +64,7 @@ class ImageFavoritesPhotoViewState extends State { }); } - Future _getCurrentImageData(ImageFavoritePro temp) async { + Future _getCurrentImageData(ImageFavorite temp) async { return (await CacheManager() .findCache(ImageFavoritesProvider.getImageKey(temp)))! .readAsBytes(); @@ -70,10 +74,10 @@ class ImageFavoritesPhotoViewState extends State { Widget build(BuildContext context) { ImageFavoritesComic curComic = widget.finalImageFavoritesComicList[curImageFavoritesComicIndex]; - ImageFavoritePro curImageFavorite = - curComic.sortedImageFavoritePros[curIndex]; + ImageFavorite curImageFavorite = + curComic.sortedImageFavorites[curIndex]; int curPage = curImageFavorite.page; - String pageText = curPage == ImageFavoritesEp.firstPage + String pageText = curPage == firstPage ? 'Cover'.tl : "Page @a".tlParams({'a': curPage}); return PopScope( @@ -88,7 +92,7 @@ class ImageFavoritesPhotoViewState extends State { color: context.colorScheme.surface, ), builder: _buildItem, - itemCount: curComic.sortedImageFavoritePros.length, + itemCount: curComic.sortedImageFavorites.length, loadingBuilder: (context, event) => Center( child: SizedBox( width: 20.0, @@ -102,8 +106,8 @@ class ImageFavoritesPhotoViewState extends State { ), ), enableRotation: true, - customSize: MediaQuery.of(context) - .size, //定义图片默认缩放基础的大小,默认全屏 MediaQuery.of(context).size + customSize: MediaQuery.of(context).size, + //定义图片默认缩放基础的大小,默认全屏 MediaQuery.of(context).size pageController: controller, onPageChanged: (index) { setState(() { @@ -144,7 +148,7 @@ class ImageFavoritesPhotoViewState extends State { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row(children: [ Text( - "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavoritePros.length}", + "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavorites.length}", style: ts.s12), Spacer(), Flexible( diff --git a/lib/pages/image_favorites_page/type.dart b/lib/pages/image_favorites_page/type.dart index 42359be..0e70036 100644 --- a/lib/pages/image_favorites_page/type.dart +++ b/lib/pages/image_favorites_page/type.dart @@ -1,5 +1,3 @@ -import 'package:venera/foundation/log.dart'; - enum ImageFavoriteSortType { title("Title"), timeAsc("Time Asc"), @@ -10,17 +8,10 @@ enum ImageFavoriteSortType { final String value; const ImageFavoriteSortType(this.value); - - static ImageFavoriteSortType fromString(String value) { - for (var type in values) { - if (type.value == value) { - return type; - } - } - return title; - } } +const numFilterList = [0, 1, 2, 5, 10, 20, 50, 100]; + enum TimeFilterEnum { all("All"), lastWeek("Last Week"), @@ -30,50 +21,14 @@ enum TimeFilterEnum { final String value; const TimeFilterEnum(this.value); - static TimeFilterEnum fromString(String value) { - for (var type in values) { - if (type.value == value) { - return type; - } - } - return all; - } -} -const timeFilterList = [ - TimeFilterEnum.all, - TimeFilterEnum.lastWeek, - TimeFilterEnum.lastMonth, - TimeFilterEnum.lastHalfYear, - TimeFilterEnum.lastYear, -]; -const numFilterList = ['0', '1', '2', '5', '10', '20', '50', '100']; -getDateTimeRangeFromFilter(String timeFilter) { - DateTime now = DateTime.now(); - DateTime start = now; - DateTime end = now; - try { - if (timeFilter == TimeFilterEnum.all.value) { - start = DateTime(2025, 1, 1); - end = DateTime(2099, 12, 31); - } else if (timeFilter == TimeFilterEnum.lastWeek.value) { - start = now.subtract(const Duration(days: 7)); - } else if (timeFilter == TimeFilterEnum.lastMonth.value) { - start = now.subtract(const Duration(days: 30)); - } else if (timeFilter == TimeFilterEnum.lastHalfYear.value) { - start = now.subtract(const Duration(days: 180)); - } else if (timeFilter == TimeFilterEnum.lastYear.value) { - start = now.subtract(const Duration(days: 365)); - } else { - // 是 2024, 2025 之类的 - int year = int.parse(timeFilter); - start = DateTime(year, 1, 1); - end = DateTime(year, 12, 31, 23, 59, 59); - } - } catch (e) { - Log.error("Date compute", e); + Duration get duration { + return switch (this) { + all => Duration(days: 365 * 100), + lastWeek => Duration(days: 7), + lastMonth => Duration(days: 30), + lastHalfYear => Duration(days: 180), + lastYear => Duration(days: 365), + }; } - - List ranges = [start, end]; - return ranges; } diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index 1139272..37cdd8e 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -33,7 +33,8 @@ class _ReaderWithLoadingState history: data.history, initialChapter: widget.initialEp ?? data.history.ep, initialPage: widget.initialPage ?? data.history.page, - comicDetails: data.comicDetails, + author: data.author, + tags: data.tags, ); } @@ -54,17 +55,19 @@ class _ReaderWithLoadingState } return Res( ReaderProps( - type: ComicType.fromKey(widget.sourceKey), - cid: widget.id, - name: localComic.title, - chapters: localComic.chapters, - history: history ?? - History.fromModel( - model: localComic, - ep: 0, - page: 0, - ), - comicDetails: localComic), + type: ComicType.fromKey(widget.sourceKey), + cid: widget.id, + name: localComic.title, + chapters: localComic.chapters, + history: history ?? + History.fromModel( + model: localComic, + ep: 0, + page: 0, + ), + author: localComic.subtitle, + tags: localComic.tags, + ), ); } else { var comic = await comicSource.loadComicInfo!(widget.id); @@ -73,17 +76,19 @@ class _ReaderWithLoadingState } return Res( ReaderProps( - type: ComicType.fromKey(widget.sourceKey), - cid: widget.id, - name: comic.data.title, - chapters: comic.data.chapters, - history: history ?? - History.fromModel( - model: comic.data, - ep: 0, - page: 0, - ), - comicDetails: comic.data), + type: ComicType.fromKey(widget.sourceKey), + cid: widget.id, + name: comic.data.title, + chapters: comic.data.chapters, + history: history ?? + History.fromModel( + model: comic.data, + ep: 0, + page: 0, + ), + author: comic.data.findAuthor() ?? "", + tags: comic.data.plainTags, + ), ); } } @@ -99,8 +104,10 @@ class ReaderProps { final Map? chapters; final History history; - // 缺少作者, 现在的作者都是上传者 - final Object comicDetails; + + final String author; + + final List tags; const ReaderProps({ required this.type, @@ -108,6 +115,7 @@ class ReaderProps { required this.name, required this.chapters, required this.history, - required this.comicDetails, + required this.author, + required this.tags, }); } diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index 87dba0f..37018ac 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -20,6 +20,7 @@ import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/reader_image.dart'; import 'package:venera/foundation/local.dart'; @@ -30,6 +31,7 @@ import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; +import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'package:venera/utils/volume.dart'; import 'package:window_manager/window_manager.dart'; @@ -58,12 +60,15 @@ class Reader extends StatefulWidget { required this.history, this.initialPage, this.initialChapter, - required this.comicDetails, + required this.author, + required this.tags, }); final ComicType type; - final Object comicDetails; + final String author; + + final List tags; final String cid; @@ -99,8 +104,6 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { String get cid => widget.cid; - Object get comicDetails => widget.comicDetails; - String get eid => widget.chapters?.keys.elementAt(chapter - 1) ?? '0'; List? images; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 016c970..e0a2d6b 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -240,22 +240,18 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } bool isLiked() { - try { - String cid = context.reader.cid; - String eid = context.reader.eid; - int ep = context.reader.chapter; - int page = context.reader.page; - String sourceKey = context.reader.type.sourceKey; - return ImageFavoriteManager.isHas(cid, sourceKey, eid, page, ep); - } catch (e, stackTrace) { - Log.error("Unhandled Exception", e.toString(), stackTrace); - return false; - } + return ImageFavoriteManager().has( + context.reader.cid, + context.reader.type.sourceKey, + context.reader.eid, + context.reader.page, + context.reader.chapter, + ); } void imageFavoritesAction() { try { - if (context.reader.images![0].contains('file:///')) { + if (context.reader.images![0].contains('file://')) { showToast( message: "Local comic collection is not supported at present".tl, context: context); @@ -270,69 +266,64 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { int page = context.reader.page; String sourceKey = context.reader.type.sourceKey; String imageKey = context.reader.images![page - 1]; - List tags = []; + List tags = context.reader.widget.tags; + String author = context.reader.widget.author; - String author = ""; var epName = context.reader.widget.chapters?.values .elementAtOrNull(context.reader.chapter - 1) ?? "E${context.reader.chapter}"; - Object comicDetails = context.reader.comicDetails; - if (comicDetails is ComicDetails) { - ImageFavoritesSomething something = - ImageFavoritesComic.getSomethingFromComicDetails(comicDetails, ep); - tags = something.tags; - author = something.author; - } else if (comicDetails is LocalComic) { - tags = comicDetails.tags; - } - var translatedTags = ImageFavoritesComic.tagsToTranslated(tags); + var translatedTags = tags.map((e) => e.translateTagsToCN).toList(); if (isLiked()) { - if (page == ImageFavoritesEp.firstPage) { + if (page == firstPage) { showToast( - message: "The cover cannot be uncollected here".tl, - context: context); + message: "The cover cannot be uncollected here".tl, + context: context, + ); return; } - ImageFavoriteManager.deleteImageFavoritePro([ - ImageFavoritePro(page, imageKey, null, eid, id, ep, sourceKey, epName) + ImageFavoriteManager().deleteImageFavorite([ + ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName) ]); showToast( - message: "Uncollect the image".tl, context: context, seconds: 1); + message: "Uncollected the image".tl, + context: context, + seconds: 1, + ); } else { - ImageFavoritesComic? imageFavoritesComic = ImageFavoriteManager - .imageFavoritesComicList - .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); - imageFavoritesComic ??= ImageFavoritesComic( - id, - [], - title, - sourceKey, - tags, - translatedTags, - DateTime.now(), - author, - {}, - subTitle, - maxPage); - ImageFavoritePro imageFavoritePro = ImageFavoritePro( - page, imageKey, null, eid, id, ep, sourceKey, epName); + var imageFavoritesComic = ImageFavoriteManager().find(id, sourceKey) ?? + ImageFavoritesComic( + id, + [], + title, + sourceKey, + tags, + translatedTags, + DateTime.now(), + author, + {}, + subTitle, + maxPage, + ); + ImageFavorite imageFavorite = + ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName); ImageFavoritesEp? imageFavoritesEp = imageFavoritesComic.imageFavoritesEp.firstWhereOrNull((e) { return e.ep == ep; }); if (imageFavoritesEp == null) { - if (page != ImageFavoritesEp.firstPage) { - ImageFavoritePro copy = ImageFavoritePro.copy(imageFavoritePro); - copy.page = ImageFavoritesEp.firstPage; - copy.imageKey = context.reader.images![0]; - copy.isAutoFavorite = true; + if (page != firstPage) { + var copy = imageFavorite.copyWith( + page: firstPage, + isAutoFavorite: true, + imageKey: context.reader.images![0], + ); // 不是第一页的话, 自动塞一个封面进去 imageFavoritesEp = ImageFavoritesEp( - eid, ep, [copy, imageFavoritePro], epName, maxPage); + eid, ep, [copy, imageFavorite], epName, maxPage); } else { imageFavoritesEp = - ImageFavoritesEp(eid, ep, [imageFavoritePro], epName, maxPage); + ImageFavoritesEp(eid, ep, [imageFavorite], epName, maxPage); } imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp); } else { @@ -343,23 +334,24 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } else { // 避免多章节漫画源的章节顺序发生变化, 如果情况比较多, 做一个以eid为准更新ep的功能 showToast( - message: - "The chapter order of the comic may have changed, temporarily not supported for collection" - .tl, - context: context); + message: + "The chapter order of the comic may have changed, temporarily not supported for collection" + .tl, + context: context, + ); return; } } - imageFavoritesEp.imageFavorites.add(imageFavoritePro); + imageFavoritesEp.imageFavorites.add(imageFavorite); } - ImageFavoriteManager.addOrUpdateOrDelete(imageFavoritesComic); + ImageFavoriteManager().addOrUpdateOrDelete(imageFavoritesComic); showToast( message: "Successfully collected".tl, context: context, seconds: 1); } update(); } catch (e, stackTrace) { - Log.error("Unhandled Exception", e.toString(), stackTrace); + Log.error("Image Favorite", e, stackTrace); showToast(message: e.toString(), context: context, seconds: 1); } } @@ -729,8 +721,6 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { onChanged: (key) { if (key == "readerMode") { context.reader.mode = ReaderMode.fromKey(appdata.settings[key]); - // 这行代码似乎会导致页面白屏, 所有 widget 都不显示 - // App.rootContext.pop(); } if (key == "enableTurnPageByVolumeKey") { if (appdata.settings[key]) { diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 459a894..14f4747 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -208,7 +208,7 @@ Future importPicaData(File file) async { ); } List imageFavoritesComicList = - ImageFavoriteManager.imageFavoritesComicList; + ImageFavoriteManager().imageFavoritesComicList; for (var comic in db.select("SELECT * FROM image_favorites;")) { String sourceKey = comic["id"].split("-")[0]; // 换名字了, 绅士漫画 @@ -226,8 +226,8 @@ Future importPicaData(File file) async { String epName = ""; ImageFavoritesComic? tempComic = imageFavoritesComicList .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); - ImageFavoritePro curImageFavorite = - ImageFavoritePro(page, "", null, "", id, ep, sourceKey, epName); + ImageFavorite curImageFavorite = + ImageFavorite(page, "", null, "", id, ep, sourceKey, epName); if (tempComic == null) { tempComic = ImageFavoritesComic(id, [], title, sourceKey, [], [], DateTime.now(), "", {}, "", 1); @@ -252,7 +252,7 @@ Future importPicaData(File file) async { } } for (var temp in imageFavoritesComicList) { - ImageFavoriteManager.addOrUpdateOrDelete(temp); + ImageFavoriteManager().addOrUpdateOrDelete(temp); } } catch (e, stack) { Log.error("Import Data", "Failed to import history: $e", stack); diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart deleted file mode 100644 index 90d9bf1..0000000 --- a/lib/utils/utils.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:async'; - -class Debouncer { - Timer? _timer; - - void run(void Function() action, - [Duration delay = const Duration(milliseconds: 300)]) { - _timer?.cancel(); - _timer = Timer(delay, action); - } - - void dispose() { - _timer?.cancel(); - } -} From 1233dc4d54ffed34069ba0c3ee205eb19f34b040 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 11 Jan 2025 18:35:22 +0800 Subject: [PATCH 19/36] fix updateValue --- lib/foundation/image_favorites.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 10fbd4c..6cef4c1 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -191,12 +191,16 @@ class ImageFavoriteManager with ChangeNotifier { factory ImageFavoriteManager() => (_cache ??= ImageFavoriteManager._()); + Timer? updateTimer; + void updateValue() { // 立刻触发, 让阅读界面可以看到图片收藏的图标状态更新了 - imageFavoritesComicList = getAll(null); + imageFavoritesComicList = getAll(); // 避免从pica导入的时候, 疯狂触发更新 - Future.delayed(const Duration(seconds: 4), () { + updateTimer?.cancel(); + updateTimer = Timer(const Duration(seconds: 4), () { notifyListeners(); + updateTimer = null; }); } From 9b5bd24e7b84d2e1420706bbd49aaa2964aac9b5 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sat, 11 Jan 2025 19:05:15 +0800 Subject: [PATCH 20/36] =?UTF-8?q?feat:=20=E5=8F=8C=E5=87=BB=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=8F=90=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 6 +++-- .../image_favorites_page.dart | 24 ++++++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index cd15699..86dd1a2 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -251,7 +251,8 @@ "Aggregated Search": "聚合搜索", "Local comic collection is not supported at present": "本地收藏暂不支持", "The cover cannot be uncollected here": "封面不能在此取消收藏", - "Uncollect the image": "取消收藏图片", + "Uncollected the image": "取消收藏图片", + "Double tap comic copy title, double tap image open gallery": "双击漫画复制标题, 双击图片放大浏览", "Successfully collected": "收藏成功", "Collect the image": "收藏图片", "Quick collect image": "快速收藏图片", @@ -562,7 +563,8 @@ "Click favorite": "點擊收藏", "Local comic collection is not supported at present": "本地收藏暫不支持", "The cover cannot be uncollected here": "封面不能在此取消收藏", - "Uncollect the image": "取消收藏圖片", + "Uncollected the image": "取消收藏圖片", + "Double tap comic copy title, double tap image open gallery": "雙擊漫畫複製標題, 雙擊圖片放大瀏覽", "Successfully collected": "收藏成功", "Collect the image": "收藏圖片", "Quick collect image": "快速收藏圖片", diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index ad22124..4eb6dd1 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -331,6 +331,27 @@ class ImageFavoritesPageState extends State { }, ), ), + if (appdata.implicitData['Guide_imageFavoritesPage_DoubleTap'] != true) + SliverToBoxAdapter( + child: Row( + children: [ + Text( + 'Double tap comic copy title, double tap image open gallery' + .tl, + ), + Spacer(), + IconButton( + icon: const Icon(Icons.check), + onPressed: () { + appdata.implicitData['Guide_imageFavoritesPage_DoubleTap'] = + true; + appdata.writeImplicitData(); + update(); + }, + ) + ], + ).paddingHorizontal(8), + ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) { @@ -516,7 +537,8 @@ class ImageFavoritesDialogState extends State FilledButton( onPressed: () { appdata.implicitData["image_favorites_sort"] = sortType.value; - appdata.implicitData["image_favorites_time_filter"] = timeFilter.value; + appdata.implicitData["image_favorites_time_filter"] = + timeFilter.value; appdata.implicitData["image_favorites_number_filter"] = numFilter; appdata.writeImplicitData(); controller.removeListener(handleTabIndex); From 19b260ffb9e217f2369a118fd0ea8811fbc58c51 Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sat, 11 Jan 2025 22:26:48 +0800 Subject: [PATCH 21/36] =?UTF-8?q?fix:=20=E8=A2=AB=E7=A1=AE=E5=AE=9A?= =?UTF-8?q?=E5=8F=96=E6=B6=88=E6=94=B6=E8=97=8F=E7=9A=84=E6=89=8D=E5=88=A0?= =?UTF-8?q?=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../image_favorites_photo_view.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index a9312f4..e817f7e 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -44,8 +44,11 @@ class ImageFavoritesPhotoViewState extends State { } void onPop() { - ImageFavoriteManager().deleteImageFavorite( - cancelImageFavorites.keys.toList()); + List tempList = cancelImageFavorites.entries + .where((e) => e.value == true) + .map((e) => e.key) + .toList(); + ImageFavoriteManager().deleteImageFavorite(tempList); } PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { @@ -74,12 +77,10 @@ class ImageFavoritesPhotoViewState extends State { Widget build(BuildContext context) { ImageFavoritesComic curComic = widget.finalImageFavoritesComicList[curImageFavoritesComicIndex]; - ImageFavorite curImageFavorite = - curComic.sortedImageFavorites[curIndex]; + ImageFavorite curImageFavorite = curComic.sortedImageFavorites[curIndex]; int curPage = curImageFavorite.page; - String pageText = curPage == firstPage - ? 'Cover'.tl - : "Page @a".tlParams({'a': curPage}); + String pageText = + curPage == firstPage ? 'Cover'.tl : "Page @a".tlParams({'a': curPage}); return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { From 7307dbc775aa578a28a067b4b7ea4a3120e93877 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 12 Jan 2025 10:52:49 +0800 Subject: [PATCH 22/36] Refactor ImageFavoritesPage --- assets/translation.json | 10 +- lib/components/appbar.dart | 45 +++ lib/components/select.dart | 4 +- .../image_favorites_item.dart | 8 +- .../image_favorites_page.dart | 284 ++++++++++-------- lib/pages/image_favorites_page/type.dart | 93 +++++- lib/utils/ext.dart | 2 + 7 files changed, 305 insertions(+), 141 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 86dd1a2..194345d 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -305,7 +305,10 @@ "Aggregated": "聚合", "Default Search Target": "默认搜索目标", "Auto Language Filters": "自动语言筛选", - "Check for updates on startup": "启动时检查更新" + "Check for updates on startup": "启动时检查更新", + "Start Time": "开始时间", + "End Time": "结束时间", + "Custom": "自定义" }, "zh_TW": { "Home": "首頁", @@ -613,6 +616,9 @@ "Aggregated": "聚合", "Default Search Target": "默認搜索目標", "Auto Language Filters": "自動語言篩選", - "Check for updates on startup": "啟動時檢查更新" + "Check for updates on startup": "啟動時檢查更新", + "Start Time": "開始時間", + "End Time": "結束時間", + "Custom": "自定義" } } \ No newline at end of file diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index 841d5fe..56f5dc4 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -577,6 +577,51 @@ class _IndicatorPainter extends CustomPainter { } } +class TabViewBody extends StatefulWidget { + /// Create a tab view body, which will show the child at the current tab index. + const TabViewBody({super.key, required this.children, this.controller}); + + final List children; + + final TabController? controller; + + @override + State createState() => _TabViewBodyState(); +} + +class _TabViewBodyState extends State { + late TabController _controller; + + int _currentIndex = 0; + + void updateIndex() { + if (_controller.index != _currentIndex) { + setState(() { + _currentIndex = _controller.index; + }); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _controller = widget.controller ?? DefaultTabController.of(context); + _controller.addListener(updateIndex); + } + + @override + void dispose() { + super.dispose(); + _controller.removeListener(updateIndex); + } + + @override + Widget build(BuildContext context) { + return widget.children[_currentIndex]; + } +} + + class SearchBarController { _SearchBarMixin? _state; diff --git a/lib/components/select.dart b/lib/components/select.dart index 1e5b456..201c78f 100644 --- a/lib/components/select.dart +++ b/lib/components/select.dart @@ -50,7 +50,7 @@ class Select extends StatelessWidget { .map((e) => PopupMenuItem( height: App.isMobile ? 46 : 40, value: e, - child: Text(e.tl), + child: Text(e), )) .toList(), ).then((value) { @@ -66,7 +66,7 @@ class Select extends StatelessWidget { constraints: BoxConstraints( minWidth: minWidth != null ? (minWidth! - 32) : 0, ), - child: Text((current ?? ' ').tl, style: ts.s14), + child: Text(current ?? ' ', style: ts.s14), ), const SizedBox(width: 8), Icon(Icons.arrow_drop_down, color: context.colorScheme.primary), diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index c6fbb54..37edf9e 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -17,8 +17,8 @@ class _ImageFavoritesItem extends StatefulWidget { final Map selectedImageFavorites; final List finalImageFavoritesComicList; final bool multiSelectMode; - final List isRefreshComicList; - final Function(LoadingImageFavoritesComicRes) setRefreshComicList; + final List<_LoadingImageFavoritesComicRes> isRefreshComicList; + final Function(_LoadingImageFavoritesComicRes) setRefreshComicList; final ImageFavoritesCompute? imageFavoritesCompute; @override @@ -30,7 +30,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { // 刷新 imageKey 失败的场景再刷新一次, 再次失败了就不重试了 bool hasRefreshImageKeyOnErr = false; - late LoadingImageFavoritesComicRes loadingImageFavoritesComicRes; + late _LoadingImageFavoritesComicRes loadingImageFavoritesComicRes; // 如果刚从pica导入(没有imageKey) 或者 imageKey 失效了, 刷新一下 void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { @@ -152,7 +152,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { (e) => e.id == widget.imageFavoritesComic.id && e.sourceKey == widget.imageFavoritesComic.sourceKey) ?? - LoadingImageFavoritesComicRes( + _LoadingImageFavoritesComicRes( isLoaded: false, isInvalid: false, id: widget.imageFavoritesComic.id, diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 4eb6dd1..547b40d 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -29,13 +29,13 @@ part "image_favorites_item.dart"; part "image_favorites_photo_view.dart"; -class LoadingImageFavoritesComicRes { +class _LoadingImageFavoritesComicRes { bool isLoaded; bool isInvalid; String id; String sourceKey; - LoadingImageFavoritesComicRes( + _LoadingImageFavoritesComicRes( {required this.isLoaded, required this.isInvalid, required this.id, @@ -48,20 +48,18 @@ class ImageFavoritesPage extends StatefulWidget { final String? initialKeyword; @override - State createState() => ImageFavoritesPageState(); + State createState() => _ImageFavoritesPageState(); } -class ImageFavoritesPageState extends State { +class _ImageFavoritesPageState extends State { late ImageFavoriteSortType sortType; ImageFavoritesCompute? imageFavoritesCompute; - late TimeFilterEnum timeFilterSelect; + late TimeRange timeFilterSelect; late int numFilterSelect; // 所有的图片收藏 List comics = []; - late List timeFilter; - List isRefreshComicList = []; - late TextEditingController _textEditingController; + List<_LoadingImageFavoritesComicRes> isRefreshComicList = []; String keyword = ""; // 进入关键词搜索模式 @@ -74,8 +72,8 @@ class ImageFavoritesPageState extends State { late List imageFavoritePros; // 避免重复请求 - void setRefreshComicList(LoadingImageFavoritesComicRes res) { - LoadingImageFavoritesComicRes? tempRes = + void setRefreshComicList(_LoadingImageFavoritesComicRes res) { + _LoadingImageFavoritesComicRes? tempRes = isRefreshComicList.firstWhereOrNull( (e) => e.id == res.id && e.sourceKey == res.sourceKey); if (tempRes == null) { @@ -92,43 +90,26 @@ class ImageFavoritesPageState extends State { } } - void updateDialogConfig(ImageFavoriteSortType sortType, - TimeFilterEnum timeFilter, int numFilter) { - setState(() { - this.sortType = sortType; - timeFilterSelect = timeFilter; - numFilterSelect = numFilter; - }); - } - - void getInitImageFavorites() async { + void updateImageFavorites() async { imageFavoritePros = []; for (var e in ImageFavoriteManager().imageFavoritesComicList) { imageFavoritePros.addAll(e.sortedImageFavorites); } - getCurImageFavorites(); + sortImageFavorites(); imageFavoritesCompute = await ImageFavoriteManager().computeImageFavorites(); update(); } - void refreshImageFavorites() async { - if (mounted) { - getInitImageFavorites(); - update(); - } - } - - void getCurImageFavorites() { + void sortImageFavorites() { comics = searchMode ? ImageFavoriteManager().search(keyword) : ImageFavoriteManager().getAll(); - var now = DateTime.now(); // 筛选到最终列表 comics = comics.where((ele) { bool isFilter = true; - if (timeFilterSelect != TimeFilterEnum.all) { - isFilter = now.difference(ele.time) <= timeFilterSelect.duration; + if (timeFilterSelect != TimeRange.all) { + isFilter = timeFilterSelect.contains(ele.time); } if (numFilterSelect != numFilterList[0]) { isFilter = ele.sortedImageFavorites.length > numFilterSelect; @@ -161,24 +142,21 @@ class ImageFavoritesPageState extends State { keyword = widget.initialKeyword!; searchMode = true; } - _textEditingController = TextEditingController(text: keyword); sortType = ImageFavoriteSortType.values.firstWhereOrNull( (e) => e.value == appdata.implicitData["image_favorites_sort"]) ?? ImageFavoriteSortType.title; - timeFilterSelect = TimeFilterEnum.values.firstWhereOrNull((e) => - e.value == appdata.implicitData["image_favorites_time_filter"]) ?? - TimeFilterEnum.all; + timeFilterSelect = TimeRange.fromString( + appdata.implicitData["image_favorites_time_filter"]); numFilterSelect = appdata.implicitData["image_favorites_number_filter"] ?? numFilterList[0]; - getInitImageFavorites(); - ImageFavoriteManager().addListener(refreshImageFavorites); + updateImageFavorites(); + ImageFavoriteManager().addListener(updateImageFavorites); super.initState(); } @override void dispose() { - _textEditingController.dispose(); - ImageFavoriteManager().removeListener(refreshImageFavorites); + ImageFavoriteManager().removeListener(updateImageFavorites); scrollController.dispose(); super.dispose(); } @@ -202,36 +180,35 @@ class ImageFavoritesPageState extends State { var scrollController = ScrollController(); - @override - Widget build(BuildContext context) { - getCurImageFavorites(); - void selectAll() { - for (var ele in imageFavoritePros) { - selectedImageFavorites[ele] = true; - } - update(); + void selectAll() { + for (var ele in imageFavoritePros) { + selectedImageFavorites[ele] = true; } + update(); + } - void deSelect() { - setState(() { - selectedImageFavorites.clear(); - }); - } + void deSelect() { + setState(() { + selectedImageFavorites.clear(); + }); + } - void addSelected(ImageFavorite i) { - if (selectedImageFavorites[i] == null) { - selectedImageFavorites[i] = true; - } else { - selectedImageFavorites.remove(i); - } - if (selectedImageFavorites.isEmpty) { - multiSelectMode = false; - } else { - multiSelectMode = true; - } - update(); + void addSelected(ImageFavorite i) { + if (selectedImageFavorites[i] == null) { + selectedImageFavorites[i] = true; + } else { + selectedImageFavorites.remove(i); } + if (selectedImageFavorites.isEmpty) { + multiSelectMode = false; + } else { + multiSelectMode = true; + } + update(); + } + @override + Widget build(BuildContext context) { List selectActions = [ IconButton( icon: const Icon(Icons.select_all), @@ -243,6 +220,7 @@ class ImageFavoritesPageState extends State { onPressed: deSelect), buildMultiSelectMenu(), ]; + var scrollWidget = SmoothCustomScrollView( controller: scrollController, slivers: [ @@ -264,7 +242,9 @@ class ImageFavoritesPageState extends State { Tooltip( message: "Sort".tl, child: IconButton( - icon: const Icon(Icons.sort), + isSelected: timeFilterSelect != TimeRange.all || + numFilterSelect != numFilterList[0], + icon: const Icon(Icons.sort_rounded), onPressed: sort, ), ), @@ -318,16 +298,14 @@ class ImageFavoritesPageState extends State { ), title: TextField( autofocus: true, - controller: _textEditingController, + controller: TextEditingController(text: keyword), decoration: InputDecoration( hintText: "Search".tl, border: InputBorder.none, ), onChanged: (v) { - Future.delayed(Duration(milliseconds: 500), () { - keyword = _textEditingController.text; - update(); - }); + keyword = v; + update(); }, ), ), @@ -408,67 +386,64 @@ class ImageFavoritesPageState extends State { showDialog( context: context, builder: (context) { - return ImageFavoritesDialog( + return _ImageFavoritesDialog( initSortType: sortType, initTimeFilterSelect: timeFilterSelect, initNumFilterSelect: numFilterSelect, - updateDialogConfig: updateDialogConfig, + updateConfig: (sortType, timeFilter, numFilter) { + setState(() { + this.sortType = sortType; + timeFilterSelect = timeFilter; + numFilterSelect = numFilter; + }); + sortImageFavorites(); + }, ); }, ); } } -class ImageFavoritesDialog extends StatefulWidget { - const ImageFavoritesDialog({ - super.key, +class _ImageFavoritesDialog extends StatefulWidget { + const _ImageFavoritesDialog({ required this.initSortType, required this.initTimeFilterSelect, required this.initNumFilterSelect, - required this.updateDialogConfig, + required this.updateConfig, }); final ImageFavoriteSortType initSortType; - final TimeFilterEnum initTimeFilterSelect; + final TimeRange initTimeFilterSelect; final int initNumFilterSelect; - final Function updateDialogConfig; + final Function updateConfig; @override - State createState() => ImageFavoritesDialogState(); + State<_ImageFavoritesDialog> createState() => _ImageFavoritesDialogState(); } -class ImageFavoritesDialogState extends State - with TickerProviderStateMixin { - late TabController controller; +class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> { List optionTypes = ['Sort', 'Filter']; - int tabIndex = 0; late var sortType = widget.initSortType; - late var timeFilter = widget.initTimeFilterSelect; late var numFilter = widget.initNumFilterSelect; - - void handleTabIndex() { - if (mounted) { - setState(() { - tabIndex = controller.index; - }); - } - } + late TimeRangeType timeRangeType; + DateTime? start; + DateTime? end; @override void initState() { - controller = TabController( - length: 2, - vsync: this, - ); - controller.addListener(handleTabIndex); super.initState(); - } - - @override - void dispose() { - controller.removeListener(handleTabIndex); - controller.dispose(); - super.dispose(); + timeRangeType = switch(widget.initTimeFilterSelect) { + TimeRange.all => TimeRangeType.all, + TimeRange.lastWeek => TimeRangeType.lastWeek, + TimeRange.lastMonth => TimeRangeType.lastMonth, + TimeRange.lastHalfYear => TimeRangeType.lastHalfYear, + TimeRange.lastYear => TimeRangeType.lastYear, + _ => TimeRangeType.custom, + }; + if (timeRangeType == TimeRangeType.custom) { + end = widget.initTimeFilterSelect.end; + start = end!.subtract(widget.initTimeFilterSelect.duration); + } } @override @@ -478,14 +453,17 @@ class ImageFavoritesDialogState extends State child: FilledTabBar( key: PageStorageKey(optionTypes), tabs: optionTypes.map((e) => Tab(text: e.tl, key: Key(e))).toList(), - controller: controller, ), ).paddingTop(context.padding.top); return ContentDialog( - content: Column(mainAxisSize: MainAxisSize.min, children: [ - tabBar, - tabIndex == 0 - ? Column( + content: DefaultTabController( + length: 2, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + tabBar, + TabViewBody(children: [ + Column( children: ImageFavoriteSortType.values .map( (e) => RadioListTile( @@ -500,23 +478,71 @@ class ImageFavoritesDialogState extends State ), ) .toList(), - ) - : Column( + ), + Column( children: [ ListTile( title: Text("Time Filter".tl), trailing: Select( - current: timeFilter.value, - values: - TimeFilterEnum.values.map((e) => e.value).toList(), + current: timeRangeType.value.tl, + values: TimeRangeType.values + .map((e) => e.value.tl) + .toList(), minWidth: 64, onTap: (index) { setState(() { - timeFilter = TimeFilterEnum.values[index]; + timeRangeType = TimeRangeType.values[index]; }); }, ), ), + if (timeRangeType == TimeRangeType.custom) + Column( + children: [ + ListTile( + title: Text("Start Time".tl), + trailing: TextButton( + onPressed: () async { + final date = await showDatePicker( + context: context, + initialDate: start ?? DateTime.now(), + firstDate: DateTime(2000), + lastDate: end ?? DateTime.now(), + ); + if (date != null) { + setState(() { + start = date; + }); + } + }, + child: Text(start == null + ? "Select Date".tl + : DateFormat("yyyy-MM-dd").format(start!)), + ), + ), + ListTile( + title: Text("End Time".tl), + trailing: TextButton( + onPressed: () async { + final date = await showDatePicker( + context: context, + initialDate: end ?? DateTime.now(), + firstDate: start ?? DateTime(2000), + lastDate: DateTime.now(), + ); + if (date != null) { + setState(() { + end = date; + }); + } + }, + child: Text(end == null + ? "Select Date".tl + : DateFormat("yyyy-MM-dd").format(end!)), + ), + ), + ], + ), ListTile( title: Text("Image Favorites Greater Than".tl), trailing: Select( @@ -532,19 +558,37 @@ class ImageFavoritesDialogState extends State ) ], ) - ]), + ]), + ], + ), + ), actions: [ FilledButton( onPressed: () { appdata.implicitData["image_favorites_sort"] = sortType.value; + TimeRange timeRange; + if (timeRangeType == TimeRangeType.custom) { + timeRange = TimeRange( + end: end, + duration: end!.difference(start!), + ); + } else { + timeRange = switch(timeRangeType) { + TimeRangeType.all => TimeRange.all, + TimeRangeType.lastWeek => TimeRange.lastWeek, + TimeRangeType.lastMonth => TimeRange.lastMonth, + TimeRangeType.lastHalfYear => TimeRange.lastHalfYear, + TimeRangeType.lastYear => TimeRange.lastYear, + _ => TimeRange.all, + }; + } appdata.implicitData["image_favorites_time_filter"] = - timeFilter.value; + timeRange.toString(); appdata.implicitData["image_favorites_number_filter"] = numFilter; appdata.writeImplicitData(); - controller.removeListener(handleTabIndex); if (mounted) { Navigator.pop(context); - widget.updateDialogConfig(sortType, timeFilter, numFilter); + widget.updateConfig(sortType, timeRange, numFilter); } }, child: Text("Confirm".tl), diff --git a/lib/pages/image_favorites_page/type.dart b/lib/pages/image_favorites_page/type.dart index 0e70036..701ab74 100644 --- a/lib/pages/image_favorites_page/type.dart +++ b/lib/pages/image_favorites_page/type.dart @@ -1,3 +1,5 @@ +import 'package:venera/utils/ext.dart'; + enum ImageFavoriteSortType { title("Title"), timeAsc("Time Asc"), @@ -12,23 +14,88 @@ enum ImageFavoriteSortType { const numFilterList = [0, 1, 2, 5, 10, 20, 50, 100]; -enum TimeFilterEnum { +class TimeRange { + /// End of the range, null means now + final DateTime? end; + + /// Duration of the range + final Duration duration; + + /// Create a time range + const TimeRange({this.end, required this.duration}); + + static const all = TimeRange(end: null, duration: Duration.zero); + + static const lastWeek = TimeRange(end: null, duration: Duration(days: 7)); + + static const lastMonth = TimeRange(end: null, duration: Duration(days: 30)); + + static const lastHalfYear = + TimeRange(end: null, duration: Duration(days: 180)); + + static const lastYear = TimeRange(end: null, duration: Duration(days: 365)); + + @override + String toString() { + return "${end?.millisecond}:${duration.inMilliseconds}"; + } + + /// Parse a time range from a string, return [TimeRange.all] if failed + factory TimeRange.fromString(String? str) { + if (str == null) { + return TimeRange.all; + } + final parts = str.split(":"); + if (parts.length != 2 || !parts[0].isInt || !parts[1].isInt) { + return TimeRange.all; + } + final end = parts[0] == "null" + ? null + : DateTime.fromMillisecondsSinceEpoch(int.parse(parts[0])); + final duration = Duration(milliseconds: int.parse(parts[1])); + return TimeRange(end: end, duration: duration); + } + + /// Check if a time is in the range + bool contains(DateTime time) { + if (end != null && time.isAfter(end!)) { + return false; + } + if (duration == Duration.zero) { + return true; + } + final start = end == null + ? DateTime.now().subtract(duration) + : end!.subtract(duration); + return time.isAfter(start); + } + + @override + bool operator ==(Object other) { + return other is TimeRange && other.end == end && other.duration == duration; + } + + @override + int get hashCode => end.hashCode ^ duration.hashCode; + + static const List values = [ + all, + lastWeek, + lastMonth, + lastHalfYear, + lastYear, + ]; +} + +enum TimeRangeType { all("All"), lastWeek("Last Week"), lastMonth("Last Month"), lastHalfYear("Last Half Year"), - lastYear("Last Year"); // 单本收藏数最多排序 + lastYear("Last Year"), + custom("Custom"); final String value; - const TimeFilterEnum(this.value); - - Duration get duration { - return switch (this) { - all => Duration(days: 365 * 100), - lastWeek => Duration(days: 7), - lastMonth => Duration(days: 30), - lastHalfYear => Duration(days: 180), - lastYear => Duration(days: 365), - }; - } + + const TimeRangeType(this.value); } diff --git a/lib/utils/ext.dart b/lib/utils/ext.dart index 172bab3..5a9e534 100644 --- a/lib/utils/ext.dart +++ b/lib/utils/ext.dart @@ -95,6 +95,8 @@ extension StringExt on String{ bool get isURL => _isURL(); bool get isNum => double.tryParse(this) != null; + + bool get isInt => int.tryParse(this) != null; } abstract class ListOrNull{ From 2d492ae2bcc5cc75298c8092d0b91dd1bc8510e2 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 12 Jan 2025 11:12:35 +0800 Subject: [PATCH 23/36] translate author --- lib/pages/home_page.dart | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 13a3957..037c067 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -916,7 +916,7 @@ class __AnimatedDownloadingIconState extends State<_AnimatedDownloadingIcon> } } -enum ImageFavoritesComputeType { +enum _ImageFavoritesComputeType { tags, authors, comicByNum, @@ -967,13 +967,15 @@ class _ImageFavoritesState extends State { Widget roundBtn( TextWithCount textWithCount, - ImageFavoritesComputeType type, + _ImageFavoritesComputeType type, ) { var enableTranslate = App.locale.languageCode == 'zh'; var text = enableTranslate - ? textWithCount.text.translateTagsToCN + ? type == _ImageFavoritesComputeType.authors + ? TagsTranslation.translationTagWithNamespace(textWithCount.text, "artist") + : textWithCount.text.translateTagsToCN : textWithCount.text; - if (type == ImageFavoritesComputeType.tags) { + if (type == _ImageFavoritesComputeType.tags) { if (text.contains(':')) { text = text.split(':').last; } @@ -999,7 +1001,7 @@ class _ImageFavoritesState extends State { } Widget listRoundBtn( - List list, ImageFavoritesComputeType type) { + List list, _ImageFavoritesComputeType type) { return Expanded( child: SizedBox( height: 24, @@ -1069,7 +1071,7 @@ class _ImageFavoritesState extends State { style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.authors, - ImageFavoritesComputeType.authors) + _ImageFavoritesComputeType.authors) ], ), const SizedBox(height: 4), @@ -1080,7 +1082,7 @@ class _ImageFavoritesState extends State { style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.tags, - ImageFavoritesComputeType.tags) + _ImageFavoritesComputeType.tags) ], ), const SizedBox(height: 4), @@ -1091,7 +1093,7 @@ class _ImageFavoritesState extends State { style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.comicByNum, - ImageFavoritesComputeType.comicByNum) + _ImageFavoritesComputeType.comicByNum) ], ), const SizedBox(height: 4), @@ -1102,7 +1104,7 @@ class _ImageFavoritesState extends State { style: const TextStyle(fontSize: 13), ), listRoundBtn(imageFavoritesCompute!.comicByPercentage, - ImageFavoritesComputeType.comicByPercentage) + _ImageFavoritesComputeType.comicByPercentage) ], ), ], From 2d59831459e822e49c970138205f08a05ab1af8c Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 12 Jan 2025 11:46:37 +0800 Subject: [PATCH 24/36] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=94=B9=E5=88=B0dialog=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 8 +++- lib/foundation/image_favorites.dart | 10 +++-- lib/pages/home_page.dart | 41 +++++++++++++++---- .../image_favorites_photo_view.dart | 7 +++- 4 files changed, 52 insertions(+), 14 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 194345d..0560450 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -260,7 +260,8 @@ "Double Tap": "双击", "Swipe": "滑动", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在图片浏览页面, 你可以根据你的阅读模式横滑或者竖滑快速收藏图片", - "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的, 括号后是图片数量或图片数比漫画页数", + "Calculate your favorite from @a comics and @b images": "从 @a 本漫画和 @b 张图片中, 计算你最喜欢的", + "After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "括号后是图片数量或图片数比漫画页数", "The chapter order of the comic may have changed, temporarily not supported for collection": "漫画的章节顺序可能发生了变化, 暂不支持收藏此章节", "Author: ": "作者: ", "Tags: ": "标签: ", @@ -285,6 +286,7 @@ "Image Favorites": "图片收藏", "Title": "标题", "@a Cover": "@a 封面", + "Delete @a images": "删除 @a 张图片", "Update the page number by the latest collection": "按最新收藏更新页数", "Copy the title successfully": "复制标题成功", "The comic is invalid, please long press to delete, you can double click the title to copy": "该漫画已失效, 请长按删除, 可以双击标题进行复制", @@ -572,7 +574,8 @@ "Collect the image": "收藏圖片", "Quick collect image": "快速收藏圖片", "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode": "在圖片瀏覽頁面, 你可以根據你的閱讀模式橫向或者縱向滑動快速收藏圖片", - "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的, 括號後是圖片數量或圖片數比漫畫頁數", + "Calculate your favorite from @a comics and @b images": "從 @a 本漫畫和 @b 張圖片中, 計算你最喜歡的", + "After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages": "括號後是圖片數量或圖片數比漫畫頁數", "The chapter order of the comic may have changed, temporarily not supported for collection": "漫畫的章節順序可能發生了變化, 暫不支持收藏此章節", "Author: ": "作者: ", "Tags: ": "標籤: ", @@ -600,6 +603,7 @@ "Image Favorites": "圖片收藏", "Title": "標題", "@a Cover": "@a 封面", + "Delete @a images": "刪除 @a 張圖片", "Update the page number by the latest collection": "按最新收藏更新頁數", "Copy the title successfully": "複製標題成功", "The comic is invalid, please long press to delete, you can double click the title to copy": "該漫畫已失效, 請長按刪除, 可以雙擊標題進行複製", diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 6cef4c1..7274399 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -377,10 +377,12 @@ class ImageFavoriteManager with ChangeNotifier { } void deleteImageFavorite(List imageFavoriteList) { + if (imageFavoriteList.isEmpty) { + return; + } for (var e in imageFavoritesComicList) { // 找到同一个漫画中的需要删除的具体图片 - List filterImageFavorites = - imageFavoriteList.where((i) { + List filterImageFavorites = imageFavoriteList.where((i) { return i.id == e.id && i.sourceKey == e.sourceKey; }).toList(); if (filterImageFavorites.isNotEmpty) { @@ -476,8 +478,8 @@ class ImageFavoriteManager with ChangeNotifier { if (comic.author != "") { String finalAuthor = comic.author; - authorCount[finalAuthor] = (authorCount[finalAuthor] ?? 0) + - comic.sortedImageFavorites.length; + authorCount[finalAuthor] = + (authorCount[finalAuthor] ?? 0) + comic.sortedImageFavorites.length; } // 小于10页的漫画不统计 if (comic.maxPageFromEp < 10) { diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 037c067..a2277af 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -943,7 +943,7 @@ class _ImageFavoritesState extends State { } setState(() {}); imageFavoritesCompute = - await ImageFavoriteManager().computeImageFavorites(); + await ImageFavoriteManager().computeImageFavorites(); if (mounted) { setState(() {}); } @@ -966,14 +966,15 @@ class _ImageFavoritesState extends State { } Widget roundBtn( - TextWithCount textWithCount, - _ImageFavoritesComputeType type, - ) { + TextWithCount textWithCount, + _ImageFavoritesComputeType type, + ) { var enableTranslate = App.locale.languageCode == 'zh'; var text = enableTranslate ? type == _ImageFavoritesComputeType.authors - ? TagsTranslation.translationTagWithNamespace(textWithCount.text, "artist") - : textWithCount.text.translateTagsToCN + ? TagsTranslation.translationTagWithNamespace( + textWithCount.text, "artist") + : textWithCount.text.translateTagsToCN : textWithCount.text; if (type == _ImageFavoritesComputeType.tags) { if (text.contains(':')) { @@ -1046,6 +1047,32 @@ class _ImageFavoritesState extends State { Center( child: Text('Image Favorites'.tl, style: ts.s18), ), + const SizedBox(width: 4), + Button.icon( + size: 18, + icon: const Icon(Icons.help_outline), + onPressed: () { + showDialog( + context: context, + builder: (context) { + return ContentDialog( + title: "Help".tl, + content: Text( + "After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages" + .tl) + .paddingHorizontal(16) + .fixWidth(double.infinity), + actions: [ + Button.filled( + onPressed: context.pop, + child: Text("OK".tl), + ), + ], + ); + }, + ); + }, + ), const Spacer(), const Icon(Icons.arrow_right), ], @@ -1055,7 +1082,7 @@ class _ImageFavoritesState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Calculate your favorite from @a comics and @b images, After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages" + "Calculate your favorite from @a comics and @b images" .tlParams({ "a": ImageFavoriteManager().length.toString(), "b": allImageFavoritePros.length diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index e817f7e..656b562 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -48,7 +48,12 @@ class ImageFavoritesPhotoViewState extends State { .where((e) => e.value == true) .map((e) => e.key) .toList(); - ImageFavoriteManager().deleteImageFavorite(tempList); + if (tempList.isNotEmpty) { + ImageFavoriteManager().deleteImageFavorite(tempList); + showToast( + message: "Delete @a images".tlParams({'a': tempList.length}), + context: context); + } } PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { From f538473ed51e2988232c5b372f57c5aab0eacfe5 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 12 Jan 2025 11:54:09 +0800 Subject: [PATCH 25/36] fix text editing --- lib/pages/image_favorites_page/image_favorites_page.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 547b40d..2aebdc2 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -305,7 +305,6 @@ class _ImageFavoritesPageState extends State { ), onChanged: (v) { keyword = v; - update(); }, ), ), From 67472648d7014a612fcd4bc7229510c76ebd08ce Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 12 Jan 2025 12:02:10 +0800 Subject: [PATCH 26/36] fix text editing --- .../image_favorites_page.dart | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 2aebdc2..3ee6c3e 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -60,7 +60,8 @@ class _ImageFavoritesPageState extends State { // 所有的图片收藏 List comics = []; List<_LoadingImageFavoritesComicRes> isRefreshComicList = []; - String keyword = ""; + late var controller = TextEditingController(text: widget.initialKeyword ?? ""); + String get keyword => controller.text; // 进入关键词搜索模式 bool searchMode = false; @@ -139,7 +140,6 @@ class _ImageFavoritesPageState extends State { @override void initState() { if (widget.initialKeyword != null) { - keyword = widget.initialKeyword!; searchMode = true; } sortType = ImageFavoriteSortType.values.firstWhereOrNull( @@ -288,23 +288,24 @@ class _ImageFavoritesPageState extends State { child: IconButton( icon: const Icon(Icons.close), onPressed: () { + controller.clear(); setState(() { searchMode = false; - keyword = ""; - update(); + controller.clear(); + updateImageFavorites(); }); }, ), ), title: TextField( autofocus: true, - controller: TextEditingController(text: keyword), + controller: controller, decoration: InputDecoration( hintText: "Search".tl, border: InputBorder.none, ), onChanged: (v) { - keyword = v; + updateImageFavorites(); }, ), ), @@ -370,11 +371,9 @@ class _ImageFavoritesPageState extends State { selectedImageFavorites.clear(); }); } else if (searchMode) { - setState(() { - searchMode = false; - keyword = ""; - update(); - }); + controller.clear(); + searchMode = false; + updateImageFavorites(); } }, child: body, From e8f695dce18cc1ab8972ea71e221c6917a5eb58c Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 12 Jan 2025 12:20:40 +0800 Subject: [PATCH 27/36] =?UTF-8?q?feat:=20=E5=8A=9F=E8=83=BD=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E6=94=BE=E5=88=B0=E9=82=AE=E4=BB=B6=E6=88=96=E9=95=BF?= =?UTF-8?q?=E6=8C=89=E8=8F=9C=E5=8D=95=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/translation.json | 4 +- .../image_favorites_item.dart | 100 ++++++++++++++---- .../image_favorites_page.dart | 30 +----- 3 files changed, 83 insertions(+), 51 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 0560450..bbce784 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -252,7 +252,6 @@ "Local comic collection is not supported at present": "本地收藏暂不支持", "The cover cannot be uncollected here": "封面不能在此取消收藏", "Uncollected the image": "取消收藏图片", - "Double tap comic copy title, double tap image open gallery": "双击漫画复制标题, 双击图片放大浏览", "Successfully collected": "收藏成功", "Collect the image": "收藏图片", "Quick collect image": "快速收藏图片", @@ -286,6 +285,7 @@ "Image Favorites": "图片收藏", "Title": "标题", "@a Cover": "@a 封面", + "Photo View": "图片浏览", "Delete @a images": "删除 @a 张图片", "Update the page number by the latest collection": "按最新收藏更新页数", "Copy the title successfully": "复制标题成功", @@ -569,7 +569,6 @@ "Local comic collection is not supported at present": "本地收藏暫不支持", "The cover cannot be uncollected here": "封面不能在此取消收藏", "Uncollected the image": "取消收藏圖片", - "Double tap comic copy title, double tap image open gallery": "雙擊漫畫複製標題, 雙擊圖片放大瀏覽", "Successfully collected": "收藏成功", "Collect the image": "收藏圖片", "Quick collect image": "快速收藏圖片", @@ -603,6 +602,7 @@ "Image Favorites": "圖片收藏", "Title": "標題", "@a Cover": "@a 封面", + "Photo View": "圖片瀏覽", "Delete @a images": "刪除 @a 張圖片", "Update the page number by the latest collection": "按最新收藏更新頁數", "Copy the title successfully": "複製標題成功", diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 37edf9e..cd5e753 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -146,6 +146,76 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { ); } + void goPhotoView(ImageFavorite imageFavorite) { + // 双击浏览大图 + App.mainNavigatorKey?.currentContext?.to( + () => ImageFavoritesPhotoView( + imageFavoritesComic: widget.imageFavoritesComic, + imageFavoritePro: imageFavorite, + finalImageFavoritesComicList: widget.finalImageFavoritesComicList, + goComicInfo: goComicInfo, + goReaderPage: goReaderPage, + ), + ); + } + + void copyTitle() { + Clipboard.setData(ClipboardData(text: widget.imageFavoritesComic.title)); + App.rootContext.showMessage(message: 'Copy the title successfully'.tl); + } + + void onLongPress(BuildContext context) { + var renderBox = context.findRenderObject() as RenderBox; + var size = renderBox.size; + var location = renderBox.localToGlobal( + Offset((size.width - 242) / 2, size.height / 2), + ); + showMenu(location, context); + } + + void onSecondaryTap(TapDownDetails details, BuildContext context) { + showMenu(details.globalPosition, context); + } + + void showMenu(Offset location, BuildContext context) { + showMenuX( + App.rootContext, + location, + [ + MenuEntry( + icon: Icons.chrome_reader_mode_outlined, + text: 'Details'.tl, + onClick: () { + goComicInfo(widget.imageFavoritesComic); + }, + ), + MenuEntry( + icon: Icons.copy, + text: 'Copy Title'.tl, + onClick: () { + copyTitle(); + }, + ), + MenuEntry( + icon: Icons.select_all, + text: 'Select All'.tl, + onClick: () { + for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { + widget.addSelected(ele); + } + }, + ), + MenuEntry( + icon: Icons.read_more, + text: 'Photo View'.tl, + onClick: () { + goPhotoView(widget.imageFavoritesComic.sortedImageFavorites.first); + }, + ), + ], + ); + } + @override void initState() { loadingImageFavoritesComicRes = widget.isRefreshComicList.firstWhereOrNull( @@ -174,7 +244,8 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { String time = DateFormat('yyyy-MM-dd HH:mm').format(widget.imageFavoritesComic.time); List hotTags = []; - for (var textWithCount in widget.imageFavoritesCompute?.tags ?? []) { + for (var textWithCount + in widget.imageFavoritesCompute?.tags ?? []) { if (widget.imageFavoritesComic.tags.contains(textWithCount.text)) { var enableTranslate = App.locale.languageCode == 'zh'; var text = enableTranslate @@ -200,16 +271,14 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { ), child: InkWell( borderRadius: BorderRadius.circular(8), + onSecondaryTapDown: (detail) => onSecondaryTap(detail, context), + onLongPress: () => onLongPress(context), onDoubleTap: () { - Clipboard.setData( - ClipboardData(text: widget.imageFavoritesComic.title)); - showToast( - message: "Copy the title successfully".tl, context: context); + copyTitle(); }, onTap: () { if (widget.multiSelectMode) { - for (var ele - in widget.imageFavoritesComic.sortedImageFavorites) { + for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { widget.addSelected(ele); } } else { @@ -217,11 +286,6 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { goComicInfo(widget.imageFavoritesComic); } }, - onLongPress: () { - for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { - widget.addSelected(ele); - } - }, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -281,17 +345,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { : curPage.toString(); return InkWell( onDoubleTap: () { - // 双击浏览大图 - App.mainNavigatorKey?.currentContext?.to( - () => ImageFavoritesPhotoView( - imageFavoritesComic: widget.imageFavoritesComic, - imageFavoritePro: curImageFavorite, - finalImageFavoritesComicList: - widget.finalImageFavoritesComicList, - goComicInfo: goComicInfo, - goReaderPage: goReaderPage, - ), - ); + goPhotoView(curImageFavorite); }, onTap: () { // 单击去阅读页面, 跳转到当前点击的page diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 2aebdc2..2b1ac04 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -308,27 +308,6 @@ class _ImageFavoritesPageState extends State { }, ), ), - if (appdata.implicitData['Guide_imageFavoritesPage_DoubleTap'] != true) - SliverToBoxAdapter( - child: Row( - children: [ - Text( - 'Double tap comic copy title, double tap image open gallery' - .tl, - ), - Spacer(), - IconButton( - icon: const Icon(Icons.check), - onPressed: () { - appdata.implicitData['Guide_imageFavoritesPage_DoubleTap'] = - true; - appdata.writeImplicitData(); - update(); - }, - ) - ], - ).paddingHorizontal(8), - ), SliverList( delegate: SliverChildBuilderDelegate( (context, index) { @@ -431,7 +410,7 @@ class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> { @override void initState() { super.initState(); - timeRangeType = switch(widget.initTimeFilterSelect) { + timeRangeType = switch (widget.initTimeFilterSelect) { TimeRange.all => TimeRangeType.all, TimeRange.lastWeek => TimeRangeType.lastWeek, TimeRange.lastMonth => TimeRangeType.lastMonth, @@ -484,9 +463,8 @@ class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> { title: Text("Time Filter".tl), trailing: Select( current: timeRangeType.value.tl, - values: TimeRangeType.values - .map((e) => e.value.tl) - .toList(), + values: + TimeRangeType.values.map((e) => e.value.tl).toList(), minWidth: 64, onTap: (index) { setState(() { @@ -572,7 +550,7 @@ class _ImageFavoritesDialogState extends State<_ImageFavoritesDialog> { duration: end!.difference(start!), ); } else { - timeRange = switch(timeRangeType) { + timeRange = switch (timeRangeType) { TimeRangeType.all => TimeRange.all, TimeRangeType.lastWeek => TimeRange.lastWeek, TimeRangeType.lastMonth => TimeRange.lastMonth, From ff1aa3304d8abec174a25d375985e29cd2b3e2fb Mon Sep 17 00:00:00 2001 From: Yoshiro_fan Date: Sun, 12 Jan 2025 12:35:55 +0800 Subject: [PATCH 28/36] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8Dtag=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E4=B8=8D=E7=94=9F=E6=95=88=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/foundation/image_favorites.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 7274399..a8f8f62 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -518,7 +518,8 @@ class ImageFavoriteManager with ChangeNotifier { if (tag.startsWith("Category:")) { return false; } - return !exceptTags.contains(tag.toLowerCase()) && !tag.isNum; + return !exceptTags.contains(tag.split(":").last.toLowerCase()) && + !tag.isNum; } return ImageFavoritesCompute( From 99e512774e16624cef07401f0f2a368f7ca3ad95 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 13 Jan 2025 18:00:35 +0800 Subject: [PATCH 29/36] Improve image loading --- lib/foundation/history.dart | 1 + lib/foundation/image_favorites.dart | 3 + .../image_provider/base_image_provider.dart | 4 +- .../image_favorites_provider.dart | 128 +++++++- lib/foundation/local.dart | 4 +- .../image_favorites_item.dart | 298 ++++-------------- .../image_favorites_page.dart | 33 -- .../image_favorites_photo_view.dart | 5 +- 8 files changed, 190 insertions(+), 286 deletions(-) diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 83e61e2..7c6bfa9 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -7,6 +7,7 @@ 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/foundation/image_provider/image_favorites_provider.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/translations.dart'; diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index a8f8f62..754cac9 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -380,6 +380,9 @@ class ImageFavoriteManager with ChangeNotifier { if (imageFavoriteList.isEmpty) { return; } + for (var i in imageFavoriteList) { + ImageFavoritesProvider.deleteFromCache(i); + } for (var e in imageFavoritesComicList) { // 找到同一个漫画中的需要删除的具体图片 List filterImageFavorites = imageFavoriteList.where((i) { diff --git a/lib/foundation/image_provider/base_image_provider.dart b/lib/foundation/image_provider/base_image_provider.dart index 788456a..e0b8f2c 100644 --- a/lib/foundation/image_provider/base_image_provider.dart +++ b/lib/foundation/image_provider/base_image_provider.dart @@ -6,6 +6,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:venera/foundation/cache_manager.dart'; +import 'package:venera/foundation/log.dart'; abstract class BaseImageProvider> extends ImageProvider { @@ -126,10 +127,11 @@ abstract class BaseImageProvider> } rethrow; } - } catch (e) { + } catch (e, s) { scheduleMicrotask(() { PaintingBinding.instance.imageCache.evict(key); }); + Log.error("Image Loading", e, s); rethrow; } finally { chunkEvents.close(); diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart index 23acde0..ec273f1 100644 --- a/lib/foundation/image_provider/image_favorites_provider.dart +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -1,7 +1,14 @@ import 'dart:async' show Future, StreamController; +import 'dart:io'; +import 'package:crypto/crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; +import 'package:venera/foundation/comic_type.dart'; +import 'package:venera/foundation/local.dart'; import 'package:venera/network/images.dart'; +import 'package:venera/utils/io.dart'; import '../history.dart'; import 'base_image_provider.dart'; import 'image_favorites_provider.dart' as image_provider; @@ -13,21 +20,101 @@ class ImageFavoritesProvider final ImageFavorite imageFavorite; + int get page => imageFavorite.page; + + String get sourceKey => imageFavorite.sourceKey; + + String get cid => imageFavorite.id; + + String get eid => imageFavorite.eid; + @override - Future load(StreamController chunkEvents) async { - String imageKey = imageFavorite.imageKey; - String sourceKey = imageFavorite.sourceKey; - String cid = imageFavorite.id; - String eid = imageFavorite.eid; + Future load(StreamController? chunkEvents) async { + var imageKey = imageFavorite.imageKey; + var localImage = await getImageFromLocal(); + if (localImage != null) { + return localImage; + } + var cacheImage = await readFromCache(); + if (cacheImage != null) { + return cacheImage; + } + var gotImageKey = false; if (imageKey == "") { - throw "Error: imageFavorites no imageKey"; + imageKey = await getImageKey(); + gotImageKey = true; } + Uint8List image; + try { + image = await getImageFromNetwork(imageKey, chunkEvents); + } catch (e) { + if (gotImageKey) { + rethrow; + } else { + imageKey = await getImageKey(); + image = await getImageFromNetwork(imageKey, chunkEvents); + } + } + await writeToCache(image); + return image; + } + + Future writeToCache(Uint8List image) async { + var fileName = md5.convert(key.codeUnits).toString(); + var file = File(FilePath.join(App.cachePath, 'image_favorites', fileName)); + if (!file.existsSync()) { + file.createSync(recursive: true); + } + await file.writeAsBytes(image); + } + + Future readFromCache() async { + var fileName = md5.convert(key.codeUnits).toString(); + var file = File(FilePath.join(App.cachePath, 'image_favorites', fileName)); + if (!file.existsSync()) { + return null; + } + return await file.readAsBytes(); + } + + /// Delete a image favorite cache + static Future deleteFromCache(ImageFavorite imageFavorite) async { + var fileName = md5.convert(imageFavorite.imageKey.codeUnits).toString(); + var file = File(FilePath.join(App.cachePath, 'image_favorites', fileName)); + if (file.existsSync()) { + await file.delete(); + } + } + + Future getImageFromLocal() async { + var localComic = + LocalManager().find(sourceKey, ComicType.fromKey(sourceKey)); + if (localComic == null) { + return null; + } + var epIndex = localComic.chapters?.keys.toList().indexOf(eid) ?? -1; + if (epIndex == -1 && localComic.hasChapters) { + return null; + } + var images = await LocalManager().getImages( + sourceKey, + ComicType.fromKey(sourceKey), + epIndex, + ); + var data = await File(images[page]).readAsBytes(); + return data; + } + + Future getImageFromNetwork( + String imageKey, StreamController? chunkEvents) async { await for (var progress in ImageDownloader.loadComicImage(imageKey, sourceKey, cid, eid)) { - chunkEvents.add(ImageChunkEvent( - cumulativeBytesLoaded: progress.currentBytes, - expectedTotalBytes: progress.totalBytes, - )); + if (chunkEvents != null) { + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: progress.currentBytes, + expectedTotalBytes: progress.totalBytes, + )); + } if (progress.imageBytes != null) { return progress.imageBytes!; } @@ -35,16 +122,25 @@ class ImageFavoritesProvider throw "Error: Empty response body."; } + Future getImageKey() async { + String sourceKey = imageFavorite.sourceKey; + String cid = imageFavorite.id; + String eid = imageFavorite.eid; + var page = imageFavorite.page; + var comicSource = ComicSource.find(sourceKey); + if (comicSource == null) { + throw "Error: Comic source not found."; + } + var res = await comicSource.loadComicPages!(cid, eid); + return res.data[page - 1]; + } + @override Future obtainKey(ImageConfiguration configuration) { return SynchronousFuture(this); } - static String getImageKey(ImageFavorite temp) { - return "${temp.imageKey}@${temp.sourceKey}@${temp.id}@${temp.eid}"; - } - - // 和 reader_image 的一样 @override - String get key => getImageKey(imageFavorite); + String get key => + "ImageFavorites ${imageFavorite.imageKey}@${imageFavorite.sourceKey}@${imageFavorite.id}@${imageFavorite.eid}"; } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index d185e32..2eabae4 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -36,6 +36,8 @@ class LocalComic with HistoryMixin implements Comic { /// chapter id is the name of the directory in `LocalManager.path/$directory` final Map? chapters; + bool get hasChapters => chapters != null; + /// relative path to the cover image @override final String cover; @@ -387,7 +389,7 @@ class LocalManager with ChangeNotifier { } var comic = find(id, type) ?? (throw "Comic Not Found"); var directory = Directory(comic.baseDir); - if (comic.chapters != null) { + if (comic.hasChapters) { var cid = ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String); directory = Directory(FilePath.join(directory.path, cid)); diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index cd5e753..2029f70 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -7,8 +7,6 @@ class _ImageFavoritesItem extends StatefulWidget { required this.addSelected, required this.multiSelectMode, required this.finalImageFavoritesComicList, - required this.isRefreshComicList, - required this.setRefreshComicList, this.imageFavoritesCompute, }); @@ -17,8 +15,6 @@ class _ImageFavoritesItem extends StatefulWidget { final Map selectedImageFavorites; final List finalImageFavoritesComicList; final bool multiSelectMode; - final List<_LoadingImageFavoritesComicRes> isRefreshComicList; - final Function(_LoadingImageFavoritesComicRes) setRefreshComicList; final ImageFavoritesCompute? imageFavoritesCompute; @override @@ -26,108 +22,6 @@ class _ImageFavoritesItem extends StatefulWidget { } class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { - bool isImageKeyLoading = false; - - // 刷新 imageKey 失败的场景再刷新一次, 再次失败了就不重试了 - bool hasRefreshImageKeyOnErr = false; - late _LoadingImageFavoritesComicRes loadingImageFavoritesComicRes; - - // 如果刚从pica导入(没有imageKey) 或者 imageKey 失效了, 刷新一下 - void refreshImageKey(ImageFavoritesEp imageFavoritesEp) async { - try { - if (isImageKeyLoading || - hasRefreshImageKeyOnErr || - loadingImageFavoritesComicRes.isLoaded) { - return; - } - loadingImageFavoritesComicRes.isLoaded = true; - widget.setRefreshComicList(loadingImageFavoritesComicRes); - isImageKeyLoading = true; - ComicSource? comicSource = - ComicSource.find(widget.imageFavoritesComic.sourceKey); - // 拿一下漫画信息和对应章节的图片 - var resArr = await Future.wait([ - comicSource!.loadComicPages!( - widget.imageFavoritesComic.id, - imageFavoritesEp.eid, - ), - comicSource.loadComicInfo!( - widget.imageFavoritesComic.id, - ) - ]); - Res> comicPagesRes = resArr[0] as Res>; - Res comicInfoRes = resArr[1] as Res; - if (comicInfoRes.errorMessage?.contains("404") ?? false) { - loadingImageFavoritesComicRes.isInvalid = true; - widget.setRefreshComicList(loadingImageFavoritesComicRes); - if (mounted) { - setState(() {}); - } - return; - } - if (!comicInfoRes.error) { - // 刷新一下值, 保存最新的 - widget.imageFavoritesComic.author = - comicInfoRes.data.findAuthor() ?? ""; - widget.imageFavoritesComic.subTitle = comicInfoRes.data.subTitle ?? ''; - widget.imageFavoritesComic.tags = comicInfoRes.data.plainTags; - widget.imageFavoritesComic.translatedTags = widget - .imageFavoritesComic.tags - .map((e) => e.translateTagsToCN) - .toList(); - imageFavoritesEp.epName = comicInfoRes.data.chapters?.values - .elementAtOrNull(imageFavoritesEp.ep - 1) ?? - ""; - } else { - return; - } - if (comicPagesRes.error) { - // 能加载漫画信息, 说明只是章节对不太上, 刷新一下章节 - var chapters = comicInfoRes.data.chapters; - // 兜底一下, 如果不是从pica导入的空字符串, 说明是调用接口更新过章节的, 比如jm, 避免丢失最初正确的eid - // 拷贝等多章节可能会更新章节顺序, 后续如果碰到这种情况多的话, 就在右上角出一个功能批量刷新一下 - if (imageFavoritesEp.eid != "") { - return; - } - var finalEid = chapters?.keys.elementAt(imageFavoritesEp.ep - 1) ?? '0'; - var resArr = await Future.wait([ - comicSource.loadComicPages!( - widget.imageFavoritesComic.id, - finalEid, - ) - ]); - comicPagesRes = resArr[0]; - if (comicPagesRes.error) { - return; - } else { - imageFavoritesEp.eid = finalEid; - } - } - List images = comicPagesRes.data; - widget.imageFavoritesComic.maxPage = images.length; - imageFavoritesEp.maxPage = images.length; - // 塞一个封面进去 - if (!imageFavoritesEp.isHasFirstPage) { - ImageFavorite copy = imageFavoritesEp.imageFavorites[0].copyWith( - page: firstPage, - isAutoFavorite: true, - ); - imageFavoritesEp.imageFavorites.insert(0, copy); - } - // 统一刷一下最新的imageKey - for (var ele in imageFavoritesEp.imageFavorites) { - ele.imageKey = images[ele.page - 1]; - } - ImageFavoriteManager().addOrUpdateOrDelete(widget.imageFavoritesComic); - if (mounted) { - setState(() {}); - } - isImageKeyLoading = false; - } catch (e, stackTrace) { - Log.error("Unhandled Exception", e.toString(), stackTrace); - } - } - void goComicInfo(ImageFavoritesComic comic) { App.mainNavigatorKey?.currentContext?.to(() => ComicPage( id: comic.id, @@ -216,30 +110,8 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { ); } - @override - void initState() { - loadingImageFavoritesComicRes = widget.isRefreshComicList.firstWhereOrNull( - (e) => - e.id == widget.imageFavoritesComic.id && - e.sourceKey == widget.imageFavoritesComic.sourceKey) ?? - _LoadingImageFavoritesComicRes( - isLoaded: false, - isInvalid: false, - id: widget.imageFavoritesComic.id, - sourceKey: widget.imageFavoritesComic.sourceKey, - ); - super.initState(); - } - @override Widget build(BuildContext context) { - int count = widget.imageFavoritesComic.sortedImageFavorites.length; - if (!widget.imageFavoritesComic.isAllHasImageKey || - !widget.imageFavoritesComic.isAllHasFirstPage) { - for (var e in widget.imageFavoritesComic.imageFavoritesEp) { - refreshImageKey(e); - } - } var enableTranslate = App.locale.languageCode == 'zh'; String time = DateFormat('yyyy-MM-dd HH:mm').format(widget.imageFavoritesComic.time); @@ -260,6 +132,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { break; } } + var imageFavorites = widget.imageFavoritesComic.sortedImageFavorites; return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( @@ -314,7 +187,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { borderRadius: BorderRadius.circular(8), ), child: Text( - "$count/${widget.imageFavoritesComic.maxPageFromEp}", + "${imageFavorites.length}/${widget.imageFavoritesComic.maxPageFromEp}", style: ts.s12), ), ], @@ -322,113 +195,74 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { ).paddingHorizontal(16), SizedBox( height: 145, - child: CustomScrollView( + child: ListView.builder( scrollDirection: Axis.horizontal, - slivers: [ - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - ImageFavorite curImageFavorite = widget - .imageFavoritesComic.sortedImageFavorites[index]; - ImageFavoritesEp curImageFavoritesEp = widget - .imageFavoritesComic.imageFavoritesEp - .firstWhere((e) { - return e.ep == curImageFavorite.ep; - }); - bool isSelected = - widget.selectedImageFavorites[curImageFavorite] ?? - false; - int curPage = curImageFavorite.page; - String pageText = curPage == firstPage - ? '@a Cover' - .tlParams({"a": curImageFavorite.epName}) - : curPage.toString(); - return InkWell( - onDoubleTap: () { - goPhotoView(curImageFavorite); - }, - onTap: () { - // 单击去阅读页面, 跳转到当前点击的page - if (widget.multiSelectMode) { - widget.addSelected(curImageFavorite); - } else { - goReaderPage(widget.imageFavoritesComic, - curImageFavorite.ep, curPage); - } - }, - onLongPress: () { - widget.addSelected(curImageFavorite); - }, + itemBuilder: (context, index) { + var imageFavorite = imageFavorites[index]; + bool isSelected = + widget.selectedImageFavorites[imageFavorite] ?? false; + int curPage = imageFavorite.page; + String pageText = curPage == firstPage + ? '@a Cover'.tlParams({"a": imageFavorite.epName}) + : curPage.toString(); + return InkWell( + onDoubleTap: () { + goPhotoView(imageFavorite); + }, + onTap: () { + // 单击去阅读页面, 跳转到当前点击的page + if (widget.multiSelectMode) { + widget.addSelected(imageFavorite); + } else { + goReaderPage(widget.imageFavoritesComic, + imageFavorite.ep, curPage); + } + }, + onLongPress: () { + widget.addSelected(imageFavorite); + }, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 98, + height: 128, + decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), - child: Container( - width: 98, - height: 128, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: isSelected - ? Theme.of(context) - .colorScheme - .primaryContainer - : null, - ), - padding: - const EdgeInsets.symmetric(horizontal: 4), - child: Column( - children: [ - Container( - height: 128, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context) - .colorScheme - .secondaryContainer, - ), - clipBehavior: Clip.antiAlias, - child: AnimatedImage( - image: ImageFavoritesProvider( - curImageFavorite), - width: 96, - height: 128, - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - onError: (Object error, - StackTrace? stackTrace) { - if (loadingImageFavoritesComicRes - .isLoaded) { - return; - } - refreshImageKey(curImageFavoritesEp); - hasRefreshImageKeyOnErr = true; - }, - )), - Text( - pageText, - style: ts.s10, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - ], - )), - ); - }, - childCount: count, - ), - ), - ], + color: isSelected + ? Theme.of(context).colorScheme.primaryContainer + : null, + ), + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + children: [ + Container( + height: 128, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context) + .colorScheme + .secondaryContainer, + ), + clipBehavior: Clip.antiAlias, + child: AnimatedImage( + image: ImageFavoritesProvider(imageFavorite), + width: 96, + height: 128, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + )), + Text( + pageText, + style: ts.s10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + ], + )), + ).paddingHorizontal(4); + }, + itemCount: imageFavorites.length, ), ).paddingHorizontal(8), - if (loadingImageFavoritesComicRes.isInvalid) - Row( - children: [ - Text( - "The comic is invalid, please long press to delete, you can double click the title to copy" - .tl, - style: TextStyle( - color: Theme.of(context).colorScheme.error, // 设置为红色 - fontSize: 12, - )), - ], - ).paddingHorizontal(16), Row( children: [ Text( diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 0634e79..5092e22 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -9,13 +9,9 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; -import 'package:venera/foundation/cache_manager.dart'; -import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; -import 'package:venera/foundation/log.dart'; -import 'package:venera/foundation/res.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/image_favorites_page/type.dart'; import 'package:venera/pages/reader/reader.dart'; @@ -29,19 +25,6 @@ part "image_favorites_item.dart"; part "image_favorites_photo_view.dart"; -class _LoadingImageFavoritesComicRes { - bool isLoaded; - bool isInvalid; - String id; - String sourceKey; - - _LoadingImageFavoritesComicRes( - {required this.isLoaded, - required this.isInvalid, - required this.id, - required this.sourceKey}); -} - class ImageFavoritesPage extends StatefulWidget { const ImageFavoritesPage({super.key, this.initialKeyword}); @@ -59,7 +42,6 @@ class _ImageFavoritesPageState extends State { // 所有的图片收藏 List comics = []; - List<_LoadingImageFavoritesComicRes> isRefreshComicList = []; late var controller = TextEditingController(text: widget.initialKeyword ?? ""); String get keyword => controller.text; @@ -72,19 +54,6 @@ class _ImageFavoritesPageState extends State { Map selectedImageFavorites = {}; late List imageFavoritePros; - // 避免重复请求 - void setRefreshComicList(_LoadingImageFavoritesComicRes res) { - _LoadingImageFavoritesComicRes? tempRes = - isRefreshComicList.firstWhereOrNull( - (e) => e.id == res.id && e.sourceKey == res.sourceKey); - if (tempRes == null) { - isRefreshComicList.add(res); - } else { - tempRes.isLoaded = res.isLoaded; - tempRes.isInvalid = res.isInvalid; - } - } - void update() { if (mounted) { setState(() {}); @@ -313,13 +282,11 @@ class _ImageFavoritesPageState extends State { delegate: SliverChildBuilderDelegate( (context, index) { return _ImageFavoritesItem( - isRefreshComicList: isRefreshComicList, imageFavoritesComic: comics[index], selectedImageFavorites: selectedImageFavorites, addSelected: addSelected, multiSelectMode: multiSelectMode, finalImageFavoritesComicList: comics, - setRefreshComicList: setRefreshComicList, imageFavoritesCompute: imageFavoritesCompute, ); }, diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index 656b562..b5061b7 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -73,9 +73,8 @@ class ImageFavoritesPhotoViewState extends State { } Future _getCurrentImageData(ImageFavorite temp) async { - return (await CacheManager() - .findCache(ImageFavoritesProvider.getImageKey(temp)))! - .readAsBytes(); + var imageProvider = ImageFavoritesProvider(temp); + return await imageProvider.load(null); } @override From bf7260a8cc2c4b68226294b2d7c9ec0152abccc3 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 13 Jan 2025 19:25:38 +0800 Subject: [PATCH 30/36] The default value of quickCollectImage should be false. --- lib/foundation/appdata.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index fc4cd69..648e841 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -143,7 +143,7 @@ class _Settings with ChangeNotifier { 'quickFavorite': null, 'enableTurnPageByVolumeKey': true, 'enableClockAndBatteryInfoInReader': true, - 'quickCollectImage': 'Swipe', // No, DoubleTap, Swipe + 'quickCollectImage': 'No', // No, DoubleTap, Swipe 'authorizationRequired': false, 'onClickFavorite': 'viewDetail', // viewDetail, read 'enableDnsOverrides': false, From 1286d1ea409b6d9d07041735f9cff008716376cf Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 13 Jan 2025 20:04:20 +0800 Subject: [PATCH 31/36] Refactor DragListener --- lib/pages/reader/gesture.dart | 40 ++++++++++------- lib/pages/reader/images.dart | 4 +- lib/pages/reader/scaffold.dart | 78 +++++++++++++++++++--------------- 3 files changed, 71 insertions(+), 51 deletions(-) diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 147e768..7c513fb 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -20,8 +20,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { static const _kTapToTurnPagePercent = 0.3; - _DragListener? dragListener; - _DragListener? dragListenerForImageFavorites; + final _dragListeners = <_DragListener>[]; int fingers = 0; @@ -46,8 +45,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { _lastTapMoveDistance = Offset.zero; _tapGestureRecognizer.addPointer(event); if (_dragInProgress) { - dragListener?.onEnd?.call(); - dragListenerForImageFavorites?.onEnd?.call(); + for (var dragListener in _dragListeners) { + dragListener.onStart?.call(event.position); + } _dragInProgress = false; } Future.delayed(_kLongPressMinTime, () { @@ -57,11 +57,10 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { _longPressInProgress = true; } else { _dragInProgress = true; - dragListener?.onStart?.call(event.position); - dragListenerForImageFavorites?.onStart?.call(event.position); - dragListener?.onMove?.call(_lastTapMoveDistance!); - dragListenerForImageFavorites?.onMove - ?.call(_lastTapMoveDistance!); + for (var dragListener in _dragListeners) { + dragListener.onStart?.call(event.position); + dragListener.onMove?.call(_lastTapMoveDistance!); + } } } }); @@ -71,8 +70,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { _lastTapMoveDistance = event.delta + _lastTapMoveDistance!; } if (_dragInProgress) { - dragListener?.onMove?.call(event.delta); - dragListenerForImageFavorites?.onMove?.call(event.delta); + for (var dragListener in _dragListeners) { + dragListener.onMove?.call(event.delta); + } } }, onPointerUp: (event) { @@ -81,8 +81,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { onLongPressedUp(event.position); } if (_dragInProgress) { - dragListener?.onEnd?.call(); - dragListenerForImageFavorites?.onEnd?.call(); + for (var dragListener in _dragListeners) { + dragListener.onEnd?.call(); + } _dragInProgress = false; } _lastTapPointer = null; @@ -94,8 +95,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { onLongPressedUp(event.position); } if (_dragInProgress) { - dragListener?.onEnd?.call(); - dragListenerForImageFavorites?.onEnd?.call(); + for (var dragListener in _dragListeners) { + dragListener.onEnd?.call(); + } _dragInProgress = false; } _lastTapPointer = null; @@ -269,6 +271,14 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> { void onLongPressedDown(Offset location) { context.reader._imageViewController?.handleLongPressDown(location); } + + void addDragListener(_DragListener listener) { + _dragListeners.add(listener); + } + + void removeDragListener(_DragListener listener) { + _dragListeners.remove(listener); + } } class _DragListener { diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index a16a0d9..d1a76ee 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -264,7 +264,7 @@ class _GalleryModeState extends State<_GalleryMode> @override void handleDoubleTap(Offset location) { if (appdata.settings['quickCollectImage'] == 'DoubleTap') { - context.readerScaffold.imageFavoritesAction(); + context.readerScaffold.addImageFavorite(); return; } var controller = photoViewControllers[reader.page]!; @@ -569,7 +569,7 @@ class _ContinuousModeState extends State<_ContinuousMode> @override void handleDoubleTap(Offset location) { if (appdata.settings['quickCollectImage'] == 'DoubleTap') { - context.readerScaffold.imageFavoritesAction(); + context.readerScaffold.addImageFavorite(); return; } double target; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index e0a2d6b..6895c49 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -30,6 +30,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { _ReaderGestureDetectorState? _gestureDetectorState; + _DragListener? _floatingButtonDragListener; + void setFloatingButton(int value) { lastValue = showFloatingButtonValue; if (value == 0) { @@ -38,12 +40,15 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { fABValue.value = 0; update(); } - _gestureDetectorState!.dragListener = null; + if (_floatingButtonDragListener != null) { + _gestureDetectorState!.removeDragListener(_floatingButtonDragListener!); + _floatingButtonDragListener = null; + } } var readerMode = context.reader.mode; if (value == 1 && showFloatingButtonValue == 0) { showFloatingButtonValue = 1; - _gestureDetectorState!.dragListener = _DragListener( + _floatingButtonDragListener = _DragListener( onMove: (offset) { if (readerMode == ReaderMode.continuousTopToBottom) { fABValue.value -= offset.dy; @@ -63,10 +68,11 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { fABValue.value = 0; }, ); + _gestureDetectorState!.addDragListener(_floatingButtonDragListener!); update(); } else if (value == -1 && showFloatingButtonValue == 0) { showFloatingButtonValue = -1; - _gestureDetectorState!.dragListener = _DragListener( + _floatingButtonDragListener = _DragListener( onMove: (offset) { if (readerMode == ReaderMode.continuousTopToBottom) { fABValue.value += offset.dy; @@ -86,41 +92,45 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { fABValue.value = 0; }, ); + _gestureDetectorState!.addDragListener(_floatingButtonDragListener!); update(); } } - void dragListenerHandler() async { - // 过一秒执行, 避免 _gestureDetectorState 还未初始化 - await Future.delayed(Duration(milliseconds: 1000)); + _DragListener? _imageFavoriteDragListener; + + void addDragListener() async { if (!mounted) return; var readerMode = context.reader.mode; + // 横向阅读的时候, 如果纵向滑就触发收藏, 纵向阅读的时候, 如果横向滑动就触发收藏 - double imageFavoritesListenDistance = 0; - _gestureDetectorState!.dragListenerForImageFavorites = null; if (appdata.settings['quickCollectImage'] == 'Swipe') { - _gestureDetectorState!.dragListenerForImageFavorites = _DragListener( - onMove: (offset) { - switch (readerMode) { - case ReaderMode.continuousTopToBottom: - case ReaderMode.galleryTopToBottom: - imageFavoritesListenDistance += offset.dx; - break; - case ReaderMode.continuousLeftToRight: - case ReaderMode.galleryLeftToRight: - case ReaderMode.galleryRightToLeft: - case ReaderMode.continuousRightToLeft: - imageFavoritesListenDistance += offset.dy; - break; - } - }, - onEnd: () { - if (imageFavoritesListenDistance.abs() > 150) { - imageFavoritesAction(); - } - imageFavoritesListenDistance = 0; - }, - ); + if (_imageFavoriteDragListener == null) { + double distance = 0; + _imageFavoriteDragListener = _DragListener( + onMove: (offset) { + switch (readerMode) { + case ReaderMode.continuousTopToBottom: + case ReaderMode.galleryTopToBottom: + distance += offset.dx; + case ReaderMode.continuousLeftToRight: + case ReaderMode.galleryLeftToRight: + case ReaderMode.galleryRightToLeft: + case ReaderMode.continuousRightToLeft: + distance += offset.dy; + } + }, + onEnd: () { + if (distance.abs() > 150) { + addImageFavorite(); + } + distance = 0; + }, + ); + } + _gestureDetectorState!.addDragListener(_imageFavoriteDragListener!); + } else if (_imageFavoriteDragListener != null) { + _gestureDetectorState!.removeDragListener(_imageFavoriteDragListener!); } } @@ -136,7 +146,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { SystemChrome.setPreferredOrientations(DeviceOrientation.values); } super.initState(); - dragListenerHandler(); + addDragListener(); } @override @@ -249,7 +259,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); } - void imageFavoritesAction() { + void addImageFavorite() { try { if (context.reader.images![0].contains('file://')) { showToast( @@ -421,7 +431,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { child: IconButton( icon: Icon( isLiked() ? Icons.favorite : Icons.favorite_border), - onPressed: imageFavoritesAction), + onPressed: addImageFavorite), ), if (App.isWindows) Tooltip( @@ -730,7 +740,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } } if (key == "quickCollectImage") { - dragListenerHandler(); + addDragListener(); } context.reader.update(); }, From a7b804bfd920933ef5524c5eef3aa82b47e8fd13 Mon Sep 17 00:00:00 2001 From: nyne Date: Mon, 13 Jan 2025 21:17:48 +0800 Subject: [PATCH 32/36] Refactor ImageFavoriteItem & ImageFavoritePhotoView --- .../image_favorites_item.dart | 313 +++++++------- .../image_favorites_page.dart | 18 +- .../image_favorites_photo_view.dart | 382 +++++++++--------- 3 files changed, 355 insertions(+), 358 deletions(-) diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 2029f70..7c537fc 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -7,7 +7,6 @@ class _ImageFavoritesItem extends StatefulWidget { required this.addSelected, required this.multiSelectMode, required this.finalImageFavoritesComicList, - this.imageFavoritesCompute, }); final ImageFavoritesComic imageFavoritesComic; @@ -15,13 +14,14 @@ class _ImageFavoritesItem extends StatefulWidget { final Map selectedImageFavorites; final List finalImageFavoritesComicList; final bool multiSelectMode; - final ImageFavoritesCompute? imageFavoritesCompute; @override State<_ImageFavoritesItem> createState() => _ImageFavoritesItemState(); } class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { + late final imageFavorites = widget.imageFavoritesComic.sortedImageFavorites; + void goComicInfo(ImageFavoritesComic comic) { App.mainNavigatorKey?.currentContext?.to(() => ComicPage( id: comic.id, @@ -30,7 +30,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { } void goReaderPage(ImageFavoritesComic comic, int ep, int page) { - App.mainNavigatorKey?.currentContext?.to( + App.rootContext.to( () => ReaderWithLoading( id: comic.id, sourceKey: comic.sourceKey, @@ -41,16 +41,11 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { } void goPhotoView(ImageFavorite imageFavorite) { - // 双击浏览大图 - App.mainNavigatorKey?.currentContext?.to( - () => ImageFavoritesPhotoView( - imageFavoritesComic: widget.imageFavoritesComic, - imageFavoritePro: imageFavorite, - finalImageFavoritesComicList: widget.finalImageFavoritesComicList, - goComicInfo: goComicInfo, - goReaderPage: goReaderPage, - ), - ); + Navigator.of(App.rootContext).push(MaterialPageRoute( + builder: (context) => ImageFavoritesPhotoView( + comic: widget.imageFavoritesComic, + imageFavorite: imageFavorite, + ))); } void copyTitle() { @@ -58,7 +53,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { App.rootContext.showMessage(message: 'Copy the title successfully'.tl); } - void onLongPress(BuildContext context) { + void onLongPress() { var renderBox = context.findRenderObject() as RenderBox; var size = renderBox.size; var location = renderBox.localToGlobal( @@ -67,7 +62,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { showMenu(location, context); } - void onSecondaryTap(TapDownDetails details, BuildContext context) { + void onSecondaryTap(TapDownDetails details) { showMenu(details.globalPosition, context); } @@ -112,27 +107,6 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { @override Widget build(BuildContext context) { - var enableTranslate = App.locale.languageCode == 'zh'; - String time = - DateFormat('yyyy-MM-dd HH:mm').format(widget.imageFavoritesComic.time); - List hotTags = []; - for (var textWithCount - in widget.imageFavoritesCompute?.tags ?? []) { - if (widget.imageFavoritesComic.tags.contains(textWithCount.text)) { - var enableTranslate = App.locale.languageCode == 'zh'; - var text = enableTranslate - ? textWithCount.text.translateTagsToCN - : textWithCount.text; - if (text.contains(':')) { - text = text.split(':').last; - } - hotTags.add(text); - } - if (hotTags.length == 5) { - break; - } - } - var imageFavorites = widget.imageFavoritesComic.sortedImageFavorites; return Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), decoration: BoxDecoration( @@ -144,11 +118,8 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { ), child: InkWell( borderRadius: BorderRadius.circular(8), - onSecondaryTapDown: (detail) => onSecondaryTap(detail, context), - onLongPress: () => onLongPress(context), - onDoubleTap: () { - copyTitle(); - }, + onSecondaryTapDown: onSecondaryTap, + onLongPress: onLongPress, onTap: () { if (widget.multiSelectMode) { for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { @@ -162,134 +133,158 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { child: Column( mainAxisSize: MainAxisSize.min, children: [ - SizedBox( - height: 56, - child: Row( - children: [ - Expanded( - child: Text( - widget.imageFavoritesComic.title, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 14.0, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - softWrap: true, - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - "${imageFavorites.length}/${widget.imageFavoritesComic.maxPageFromEp}", - style: ts.s12), - ), - ], - ), - ).paddingHorizontal(16), + buildTop(), SizedBox( height: 145, child: ListView.builder( scrollDirection: Axis.horizontal, - itemBuilder: (context, index) { - var imageFavorite = imageFavorites[index]; - bool isSelected = - widget.selectedImageFavorites[imageFavorite] ?? false; - int curPage = imageFavorite.page; - String pageText = curPage == firstPage - ? '@a Cover'.tlParams({"a": imageFavorite.epName}) - : curPage.toString(); - return InkWell( - onDoubleTap: () { - goPhotoView(imageFavorite); - }, - onTap: () { - // 单击去阅读页面, 跳转到当前点击的page - if (widget.multiSelectMode) { - widget.addSelected(imageFavorite); - } else { - goReaderPage(widget.imageFavoritesComic, - imageFavorite.ep, curPage); - } - }, - onLongPress: () { - widget.addSelected(imageFavorite); - }, - borderRadius: BorderRadius.circular(8), - child: Container( - width: 98, - height: 128, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: isSelected - ? Theme.of(context).colorScheme.primaryContainer - : null, - ), - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Column( - children: [ - Container( - height: 128, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: Theme.of(context) - .colorScheme - .secondaryContainer, - ), - clipBehavior: Clip.antiAlias, - child: AnimatedImage( - image: ImageFavoritesProvider(imageFavorite), - width: 96, - height: 128, - fit: BoxFit.cover, - filterQuality: FilterQuality.medium, - )), - Text( - pageText, - style: ts.s10, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - ], - )), - ).paddingHorizontal(4); - }, + itemBuilder: buildItem, itemCount: imageFavorites.length, ), ).paddingHorizontal(8), - Row( - children: [ - Text( - "${"Collection time".tl}:$time | ${widget.imageFavoritesComic.sourceKey}", - textAlign: TextAlign.left, - style: const TextStyle( - fontSize: 12.0, - ), - ).paddingRight(8), - if (hotTags.isNotEmpty) - Expanded( - child: Text( - hotTags - .map((e) => enableTranslate ? e.translateTagsToCN : e) - .join(" "), - textAlign: TextAlign.right, - style: const TextStyle( - fontSize: 12.0, - overflow: TextOverflow.ellipsis, - ), - ), - ) - ], - ).paddingHorizontal(8).paddingBottom(8), + buildBottom(), ], ), ), ); } + + Widget buildItem(BuildContext context, int index) { + var image = imageFavorites[index]; + bool isSelected = widget.selectedImageFavorites[image] ?? false; + int curPage = image.page; + String pageText = curPage == firstPage + ? '@a Cover'.tlParams({"a": image.epName}) + : curPage.toString(); + + return InkWell( + onTap: () { + // 单击去阅读页面, 跳转到当前点击的page + if (widget.multiSelectMode) { + widget.addSelected(image); + } else { + goReaderPage(widget.imageFavoritesComic, image.ep, curPage); + } + }, + onLongPress: () { + goPhotoView(image); + }, + borderRadius: BorderRadius.circular(8), + child: Container( + width: 98, + height: 128, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: isSelected + ? Theme.of(context).colorScheme.primaryContainer + : null, + ), + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Column( + children: [ + Container( + height: 128, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Theme.of(context).colorScheme.secondaryContainer, + ), + clipBehavior: Clip.antiAlias, + child: Hero( + tag: "${image.sourceKey}${image.ep}${image.page}", + child: AnimatedImage( + image: ImageFavoritesProvider(image), + width: 96, + height: 128, + fit: BoxFit.cover, + filterQuality: FilterQuality.medium, + ), + ), + ), + Text( + pageText, + style: ts.s10, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + ], + ), + ), + ).paddingHorizontal(4); + } + + Widget buildTop() { + return SizedBox( + height: 46, + child: Row( + children: [ + Expanded( + child: Text( + widget.imageFavoritesComic.title, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "${imageFavorites.length}/${widget.imageFavoritesComic.maxPageFromEp}", + style: ts.s12), + ), + ], + ), + ).paddingHorizontal(16); + } + + Widget buildBottom() { + var enableTranslate = App.locale.languageCode == 'zh'; + String time = + DateFormat('yyyy-MM-dd').format(widget.imageFavoritesComic.time); + List tags = []; + for (var tag in widget.imageFavoritesComic.tags) { + var text = enableTranslate ? tag.translateTagsToCN : tag; + if (text.contains(':')) { + text = text.split(':').last; + } + tags.add(text); + if (tags.length == 5) { + break; + } + } + var comicSource = ComicSource.find(widget.imageFavoritesComic.sourceKey); + return Row( + children: [ + Text( + "$time | ${comicSource?.name ?? "Unknown"}", + textAlign: TextAlign.left, + style: const TextStyle( + fontSize: 12.0, + ), + ).paddingRight(8), + if (tags.isNotEmpty) + Expanded( + child: Text( + tags + .map((e) => enableTranslate ? e.translateTagsToCN : e) + .join(" "), + textAlign: TextAlign.right, + style: const TextStyle( + fontSize: 12.0, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + ], + ).paddingHorizontal(8).paddingBottom(8); + } } diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 5092e22..f44af3c 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:intl/intl.dart'; @@ -9,6 +10,7 @@ import 'package:photo_view/photo_view_gallery.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/consts.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/image_provider/image_favorites_provider.dart'; @@ -36,12 +38,12 @@ class ImageFavoritesPage extends StatefulWidget { class _ImageFavoritesPageState extends State { late ImageFavoriteSortType sortType; - ImageFavoritesCompute? imageFavoritesCompute; late TimeRange timeFilterSelect; late int numFilterSelect; // 所有的图片收藏 List comics = []; + late var controller = TextEditingController(text: widget.initialKeyword ?? ""); String get keyword => controller.text; @@ -52,7 +54,7 @@ class _ImageFavoritesPageState extends State { // 多选的时候选中的图片 Map selectedImageFavorites = {}; - late List imageFavoritePros; + late List imageFavorites; void update() { if (mounted) { @@ -61,13 +63,11 @@ class _ImageFavoritesPageState extends State { } void updateImageFavorites() async { - imageFavoritePros = []; + imageFavorites = []; for (var e in ImageFavoriteManager().imageFavoritesComicList) { - imageFavoritePros.addAll(e.sortedImageFavorites); + imageFavorites.addAll(e.sortedImageFavorites); } sortImageFavorites(); - imageFavoritesCompute = - await ImageFavoriteManager().computeImageFavorites(); update(); } @@ -150,7 +150,7 @@ class _ImageFavoritesPageState extends State { var scrollController = ScrollController(); void selectAll() { - for (var ele in imageFavoritePros) { + for (var ele in imageFavorites) { selectedImageFavorites[ele] = true; } update(); @@ -246,8 +246,7 @@ class _ImageFavoritesPageState extends State { }, ), ), - title: Text( - "Selected @c ".tlParams({"c": selectedImageFavorites.length})), + title: Text(selectedImageFavorites.length.toString()), actions: selectActions, ) else if (searchMode) @@ -287,7 +286,6 @@ class _ImageFavoritesPageState extends State { addSelected: addSelected, multiSelectMode: multiSelectMode, finalImageFavoritesComicList: comics, - imageFavoritesCompute: imageFavoritesCompute, ); }, childCount: comics.length, diff --git a/lib/pages/image_favorites_page/image_favorites_photo_view.dart b/lib/pages/image_favorites_page/image_favorites_photo_view.dart index b5061b7..e8543b1 100644 --- a/lib/pages/image_favorites_page/image_favorites_photo_view.dart +++ b/lib/pages/image_favorites_page/image_favorites_photo_view.dart @@ -1,45 +1,43 @@ part of 'image_favorites_page.dart'; class ImageFavoritesPhotoView extends StatefulWidget { - const ImageFavoritesPhotoView( - {super.key, - required this.imageFavoritesComic, - required this.imageFavoritePro, - required this.finalImageFavoritesComicList, - required this.goComicInfo, - required this.goReaderPage}); + const ImageFavoritesPhotoView({ + super.key, + required this.comic, + required this.imageFavorite, + }); - final ImageFavoritesComic imageFavoritesComic; - final ImageFavorite imageFavoritePro; - final List finalImageFavoritesComicList; - final Function(ImageFavoritesComic) goComicInfo; - final Function(ImageFavoritesComic, int, int) goReaderPage; + final ImageFavoritesComic comic; + final ImageFavorite imageFavorite; @override State createState() => - ImageFavoritesPhotoViewState(); + _ImageFavoritesPhotoViewState(); } -class ImageFavoritesPhotoViewState extends State { +class _ImageFavoritesPhotoViewState extends State { late PageController controller; Map cancelImageFavorites = {}; - // 图片当前的 index - late int curIndex; - late int curImageFavoritesComicIndex; + var images = []; + + int currentPage = 0; + + bool isAppBarShow = false; @override void initState() { - curIndex = - widget.imageFavoritesComic.sortedImageFavorites.indexWhere((ele) { - return ele.page == widget.imageFavoritePro.page && - ele.ep == widget.imageFavoritePro.ep; - }); - controller = PageController(initialPage: curIndex); - curImageFavoritesComicIndex = - widget.finalImageFavoritesComicList.indexWhere((ele) { - return ele.id == widget.imageFavoritesComic.id; - }); + var current = 0; + for (var ep in widget.comic.imageFavoritesEp) { + for (var image in ep.imageFavorites) { + images.add(image); + if (image == widget.imageFavorite) { + current = images.length - 1; + } + } + } + currentPage = current; + controller = PageController(initialPage: current); super.initState(); } @@ -57,193 +55,199 @@ class ImageFavoritesPhotoViewState extends State { } PhotoViewGalleryPageOptions _buildItem(BuildContext context, int index) { - final ImageFavorite curImageFavorite = widget - .finalImageFavoritesComicList[curImageFavoritesComicIndex] - .sortedImageFavorites[index]; + var image = images[index]; return PhotoViewGalleryPageOptions( - // 图片加载器 支持本地、网络 - imageProvider: ImageFavoritesProvider(curImageFavorite), - // 初始化大小 全部展示 - minScale: PhotoViewComputedScale.contained * 1.0, - maxScale: PhotoViewComputedScale.covered * 10.0, - onTapUp: (context, details, controllerValue) { - Navigator.pop(context); - onPop(); + // 图片加载器 支持本地、网络 + imageProvider: ImageFavoritesProvider(image), + // 初始化大小 全部展示 + minScale: PhotoViewComputedScale.contained * 1.0, + maxScale: PhotoViewComputedScale.covered * 10.0, + onTapUp: (context, details, controllerValue) { + setState(() { + isAppBarShow = !isAppBarShow; }); - } - - Future _getCurrentImageData(ImageFavorite temp) async { - var imageProvider = ImageFavoritesProvider(temp); - return await imageProvider.load(null); + }, + heroAttributes: PhotoViewHeroAttributes( + tag: "${image.sourceKey}${image.ep}${image.page}", + ), + ); } @override Widget build(BuildContext context) { - ImageFavoritesComic curComic = - widget.finalImageFavoritesComicList[curImageFavoritesComicIndex]; - ImageFavorite curImageFavorite = curComic.sortedImageFavorites[curIndex]; - int curPage = curImageFavorite.page; - String pageText = - curPage == firstPage ? 'Cover'.tl : "Page @a".tlParams({'a': curPage}); return PopScope( onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { onPop(); } }, - child: Stack(children: [ - PhotoViewGallery.builder( - backgroundDecoration: BoxDecoration( - color: context.colorScheme.surface, - ), - builder: _buildItem, - itemCount: curComic.sortedImageFavorites.length, - loadingBuilder: (context, event) => Center( - child: SizedBox( - width: 20.0, - height: 20.0, - child: CircularProgressIndicator( - backgroundColor: context.colorScheme.surfaceContainerHigh, - value: event == null || event.expectedTotalBytes == null - ? null - : event.cumulativeBytesLoaded / event.expectedTotalBytes!, + child: Listener( + onPointerSignal: (event) { + if (HardwareKeyboard.instance.isControlPressed) { + return; + } + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy > 0) { + if (controller.page! >= images.length - 1) { + return; + } + controller.nextPage( + duration: Duration(milliseconds: 180), curve: Curves.ease); + } else { + if (controller.page! <= 0) { + return; + } + controller.previousPage( + duration: Duration(milliseconds: 180), curve: Curves.ease); + } + } + }, + child: Stack(children: [ + Positioned.fill( + child: PhotoViewGallery.builder( + backgroundDecoration: BoxDecoration( + color: context.colorScheme.surface, ), - ), - ), - enableRotation: true, - customSize: MediaQuery.of(context).size, - //定义图片默认缩放基础的大小,默认全屏 MediaQuery.of(context).size - pageController: controller, - onPageChanged: (index) { - setState(() { - curIndex = index; - }); - }, - ), - Positioned( - top: 20, - left: 0, - right: 0, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: InkWell( - onTap: () { - widget.goComicInfo(curComic); - }, - onDoubleTap: () { - Clipboard.setData(ClipboardData(text: curComic.title)); - showToast( - message: "Copy the title successfully".tl, - context: context); + builder: _buildItem, + itemCount: images.length, + loadingBuilder: (context, event) => Center( + child: SizedBox( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + backgroundColor: context.colorScheme.surfaceContainerHigh, + value: event == null || event.expectedTotalBytes == null + ? null + : event.cumulativeBytesLoaded / + event.expectedTotalBytes!, + ), + ), + ), + pageController: controller, + onPageChanged: (index) { + setState(() { + currentPage = index; + }); }, - child: Text( - curComic.title, - style: ts.s18, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).paddingTop(20), ), ), - ), - Positioned( - bottom: 20, - left: 0, - right: 0, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row(children: [ - Text( - "${curImageFavorite.epName} : $pageText : ${curIndex + 1}/${curComic.sortedImageFavorites.length}", - style: ts.s12), - Spacer(), - Flexible( - flex: 1, - child: IconButton( - icon: Icon(Icons.arrow_circle_left), - onPressed: () { - if (curImageFavoritesComicIndex == 0) { - curImageFavoritesComicIndex = - widget.finalImageFavoritesComicList.length - 1; - } else { - curImageFavoritesComicIndex -= 1; - } - curIndex = 0; - controller.jumpToPage(0); - setState(() {}); - }, - ), - ), - Flexible( - flex: 1, - child: IconButton( - icon: cancelImageFavorites[curImageFavorite] == true - ? Icon(Icons.favorite_border) - : Icon(Icons.favorite), - onPressed: () { - if (cancelImageFavorites[curImageFavorite] == true) { - cancelImageFavorites[curImageFavorite] = false; - } else { - cancelImageFavorites[curImageFavorite] = true; - } + buildPageInfo(), + AnimatedPositioned( + top: isAppBarShow ? 0 : -(context.padding.top + 52), + left: 0, + right: 0, + duration: Duration(milliseconds: 180), + child: buildAppBar(), + ), + ]), + ), + ); + } - setState(() {}); - }, - ), + Widget buildPageInfo() { + var text = "${currentPage + 1}/${images.length}"; + return Positioned( + height: 40, + left: 0, + right: 0, + bottom: 0, + child: Center( + child: Stack( + children: [ + Text( + text, + style: TextStyle( + fontSize: 14, + foreground: Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.4 + ..color = context.colorScheme.onInverseSurface, ), - Flexible( - flex: 1, - child: IconButton( - icon: Icon(Icons.play_arrow), - onPressed: () { - widget.goReaderPage(curComic, curImageFavorite.ep, curPage); - }, - ), + ), + Text(text), + ], + ), + ), + ); + } + + Widget buildAppBar() { + return Material( + color: context.colorScheme.surface.toOpacity(0.72), + child: BlurEffect( + child: Container( + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: context.colorScheme.outlineVariant, + width: 0.5, ), - Flexible( - flex: 1, - child: IconButton( - icon: Icon(Icons.menu_book), - onPressed: () { - widget.goComicInfo(curComic); - }, - ), + ), + ), + height: 52, + child: Row( + children: [ + const SizedBox(width: 8), + IconButton( + icon: Icon(Icons.close), + onPressed: () { + Navigator.of(context).pop(); + }, ), - Flexible( - flex: 1, - child: IconButton( - icon: const Icon(Icons.download), - onPressed: () async { - var data = await _getCurrentImageData(curImageFavorite); - if (data == null) { - return; - } - var fileType = detectFileType(data); - var filename = "${curImageFavorite.page}${fileType.ext}"; - saveFile(data: data, filename: filename); - }, + const SizedBox(width: 8), + Expanded( + child: Text( + widget.comic.title, + style: TextStyle(fontSize: 18), ), ), - Flexible( - flex: 1, - child: IconButton( - icon: Icon(Icons.arrow_circle_right), - onPressed: () { - if (curImageFavoritesComicIndex == - widget.finalImageFavoritesComicList.length - 1) { - curImageFavoritesComicIndex = 0; - } else { - curImageFavoritesComicIndex += 1; - } - curIndex = 0; - controller.jumpToPage(0); - setState(() {}); - }, - ), + IconButton( + icon: Icon(Icons.more_vert), + onPressed: showMenu, ), - ]), + const SizedBox(width: 8), + ], ), + ).paddingTop(context.padding.top), + ), + ); + } + + void showMenu() { + showMenuX( + context, + Offset(context.width, context.padding.top), + [ + MenuEntry( + icon: Icons.image_outlined, + text: "Save Image".tl, + onClick: () async { + var temp = images[currentPage]; + var imageProvider = ImageFavoritesProvider(temp); + var data = await imageProvider.load(null); + var fileType = detectFileType(data); + var fileName = "${currentPage + 1}.${fileType.ext}"; + await saveFile(filename: fileName, data: data); + }, + ), + MenuEntry( + icon: Icons.menu_book_outlined, + text: "Read".tl, + onClick: () async { + var comic = widget.comic; + var ep = images[currentPage].ep; + var page = images[currentPage].page; + App.rootContext.to( + () => ReaderWithLoading( + id: comic.id, + sourceKey: comic.sourceKey, + initialEp: ep, + initialPage: page, + ), + ); + }, ), - ]), + ], ); } } From 9caf5838420c5037e329ad8aaa46ff6989142546 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 14 Jan 2025 18:37:58 +0800 Subject: [PATCH 33/36] Refactor --- lib/components/scroll.dart | 35 +- lib/foundation/context.dart | 2 + lib/foundation/history.dart | 9 + lib/foundation/image_favorites.dart | 274 +++++++------- lib/pages/home_page.dart | 336 ++++++++++-------- .../image_favorites_page.dart | 23 +- lib/pages/reader/scaffold.dart | 2 +- lib/utils/data.dart | 7 +- 8 files changed, 361 insertions(+), 327 deletions(-) diff --git a/lib/components/scroll.dart b/lib/components/scroll.dart index 98fd287..9eeedd1 100644 --- a/lib/components/scroll.dart +++ b/lib/components/scroll.dart @@ -102,13 +102,36 @@ class _SmoothScrollProviderState extends State { duration: _fastAnimationDuration, curve: Curves.linear); } }, - child: widget.builder( - context, - _controller, - _isMouseScroll - ? const NeverScrollableScrollPhysics() - : const BouncingScrollPhysics(), + child: ScrollControllerProvider._( + controller: _controller, + child: widget.builder( + context, + _controller, + _isMouseScroll + ? const NeverScrollableScrollPhysics() + : const BouncingScrollPhysics(), + ), ), ); } } + +class ScrollControllerProvider extends InheritedWidget { + const ScrollControllerProvider._({ + required this.controller, + required super.child, + }); + + final ScrollController controller; + + static ScrollController of(BuildContext context) { + final ScrollControllerProvider? provider = + context.dependOnInheritedWidgetOfExactType(); + return provider!.controller; + } + + @override + bool updateShouldNotify(ScrollControllerProvider oldWidget) { + return oldWidget.controller != controller; + } +} diff --git a/lib/foundation/context.dart b/lib/foundation/context.dart index 00ae620..b8b1a58 100644 --- a/lib/foundation/context.dart +++ b/lib/foundation/context.dart @@ -36,6 +36,8 @@ extension Navigation on BuildContext { Brightness get brightness => Theme.of(this).brightness; + bool get isDarkMode => brightness == Brightness.dark; + void showMessage({required String message}) { showToast(message: message, context: this); } diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index 7c6bfa9..920f1b6 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:isolate'; import 'dart:math'; import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart' show ChangeNotifier; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; @@ -14,6 +16,7 @@ import 'package:venera/utils/translations.dart'; import 'app.dart'; import 'consts.dart'; + part "image_favorites.dart"; typedef HistoryType = ComicType; @@ -209,7 +212,12 @@ class HistoryManager with ChangeNotifier { Map? _cachedHistory; + bool isInitialized = false; + Future init() async { + if (isInitialized) { + return; + } _db = sqlite3.open("${App.dataPath}/history.db"); _db.execute(""" @@ -229,6 +237,7 @@ class HistoryManager with ChangeNotifier { notifyListeners(); ImageFavoriteManager().init(); + isInitialized = true; } /// add history. if exists, update time. diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index 754cac9..df2b676 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -179,11 +179,47 @@ class ImageFavoritesComic { @override int get hashCode => Object.hash(id, sourceKey); + + factory ImageFavoritesComic.fromRow(Row r) { + var tempImageFavoritesEp = jsonDecode(r["image_favorites_ep"]); + List finalImageFavoritesEp = []; + tempImageFavoritesEp.forEach((i) { + List temp = []; + i["imageFavorites"].forEach((j) { + temp.add(ImageFavorite( + j["page"], + j["imageKey"], + j["isAutoFavorite"], + i["eid"], + r["id"], + i["ep"], + r["source_key"], + i["epName"], + )); + }); + finalImageFavoritesEp.add(ImageFavoritesEp( + i["eid"], i["ep"], temp, i["epName"], i["maxPage"] ?? 1)); + }); + return ImageFavoritesComic( + r["id"], + finalImageFavoritesEp, + r["title"], + r["source_key"], + r["tags"].split(","), + r["translated_tags"].split(","), + DateTime.fromMillisecondsSinceEpoch(r["time"]), + r["author"], + jsonDecode(r["other"]), + r["sub_title"], + r["max_page"], + ); + } } class ImageFavoriteManager with ChangeNotifier { Database get _db => HistoryManager()._db; - late List imageFavoritesComicList = getAll(null); + + List get comics => getAll(); static ImageFavoriteManager? _cache; @@ -191,19 +227,6 @@ class ImageFavoriteManager with ChangeNotifier { factory ImageFavoriteManager() => (_cache ??= ImageFavoriteManager._()); - Timer? updateTimer; - - void updateValue() { - // 立刻触发, 让阅读界面可以看到图片收藏的图标状态更新了 - imageFavoritesComicList = getAll(); - // 避免从pica导入的时候, 疯狂触发更新 - updateTimer?.cancel(); - updateTimer = Timer(const Duration(seconds: 4), () { - notifyListeners(); - updateTimer = null; - }); - } - /// 检查表image_favorites是否存在, 不存在则创建 void init() { _db.execute("CREATE TABLE IF NOT EXISTS image_favorites (" @@ -223,7 +246,7 @@ class ImageFavoriteManager with ChangeNotifier { } // 做排序和去重的操作 - void addOrUpdateOrDelete(ImageFavoritesComic favorite) { + void addOrUpdateOrDelete(ImageFavoritesComic favorite, [bool notify = true]) { // 没有章节了就删掉 if (favorite.imageFavoritesEp.isEmpty) { _db.execute(""" @@ -287,145 +310,90 @@ class ImageFavoriteManager with ChangeNotifier { jsonEncode(favorite.other) ]); } - ImageFavoriteManager().updateValue(); + if (notify) { + notifyListeners(); + } } - ImageFavoritesComic? findFromComicList(List tempList, - String id, String sourceKey, String eid, int page, int ep) { - ImageFavoritesComic? temp = tempList - .firstWhereOrNull((e) => e.id == id && e.sourceKey == sourceKey); - if (temp == null) { - return null; - } else { - ImageFavoritesEp? tempEp = temp.imageFavoritesEp.firstWhereOrNull((e) { - return e.ep == ep; - }); - if (tempEp == null) { - return null; - } else { - ImageFavorite? tempFavorite = - tempEp.imageFavorites.firstWhereOrNull((e) => e.page == page); - if (tempFavorite != null) { - return temp; + bool has(String id, String sourceKey, String eid, int page, int ep) { + var res = _db.select( + """ + select * from image_favorites + where id == ? and source_key == ?; + """, + [id, sourceKey], + ); + if (res.isEmpty) { + return false; + } + var tempImageFavoritesEp = jsonDecode(res.first["image_favorites_ep"]); + for (var e in tempImageFavoritesEp) { + if (e["ep"] == ep.toString() && e["eid"] == eid) { + for (var i in e["imageFavorites"]) { + if (i["page"] == page) { + return true; + } } - return null; } } - } - - bool has(String id, String sourceKey, String eid, int page, int ep) { - return findFromComicList( - imageFavoritesComicList, id, sourceKey, eid, page, ep) != - null; + return false; } List getAll([String? keyword]) { - var res = []; + ResultSet res; if (keyword == null || keyword == "") { res = _db.select("select * from image_favorites;"); } else { res = _db.select( """ select * from image_favorites - WHERE LOWER(title) LIKE LOWER(?) - OR LOWER(sub_title) LIKE LOWER(?) + WHERE title LIKE ? + OR sub_title LIKE ? OR LOWER(tags) LIKE LOWER(?) OR LOWER(translated_tags) LIKE LOWER(?) - OR LOWER(author) LIKE LOWER(?); + OR author LIKE ?; """, ['%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%', '%$keyword%'], ); } try { - return res.map((e) { - var tempImageFavoritesEp = jsonDecode(e["image_favorites_ep"]); - List finalImageFavoritesEp = []; - tempImageFavoritesEp.forEach((i) { - List temp = []; - i["imageFavorites"].forEach((j) { - temp.add(ImageFavorite( - j["page"], - j["imageKey"], - j["isAutoFavorite"], - i["eid"], - e["id"], - i["ep"], - e["source_key"], - i["epName"])); - }); - finalImageFavoritesEp.add(ImageFavoritesEp( - i["eid"], i["ep"], temp, i["epName"], i["maxPage"] ?? 1)); - }); - return ImageFavoritesComic( - e["id"], - finalImageFavoritesEp, - e["title"], - e["source_key"], - e["tags"].split(","), - e["translated_tags"].split(","), - DateTime.fromMillisecondsSinceEpoch(e["time"]), - e["author"], - jsonDecode(e["other"]), - e["sub_title"], - e["max_page"], - ); - }).toList(); + return res.map((e) => ImageFavoritesComic.fromRow(e)).toList(); } catch (e, stackTrace) { Log.error("Unhandled Exception", e.toString(), stackTrace); return []; } } - void deleteImageFavorite(List imageFavoriteList) { + void deleteImageFavorite(Iterable imageFavoriteList) { if (imageFavoriteList.isEmpty) { return; } for (var i in imageFavoriteList) { ImageFavoritesProvider.deleteFromCache(i); } - for (var e in imageFavoritesComicList) { - // 找到同一个漫画中的需要删除的具体图片 - List filterImageFavorites = imageFavoriteList.where((i) { - return i.id == e.id && i.sourceKey == e.sourceKey; - }).toList(); - if (filterImageFavorites.isNotEmpty) { - e.imageFavoritesEp = e.imageFavoritesEp.where((i) { - // 去掉匹配到的具体图片 - i.imageFavorites = i.imageFavorites.where((j) { - ImageFavorite? temp = filterImageFavorites.firstWhereOrNull((k) { - return k.page == j.page && k.ep == j.ep; - }); - // 如果没有匹配到, 说明不是这个章节和page, 就留着 - return temp == null; - }).toList(); - // 如果一张图片都没了, 或者只有一张自动收藏的firstPage, 就去掉这个章节 - if (i.imageFavorites.length == 1 && - i.imageFavorites.first.isAutoFavorite == true) { - return false; - } - return i.imageFavorites.isNotEmpty; - }).toList(); - addOrUpdateOrDelete(e); + var comics = {}; + for (var i in imageFavoriteList) { + var comic = comics + .where((c) => c.id == i.id && c.sourceKey == i.sourceKey) + .firstOrNull ?? + find(i.id, i.sourceKey); + if (comic == null) { + continue; } + var ep = comic.imageFavoritesEp.firstWhereOrNull((e) => e.ep == i.ep); + if (ep == null) { + continue; + } + ep.imageFavorites.remove(i); + if (ep.imageFavorites.isEmpty) { + comic.imageFavoritesEp.remove(ep); + } + comics.add(comic); } - ImageFavoriteManager().updateValue(); - } - - List get earliestTimeToNow { - var res = _db.select("select MIN(time) from image_favorites;"); - if (res.first.values.first == null) { - return []; - } - int earliestYear = - DateTime.fromMillisecondsSinceEpoch(res.first.values.first! as int) - .year; - DateTime now = DateTime.now(); - int currentYear = now.year; - List yearsList = []; - for (int year = earliestYear; year <= currentYear; year++) { - yearsList.add(year.toString()); + for (var i in comics) { + addOrUpdateOrDelete(i, false); } - return yearsList; + notifyListeners(); } int get length { @@ -440,15 +408,27 @@ class ImageFavoriteManager with ChangeNotifier { return getAll(keyword); } - Future computeImageFavorites() { - return compute( - _computeImageFavorites, - imageFavoritesComicList, - ); + static Future computeImageFavorites() { + var token = ServicesBinding.rootIsolateToken!; + var count = ImageFavoriteManager().length; + if (count == 0) { + return Future.value(ImageFavoritesComputed([], [], [])); + } else if (count > 100) { + return Isolate.run(() async { + BackgroundIsolateBinaryMessenger.ensureInitialized(token); + await App.init(); + await HistoryManager().init(); + return _computeImageFavorites(); + }); + } else { + return Future.value(_computeImageFavorites()); + } } - static ImageFavoritesCompute _computeImageFavorites( - List comics) { + static ImageFavoritesComputed _computeImageFavorites() { + const maxLength = 6; + + var comics = ImageFavoriteManager().getAll(); // 去掉这些没有意义的标签 const List exceptTags = [ '連載中', @@ -506,17 +486,6 @@ class ImageFavoriteManager with ChangeNotifier { comicImageCount.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); - // 按收藏比例排序漫画 - List> sortedComicsByPercentage = - comicImageCount.entries.toList() - ..sort((a, b) { - double percentageA = - comicImageCount[a.key]! / comicMaxPages[a.key]!; - double percentageB = - comicImageCount[b.key]! / comicMaxPages[b.key]!; - return percentageB.compareTo(percentageA); - }); - validateTag(String tag) { if (tag.startsWith("Category:")) { return false; @@ -525,27 +494,32 @@ class ImageFavoriteManager with ChangeNotifier { !tag.isNum; } - return ImageFavoritesCompute( + return ImageFavoritesComputed( sortedTags .where(validateTag) .map((tag) => TextWithCount(tag, tagCount[tag]!)) + .take(maxLength) .toList(), sortedAuthors .map((author) => TextWithCount(author, authorCount[author]!)) + .take(maxLength) .toList(), sortedComicsByNum .map((comic) => TextWithCount(comic.key.title, comic.value)) - .toList(), - sortedComicsByPercentage - .map((comic) => TextWithCount(comic.key.title, comic.value)) + .take(maxLength) .toList(), ); } ImageFavoritesComic? find(String id, String sourceKey) { - return imageFavoritesComicList.firstWhereOrNull( - (comic) => comic.id == id && comic.sourceKey == sourceKey, - ); + var row = _db.select(""" + select * from image_favorites + where id == ? and source_key == ?; + """, [id, sourceKey]); + if (row.isEmpty) { + return null; + } + return ImageFavoritesComic.fromRow(row.first); } } @@ -556,7 +530,7 @@ class TextWithCount { const TextWithCount(this.text, this.count); } -class ImageFavoritesCompute { +class ImageFavoritesComputed { /// 基于收藏的标签数排序 final List tags; @@ -564,16 +538,14 @@ class ImageFavoritesCompute { final List authors; /// 基于喜欢的图片数排序 - final List comicByNum; - - // 基于图片数比上总页数排序 - final List comicByPercentage; + final List comics; /// 计算后的图片收藏数据 - const ImageFavoritesCompute( + const ImageFavoritesComputed( this.tags, this.authors, - this.comicByNum, - this.comicByPercentage, + this.comics, ); + + bool get isEmpty => tags.isEmpty && authors.isEmpty && comics.isEmpty; } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index a2277af..6caeb7e 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -916,13 +916,6 @@ class __AnimatedDownloadingIconState extends State<_AnimatedDownloadingIcon> } } -enum _ImageFavoritesComputeType { - tags, - authors, - comicByNum, - comicByPercentage, -} - class ImageFavorites extends StatefulWidget { const ImageFavorites({super.key}); @@ -931,19 +924,14 @@ class ImageFavorites extends StatefulWidget { } class _ImageFavoritesState extends State { - ImageFavoritesCompute? imageFavoritesCompute; - List allImageFavoritePros = []; + ImageFavoritesComputed? imageFavoritesCompute; + + int displayType = 0; void refreshImageFavorites() async { try { - imageFavoritesCompute = null; - allImageFavoritePros = []; - for (var comic in ImageFavoriteManager().imageFavoritesComicList) { - allImageFavoritePros.addAll(comic.sortedImageFavorites); - } - setState(() {}); imageFavoritesCompute = - await ImageFavoriteManager().computeImageFavorites(); + await ImageFavoriteManager.computeImageFavorites(); if (mounted) { setState(() {}); } @@ -965,63 +953,10 @@ class _ImageFavoritesState extends State { super.dispose(); } - Widget roundBtn( - TextWithCount textWithCount, - _ImageFavoritesComputeType type, - ) { - var enableTranslate = App.locale.languageCode == 'zh'; - var text = enableTranslate - ? type == _ImageFavoritesComputeType.authors - ? TagsTranslation.translationTagWithNamespace( - textWithCount.text, "artist") - : textWithCount.text.translateTagsToCN - : textWithCount.text; - if (type == _ImageFavoritesComputeType.tags) { - if (text.contains(':')) { - text = text.split(':').last; - } - } - if (text.length > 20) { - text = '${text.substring(0, 20)}...'; - } - text += "(${textWithCount.count})"; - return InkWell( - onTap: () { - context - .to(() => ImageFavoritesPage(initialKeyword: textWithCount.text)); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(text), - ), - ); - } - - Widget listRoundBtn( - List list, _ImageFavoritesComputeType type) { - return Expanded( - child: SizedBox( - height: 24, - child: ListView.separated( - separatorBuilder: (BuildContext context, int index) { - return SizedBox(width: 4); - }, - scrollDirection: Axis.horizontal, - itemCount: list.length, - itemBuilder: (context, index) { - return roundBtn(list[index], type); - }, - ), - ), - ); - } - @override Widget build(BuildContext context) { + bool hasData = + imageFavoritesCompute != null && !imageFavoritesCompute!.isEmpty; return SliverToBoxAdapter( child: Container( margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), @@ -1047,100 +982,191 @@ class _ImageFavoritesState extends State { Center( child: Text('Image Favorites'.tl, style: ts.s18), ), - const SizedBox(width: 4), - Button.icon( - size: 18, - icon: const Icon(Icons.help_outline), - onPressed: () { - showDialog( - context: context, - builder: (context) { - return ContentDialog( - title: "Help".tl, - content: Text( - "After the parentheses are the number of pictures or the number of pictures compared to the number of comic pages" - .tl) - .paddingHorizontal(16) - .fixWidth(double.infinity), - actions: [ - Button.filled( - onPressed: context.pop, - child: Text("OK".tl), - ), - ], - ); - }, - ); - }, - ), const Spacer(), const Icon(Icons.arrow_right), ], ), ).paddingHorizontal(16), - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - "Calculate your favorite from @a comics and @b images" - .tlParams({ - "a": ImageFavoriteManager().length.toString(), - "b": allImageFavoritePros.length - }), - style: const TextStyle(fontSize: 15), - ), - if (imageFavoritesCompute != null) ...[ - const SizedBox(height: 8), - Row( - children: [ - Text( - "Author: ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.authors, - _ImageFavoritesComputeType.authors) - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "Tags: ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.tags, - _ImageFavoritesComputeType.tags) - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "Comics(number): ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.comicByNum, - _ImageFavoritesComputeType.comicByNum) - ], - ), - const SizedBox(height: 4), - Row( - children: [ - Text( - "Comics(percentage): ".tl, - style: const TextStyle(fontSize: 13), - ), - listRoundBtn(imageFavoritesCompute!.comicByPercentage, - _ImageFavoritesComputeType.comicByPercentage) - ], - ), + if (hasData) + Row( + children: [ + const Spacer(), + buildTypeButton(0, "Tags".tl), + const Spacer(), + buildTypeButton(1, "Authors".tl), + const Spacer(), + buildTypeButton(2, "Comics".tl), + const Spacer(), ], - ], - ).paddingHorizontal(16).paddingBottom(16), + ), + if (hasData) const SizedBox(height: 8), + if (hasData) + buildChart(switch (displayType) { + 0 => imageFavoritesCompute!.tags, + 1 => imageFavoritesCompute!.authors, + 2 => imageFavoritesCompute!.comics, + _ => [], + }) + .paddingHorizontal(16) + .paddingBottom(16), ], ), ), ), ); } + + Widget buildTypeButton(int type, String text) { + const radius = 24.0; + return InkWell( + borderRadius: BorderRadius.circular(radius), + onTap: () async { + setState(() { + displayType = type; + }); + await Future.delayed(const Duration(milliseconds: 20)); + var scrollController = ScrollControllerProvider.of(context); + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 200), + curve: Curves.ease, + ); + }, + child: AnimatedContainer( + width: 104, + padding: const EdgeInsets.symmetric(vertical: 6), + decoration: BoxDecoration( + color: + displayType == type ? context.colorScheme.primaryContainer : null, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, + width: 0.6, + ), + borderRadius: BorderRadius.circular(radius), + ), + duration: const Duration(milliseconds: 200), + child: Center( + child: Text( + text, + style: ts.s16, + ), + ), + ), + ); + } + + Widget buildChart(List data) { + if (data.isEmpty) { + return const SizedBox(); + } + var maxCount = data.map((e) => e.count).reduce((a, b) => a > b ? a : b); + return Column( + key: ValueKey(displayType), + children: data.map((e) { + return _ChartLine( + text: e.text, + count: e.count, + maxCount: maxCount, + enableTranslation: displayType != 2, + ); + }).toList(), + ); + } +} + +class _ChartLine extends StatefulWidget { + const _ChartLine({ + required this.text, + required this.count, + required this.maxCount, + required this.enableTranslation, + }); + + final String text; + + final int count; + + final int maxCount; + + final bool enableTranslation; + + @override + State<_ChartLine> createState() => __ChartLineState(); +} + +class __ChartLineState extends State<_ChartLine> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 200), + value: 0, + )..forward(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var text = widget.text; + var enableTranslation = + App.locale.countryCode == 'cn' && widget.enableTranslation; + if (enableTranslation) { + text = text.translateTagsToCN; + } + if (widget.enableTranslation && text.contains(':')) { + text = text.split(':').last; + } + return Row( + children: [ + Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ).fixWidth(context.width > 600 ? 120 : 80), + const SizedBox(width: 8), + Expanded( + child: LayoutBuilder(builder: (context, constrains) { + var width = constrains.maxWidth * widget.count / widget.maxCount; + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return Container( + width: width * _controller.value, + height: 16, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(2), + gradient: LinearGradient( + colors: context.isDarkMode + ? [ + Colors.blue.shade800, + Colors.blue.shade500, + ] + : [ + Colors.blue.shade300, + Colors.blue.shade600, + ], + ), + ), + ).toAlign(Alignment.centerLeft); + }, + ); + }), + ), + const SizedBox(width: 8), + Text( + widget.count.toString(), + style: ts.s12, + ).fixWidth(context.width > 600 ? 60 : 30), + ], + ).fixHeight(24); + } } diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index f44af3c..15f051f 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -1,6 +1,3 @@ -import 'dart:math'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -44,7 +41,9 @@ class _ImageFavoritesPageState extends State { // 所有的图片收藏 List comics = []; - late var controller = TextEditingController(text: widget.initialKeyword ?? ""); + late var controller = + TextEditingController(text: widget.initialKeyword ?? ""); + String get keyword => controller.text; // 进入关键词搜索模式 @@ -54,7 +53,6 @@ class _ImageFavoritesPageState extends State { // 多选的时候选中的图片 Map selectedImageFavorites = {}; - late List imageFavorites; void update() { if (mounted) { @@ -63,10 +61,9 @@ class _ImageFavoritesPageState extends State { } void updateImageFavorites() async { - imageFavorites = []; - for (var e in ImageFavoriteManager().imageFavoritesComicList) { - imageFavorites.addAll(e.sortedImageFavorites); - } + comics = searchMode + ? ImageFavoriteManager().search(keyword) + : ImageFavoriteManager().getAll(); sortImageFavorites(); update(); } @@ -137,7 +134,7 @@ class _ImageFavoritesPageState extends State { text: "Delete".tl, onClick: () { ImageFavoriteManager() - .deleteImageFavorite(selectedImageFavorites.keys.toList()); + .deleteImageFavorite(selectedImageFavorites.keys); setState(() { multiSelectMode = false; selectedImageFavorites.clear(); @@ -150,8 +147,10 @@ class _ImageFavoritesPageState extends State { var scrollController = ScrollController(); void selectAll() { - for (var ele in imageFavorites) { - selectedImageFavorites[ele] = true; + for (var c in comics) { + for (var i in c.sortedImageFavorites) { + selectedImageFavorites[i] = true; + } } update(); } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 6895c49..3249ae3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -146,7 +146,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { SystemChrome.setPreferredOrientations(DeviceOrientation.values); } super.initState(); - addDragListener(); + Future.delayed(const Duration(milliseconds: 200), addDragListener); } @override diff --git a/lib/utils/data.dart b/lib/utils/data.dart index 14f4747..08ab2e7 100644 --- a/lib/utils/data.dart +++ b/lib/utils/data.dart @@ -208,7 +208,7 @@ Future importPicaData(File file) async { ); } List imageFavoritesComicList = - ImageFavoriteManager().imageFavoritesComicList; + ImageFavoriteManager().comics; for (var comic in db.select("SELECT * FROM image_favorites;")) { String sourceKey = comic["id"].split("-")[0]; // 换名字了, 绅士漫画 @@ -252,7 +252,10 @@ Future importPicaData(File file) async { } } for (var temp in imageFavoritesComicList) { - ImageFavoriteManager().addOrUpdateOrDelete(temp); + ImageFavoriteManager().addOrUpdateOrDelete( + temp, + temp == imageFavoritesComicList.last, + ); } } catch (e, stack) { Log.error("Import Data", "Failed to import history: $e", stack); From e93487079009e98fb30b4e3ac03db1b2a63602d9 Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 14 Jan 2025 18:44:28 +0800 Subject: [PATCH 34/36] Fix `ImageFavoriteManager.has` --- lib/foundation/image_favorites.dart | 36 ++++++------------- .../image_favorites_item.dart | 8 ++--- .../image_favorites_page.dart | 12 +++---- 3 files changed, 20 insertions(+), 36 deletions(-) diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index df2b676..acde8b2 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -160,14 +160,10 @@ class ImageFavoritesComic { return imageFavoritesEp.every((e) => e.isHasFirstPage); } - List get sortedImageFavorites { - List temp = []; + Iterable get images sync*{ for (var e in imageFavoritesEp) { - for (var i in e.imageFavorites) { - temp.add(i); - } + yield* e.imageFavorites; } - return temp; } @override @@ -316,27 +312,15 @@ class ImageFavoriteManager with ChangeNotifier { } bool has(String id, String sourceKey, String eid, int page, int ep) { - var res = _db.select( - """ - select * from image_favorites - where id == ? and source_key == ?; - """, - [id, sourceKey], - ); - if (res.isEmpty) { + var comic = find(id, sourceKey); + if (comic == null) { return false; } - var tempImageFavoritesEp = jsonDecode(res.first["image_favorites_ep"]); - for (var e in tempImageFavoritesEp) { - if (e["ep"] == ep.toString() && e["eid"] == eid) { - for (var i in e["imageFavorites"]) { - if (i["page"] == page) { - return true; - } - } - } + var epIndex = comic.imageFavoritesEp.where((e) => e.eid == eid).firstOrNull; + if (epIndex == null) { + return false; } - return false; + return epIndex.imageFavorites.any((e) => e.page == page && e.ep == ep); } List getAll([String? keyword]) { @@ -462,14 +446,14 @@ class ImageFavoriteManager with ChangeNotifier { if (comic.author != "") { String finalAuthor = comic.author; authorCount[finalAuthor] = - (authorCount[finalAuthor] ?? 0) + comic.sortedImageFavorites.length; + (authorCount[finalAuthor] ?? 0) + comic.images.length; } // 小于10页的漫画不统计 if (comic.maxPageFromEp < 10) { continue; } comicImageCount[comic] = - (comicImageCount[comic] ?? 0) + comic.sortedImageFavorites.length; + (comicImageCount[comic] ?? 0) + comic.images.length; comicMaxPages[comic] = (comicMaxPages[comic] ?? 0) + comic.maxPageFromEp; } diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 7c537fc..5679cc5 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -20,7 +20,7 @@ class _ImageFavoritesItem extends StatefulWidget { } class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { - late final imageFavorites = widget.imageFavoritesComic.sortedImageFavorites; + late final imageFavorites = widget.imageFavoritesComic.images.toList(); void goComicInfo(ImageFavoritesComic comic) { App.mainNavigatorKey?.currentContext?.to(() => ComicPage( @@ -89,7 +89,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { icon: Icons.select_all, text: 'Select All'.tl, onClick: () { - for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { + for (var ele in widget.imageFavoritesComic.images) { widget.addSelected(ele); } }, @@ -98,7 +98,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { icon: Icons.read_more, text: 'Photo View'.tl, onClick: () { - goPhotoView(widget.imageFavoritesComic.sortedImageFavorites.first); + goPhotoView(widget.imageFavoritesComic.images.first); }, ), ], @@ -122,7 +122,7 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { onLongPress: onLongPress, onTap: () { if (widget.multiSelectMode) { - for (var ele in widget.imageFavoritesComic.sortedImageFavorites) { + for (var ele in widget.imageFavoritesComic.images) { widget.addSelected(ele); } } else { diff --git a/lib/pages/image_favorites_page/image_favorites_page.dart b/lib/pages/image_favorites_page/image_favorites_page.dart index 15f051f..ea1b7ac 100644 --- a/lib/pages/image_favorites_page/image_favorites_page.dart +++ b/lib/pages/image_favorites_page/image_favorites_page.dart @@ -79,7 +79,7 @@ class _ImageFavoritesPageState extends State { isFilter = timeFilterSelect.contains(ele.time); } if (numFilterSelect != numFilterList[0]) { - isFilter = ele.sortedImageFavorites.length > numFilterSelect; + isFilter = ele.images.length > numFilterSelect; } return isFilter; }).toList(); @@ -92,12 +92,12 @@ class _ImageFavoritesPageState extends State { case ImageFavoriteSortType.timeDesc: comics.sort((a, b) => b.time.compareTo(a.time)); case ImageFavoriteSortType.maxFavorites: - comics.sort((a, b) => b.sortedImageFavorites.length - .compareTo(a.sortedImageFavorites.length)); + comics.sort((a, b) => b.images.length + .compareTo(a.images.length)); case ImageFavoriteSortType.favoritesCompareComicPages: comics.sort((a, b) { - double tempA = a.sortedImageFavorites.length / a.maxPageFromEp; - double tempB = b.sortedImageFavorites.length / b.maxPageFromEp; + double tempA = a.images.length / a.maxPageFromEp; + double tempB = b.images.length / b.maxPageFromEp; return tempB.compareTo(tempA); }); } @@ -148,7 +148,7 @@ class _ImageFavoritesPageState extends State { void selectAll() { for (var c in comics) { - for (var i in c.sortedImageFavorites) { + for (var i in c.images) { selectedImageFavorites[i] = true; } } From 20115e12c80e8e9763340d34f7f909d94bfba17d Mon Sep 17 00:00:00 2001 From: nyne Date: Tue, 14 Jan 2025 19:10:39 +0800 Subject: [PATCH 35/36] Fix UI --- lib/pages/home_page.dart | 2 +- .../image_favorites_item.dart | 49 +++++++++---------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 6caeb7e..ccb529c 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1118,7 +1118,7 @@ class __ChartLineState extends State<_ChartLine> Widget build(BuildContext context) { var text = widget.text; var enableTranslation = - App.locale.countryCode == 'cn' && widget.enableTranslation; + App.locale.countryCode == 'CN' && widget.enableTranslation; if (enableTranslation) { text = text.translateTagsToCN; } diff --git a/lib/pages/image_favorites_page/image_favorites_item.dart b/lib/pages/image_favorites_page/image_favorites_item.dart index 5679cc5..120332b 100644 --- a/lib/pages/image_favorites_page/image_favorites_item.dart +++ b/lib/pages/image_favorites_page/image_favorites_item.dart @@ -213,35 +213,32 @@ class _ImageFavoritesItemState extends State<_ImageFavoritesItem> { } Widget buildTop() { - return SizedBox( - height: 46, - child: Row( - children: [ - Expanded( - child: Text( - widget.imageFavoritesComic.title, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 16.0, - ), - maxLines: 2, - overflow: TextOverflow.ellipsis, - softWrap: true, + return Row( + children: [ + Expanded( + child: Text( + widget.imageFavoritesComic.title, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - "${imageFavorites.length}/${widget.imageFavoritesComic.maxPageFromEp}", - style: ts.s12), + ), + Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), ), - ], - ), - ).paddingHorizontal(16); + child: Text( + "${imageFavorites.length}/${widget.imageFavoritesComic.maxPageFromEp}", + style: ts.s12), + ), + ], + ).paddingHorizontal(16).paddingVertical(8); } Widget buildBottom() { From 2314021d9b64cf008915e0a9bb4ba321970bd021 Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 15 Jan 2025 16:06:01 +0800 Subject: [PATCH 36/36] Improve UI --- lib/foundation/image_favorites.dart | 2 +- lib/pages/home_page.dart | 61 ++++++++++++++++++++--------- 2 files changed, 43 insertions(+), 20 deletions(-) diff --git a/lib/foundation/image_favorites.dart b/lib/foundation/image_favorites.dart index acde8b2..865ef9f 100644 --- a/lib/foundation/image_favorites.dart +++ b/lib/foundation/image_favorites.dart @@ -410,7 +410,7 @@ class ImageFavoriteManager with ChangeNotifier { } static ImageFavoritesComputed _computeImageFavorites() { - const maxLength = 6; + const maxLength = 20; var comics = ImageFavoriteManager().getAll(); // 去掉这些没有意义的标签 diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ccb529c..67cb9a3 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1033,8 +1033,8 @@ class _ImageFavoritesState extends State { ); }, child: AnimatedContainer( - width: 104, - padding: const EdgeInsets.symmetric(vertical: 6), + width: 96, + padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( color: displayType == type ? context.colorScheme.primaryContainer : null, @@ -1060,16 +1060,26 @@ class _ImageFavoritesState extends State { return const SizedBox(); } var maxCount = data.map((e) => e.count).reduce((a, b) => a > b ? a : b); - return Column( - key: ValueKey(displayType), - children: data.map((e) { - return _ChartLine( - text: e.text, - count: e.count, - maxCount: maxCount, - enableTranslation: displayType != 2, - ); - }).toList(), + return ConstrainedBox( + constraints: BoxConstraints( + maxHeight: 164, + ), + child: SingleChildScrollView( + child: Column( + key: ValueKey(displayType), + children: data.map((e) { + return _ChartLine( + text: e.text, + count: e.count, + maxCount: maxCount, + enableTranslation: displayType != 2, + onTap: (text) { + context.to(() => ImageFavoritesPage(initialKeyword: text)); + }, + ); + }).toList(), + ), + ), ); } } @@ -1080,6 +1090,7 @@ class _ChartLine extends StatefulWidget { required this.count, required this.maxCount, required this.enableTranslation, + this.onTap, }); final String text; @@ -1090,6 +1101,8 @@ class _ChartLine extends StatefulWidget { final bool enableTranslation; + final void Function(String text)? onTap; + @override State<_ChartLine> createState() => __ChartLineState(); } @@ -1127,11 +1140,21 @@ class __ChartLineState extends State<_ChartLine> } return Row( children: [ - Text( - text, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ).fixWidth(context.width > 600 ? 120 : 80), + InkWell( + borderRadius: BorderRadius.circular(4), + onTap: () { + widget.onTap?.call(widget.text); + }, + child: Text( + text, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + .paddingHorizontal(4) + .toAlign(Alignment.centerLeft) + .fixWidth(context.width > 600 ? 120 : 80) + .fixHeight(double.infinity), + ), const SizedBox(width: 8), Expanded( child: LayoutBuilder(builder: (context, constrains) { @@ -1141,7 +1164,7 @@ class __ChartLineState extends State<_ChartLine> builder: (context, child) { return Container( width: width * _controller.value, - height: 16, + height: 18, decoration: BoxDecoration( borderRadius: BorderRadius.circular(2), gradient: LinearGradient( @@ -1167,6 +1190,6 @@ class __ChartLineState extends State<_ChartLine> style: ts.s12, ).fixWidth(context.width > 600 ? 60 : 30), ], - ).fixHeight(24); + ).fixHeight(28); } }