Merge pull request #191 from venera-app/v1.2.5-dev

V1.2.5
This commit is contained in:
nyne
2025-02-13 11:05:20 +08:00
committed by GitHub
28 changed files with 460 additions and 238 deletions

View File

@@ -496,7 +496,7 @@ let Network = {
/** /**
* [fetch] function for sending HTTP requests. Same api as the browser fetch. * [fetch] function for sending HTTP requests. Same api as the browser fetch.
* @param url {string} * @param url {string}
* @param options {{method: string, headers: Object, body: any}} * @param [options] {{method?: string, headers?: Object, body?: any}}
* @returns {Promise<{ok: boolean, status: number, statusText: string, headers: {}, arrayBuffer: (function(): Promise<ArrayBuffer>), text: (function(): Promise<string>), json: (function(): Promise<any>)}>} * @returns {Promise<{ok: boolean, status: number, statusText: string, headers: {}, arrayBuffer: (function(): Promise<ArrayBuffer>), text: (function(): Promise<string>), json: (function(): Promise<any>)}>}
* @since 1.2.0 * @since 1.2.0
*/ */
@@ -921,7 +921,7 @@ function Comic({id, title, subtitle, subTitle, cover, tags, description, maxPage
* @param description {string?} * @param description {string?}
* @param tags {Map<string, string[]> | {} | null | undefined} * @param tags {Map<string, string[]> | {} | null | undefined}
* @param chapters {Map<string, string> | {} | null | undefined} - key: chapter id, value: chapter title * @param chapters {Map<string, string> | {} | null | undefined} - key: chapter id, value: chapter title
* @param isFavorite {boolean | null | undefined} - favorite status. If the comic source supports multiple folders, this field should be null * @param isFavorite {boolean | null | undefined} - favorite status.
* @param subId {string?} - a param which is passed to comments api * @param subId {string?} - a param which is passed to comments api
* @param thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails * @param thumbnails {string[]?} - for multiple page thumbnails, set this to null, and use `loadThumbnails` api to load thumbnails
* @param recommend {Comic[]?} - related comics * @param recommend {Comic[]?} - related comics
@@ -1086,6 +1086,19 @@ class ComicSource {
}); });
} }
translation = {}
/**
* Translate given string with the current locale using the translation object.
* @param key {string}
* @returns {string}
* @since 1.2.5
*/
translate(key) {
let locale = APP.locale;
return this.translation[locale]?.[key] ?? key;
}
init() { } init() { }
static sources = {} static sources = {}

View File

@@ -139,8 +139,8 @@
"Block": "屏蔽", "Block": "屏蔽",
"Add new favorite to": "添加新收藏到", "Add new favorite to": "添加新收藏到",
"Move favorite after reading": "阅读后移动收藏", "Move favorite after reading": "阅读后移动收藏",
"Delete folder?" : "除文件夾?", "Delete folder?" : "除文件夹?",
"Delete folder '@f' ?" : "删除文件夹 '@f' ", "Delete folder '@f' ?" : "删除文件夹 '@f' ?",
"Import from file": "从文件导入", "Import from file": "从文件导入",
"Failed to import": "导入失败", "Failed to import": "导入失败",
"Cache Limit": "缓存限制", "Cache Limit": "缓存限制",
@@ -324,7 +324,15 @@
"Success": "成功", "Success": "成功",
"Compressing": "压缩中", "Compressing": "压缩中",
"Exporting": "导出中", "Exporting": "导出中",
"Search Sources": "搜索源" "Search Sources": "搜索源",
"Removed": "已移除",
"Added to favorites": "已添加到收藏",
"Not added": "未添加",
"Create a folder": "新建收藏夹",
"Created successfully": "创建成功",
"name": "名称",
"Reverse tap to turn Pages": "反转点击翻页",
"Show all": "显示全部"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -651,6 +659,14 @@
"Success": "成功", "Success": "成功",
"Compressing": "壓縮中", "Compressing": "壓縮中",
"Exporting": "匯出中", "Exporting": "匯出中",
"Search Sources": "搜索源" "Search Sources": "搜索源",
"Removed": "已移除",
"Added to favorites": "已添加到收藏",
"Not added": "未添加",
"Create a folder": "新建收藏夾",
"Created successfully": "創建成功",
"name": "名稱",
"Reverse tap to turn Pages": "反轉點擊翻頁",
"Show all": "顯示全部"
} }
} }

View File

@@ -742,7 +742,7 @@ class _SliverGridComicsState extends State<SliverGridComics> {
@override @override
void didUpdateWidget(covariant SliverGridComics oldWidget) { void didUpdateWidget(covariant SliverGridComics oldWidget) {
if (oldWidget.comics != widget.comics) { if (oldWidget.comics.isEqualTo(widget.comics)) {
comics.clear(); comics.clear();
for (var comic in widget.comics) { for (var comic in widget.comics) {
if (isBlocked(comic) == null) { if (isBlocked(comic) == null) {

View File

@@ -10,7 +10,7 @@ export "widget_utils.dart";
export "context.dart"; export "context.dart";
class _App { class _App {
final version = "1.2.4"; final version = "1.2.5";
bool get isAndroid => Platform.isAndroid; bool get isAndroid => Platform.isAndroid;

View File

@@ -135,6 +135,7 @@ class _Settings with ChangeNotifier {
'readerMode': 'galleryLeftToRight', // values of [ReaderMode] 'readerMode': 'galleryLeftToRight', // values of [ReaderMode]
'readerScreenPicNumber': 1, // 1 - 5 'readerScreenPicNumber': 1, // 1 - 5
'enableTapToTurnPages': true, 'enableTapToTurnPages': true,
'reverseTapToTurnPages': false,
'enablePageAnimation': true, 'enablePageAnimation': true,
'language': 'system', // system, zh-CN, zh-TW, en-US 'language': 'system', // system, zh-CN, zh-TW, en-US
'cacheSize': 2048, // in MB 'cacheSize': 2048, // in MB

View File

@@ -417,7 +417,7 @@ class SearchOptions {
const SearchOptions(this.options, this.label, this.type, this.defaultVal); const SearchOptions(this.options, this.label, this.type, this.defaultVal);
String get defaultValue => defaultVal ?? options.keys.first; String get defaultValue => defaultVal ?? options.keys.firstOrNull ?? "";
} }
typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function( typedef CategoryComicsLoader = Future<Res<List<Comic>>> Function(

View File

@@ -37,6 +37,8 @@ class FavoriteData {
final AddOrDelFavFunc? addOrDelFavorite; final AddOrDelFavFunc? addOrDelFavorite;
final bool singleFolderForSingleComic;
const FavoriteData({ const FavoriteData({
required this.key, required this.key,
required this.title, required this.title,
@@ -49,6 +51,7 @@ class FavoriteData {
this.allFavoritesId, this.allFavoritesId,
this.addOrDelFavorite, this.addOrDelFavorite,
this.isOldToNewSort, this.isOldToNewSort,
this.singleFolderForSingleComic = false,
}); });
} }

View File

@@ -620,6 +620,7 @@ class ComicSourceParser {
final bool multiFolder = _getValue("favorites.multiFolder"); final bool multiFolder = _getValue("favorites.multiFolder");
final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort"); final bool? isOldToNewSort = _getValue("favorites.isOldToNewSort");
final bool? singleFolderForSingleComic = _getValue("favorites.singleFolderForSingleComic");
Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async { Future<Res<T>> retryZone<T>(Future<Res<T>> Function() func) async {
if (!ComicSource.find(_key!)!.isLogged) { if (!ComicSource.find(_key!)!.isLogged) {
@@ -773,6 +774,7 @@ class ComicSourceParser {
deleteFolder: deleteFolder, deleteFolder: deleteFolder,
addOrDelFavorite: addOrDelFavFunc, addOrDelFavorite: addOrDelFavFunc,
isOldToNewSort: isOldToNewSort, isOldToNewSort: isOldToNewSort,
singleFolderForSingleComic: singleFolderForSingleComic ?? false,
); );
} }

View File

@@ -156,7 +156,7 @@ class JsEngine with _JSEngineApi, JsUiApi {
case "UI": case "UI":
return handleUIMessage(Map.from(message)); return handleUIMessage(Map.from(message));
case "getLocale": case "getLocale":
return "${App.locale.languageCode}-${App.locale.countryCode}"; return "${App.locale.languageCode}_${App.locale.countryCode}";
case "getPlatform": case "getPlatform":
return Platform.operatingSystem; return Platform.operatingSystem;
} }

View File

@@ -118,7 +118,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async {
// windows version of package `flutter_inappwebview` cannot get some cookies // windows version of package `flutter_inappwebview` cannot get some cookies
// Using DesktopWebview instead // Using DesktopWebview instead
if (App.isLinux || App.isWindows) { if (App.isLinux) {
var webview = DesktopWebview( var webview = DesktopWebview(
initialUrl: url, initialUrl: url,
onTitleChange: (title, controller) async { onTitleChange: (title, controller) async {

View File

@@ -58,7 +58,11 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(context, index) { (context, index) {
final source = sources[index]; final source = sources[index];
return _SliverSearchResult(source: source, keyword: _keyword); return _SliverSearchResult(
key: ValueKey(source.key),
source: source,
keyword: _keyword,
);
}, },
childCount: sources.length, childCount: sources.length,
), ),
@@ -68,7 +72,11 @@ class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
} }
class _SliverSearchResult extends StatefulWidget { class _SliverSearchResult extends StatefulWidget {
const _SliverSearchResult({required this.source, required this.keyword}); const _SliverSearchResult({
required this.source,
required this.keyword,
super.key,
});
final ComicSource source; final ComicSource source;
@@ -90,6 +98,8 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
List<Comic>? comics; List<Comic>? comics;
String? error;
void load() async { void load() async {
final data = widget.source.searchPageData!; final data = widget.source.searchPageData!;
var options = var options =
@@ -101,6 +111,11 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
comics = res.data; comics = res.data;
isLoading = false; isLoading = false;
}); });
} else {
setState(() {
error = res.errorMessage ?? "Unknown error".tl;
isLoading = false;
});
} }
} else if (data.loadNext != null) { } else if (data.loadNext != null) {
var res = await data.loadNext!(widget.keyword, null, options); var res = await data.loadNext!(widget.keyword, null, options);
@@ -109,6 +124,11 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
comics = res.data; comics = res.data;
isLoading = false; isLoading = false;
}); });
} else {
setState(() {
error = res.errorMessage ?? "Unknown error".tl;
isLoading = false;
});
} }
} }
} }
@@ -139,6 +159,9 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (error != null && error!.startsWith("CloudflareException")) {
error = "Cloudflare verification required".tl;
}
super.build(context); super.build(context);
return InkWell( return InkWell(
onTap: () { onTap: () {
@@ -181,7 +204,7 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
}), }),
), ),
) )
else if (comics == null || comics!.isEmpty) else if (error != null || comics == null || comics!.isEmpty)
SizedBox( SizedBox(
height: _kComicHeight, height: _kComicHeight,
child: Column( child: Column(
@@ -190,7 +213,13 @@ class _SliverSearchResultState extends State<_SliverSearchResult>
children: [ children: [
const Icon(Icons.error_outline), const Icon(Icons.error_outline),
const SizedBox(width: 8), const SizedBox(width: 8),
Text("No search results found".tl), Expanded(
child: Text(
error ?? "No search results found".tl,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
)
], ],
), ),
const Spacer(), const Spacer(),

View File

@@ -32,7 +32,7 @@ class _CategoriesPageState extends State<CategoriesPage> {
.toList(); .toList();
categories = categories =
categories.where((element) => allCategories.contains(element)).toList(); categories.where((element) => allCategories.contains(element)).toList();
if (!categories.isEqualsTo(this.categories)) { if (!categories.isEqualTo(this.categories)) {
setState(() { setState(() {
this.categories = categories; this.categories = categories;
}); });

View File

@@ -49,19 +49,19 @@ class ComicPage extends StatefulWidget {
class _ComicPageState extends LoadingState<ComicPage, ComicDetails> class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
with _ComicPageActions { with _ComicPageActions {
@override
History? history;
bool showAppbarTitle = false; bool showAppbarTitle = false;
var scrollController = ScrollController(); var scrollController = ScrollController();
bool isDownloaded = false; bool isDownloaded = false;
void updateHistory() async { @override
var newHistory = await HistoryManager() void onReadEnd() {
.find(widget.id, ComicType(widget.sourceKey.hashCode)); // The history is passed by reference, so it will be updated automatically.
if (newHistory?.ep != history?.ep || newHistory?.page != history?.page) { update();
history = newHistory;
update();
}
} }
@override @override
@@ -77,14 +77,12 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
@override @override
void initState() { void initState() {
scrollController.addListener(onScroll); scrollController.addListener(onScroll);
HistoryManager().addListener(updateHistory);
super.initState(); super.initState();
} }
@override @override
void dispose() { void dispose() {
scrollController.removeListener(onScroll); scrollController.removeListener(onScroll);
HistoryManager().removeListener(updateHistory);
super.dispose(); super.dispose();
} }
@@ -552,7 +550,7 @@ class _ComicPageState extends LoadingState<ComicPage, ComicDetails>
if (comic.chapters == null) { if (comic.chapters == null) {
return const SliverPadding(padding: EdgeInsets.zero); return const SliverPadding(padding: EdgeInsets.zero);
} }
return const _ComicChapters(); return _ComicChapters(history);
} }
Widget buildThumbnails() { Widget buildThumbnails() {
@@ -594,7 +592,7 @@ abstract mixin class _ComicPageActions {
ComicSource get comicSource => ComicSource.find(comic.sourceKey)!; ComicSource get comicSource => ComicSource.find(comic.sourceKey)!;
History? history; History? get history;
bool isLiking = false; bool isLiking = false;
@@ -614,8 +612,10 @@ abstract mixin class _ComicPageActions {
update(); update();
} }
/// whether the comic is added to local favorite
bool isAddToLocalFav = false; bool isAddToLocalFav = false;
/// whether the comic is favorite on the server
bool isFavorite = false; bool isFavorite = false;
FavoriteItem _toFavoriteItem() { FavoriteItem _toFavoriteItem() {
@@ -686,11 +686,13 @@ abstract mixin class _ComicPageActions {
chapters: comic.chapters, chapters: comic.chapters,
initialChapter: ep, initialChapter: ep,
initialPage: page, initialPage: page,
history: History.fromModel(model: comic, ep: 0, page: 0), history: history ?? History.fromModel(model: comic, ep: 0, page: 0),
author: comic.findAuthor() ?? '', author: comic.findAuthor() ?? '',
tags: comic.plainTags, tags: comic.plainTags,
), ),
); ).then((_) {
onReadEnd();
});
} }
void continueRead() { void continueRead() {
@@ -699,6 +701,8 @@ abstract mixin class _ComicPageActions {
read(ep, page); read(ep, page);
} }
void onReadEnd();
void download() async { void download() async {
if (LocalManager().isDownloading(comic.id, comic.comicType)) { if (LocalManager().isDownloading(comic.id, comic.comicType)) {
App.rootContext.showMessage(message: "The comic is downloading".tl); App.rootContext.showMessage(message: "The comic is downloading".tl);
@@ -1081,7 +1085,9 @@ class _ActionButton extends StatelessWidget {
} }
class _ComicChapters extends StatefulWidget { class _ComicChapters extends StatefulWidget {
const _ComicChapters(); const _ComicChapters(this.history);
final History? history;
@override @override
State<_ComicChapters> createState() => _ComicChaptersState(); State<_ComicChapters> createState() => _ComicChaptersState();
@@ -1094,104 +1100,133 @@ class _ComicChaptersState extends State<_ComicChapters> {
bool showAll = false; bool showAll = false;
late History? history;
@override
void initState() {
super.initState();
history = widget.history;
}
@override @override
void didChangeDependencies() { void didChangeDependencies() {
state = context.findAncestorStateOfType<_ComicPageState>()!; state = context.findAncestorStateOfType<_ComicPageState>()!;
super.didChangeDependencies(); super.didChangeDependencies();
} }
@override
void didUpdateWidget(covariant _ComicChapters oldWidget) {
super.didUpdateWidget(oldWidget);
setState(() {
history = widget.history;
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final eps = state.comic.chapters!; final eps = state.comic.chapters!;
int length = eps.length; return SliverLayoutBuilder(
builder: (context, constrains) {
int length = eps.length;
bool canShowAll = showAll;
if (!showAll) {
var width = constrains.crossAxisExtent - 16;
var crossItems = width ~/ 200;
if (width % 200 != 0) {
crossItems += 1;
}
length = math.min(length, crossItems * 8);
if (length == eps.length) {
canShowAll = true;
}
}
if (!showAll) { return SliverMainAxisGroup(
length = math.min(length, 20); slivers: [
} SliverToBoxAdapter(
child: ListTile(
return SliverMainAxisGroup( title: Text("Chapters".tl),
slivers: [ trailing: Tooltip(
SliverToBoxAdapter( message: "Order".tl,
child: ListTile( child: IconButton(
title: Text("Chapters".tl), icon: Icon(reverse
trailing: Tooltip( ? Icons.vertical_align_top
message: "Order".tl, : Icons.vertical_align_bottom_outlined),
child: IconButton( onPressed: () {
icon: Icon(reverse setState(() {
? Icons.vertical_align_top reverse = !reverse;
: Icons.vertical_align_bottom_outlined), });
onPressed: () { },
setState(() {
reverse = !reverse;
});
},
),
),
),
),
SliverGrid(
delegate:
SliverChildBuilderDelegate(childCount: length, (context, i) {
if (reverse) {
i = eps.length - i - 1;
}
var key = eps.keys.elementAt(i);
var value = eps[key]!;
bool visited =
(state.history?.readEpisode ?? const {}).contains(i + 1);
return Padding(
padding: const EdgeInsets.fromLTRB(8, 4, 8, 4),
child: Material(
color: context.colorScheme.surfaceContainer,
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: InkWell(
onTap: () => state.read(i + 1),
borderRadius: const BorderRadius.all(Radius.circular(12)),
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Center(
child: Text(
value,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: visited ? context.colorScheme.outline : null,
),
),
),
), ),
), ),
), ),
);
}),
gridDelegate: const SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: 200, itemHeight: 48),
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
if (eps.length > 20 && !showAll)
SliverToBoxAdapter(
child: Align(
alignment: Alignment.center,
child: FilledButton.tonal(
style: ButtonStyle(
shape: WidgetStateProperty.all(const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(8)))),
),
onPressed: () {
setState(() {
showAll = true;
});
},
child: Text("${"Show all".tl} (${eps.length})"),
).paddingTop(12),
), ),
), SliverGrid(
const SliverToBoxAdapter( delegate: SliverChildBuilderDelegate(
child: Divider(), childCount: length,
), (context, i) {
], if (reverse) {
i = eps.length - i - 1;
}
var key = eps.keys.elementAt(i);
var value = eps[key]!;
bool visited = (history?.readEpisode ?? {}).contains(i + 1);
return Padding(
padding: const EdgeInsets.fromLTRB(6, 4, 6, 4),
child: Material(
color: context.colorScheme.surfaceContainer,
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: () => state.read(i + 1),
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Center(
child: Text(
value,
maxLines: 1,
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: visited
? context.colorScheme.outline
: null,
),
),
),
),
),
),
);
},
),
gridDelegate: const SliverGridDelegateWithFixedHeight(
maxCrossAxisExtent: 200,
itemHeight: 48,
),
).sliverPadding(const EdgeInsets.symmetric(horizontal: 8)),
if (eps.length > 20 && !canShowAll)
SliverToBoxAdapter(
child: Align(
alignment: Alignment.center,
child: TextButton.icon(
icon: const Icon(Icons.arrow_drop_down),
onPressed: () {
setState(() {
showAll = true;
});
},
label: Text("${"Show all".tl} (${eps.length})"),
).paddingTop(12),
),
),
const SliverToBoxAdapter(
child: Divider(),
),
],
);
},
); );
} }
} }
@@ -1672,6 +1707,42 @@ class _NetworkFavoritesState extends State<_NetworkFavorites> {
} }
Widget buildMultiFolder() { Widget buildMultiFolder() {
if (widget.isFavorite == true &&
widget.comicSource.favoriteData!.singleFolderForSingleComic) {
return Column(
children: [
Expanded(
child: Center(
child: Text("Added to favorites".tl),
),
),
Center(
child: Button.filled(
isLoading: isLoading,
onPressed: () async {
setState(() {
isLoading = true;
});
var res = await widget.comicSource.favoriteData!
.addOrDelFavorite!(widget.cid, '', false, null);
if (res.success) {
widget.onFavorite(false);
context.pop();
App.rootContext.showMessage(message: "Removed".tl);
} else {
setState(() {
isLoading = false;
});
context.showMessage(message: res.errorMessage!);
}
},
child: Text("Remove".tl),
).paddingVertical(8),
),
],
);
}
if (isLoadingFolders) { if (isLoadingFolders) {
loadFolders(); loadFolders();
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());

View File

@@ -1011,7 +1011,7 @@ class _LoginPageState extends State<_LoginPage> {
if (widget.config.loginWebsite != null) if (widget.config.loginWebsite != null)
TextButton( TextButton(
onPressed: () { onPressed: () {
if (App.isWindows || App.isLinux) { if (App.isLinux) {
loginWithWebview2(); loginWithWebview2();
} else { } else {
loginWithWebview(); loginWithWebview();
@@ -1127,7 +1127,7 @@ class _LoginPageState extends State<_LoginPage> {
} }
} }
// for windows and linux // for linux
void loginWithWebview2() async { void loginWithWebview2() async {
if (!await DesktopWebview.isAvailable()) { if (!await DesktopWebview.isAvailable()) {
context.showMessage(message: "Webview is not available".tl); context.showMessage(message: "Webview is not available".tl);

View File

@@ -37,7 +37,7 @@ class _ExplorePageState extends State<ExplorePage>
.expand((e) => e.map((e) => e.title)) .expand((e) => e.map((e) => e.title))
.toList(); .toList();
explorePages = explorePages.where((e) => all.contains(e)).toList(); explorePages = explorePages.where((e) => all.contains(e)).toList();
if (!pages.isEqualsTo(explorePages)) { if (!pages.isEqualTo(explorePages)) {
setState(() { setState(() {
pages = explorePages; pages = explorePages;
controller = TabController( controller = TabController(

View File

@@ -476,55 +476,47 @@ class _CreateFolderDialogState extends State<_CreateFolderDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SimpleDialog( return ContentDialog(
title: Text("Create a folder".tl), title: "Create a folder".tl,
children: [ content: Column(
Padding( children: [
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), Padding(
child: TextField( padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
controller: controller, child: TextField(
decoration: InputDecoration( controller: controller,
border: const OutlineInputBorder(), decoration: InputDecoration(
labelText: "name".tl, border: const OutlineInputBorder(),
), labelText: "name".tl,
),
),
const SizedBox(
width: 200,
height: 10,
),
if (loading)
Center(
child: const CircularProgressIndicator(
strokeWidth: 2,
).fixWidth(24).fixHeight(24),
)
else
SizedBox(
height: 35,
child: Center(
child: TextButton(
onPressed: () {
setState(() {
loading = true;
});
widget.data.addFolder!(controller.text).then((b) {
if (b.error) {
context.showMessage(message: b.errorMessage!);
setState(() {
loading = false;
});
} else {
context.pop();
context.showMessage(message: "Created successfully".tl);
widget.updateState();
}
});
},
child: Text("Submit".tl),
), ),
), ),
) ),
const SizedBox(
height: 16
),
],
),
actions: [
Button.filled(
isLoading: loading,
onPressed: () {
setState(() {
loading = true;
});
widget.data.addFolder!(controller.text).then((b) {
if (b.error) {
context.showMessage(message: b.errorMessage!);
setState(() {
loading = false;
});
} else {
context.pop();
context.showMessage(message: "Created successfully".tl);
widget.updateState();
}
});
},
child: Text("Submit".tl),
)
], ],
); );
} }

View File

@@ -24,6 +24,8 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
int fingers = 0; int fingers = 0;
late _ReaderState reader;
@override @override
void initState() { void initState() {
_tapGestureRecognizer = TapGestureRecognizer() _tapGestureRecognizer = TapGestureRecognizer()
@@ -33,6 +35,7 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
}; };
super.initState(); super.initState();
context.readerScaffold._gestureDetectorState = this; context.readerScaffold._gestureDetectorState = this;
reader = context.reader;
} }
@override @override
@@ -166,7 +169,9 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
} }
void onTap(Offset location) { void onTap(Offset location) {
if (context.readerScaffold.isOpen) { if (reader._imageViewController!.handleOnTap(location)) {
return;
} else if (context.readerScaffold.isOpen) {
context.readerScaffold.openOrClose(); context.readerScaffold.openOrClose();
} else { } else {
if (appdata.settings['enableTapToTurnPages']) { if (appdata.settings['enableTapToTurnPages']) {
@@ -186,31 +191,37 @@ class _ReaderGestureDetectorState extends State<_ReaderGestureDetector> {
isBottom = true; isBottom = true;
} }
bool isCenter = false; bool isCenter = false;
var prev = context.reader.toPrevPage;
var next = context.reader.toNextPage;
if (appdata.settings['reverseTapToTurnPages']) {
prev = context.reader.toNextPage;
next = context.reader.toPrevPage;
}
switch (context.reader.mode) { switch (context.reader.mode) {
case ReaderMode.galleryLeftToRight: case ReaderMode.galleryLeftToRight:
case ReaderMode.continuousLeftToRight: case ReaderMode.continuousLeftToRight:
if (isLeft) { if (isLeft) {
context.reader.toPrevPage(); prev();
} else if (isRight) { } else if (isRight) {
context.reader.toNextPage(); next();
} else { } else {
isCenter = true; isCenter = true;
} }
case ReaderMode.galleryRightToLeft: case ReaderMode.galleryRightToLeft:
case ReaderMode.continuousRightToLeft: case ReaderMode.continuousRightToLeft:
if (isLeft) { if (isLeft) {
context.reader.toNextPage(); next();
} else if (isRight) { } else if (isRight) {
context.reader.toPrevPage(); prev();
} else { } else {
isCenter = true; isCenter = true;
} }
case ReaderMode.galleryTopToBottom: case ReaderMode.galleryTopToBottom:
case ReaderMode.continuousTopToBottom: case ReaderMode.continuousTopToBottom:
if (isTop) { if (isTop) {
context.reader.toPrevPage(); prev();
} else if (isBottom) { } else if (isBottom) {
context.reader.toNextPage(); next();
} else { } else {
isCenter = true; isCenter = true;
} }

View File

@@ -335,6 +335,11 @@ class _GalleryModeState extends State<_GalleryMode>
} }
} }
} }
@override
bool handleOnTap(Offset location) {
return false;
}
} }
const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{ const Set<PointerDeviceKind> _kTouchLikeDeviceTypes = <PointerDeviceKind>{
@@ -366,6 +371,18 @@ class _ContinuousModeState extends State<_ContinuousMode>
var fingers = 0; var fingers = 0;
bool disableScroll = false; bool disableScroll = false;
/// Whether the user was scrolling the page.
/// The gesture detector has a delay to detect tap event.
/// To handle the tap event, we need to know if the user was scrolling before the delay.
bool delayedIsScrolling = false;
void delayedSetIsScrolling(bool value) {
Future.delayed(
const Duration(milliseconds: 300),
() => delayedIsScrolling = value,
);
}
@override @override
void initState() { void initState() {
reader = context.reader; reader = context.reader;
@@ -374,6 +391,12 @@ class _ContinuousModeState extends State<_ContinuousMode>
super.initState(); super.initState();
} }
@override
void dispose() {
itemPositionsListener.itemPositions.removeListener(onPositionChanged);
super.dispose();
}
void onPositionChanged() { void onPositionChanged() {
var page = itemPositionsListener.itemPositions.value.first.index; var page = itemPositionsListener.itemPositions.value.first.index;
page = page.clamp(1, reader.maxPage); page = page.clamp(1, reader.maxPage);
@@ -489,6 +512,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
}); });
} }
}, },
onPointerCancel: (event) {
fingers--;
if (fingers <= 1 && disableScroll) {
setState(() {
disableScroll = false;
});
}
},
onPointerPanZoomUpdate: (event) { onPointerPanZoomUpdate: (event) {
if (event.scale == 1.0) { if (event.scale == 1.0) {
smoothTo(0 - event.panDelta.dy); smoothTo(0 - event.panDelta.dy);
@@ -516,8 +547,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
child: widget, child: widget,
); );
widget = NotificationListener<ScrollUpdateNotification>( widget = NotificationListener<ScrollNotification>(
onNotification: (notification) { onNotification: (notification) {
if (notification is ScrollStartNotification) {
delayedSetIsScrolling(true);
} else if (notification is ScrollEndNotification) {
delayedSetIsScrolling(false);
}
var length = reader.maxChapter; var length = reader.maxChapter;
if (!scrollController.hasClients) return false; if (!scrollController.hasClients) return false;
if (scrollController.position.pixels <= if (scrollController.position.pixels <=
@@ -592,7 +629,7 @@ class _ContinuousModeState extends State<_ContinuousMode>
@override @override
void handleLongPressDown(Offset location) { void handleLongPressDown(Offset location) {
if (!appdata.settings['enableLongPressToZoom']) { if (!appdata.settings['enableLongPressToZoom'] || delayedIsScrolling) {
return; return;
} }
double target = photoViewController.getInitialScale!.call()! * 1.75; double target = photoViewController.getInitialScale!.call()! * 1.75;
@@ -667,6 +704,14 @@ class _ContinuousModeState extends State<_ContinuousMode>
); );
} }
} }
@override
bool handleOnTap(Offset location) {
if (delayedIsScrolling) {
return true;
}
return false;
}
} }
ImageProvider _createImageProviderFromKey( ImageProvider _createImageProviderFromKey(

View File

@@ -237,6 +237,7 @@ class _ReaderState extends State<Reader> with _ReaderLocation, _ReaderWindow {
history!.maxPage = maxPage; history!.maxPage = maxPage;
} }
history!.readEpisode.add(chapter); history!.readEpisode.add(chapter);
print(history!.readEpisode);
history!.time = DateTime.now(); history!.time = DateTime.now();
HistoryManager().addHistory(history!); HistoryManager().addHistory(history!);
} }
@@ -430,4 +431,7 @@ abstract interface class _ImageViewController {
void handleLongPressUp(Offset location); void handleLongPressUp(Offset location);
void handleKeyEvent(KeyEvent event); void handleKeyEvent(KeyEvent event);
/// Returns true if the event is handled.
bool handleOnTap(Offset location);
} }

View File

@@ -660,12 +660,16 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> {
App.rootContext.pop(); App.rootContext.pop();
}, },
child: Container( child: Container(
decoration: BoxDecoration( foregroundDecoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)), borderRadius: BorderRadius.circular(16),
border: Border.all( border: Border.all(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
), ),
), ),
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
width: double.infinity, width: double.infinity,
height: double.infinity, height: double.infinity,
child: Image( child: Image(

View File

@@ -189,7 +189,7 @@ class _SearchPageState extends State<SearchPage> {
void updateSearchSourcesIfNeeded() { void updateSearchSourcesIfNeeded() {
var old = searchSources; var old = searchSources;
findSearchSources(); findSearchSources();
if (old.isEqualsTo(searchSources)) { if (old.isEqualTo(searchSources)) {
return; return;
} }
setState(() {}); setState(() {});

View File

@@ -196,7 +196,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
return _SearchSettingsDialog(state: this); return _SearchSettingsDialog(state: this);
}, },
); );
if (!previousOptions.isEqualsTo(options) || if (!previousOptions.isEqualTo(options) ||
previousSourceKey != sourceKey) { previousSourceKey != sourceKey) {
text = checkAutoLanguage(controller.text); text = checkAutoLanguage(controller.text);
controller.currentText = text; controller.currentText = text;

View File

@@ -22,6 +22,13 @@ class _ReaderSettingsState extends State<ReaderSettings> {
widget.onChanged?.call("enableTapToTurnPages"); widget.onChanged?.call("enableTapToTurnPages");
}, },
).toSliver(), ).toSliver(),
_SwitchSetting(
title: "Reverse tap to turn Pages".tl,
settingKey: "reverseTapToTurnPages",
onChanged: () {
widget.onChanged?.call("reverseTapToTurnPages");
},
).toSliver(),
_SwitchSetting( _SwitchSetting(
title: "Page animation".tl, title: "Page animation".tl,
settingKey: "enablePageAnimation", settingKey: "enablePageAnimation",

View File

@@ -25,8 +25,13 @@ extension WebviewExtension on InAppWebViewController {
if (url[url.length - 1] == '/') { if (url[url.length - 1] == '/') {
url = url.substring(0, url.length - 1); url = url.substring(0, url.length - 1);
} }
CookieManager cookieManager = CookieManager.instance(); CookieManager cookieManager = CookieManager.instance(
final cookies = await cookieManager.getCookies(url: WebUri(url)); webViewEnvironment: AppWebview.webViewEnvironment,
);
final cookies = await cookieManager.getCookies(
url: WebUri(url),
webViewController: this,
);
var res = <io.Cookie>[]; var res = <io.Cookie>[];
for (var cookie in cookies) { for (var cookie in cookies) {
var c = io.Cookie(cookie.name, cookie.value); var c = io.Cookie(cookie.name, cookie.value);
@@ -90,7 +95,8 @@ class _AppWebviewState extends State<AppWebview> {
var proxy = appdata.settings['proxy'].toString(); var proxy = appdata.settings['proxy'].toString();
if (proxy != "system" && proxy != "direct") { if (proxy != "system" && proxy != "direct") {
var proxyAvailable = await WebViewFeature.isFeatureSupported( var proxyAvailable = await WebViewFeature.isFeatureSupported(
WebViewFeature.PROXY_OVERRIDE); WebViewFeature.PROXY_OVERRIDE,
);
if (proxyAvailable) { if (proxyAvailable) {
ProxyController proxyController = ProxyController.instance(); ProxyController proxyController = ProxyController.instance();
await proxyController.clearProxyOverride(); await proxyController.clearProxyOverride();
@@ -147,22 +153,21 @@ class _AppWebviewState extends State<AppWebview> {
) )
]; ];
Widget body = (App.isWindows && AppWebview.webViewEnvironment == null) Widget body = FutureBuilder(
? FutureBuilder( future: future,
future: future, builder: (context, e) {
builder: (context, e) { if (e.error != null) {
if (e.error != null) { return Center(child: Text("Error: ${e.error}"));
return Center(child: Text("Error: ${e.error}")); }
} if (e.data == null) {
if (e.data == null) { return const SizedBox();
return const Center(child: CircularProgressIndicator()); }
} AppWebview.webViewEnvironment = e.data;
AppWebview.webViewEnvironment = e.data; return createWebviewWithEnvironment(
return createWebviewWithEnvironment( AppWebview.webViewEnvironment,
AppWebview.webViewEnvironment); );
}, },
) );
: createWebviewWithEnvironment(AppWebview.webViewEnvironment);
body = Stack( body = Stack(
children: [ children: [

View File

@@ -115,7 +115,17 @@ abstract class CBZ {
cache.deleteSync(recursive: true); cache.deleteSync(recursive: true);
throw Exception('No images found in the archive'); throw Exception('No images found in the archive');
} }
files.sort((a, b) => a.path.compareTo(b.path)); files.sort((a, b) {
var aName = a.basenameWithoutExt;
var bName = b.basenameWithoutExt;
var aIndex = int.tryParse(aName);
var bIndex = int.tryParse(bName);
if (aIndex != null && bIndex != null) {
return aIndex.compareTo(bIndex);
} else {
return a.path.compareTo(b.path);
}
});
var coverFile = files.firstWhereOrNull( var coverFile = files.firstWhereOrNull(
(element) => (element) =>
element.path.endsWith('cover.${element.path.split('.').last}'), element.path.endsWith('cover.${element.path.split('.').last}'),

View File

@@ -25,7 +25,9 @@ extension ListExt<T> on List<T>{
} }
} }
bool isEqualsTo(List<T> list){ /// Compare every element of this list with another list.
/// Return true if all elements are equal.
bool isEqualTo(List<T> list){
if(length != list.length){ if(length != list.length){
return false; return false;
} }
@@ -81,10 +83,6 @@ extension StringExt on String{
return '$before$to$after'; return '$before$to$after';
} }
static bool hasMatch(String? value, String pattern) {
return (value == null) ? false : RegExp(pattern).hasMatch(value);
}
bool _isURL(){ bool _isURL(){
final regex = RegExp( final regex = RegExp(
r'^((http|https|ftp)://)[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$', r'^((http|https|ftp)://)[\w-]+(\.[\w-]+)+([\w.,@?^=%&:/~+#-|]*[\w@?^=%&/~+#-])?$',

View File

@@ -307,19 +307,21 @@ packages:
flutter_inappwebview: flutter_inappwebview:
dependency: "direct main" dependency: "direct main"
description: description:
name: flutter_inappwebview path: flutter_inappwebview
sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "6.1.5" source: git
version: "6.2.0-beta.3"
flutter_inappwebview_android: flutter_inappwebview_android:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_android path: flutter_inappwebview_android
sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.3" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_internal_annotations: flutter_inappwebview_internal_annotations:
dependency: transitive dependency: transitive
description: description:
@@ -331,43 +333,48 @@ packages:
flutter_inappwebview_ios: flutter_inappwebview_ios:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_ios path: flutter_inappwebview_ios
sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.2" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_macos: flutter_inappwebview_macos:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_macos path: flutter_inappwebview_macos
sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.2" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_platform_interface: flutter_inappwebview_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_platform_interface path: flutter_inappwebview_platform_interface
sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.3.0+1" source: git
version: "1.4.0-beta.3"
flutter_inappwebview_web: flutter_inappwebview_web:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_web path: flutter_inappwebview_web
sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "1.1.2" source: git
version: "1.2.0-beta.3"
flutter_inappwebview_windows: flutter_inappwebview_windows:
dependency: transitive dependency: transitive
description: description:
name: flutter_inappwebview_windows path: flutter_inappwebview_windows
sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
url: "https://pub.dev" resolved-ref: "0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676"
source: hosted url: "https://github.com/pichillilorenzo/flutter_inappwebview"
version: "0.6.0" source: git
version: "0.7.0-beta.3"
flutter_lints: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:

View File

@@ -2,7 +2,7 @@ name: venera
description: "A comic app." description: "A comic app."
publish_to: 'none' publish_to: 'none'
version: 1.2.4+124 version: 1.2.5+125
environment: environment:
sdk: '>=3.6.0 <4.0.0' sdk: '>=3.6.0 <4.0.0'
@@ -43,7 +43,11 @@ dependencies:
git: git:
url: https://github.com/wgh136/flutter_desktop_webview url: https://github.com/wgh136/flutter_desktop_webview
path: packages/desktop_webview_window path: packages/desktop_webview_window
flutter_inappwebview: ^6.1.5 flutter_inappwebview:
git:
url: https://github.com/pichillilorenzo/flutter_inappwebview
path: flutter_inappwebview
ref: 0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676
app_links: ^6.3.3 app_links: ^6.3.3
sliver_tools: ^0.2.12 sliver_tools: ^0.2.12
flutter_file_dialog: ^3.0.2 flutter_file_dialog: ^3.0.2