Merge pull request #349 from venera-app/v1.4.2-dev

V1.4.2
This commit is contained in:
nyne
2025-04-29 11:32:40 +08:00
committed by GitHub
13 changed files with 450 additions and 218 deletions

View File

@@ -53,5 +53,7 @@
<true/> <true/>
<key>NSFaceIDUsageDescription</key> <key>NSFaceIDUsageDescription</key>
<string>Ensure that the operation is being performed by the user themselves.</string> <string>Ensure that the operation is being performed by the user themselves.</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.books</string>
</dict> </dict>
</plist> </plist>

View File

@@ -13,7 +13,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.4.1"; final version = "1.4.2";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -185,6 +185,7 @@ class Settings with ChangeNotifier {
'comicListDisplayMode': 'paging', // paging, continuous 'comicListDisplayMode': 'paging', // paging, continuous
'showPageNumberInReader': true, 'showPageNumberInReader': true,
'showSingleImageOnFirstPage': false, 'showSingleImageOnFirstPage': false,
'enableDoubleTapToZoom': true,
}; };
operator [](String key) { operator [](String key) {

View File

@@ -1,4 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ffi';
import 'dart:isolate';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:sqlite3/sqlite3.dart'; import 'package:sqlite3/sqlite3.dart';
@@ -209,7 +211,22 @@ class LocalFavoritesManager with ChangeNotifier {
late Database _db; late Database _db;
late Map<String, int> 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<void> init() async { Future<void> init() async {
counts = {};
_db = sqlite3.open("${App.dataPath}/local_favorite.db"); _db = sqlite3.open("${App.dataPath}/local_favorite.db");
_db.execute(""" _db.execute("""
create table if not exists folder_order ( create table if not exists folder_order (
@@ -256,6 +273,13 @@ class LocalFavoritesManager with ChangeNotifier {
} else { } else {
appdata.settings['followUpdatesFolder'] = null; appdata.settings['followUpdatesFolder'] = null;
} }
initCounts();
}
void initCounts() {
for (var folder in folderNames) {
counts[folder] = count(folder);
}
} }
List<String> find(String id, ComicType type) { List<String> find(String id, ComicType type) {
@@ -357,6 +381,23 @@ class LocalFavoritesManager with ChangeNotifier {
return rows.map((element) => FavoriteItem.fromRow(element)).toList(); return rows.map((element) => FavoriteItem.fromRow(element)).toList();
} }
static Future<List<FavoriteItem>> _getFolderComicsAsync(
String folder, Pointer<void> 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<List<FavoriteItem>> getFolderComicsAsync(String folder) {
return _getFolderComicsAsync(folder, _db.handle);
}
List<FavoriteItem> getAllComics() { List<FavoriteItem> getAllComics() {
var res = <FavoriteItem>{}; var res = <FavoriteItem>{};
for (final folder in folderNames) { for (final folder in folderNames) {
@@ -368,6 +409,26 @@ class LocalFavoritesManager with ChangeNotifier {
return res.toList(); return res.toList();
} }
static Future<List<FavoriteItem>> _getAllComicsAsync(
List<String> folders, Pointer<void> p) {
return Isolate.run(() {
var db = sqlite3.fromPointer(p);
var res = <FavoriteItem>{};
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<List<FavoriteItem>> getAllComicsAsync() {
return _getAllComicsAsync(folderNames, _db.handle);
}
void addTagTo(String folder, String id, String tag) { void addTagTo(String folder, String id, String tag) {
_db.execute(""" _db.execute("""
update "$folder" update "$folder"
@@ -433,6 +494,7 @@ class LocalFavoritesManager with ChangeNotifier {
); );
"""); """);
notifyListeners(); notifyListeners();
counts[name] = 0;
return name; return name;
} }
@@ -547,6 +609,11 @@ class LocalFavoritesManager with ChangeNotifier {
""", [updateTime, comic.id, comic.type.value]); """, [updateTime, comic.id, comic.type.value]);
} }
} }
if (counts[folder] == null) {
counts[folder] = count(folder);
} else {
counts[folder] = counts[folder]! + 1;
}
notifyListeners(); notifyListeners();
return true; return true;
} }
@@ -596,6 +663,7 @@ class LocalFavoritesManager with ChangeNotifier {
delete from folder_order delete from folder_order
where folder_name == ?; where folder_name == ?;
""", [name]); """, [name]);
counts.remove(name);
notifyListeners(); notifyListeners();
} }
@@ -611,6 +679,11 @@ class LocalFavoritesManager with ChangeNotifier {
delete from "$folder" delete from "$folder"
where id == ? and type == ?; where id == ? and type == ?;
""", [id, type.value]); """, [id, type.value]);
if (counts[folder] != null) {
counts[folder] = counts[folder]! - 1;
} else {
counts[folder] = count(folder);
}
notifyListeners(); notifyListeners();
} }

View File

@@ -18,7 +18,9 @@ import 'package:venera/network/download.dart';
import 'package:venera/pages/comic_details_page/comic_page.dart'; import 'package:venera/pages/comic_details_page/comic_page.dart';
import 'package:venera/pages/reader/reader.dart'; import 'package:venera/pages/reader/reader.dart';
import 'package:venera/pages/settings/settings_page.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/io.dart';
import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart'; import 'package:venera/utils/translations.dart';
part 'favorite_actions.dart'; part 'favorite_actions.dart';

View File

@@ -2,6 +2,10 @@ part of 'favorites_page.dart';
const _localAllFolderLabel = '^_^[%local_all%]^_^'; 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 { class _LocalFavoritesPage extends StatefulWidget {
const _LocalFavoritesPage({required this.folder, super.key}); const _LocalFavoritesPage({required this.folder, super.key});
@@ -35,40 +39,110 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
bool get isAllFolder => widget.folder == _localAllFolderLabel; bool get isAllFolder => widget.folder == _localAllFolderLabel;
LocalFavoritesManager get manager => LocalFavoritesManager();
bool isLoading = false;
var searchResults = <FavoriteItem>[];
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() { void updateComics() {
if (keyword.isEmpty) { if (isLoading) return;
setState(() {
if (isAllFolder) { if (isAllFolder) {
comics = LocalFavoritesManager().getAllComics(); var totalComics = manager.totalComics;
if (totalComics < _asyncDataFetchLimit) {
comics = manager.getAllComics();
} else { } else {
comics = LocalFavoritesManager().getFolderComics(widget.folder); 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);
} }
}); });
} }
} else {
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 @override
void initState() { void initState() {
favPage = context.findAncestorStateOfType<_FavoritesPageState>()!; favPage = context.findAncestorStateOfType<_FavoritesPageState>()!;
if (!isAllFolder) { if (!isAllFolder) {
comics = LocalFavoritesManager().getFolderComics(widget.folder);
var (a, b) = LocalFavoritesManager().findLinked(widget.folder); var (a, b) = LocalFavoritesManager().findLinked(widget.folder);
networkSource = a; networkSource = a;
networkFolder = b; networkFolder = b;
} else { } else {
comics = LocalFavoritesManager().getAllComics();
networkSource = null; networkSource = null;
networkFolder = null; networkFolder = null;
} }
comics = [];
updateComics();
LocalFavoritesManager().addListener(updateComics); LocalFavoritesManager().addListener(updateComics);
super.initState(); super.initState();
} }
@@ -215,7 +289,9 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
onPressed: () { onPressed: () {
setState(() { setState(() {
keyword = "";
searchMode = true; searchMode = true;
updateSearchResult();
}); });
}, },
), ),
@@ -410,10 +486,10 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
onPressed: () { onPressed: () {
setState(() {
setState(() { setState(() {
searchMode = false; searchMode = false;
keyword = ""; });
updateComics();
}); });
}, },
), ),
@@ -422,16 +498,26 @@ class _LocalFavoritesPageState extends State<_LocalFavoritesPage> {
autofocus: true, autofocus: true,
decoration: InputDecoration( decoration: InputDecoration(
hintText: "Search".tl, hintText: "Search".tl,
border: InputBorder.none, border: UnderlineInputBorder(),
), ),
onChanged: (v) { onChanged: (v) {
keyword = v; keyword = v;
updateComics(); updateSearchResult();
}, },
).paddingBottom(8).paddingRight(8),
),
if (isLoading)
SliverToBoxAdapter(
child: SizedBox(
height: 200,
child: const Center(
child: CircularProgressIndicator(),
), ),
), ),
)
else
SliverGridComics( SliverGridComics(
comics: comics, comics: searchMode ? searchResults : comics,
selections: selectedComics, selections: selectedComics,
menuBuilder: (c) { menuBuilder: (c) {
return [ return [

View File

@@ -86,9 +86,34 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
padding: widget.withAppbar padding: widget.withAppbar
? EdgeInsets.zero ? EdgeInsets.zero
: EdgeInsets.only(top: context.padding.top), : EdgeInsets.only(top: context.padding.top),
itemCount: folders.length + networkFolders.length + 2, itemCount: folders.length + networkFolders.length + 3,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index == 0) { if (index == 0) {
return buildLocalTitle();
}
index--;
if (index == 0) {
return buildLocalFolder(_localAllFolderLabel);
}
index--;
if (index < folders.length) {
return buildLocalFolder(folders[index]);
}
index -= folders.length;
if (index == 0) {
return buildNetworkTitle();
}
index--;
return buildNetworkFolder(networkFolders[index]);
},
),
)
],
),
);
}
Widget buildLocalTitle() {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Row( child: Row(
@@ -108,8 +133,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
onClick: () { onClick: () {
newFolder().then((value) { newFolder().then((value) {
setState(() { setState(() {
folders = folders = LocalFavoritesManager().folderNames;
LocalFavoritesManager().folderNames;
}); });
}); });
}, },
@@ -120,8 +144,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
onClick: () { onClick: () {
sortFolders().then((value) { sortFolders().then((value) {
setState(() { setState(() {
folders = folders = LocalFavoritesManager().folderNames;
LocalFavoritesManager().folderNames;
}); });
}); });
}, },
@@ -132,16 +155,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
).paddingHorizontal(16), ).paddingHorizontal(16),
); );
} }
index--;
if (index == 0) { Widget buildNetworkTitle() {
return buildLocalFolder(_localAllFolderLabel);
}
index--;
if (index < folders.length) {
return buildLocalFolder(folders[index]);
}
index -= folders.length;
if (index == 0) {
return Container( return Container(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
margin: const EdgeInsets.only(top: 8), margin: const EdgeInsets.only(top: 8),
@@ -175,18 +190,18 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
).paddingHorizontal(16), ).paddingHorizontal(16),
); );
} }
index--;
return buildNetworkFolder(networkFolders[index]);
},
),
)
],
),
);
}
Widget buildLocalFolder(String name) { Widget buildLocalFolder(String name) {
bool isSelected = name == favPage.folder && !favPage.isNetwork; 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( return InkWell(
onTap: () { onTap: () {
if (isSelected) { if (isSelected) {
@@ -211,9 +226,25 @@ class _LeftBarState extends State<_LeftBar> implements FolderList {
), ),
), ),
padding: const EdgeInsets.only(left: 16), padding: const EdgeInsets.only(left: 16),
child: Text(name == _localAllFolderLabel child: Row(
? "All".tl children: [
: getFavoriteDataOrNull(name)?.title ?? name), 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()),
),
],
),
), ),
); );
} }

View File

@@ -152,12 +152,18 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet
bool _dragInProgress = false; bool _dragInProgress = false;
bool get _enableDoubleTapToZoom => appdata.settings["enableDoubleTapToZoom"];
void onTapUp(TapUpDetails event) { void onTapUp(TapUpDetails event) {
if (_longPressInProgress) { if (_longPressInProgress) {
_longPressInProgress = false; _longPressInProgress = false;
return; return;
} }
final location = event.globalPosition; final location = event.globalPosition;
if (!_enableDoubleTapToZoom) {
onTap(location);
return;
}
final previousLocation = _previousEvent?.globalPosition; final previousLocation = _previousEvent?.globalPosition;
if (previousLocation != null) { if (previousLocation != null) {
if ((location - previousLocation).distanceSquared < if ((location - previousLocation).distanceSquared <

View File

@@ -119,7 +119,7 @@ class _GalleryModeState extends State<_GalleryMode>
/// [totalPages] is the total number of pages in the current chapter. /// [totalPages] is the total number of pages in the current chapter.
/// More than one images can be displayed on one page. /// More than one images can be displayed on one page.
int get totalPages { int get totalPages {
if (!showSingleImageOnFirstPage) { if (!reader.showSingleImageOnFirstPage) {
return (reader.images!.length / reader.imagesPerPage).ceil(); return (reader.images!.length / reader.imagesPerPage).ceil();
} else { } else {
return 1 + return 1 +
@@ -144,11 +144,9 @@ class _GalleryModeState extends State<_GalleryMode>
super.initState(); super.initState();
} }
bool get showSingleImageOnFirstPage => appdata.settings["showSingleImageOnFirstPage"];
/// Get the range of images for the given page. [page] is 1-based. /// Get the range of images for the given page. [page] is 1-based.
(int start, int end) getPageImagesRange(int page) { (int start, int end) getPageImagesRange(int page) {
if (showSingleImageOnFirstPage) { if (reader.showSingleImageOnFirstPage) {
if (page == 1) { if (page == 1) {
return (0, 1); return (0, 1);
} else { } else {
@@ -252,6 +250,7 @@ class _GalleryModeState extends State<_GalleryMode>
} }
return PhotoViewGalleryPageOptions.customChild( return PhotoViewGalleryPageOptions.customChild(
childSize: reader.size * 2,
controller: photoViewControllers[index], controller: photoViewControllers[index],
minScale: PhotoViewComputedScale.contained * 1.0, minScale: PhotoViewComputedScale.contained * 1.0,
maxScale: PhotoViewComputedScale.covered * 10.0, maxScale: PhotoViewComputedScale.covered * 10.0,

View File

@@ -111,7 +111,16 @@ class _ReaderState extends State<Reader>
} }
@override @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; ComicType get type => widget.type;
@@ -125,7 +134,8 @@ class _ReaderState extends State<Reader>
late ReaderMode mode; late ReaderMode mode;
@override @override
bool get isPortrait => MediaQuery.of(context).orientation == Orientation.portrait; bool get isPortrait =>
MediaQuery.of(context).orientation == Orientation.portrait;
History? history; History? history;
@@ -343,6 +353,9 @@ abstract mixin class _ImagePerPageHandler {
} }
} }
bool get showSingleImageOnFirstPage =>
appdata.settings["showSingleImageOnFirstPage"];
/// The number of images displayed on one screen /// The number of images displayed on one screen
int get imagesPerPage { int get imagesPerPage {
if (mode.isContinuous) return 1; if (mode.isContinuous) return 1;

View File

@@ -113,6 +113,14 @@ class _ReaderSettingsState extends State<ReaderSettings> {
}, },
), ),
), ),
_SwitchSetting(
title: 'Double tap to zoom'.tl,
settingKey: 'enableDoubleTapToZoom',
onChanged: () {
setState(() {});
widget.onChanged?.call('enableDoubleTapToZoom');
},
).toSliver(),
_SwitchSetting( _SwitchSetting(
title: 'Long press to zoom'.tl, title: 'Long press to zoom'.tl,
settingKey: 'enableLongPressToZoom', settingKey: 'enableLongPressToZoom',

View File

@@ -108,3 +108,14 @@ abstract class MapOrNull{
return i == null ? null : Map<K, V>.from(i); return i == null ? null : Map<K, V>.from(i);
} }
} }
extension FutureExt<T> on Future<T>{
/// Wrap the future to make sure it will return at least the duration.
Future<T> minTime(Duration duration) async {
var res = await Future.wait([
this,
Future.delayed(duration),
]);
return res[0];
}
}

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.4.1+141 version: 1.4.2+142
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'