diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index be8d5d6..bf6fc29 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -46,12 +46,14 @@
UIApplicationSupportsIndirectInputEvents
NSPhotoLibraryUsageDescription
- Choose images
+ Choose images
UIFileSharingEnabled
LSSupportsOpeningDocumentsInPlace
NSFaceIDUsageDescription
- Ensure that the operation is being performed by the user themselves.
+ Ensure that the operation is being performed by the user themselves.
+ LSApplicationCategoryType
+ public.app-category.books
diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart
index 67c6ad7..87cc8a1 100644
--- a/lib/foundation/app.dart
+++ b/lib/foundation/app.dart
@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart";
class _App {
- final version = "1.4.1";
+ final version = "1.4.2";
bool get isAndroid => Platform.isAndroid;
diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart
index 7b1a898..9c1f210 100644
--- a/lib/foundation/appdata.dart
+++ b/lib/foundation/appdata.dart
@@ -185,6 +185,7 @@ class Settings with ChangeNotifier {
'comicListDisplayMode': 'paging', // paging, continuous
'showPageNumberInReader': true,
'showSingleImageOnFirstPage': false,
+ 'enableDoubleTapToZoom': true,
};
operator [](String key) {
diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart
index ad2baf6..2fce8f5 100644
--- a/lib/foundation/favorites.dart
+++ b/lib/foundation/favorites.dart
@@ -1,4 +1,6 @@
import 'dart:convert';
+import 'dart:ffi';
+import 'dart:isolate';
import 'package:flutter/foundation.dart';
import 'package:sqlite3/sqlite3.dart';
@@ -209,7 +211,22 @@ class LocalFavoritesManager with ChangeNotifier {
late Database _db;
+ late Map counts;
+
+ int get totalComics {
+ int total = 0;
+ for (var t in counts.values) {
+ total += t;
+ }
+ return total;
+ }
+
+ int folderComics(String folder) {
+ return counts[folder] ?? 0;
+ }
+
Future init() async {
+ counts = {};
_db = sqlite3.open("${App.dataPath}/local_favorite.db");
_db.execute("""
create table if not exists folder_order (
@@ -256,6 +273,13 @@ class LocalFavoritesManager with ChangeNotifier {
} else {
appdata.settings['followUpdatesFolder'] = null;
}
+ initCounts();
+ }
+
+ void initCounts() {
+ for (var folder in folderNames) {
+ counts[folder] = count(folder);
+ }
}
List find(String id, ComicType type) {
@@ -357,6 +381,23 @@ class LocalFavoritesManager with ChangeNotifier {
return rows.map((element) => FavoriteItem.fromRow(element)).toList();
}
+ static Future> _getFolderComicsAsync(
+ String folder, Pointer p) {
+ return Isolate.run(() {
+ var db = sqlite3.fromPointer(p);
+ var rows = db.select("""
+ select * from "$folder"
+ ORDER BY display_order;
+ """);
+ return rows.map((element) => FavoriteItem.fromRow(element)).toList();
+ });
+ }
+
+ /// Start a new isolate to get the comics in the folder
+ Future> getFolderComicsAsync(String folder) {
+ return _getFolderComicsAsync(folder, _db.handle);
+ }
+
List getAllComics() {
var res = {};
for (final folder in folderNames) {
@@ -368,6 +409,26 @@ class LocalFavoritesManager with ChangeNotifier {
return res.toList();
}
+ static Future> _getAllComicsAsync(
+ List folders, Pointer p) {
+ return Isolate.run(() {
+ var db = sqlite3.fromPointer(p);
+ var res = {};
+ for (final folder in folders) {
+ var comics = db.select("""
+ select * from "$folder";
+ """);
+ res.addAll(comics.map((element) => FavoriteItem.fromRow(element)));
+ }
+ return res.toList();
+ });
+ }
+
+ /// Start a new isolate to get all the comics
+ Future> getAllComicsAsync() {
+ return _getAllComicsAsync(folderNames, _db.handle);
+ }
+
void addTagTo(String folder, String id, String tag) {
_db.execute("""
update "$folder"
@@ -433,6 +494,7 @@ class LocalFavoritesManager with ChangeNotifier {
);
""");
notifyListeners();
+ counts[name] = 0;
return name;
}
@@ -547,6 +609,11 @@ class LocalFavoritesManager with ChangeNotifier {
""", [updateTime, comic.id, comic.type.value]);
}
}
+ if (counts[folder] == null) {
+ counts[folder] = count(folder);
+ } else {
+ counts[folder] = counts[folder]! + 1;
+ }
notifyListeners();
return true;
}
@@ -596,6 +663,7 @@ class LocalFavoritesManager with ChangeNotifier {
delete from folder_order
where folder_name == ?;
""", [name]);
+ counts.remove(name);
notifyListeners();
}
@@ -611,6 +679,11 @@ class LocalFavoritesManager with ChangeNotifier {
delete from "$folder"
where id == ? and type == ?;
""", [id, type.value]);
+ if (counts[folder] != null) {
+ counts[folder] = counts[folder]! - 1;
+ } else {
+ counts[folder] = count(folder);
+ }
notifyListeners();
}
diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart
index aa14a64..664ec46 100644
--- a/lib/pages/favorites/favorites_page.dart
+++ b/lib/pages/favorites/favorites_page.dart
@@ -18,7 +18,9 @@ import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_details_page/comic_page.dart';
import 'package:venera/pages/reader/reader.dart';
import 'package:venera/pages/settings/settings_page.dart';
+import 'package:venera/utils/ext.dart';
import 'package:venera/utils/io.dart';
+import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart';
part 'favorite_actions.dart';
diff --git a/lib/pages/favorites/local_favorites_page.dart b/lib/pages/favorites/local_favorites_page.dart
index a807b08..7dfe925 100644
--- a/lib/pages/favorites/local_favorites_page.dart
+++ b/lib/pages/favorites/local_favorites_page.dart
@@ -2,6 +2,10 @@ part of 'favorites_page.dart';
const _localAllFolderLabel = '^_^[%local_all%]^_^';
+/// If the number of comics in a folder exceeds this limit, it will be
+/// fetched asynchronously.
+const _asyncDataFetchLimit = 500;
+
class _LocalFavoritesPage extends StatefulWidget {
const _LocalFavoritesPage({required this.folder, super.key});
@@ -35,40 +39,110 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
bool get isAllFolder => widget.folder == _localAllFolderLabel;
+ LocalFavoritesManager get manager => LocalFavoritesManager();
+
+ bool isLoading = false;
+
+ var searchResults = [];
+
+ void updateSearchResult() {
+ setState(() {
+ if (keyword.trim().isEmpty) {
+ searchResults = comics;
+ } else {
+ searchResults = [];
+ for (var comic in comics) {
+ if (matchKeyword(keyword, comic)) {
+ searchResults.add(comic);
+ }
+ }
+ }
+ });
+ }
+
void updateComics() {
- if (keyword.isEmpty) {
- setState(() {
- if (isAllFolder) {
- comics = LocalFavoritesManager().getAllComics();
- } else {
- comics = LocalFavoritesManager().getFolderComics(widget.folder);
- }
- });
+ if (isLoading) return;
+ if (isAllFolder) {
+ var totalComics = manager.totalComics;
+ if (totalComics < _asyncDataFetchLimit) {
+ comics = manager.getAllComics();
+ } else {
+ isLoading = true;
+ manager
+ .getAllComicsAsync()
+ .minTime(const Duration(milliseconds: 200))
+ .then((value) {
+ if (mounted) {
+ setState(() {
+ isLoading = false;
+ comics = value;
+ });
+ }
+ });
+ }
} else {
- setState(() {
- if (isAllFolder) {
- comics = LocalFavoritesManager().search(keyword);
- } else {
- comics =
- LocalFavoritesManager().searchInFolder(widget.folder, keyword);
- }
- });
+ var folderComics = manager.folderComics(widget.folder);
+ if (folderComics < _asyncDataFetchLimit) {
+ comics = manager.getFolderComics(widget.folder);
+ } else {
+ isLoading = true;
+ manager
+ .getFolderComicsAsync(widget.folder)
+ .minTime(const Duration(milliseconds: 200))
+ .then((value) {
+ if (mounted) {
+ setState(() {
+ isLoading = false;
+ comics = value;
+ });
+ }
+ });
+ }
}
+ setState(() {});
+ }
+
+ bool matchKeyword(String keyword, FavoriteItem comic) {
+ var list = keyword.split(" ");
+ for (var k in list) {
+ if (k.isEmpty) continue;
+ if (comic.title.contains(k)) {
+ continue;
+ } else if (comic.subtitle != null && comic.subtitle!.contains(k)) {
+ continue;
+ } else if (comic.tags.any((tag) {
+ if (tag == k) {
+ return true;
+ } else if (tag.contains(':') && tag.split(':')[1] == k) {
+ return true;
+ } else if (App.locale.languageCode != 'en' &&
+ tag.translateTagsToCN == k) {
+ return true;
+ }
+ return false;
+ })) {
+ continue;
+ } else if (comic.author == k) {
+ continue;
+ }
+ return false;
+ }
+ return true;
}
@override
void initState() {
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
if (!isAllFolder) {
- comics = LocalFavoritesManager().getFolderComics(widget.folder);
var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
networkSource = a;
networkFolder = b;
} else {
- comics = LocalFavoritesManager().getAllComics();
networkSource = null;
networkFolder = null;
}
+ comics = [];
+ updateComics();
LocalFavoritesManager().addListener(updateComics);
super.initState();
}
@@ -215,7 +289,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
icon: const Icon(Icons.search),
onPressed: () {
setState(() {
+ keyword = "";
searchMode = true;
+ updateSearchResult();
});
},
),
@@ -411,9 +487,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
icon: const Icon(Icons.close),
onPressed: () {
setState(() {
- searchMode = false;
- keyword = "";
- updateComics();
+ setState(() {
+ searchMode = false;
+ });
});
},
),
@@ -422,132 +498,142 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
autofocus: true,
decoration: InputDecoration(
hintText: "Search".tl,
- border: InputBorder.none,
+ border: UnderlineInputBorder(),
),
onChanged: (v) {
keyword = v;
- updateComics();
+ updateSearchResult();
},
- ),
+ ).paddingBottom(8).paddingRight(8),
),
- SliverGridComics(
- comics: comics,
- selections: selectedComics,
- menuBuilder: (c) {
- return [
- if (!isAllFolder)
+ if (isLoading)
+ SliverToBoxAdapter(
+ child: SizedBox(
+ height: 200,
+ child: const Center(
+ child: CircularProgressIndicator(),
+ ),
+ ),
+ )
+ else
+ SliverGridComics(
+ comics: searchMode ? searchResults : comics,
+ selections: selectedComics,
+ menuBuilder: (c) {
+ return [
+ if (!isAllFolder)
+ MenuEntry(
+ icon: Icons.delete,
+ text: "Delete".tl,
+ onClick: () {
+ LocalFavoritesManager().deleteComicWithId(
+ widget.folder,
+ c.id,
+ (c as FavoriteItem).type,
+ );
+ },
+ ),
MenuEntry(
- icon: Icons.delete,
- text: "Delete".tl,
+ icon: Icons.check,
+ text: "Select".tl,
onClick: () {
- LocalFavoritesManager().deleteComicWithId(
- widget.folder,
- c.id,
- (c as FavoriteItem).type,
+ setState(() {
+ if (!multiSelectMode) {
+ multiSelectMode = true;
+ }
+ if (selectedComics.containsKey(c as FavoriteItem)) {
+ selectedComics.remove(c);
+ _checkExitSelectMode();
+ } else {
+ selectedComics[c] = true;
+ }
+ lastSelectedIndex = comics.indexOf(c);
+ });
+ },
+ ),
+ MenuEntry(
+ icon: Icons.download,
+ text: "Download".tl,
+ onClick: () {
+ downloadComic(c as FavoriteItem);
+ context.showMessage(
+ message: "Download started".tl,
);
},
),
- MenuEntry(
- icon: Icons.check,
- text: "Select".tl,
- onClick: () {
- setState(() {
- if (!multiSelectMode) {
- multiSelectMode = true;
- }
- if (selectedComics.containsKey(c as FavoriteItem)) {
- selectedComics.remove(c);
- _checkExitSelectMode();
- } else {
- selectedComics[c] = true;
- }
- lastSelectedIndex = comics.indexOf(c);
- });
- },
- ),
- MenuEntry(
- icon: Icons.download,
- text: "Download".tl,
- onClick: () {
- downloadComic(c as FavoriteItem);
- context.showMessage(
- message: "Download started".tl,
- );
- },
- ),
- if (appdata.settings["onClickFavorite"] == "viewDetail")
- MenuEntry(
- icon: Icons.menu_book_outlined,
- text: "Read".tl,
- onClick: () {
- App.mainNavigatorKey?.currentContext?.to(
- () => ReaderWithLoading(
- id: c.id,
- sourceKey: c.sourceKey,
- ),
- );
- },
- ),
- ];
- },
- onTap: (c) {
- if (multiSelectMode) {
- setState(() {
- if (selectedComics.containsKey(c as FavoriteItem)) {
- selectedComics.remove(c);
- _checkExitSelectMode();
- } else {
- selectedComics[c] = true;
- }
- lastSelectedIndex = comics.indexOf(c);
- });
- } else if (appdata.settings["onClickFavorite"] == "viewDetail") {
- App.mainNavigatorKey?.currentContext
- ?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
- } else {
- App.mainNavigatorKey?.currentContext?.to(
- () => ReaderWithLoading(
- id: c.id,
- sourceKey: c.sourceKey,
- ),
- );
- }
- },
- onLongPressed: (c) {
- setState(() {
- if (!multiSelectMode) {
- multiSelectMode = true;
- if (!selectedComics.containsKey(c as FavoriteItem)) {
- selectedComics[c] = true;
- }
- lastSelectedIndex = comics.indexOf(c);
+ if (appdata.settings["onClickFavorite"] == "viewDetail")
+ MenuEntry(
+ icon: Icons.menu_book_outlined,
+ text: "Read".tl,
+ onClick: () {
+ App.mainNavigatorKey?.currentContext?.to(
+ () => ReaderWithLoading(
+ id: c.id,
+ sourceKey: c.sourceKey,
+ ),
+ );
+ },
+ ),
+ ];
+ },
+ onTap: (c) {
+ if (multiSelectMode) {
+ setState(() {
+ if (selectedComics.containsKey(c as FavoriteItem)) {
+ selectedComics.remove(c);
+ _checkExitSelectMode();
+ } else {
+ selectedComics[c] = true;
+ }
+ lastSelectedIndex = comics.indexOf(c);
+ });
+ } else if (appdata.settings["onClickFavorite"] == "viewDetail") {
+ App.mainNavigatorKey?.currentContext
+ ?.to(() => ComicPage(id: c.id, sourceKey: c.sourceKey));
} else {
- if (lastSelectedIndex != null) {
- int start = lastSelectedIndex!;
- int end = comics.indexOf(c as FavoriteItem);
- if (start > end) {
- int temp = start;
- start = end;
- end = temp;
+ App.mainNavigatorKey?.currentContext?.to(
+ () => ReaderWithLoading(
+ id: c.id,
+ sourceKey: c.sourceKey,
+ ),
+ );
+ }
+ },
+ onLongPressed: (c) {
+ setState(() {
+ if (!multiSelectMode) {
+ multiSelectMode = true;
+ if (!selectedComics.containsKey(c as FavoriteItem)) {
+ selectedComics[c] = true;
}
+ lastSelectedIndex = comics.indexOf(c);
+ } else {
+ if (lastSelectedIndex != null) {
+ int start = lastSelectedIndex!;
+ int end = comics.indexOf(c as FavoriteItem);
+ if (start > end) {
+ int temp = start;
+ start = end;
+ end = temp;
+ }
- for (int i = start; i <= end; i++) {
- if (i == lastSelectedIndex) continue;
+ for (int i = start; i <= end; i++) {
+ if (i == lastSelectedIndex) continue;
- var comic = comics[i];
- if (selectedComics.containsKey(comic)) {
- selectedComics.remove(comic);
- } else {
- selectedComics[comic] = true;
+ var comic = comics[i];
+ if (selectedComics.containsKey(comic)) {
+ selectedComics.remove(comic);
+ } else {
+ selectedComics[comic] = true;
+ }
}
}
+ lastSelectedIndex = comics.indexOf(c as FavoriteItem);
}
- lastSelectedIndex = comics.indexOf(c as FavoriteItem);
- }
- _checkExitSelectMode();
- });
- },
- ),
+ _checkExitSelectMode();
+ });
+ },
+ ),
],
);
body = AppScrollBar(
diff --git a/lib/pages/favorites/side_bar.dart b/lib/pages/favorites/side_bar.dart
index 71f2c9b..464ad75 100644
--- a/lib/pages/favorites/side_bar.dart
+++ b/lib/pages/favorites/side_bar.dart
@@ -86,51 +86,10 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
padding: widget.withAppbar
? EdgeInsets.zero
: EdgeInsets.only(top: context.padding.top),
- itemCount: folders.length + networkFolders.length + 2,
+ itemCount: folders.length + networkFolders.length + 3,
itemBuilder: (context, index) {
if (index == 0) {
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 8),
- child: Row(
- children: [
- Icon(
- Icons.local_activity,
- color: context.colorScheme.secondary,
- ),
- const SizedBox(width: 12),
- Text("Local".tl),
- const Spacer(),
- MenuButton(
- entries: [
- MenuEntry(
- icon: Icons.add,
- text: 'Create Folder'.tl,
- onClick: () {
- newFolder().then((value) {
- setState(() {
- folders =
- LocalFavoritesManager().folderNames;
- });
- });
- },
- ),
- MenuEntry(
- icon: Icons.reorder,
- text: 'Sort'.tl,
- onClick: () {
- sortFolders().then((value) {
- setState(() {
- folders =
- LocalFavoritesManager().folderNames;
- });
- });
- },
- ),
- ],
- ),
- ],
- ).paddingHorizontal(16),
- );
+ return buildLocalTitle();
}
index--;
if (index == 0) {
@@ -142,38 +101,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
}
index -= folders.length;
if (index == 0) {
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 12),
- margin: const EdgeInsets.only(top: 8),
- decoration: BoxDecoration(
- border: Border(
- top: BorderSide(
- color: context.colorScheme.outlineVariant,
- width: 0.6,
- ),
- ),
- ),
- child: Row(
- children: [
- Icon(
- Icons.cloud,
- color: context.colorScheme.secondary,
- ),
- const SizedBox(width: 12),
- Text("Network".tl),
- const Spacer(),
- IconButton(
- icon: const Icon(Icons.settings),
- onPressed: () {
- showPopUpWidget(
- App.rootContext,
- setFavoritesPagesWidget(),
- );
- },
- ),
- ],
- ).paddingHorizontal(16),
- );
+ return buildNetworkTitle();
}
index--;
return buildNetworkFolder(networkFolders[index]);
@@ -185,8 +113,95 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
);
}
+ Widget buildLocalTitle() {
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: Row(
+ children: [
+ Icon(
+ Icons.local_activity,
+ color: context.colorScheme.secondary,
+ ),
+ const SizedBox(width: 12),
+ Text("Local".tl),
+ const Spacer(),
+ MenuButton(
+ entries: [
+ MenuEntry(
+ icon: Icons.add,
+ text: 'Create Folder'.tl,
+ onClick: () {
+ newFolder().then((value) {
+ setState(() {
+ folders = LocalFavoritesManager().folderNames;
+ });
+ });
+ },
+ ),
+ MenuEntry(
+ icon: Icons.reorder,
+ text: 'Sort'.tl,
+ onClick: () {
+ sortFolders().then((value) {
+ setState(() {
+ folders = LocalFavoritesManager().folderNames;
+ });
+ });
+ },
+ ),
+ ],
+ ),
+ ],
+ ).paddingHorizontal(16),
+ );
+ }
+
+ Widget buildNetworkTitle() {
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 12),
+ margin: const EdgeInsets.only(top: 8),
+ decoration: BoxDecoration(
+ border: Border(
+ top: BorderSide(
+ color: context.colorScheme.outlineVariant,
+ width: 0.6,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ Icon(
+ Icons.cloud,
+ color: context.colorScheme.secondary,
+ ),
+ const SizedBox(width: 12),
+ Text("Network".tl),
+ const Spacer(),
+ IconButton(
+ icon: const Icon(Icons.settings),
+ onPressed: () {
+ showPopUpWidget(
+ App.rootContext,
+ setFavoritesPagesWidget(),
+ );
+ },
+ ),
+ ],
+ ).paddingHorizontal(16),
+ );
+ }
+
Widget buildLocalFolder(String name) {
bool isSelected = name == favPage.folder && !favPage.isNetwork;
+ int count = 0;
+ if (name == _localAllFolderLabel) {
+ count = LocalFavoritesManager().totalComics;
+ } else {
+ count = LocalFavoritesManager().folderComics(name);
+ }
+ var folderName = name == _localAllFolderLabel
+ ? "All".tl
+ : getFavoriteDataOrNull(name)?.title ?? name;
return InkWell(
onTap: () {
if (isSelected) {
@@ -211,9 +226,25 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
),
),
padding: const EdgeInsets.only(left: 16),
- child: Text(name == _localAllFolderLabel
- ? "All".tl
- : getFavoriteDataOrNull(name)?.title ?? name),
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(folderName),
+ ),
+ Container(
+ margin: EdgeInsets.only(right: 8),
+ padding: EdgeInsets.symmetric(
+ horizontal: 8,
+ vertical: 2,
+ ),
+ decoration: BoxDecoration(
+ color: context.colorScheme.surfaceContainer,
+ borderRadius: BorderRadius.circular(8),
+ ),
+ child: Text(count.toString()),
+ ),
+ ],
+ ),
),
);
}
diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart
index 751af23..04a68e9 100644
--- a/lib/pages/reader/gesture.dart
+++ b/lib/pages/reader/gesture.dart
@@ -152,12 +152,18 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
bool _dragInProgress = false;
+ bool get _enableDoubleTapToZoom => appdata.settings["enableDoubleTapToZoom"];
+
void onTapUp(TapUpDetails event) {
if (_longPressInProgress) {
_longPressInProgress = false;
return;
}
final location = event.globalPosition;
+ if (!_enableDoubleTapToZoom) {
+ onTap(location);
+ return;
+ }
final previousLocation = _previousEvent?.globalPosition;
if (previousLocation != null) {
if ((location - previousLocation).distanceSquared <
diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart
index 752606a..fe4be2b 100644
--- a/lib/pages/reader/images.dart
+++ b/lib/pages/reader/images.dart
@@ -119,7 +119,7 @@ class _GalleryModeState extends State<_GalleryMode>
/// [totalPages] is the total number of pages in the current chapter.
/// More than one images can be displayed on one page.
int get totalPages {
- if (!showSingleImageOnFirstPage) {
+ if (!reader.showSingleImageOnFirstPage) {
return (reader.images!.length / reader.imagesPerPage).ceil();
} else {
return 1 +
@@ -144,11 +144,9 @@ class _GalleryModeState extends State<_GalleryMode>
super.initState();
}
- bool get showSingleImageOnFirstPage => appdata.settings["showSingleImageOnFirstPage"];
-
/// Get the range of images for the given page. [page] is 1-based.
(int start, int end) getPageImagesRange(int page) {
- if (showSingleImageOnFirstPage) {
+ if (reader.showSingleImageOnFirstPage) {
if (page == 1) {
return (0, 1);
} else {
@@ -252,6 +250,7 @@ class _GalleryModeState extends State<_GalleryMode>
}
return PhotoViewGalleryPageOptions.customChild(
+ childSize: reader.size * 2,
controller: photoViewControllers[index],
minScale: PhotoViewComputedScale.contained * 1.0,
maxScale: PhotoViewComputedScale.covered * 10.0,
diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart
index 0686bf3..6b732ba 100644
--- a/lib/pages/reader/reader.dart
+++ b/lib/pages/reader/reader.dart
@@ -111,7 +111,16 @@ class _ReaderState extends State
}
@override
- int get maxPage => ((images?.length ?? 1) / imagesPerPage).ceil();
+ int get maxPage {
+ if (images == null) {
+ return 1;
+ }
+ if (!showSingleImageOnFirstPage) {
+ return (images!.length / imagesPerPage).ceil();
+ } else {
+ return 1 + ((images!.length - 1) / imagesPerPage).ceil();
+ }
+ }
ComicType get type => widget.type;
@@ -125,7 +134,8 @@ class _ReaderState extends State
late ReaderMode mode;
@override
- bool get isPortrait => MediaQuery.of(context).orientation == Orientation.portrait;
+ bool get isPortrait =>
+ MediaQuery.of(context).orientation == Orientation.portrait;
History? history;
@@ -343,6 +353,9 @@ abstract mixin class _ImagePerPageHandler {
}
}
+ bool get showSingleImageOnFirstPage =>
+ appdata.settings["showSingleImageOnFirstPage"];
+
/// The number of images displayed on one screen
int get imagesPerPage {
if (mode.isContinuous) return 1;
diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart
index cce23e6..3b5744d 100644
--- a/lib/pages/settings/reader.dart
+++ b/lib/pages/settings/reader.dart
@@ -113,6 +113,14 @@ class _ReaderSettingsState extends State {
},
),
),
+ _SwitchSetting(
+ title: 'Double tap to zoom'.tl,
+ settingKey: 'enableDoubleTapToZoom',
+ onChanged: () {
+ setState(() {});
+ widget.onChanged?.call('enableDoubleTapToZoom');
+ },
+ ).toSliver(),
_SwitchSetting(
title: 'Long press to zoom'.tl,
settingKey: 'enableLongPressToZoom',
diff --git a/lib/utils/ext.dart b/lib/utils/ext.dart
index c4fa3ed..404e179 100644
--- a/lib/utils/ext.dart
+++ b/lib/utils/ext.dart
@@ -107,4 +107,15 @@ abstract class MapOrNull{
static Map? from(Map? i){
return i == null ? null : Map.from(i);
}
+}
+
+extension FutureExt on Future{
+ /// Wrap the future to make sure it will return at least the duration.
+ Future minTime(Duration duration) async {
+ var res = await Future.wait([
+ this,
+ Future.delayed(duration),
+ ]);
+ return res[0];
+ }
}
\ No newline at end of file
diff --git a/pubspec.yaml b/pubspec.yaml
index 953bf25..708f5b7 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@ name: venera
description: "A comic app."
publish_to: 'none'
-version: 1.4.1+141
+version: 1.4.2+142
environment:
sdk: '>=3.6.0 <4.0.0'