From 23404b86f6d154665443008a3e7d254a79d6739f Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 5 Feb 2025 20:40:14 +0800 Subject: [PATCH 01/18] Record the last state of the favorite pane. --- lib/pages/comic_page.dart | 59 +++++++++++++++++++++++++----------- lib/pages/comments_page.dart | 2 +- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 7267d8e..716ca05 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -1388,42 +1388,67 @@ class _FavoritePanel extends StatefulWidget { State<_FavoritePanel> createState() => _FavoritePanelState(); } -class _FavoritePanelState extends State<_FavoritePanel> { +class _FavoritePanelState extends State<_FavoritePanel> + with SingleTickerProviderStateMixin { late ComicSource comicSource; + late TabController tabController; + + late bool hasNetwork; + @override void initState() { comicSource = widget.type.comicSource!; localFolders = LocalFavoritesManager().folderNames; added = LocalFavoritesManager().find(widget.cid, widget.type); + hasNetwork = comicSource.favoriteData != null && comicSource.isLogged; + var initIndex = 0; + if (appdata.implicitData['favoritePanelIndex'] is int) { + initIndex = appdata.implicitData['favoritePanelIndex']; + } + initIndex = initIndex.clamp(0, hasNetwork ? 1 : 0); + tabController = TabController( + initialIndex: initIndex, + length: hasNetwork ? 2 : 1, + vsync: this, + ); super.initState(); } + @override + void dispose() { + var currentIndex = tabController.index; + appdata.implicitData['favoritePanelIndex'] = currentIndex; + appdata.writeImplicitData(); + tabController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { - var hasNetwork = comicSource.favoriteData != null && comicSource.isLogged; return Scaffold( appBar: Appbar( title: Text("Favorite".tl), ), - body: DefaultTabController( - length: hasNetwork ? 2 : 1, - child: Column( - children: [ - TabBar(tabs: [ + body: Column( + children: [ + TabBar( + controller: tabController, + tabs: [ Tab(text: "Local".tl), if (hasNetwork) Tab(text: "Network".tl), - ]), - Expanded( - child: TabBarView( - children: [ - buildLocal(), - if (hasNetwork) buildNetwork(), - ], - ), + ], + ), + Expanded( + child: TabBarView( + controller: tabController, + children: [ + buildLocal(), + if (hasNetwork) buildNetwork(), + ], ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/comments_page.dart b/lib/pages/comments_page.dart index 9490620..befc94f 100644 --- a/lib/pages/comments_page.dart +++ b/lib/pages/comments_page.dart @@ -42,7 +42,7 @@ class _CommentsPageState extends State { _error = res.errorMessage; _loading = false; }); - } else { + } else if (mounted) { setState(() { _comments = res.data; _loading = false; From 58d6ccdde1d0758434f1f4acebfee50ce8b1351d Mon Sep 17 00:00:00 2001 From: nyne Date: Wed, 5 Feb 2025 21:21:20 +0800 Subject: [PATCH 02/18] Fix an issue where an application turns to a white screen after finishing cloudflare verification. Close #169 --- lib/foundation/app.dart | 2 +- lib/network/cloudflare.dart | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 9ed44e8..aecd9e5 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -52,7 +52,7 @@ class _App { BuildContext get rootContext => rootNavigatorKey.currentContext!; void rootPop() { - rootNavigatorKey.currentState?.pop(); + rootNavigatorKey.currentState?.maybePop(); } void pop() { diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index cf61de1..810bafa 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -152,6 +152,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { ); webview.open(); } else { + bool success = false; void check(InAppWebViewController controller) async { var head = await controller.evaluateJavascript( source: "document.head.innerHTML") as String; @@ -176,7 +177,10 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { return; } SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies); - App.rootPop(); + if (!success) { + App.rootPop(); + success = true; + } } } From 3aca3baafcbfb4c0ab75c6bd941344902cfc67a0 Mon Sep 17 00:00:00 2001 From: buste <32890006+bustesoul@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:03:52 +0800 Subject: [PATCH 03/18] Fix ensure searchTarget is properly initialized for aggregatedSearch mode (#173) Set searchTarget = defaultSearchTarget when aggregatedSearch is enabled, ensuring correct initialization and preventing missing suggestions on first input. Without this fix, when opening the search page for the first time with aggregatedSearch enabled by default, entering an ID that matches a comic source does not trigger the "Open comic" suggestion. However, after toggling aggregatedSearch off and then back on, the same ID input correctly displays the suggestion. --- lib/pages/search_page.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index 6d59223..f3d6cbe 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -142,6 +142,8 @@ class _SearchPageState extends State { var defaultSearchTarget = appdata.settings['defaultSearchTarget']; if (defaultSearchTarget == "_aggregated_") { aggregatedSearch = true; + searchTarget = ComicSource.all().where((e) => e.searchPageData != null) + .toList().first.key; } else if (defaultSearchTarget != null && ComicSource.find(defaultSearchTarget) != null) { searchTarget = defaultSearchTarget; From 13081332f25a24521a5e5efd2497daf0fe60f290 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 7 Feb 2025 17:19:04 +0800 Subject: [PATCH 04/18] Improve tags display --- lib/components/comic.dart | 66 +++++++++++++++++++-------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/lib/components/comic.dart b/lib/components/comic.dart index 57353a1..8644995 100644 --- a/lib/components/comic.dart +++ b/lib/components/comic.dart @@ -550,7 +550,7 @@ class _ComicDescription extends StatelessWidget { int cnt = (constraints.maxHeight - 22).toInt() ~/ 25; return Container( clipBehavior: Clip.antiAlias, - height: 22 + cnt * 25, + height: 21 + cnt * 24, width: double.infinity, decoration: const BoxDecoration(), child: Wrap( @@ -562,31 +562,30 @@ class _ComicDescription extends StatelessWidget { children: [ for (var s in tags!) Container( - height: 22, - padding: const EdgeInsets.fromLTRB(3, 2, 3, 2), - constraints: BoxConstraints( - maxWidth: constraints.maxWidth * 0.45, + height: 21, + padding: const EdgeInsets.symmetric(horizontal: 4), + constraints: BoxConstraints( + maxWidth: constraints.maxWidth * 0.45, + ), + decoration: BoxDecoration( + color: s == "Unavailable" + ? context.colorScheme.errorContainer + : context.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Center( + widthFactor: 1, + child: Text( + enableTranslate + ? TagsTranslation.translateTag(s) + : s.split(':').last, + style: const TextStyle(fontSize: 12), + softWrap: true, + overflow: TextOverflow.ellipsis, + maxLines: 1, ), - decoration: BoxDecoration( - color: s == "Unavailable" - ? Theme.of(context).colorScheme.errorContainer - : Theme.of(context) - .colorScheme - .secondaryContainer, - borderRadius: - const BorderRadius.all(Radius.circular(8)), - ), - child: Center( - widthFactor: 1, - child: Text( - enableTranslate - ? TagsTranslation.translateTag(s) - : s.split(':').last, - style: const TextStyle(fontSize: 12), - softWrap: true, - overflow: TextOverflow.ellipsis, - maxLines: 1, - ))), + ), + ), ], ), ).toAlign(Alignment.topCenter); @@ -1520,14 +1519,15 @@ class SimpleComicTile extends StatelessWidget { return AnimatedTapRegion( borderRadius: 8, - onTap: onTap ?? () { - context.to( - () => ComicPage( - id: comic.id, - sourceKey: comic.sourceKey, - ), - ); - }, + onTap: onTap ?? + () { + context.to( + () => ComicPage( + id: comic.id, + sourceKey: comic.sourceKey, + ), + ); + }, child: Container( width: 92, height: 114, From 33a9fa062b8a721a2c684461d37eb5872d591b50 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 7 Feb 2025 17:19:26 +0800 Subject: [PATCH 05/18] flutter 3.27.4 --- pubspec.lock | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 25c9852..6f5ba9f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1100,4 +1100,4 @@ packages: version: "0.0.10" sdks: dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.3" + flutter: ">=3.27.4" diff --git a/pubspec.yaml b/pubspec.yaml index 0624920..b35e7fb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.2.3+123 environment: sdk: '>=3.6.0 <4.0.0' - flutter: 3.27.3 + flutter: 3.27.4 dependencies: flutter: From 0122bb8f28da7b55ef79aa2df78366a0e10e140f Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 7 Feb 2025 17:28:03 +0800 Subject: [PATCH 06/18] fix windows font --- lib/main.dart | 51 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 42c9c82..b823639 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -132,6 +132,38 @@ class _MyAppState extends State with WidgetsBindingObserver { }; } + ThemeData getTheme( + Color primary, + Color? secondary, + Color? tertiary, + Brightness brightness, + ) { + String? font; + List? fallback; + if (App.isWindows) { + font = 'Segoe UI'; + fallback = [ + 'Segoe UI', + 'Microsoft YaHei', + 'PingFang SC', + 'Noto Sans CJK', + 'Arial', + 'sans-serif' + ]; + } + return ThemeData( + colorScheme: SeedColorScheme.fromSeeds( + primaryKey: primary, + secondaryKey: secondary, + tertiaryKey: tertiary, + brightness: brightness, + tones: FlexTones.vividBackground(brightness), + ), + fontFamily: font, + fontFamilyFallback: fallback, + ); + } + @override Widget build(BuildContext context) { Widget home; @@ -158,24 +190,9 @@ class _MyAppState extends State with WidgetsBindingObserver { return MaterialApp( home: home, debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: SeedColorScheme.fromSeeds( - primaryKey: primary, - secondaryKey: secondary, - tertiaryKey: tertiary, - tones: FlexTones.vividBackground(Brightness.light), - ), - ), + theme: getTheme(primary, secondary, tertiary, Brightness.light), navigatorKey: App.rootNavigatorKey, - darkTheme: ThemeData( - colorScheme: SeedColorScheme.fromSeeds( - primaryKey: primary, - secondaryKey: secondary, - tertiaryKey: tertiary, - brightness: Brightness.dark, - tones: FlexTones.vividBackground(Brightness.dark), - ), - ), + darkTheme: getTheme(primary, secondary, tertiary, Brightness.dark), themeMode: switch (appdata.settings['theme_mode']) { 'light' => ThemeMode.light, 'dark' => ThemeMode.dark, From 998d4c31d36cce51ebc153492dca6347c3748c54 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 7 Feb 2025 17:32:51 +0800 Subject: [PATCH 07/18] Improve importing comic: If the archive has only one directory, set working dir as it. --- lib/utils/cbz.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 16eee6a..214c787 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -85,6 +85,10 @@ abstract class CBZ { if (cache.existsSync()) cache.deleteSync(recursive: true); cache.createSync(); await extractArchive(file, cache); + var f = cache.listSync(); + if (f.length == 1 && f.first is Directory) { + cache = f.first as Directory; + } var metaDataFile = File(FilePath.join(cache.path, 'metadata.json')); ComicMetaData? metaData; if (metaDataFile.existsSync()) { From 35429c132ce5e14bf24ab5ac32a67a59a9140036 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 7 Feb 2025 18:15:36 +0800 Subject: [PATCH 08/18] Improve comic page performance --- lib/components/layout.dart | 15 ++++++ lib/pages/comic_page.dart | 102 +++++++++++++++++++------------------ 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/lib/components/layout.dart b/lib/components/layout.dart index 51351c2..0238863 100644 --- a/lib/components/layout.dart +++ b/lib/components/layout.dart @@ -148,3 +148,18 @@ class SliverGridDelegateWithComics extends SliverGridDelegate { return false; } } + +class SliverLazyToBoxAdapter extends StatelessWidget { + /// Creates a sliver that contains a single box widget which can be lazy loaded. + const SliverLazyToBoxAdapter({super.key, required this.child}); + + final Widget child; + + @override + Widget build(BuildContext context) { + return SliverList.list(children: [ + SizedBox(), + child, + ]); + } +} diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 716ca05..6bf09da 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -206,62 +206,64 @@ class _ComicPageState extends LoadingState yield const SliverPadding(padding: EdgeInsets.only(top: 8)); - yield Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(width: 16), - Hero( - tag: "cover${comic.id}${comic.sourceKey}", - child: Container( - decoration: BoxDecoration( - color: context.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - color: context.colorScheme.outlineVariant, - blurRadius: 1, - offset: const Offset(0, 1), + yield SliverLazyToBoxAdapter( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(width: 16), + Hero( + tag: "cover${comic.id}${comic.sourceKey}", + child: Container( + decoration: BoxDecoration( + color: context.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + color: context.colorScheme.outlineVariant, + blurRadius: 1, + offset: const Offset(0, 1), + ), + ], + ), + height: 144, + width: 144 * 0.72, + clipBehavior: Clip.antiAlias, + child: AnimatedImage( + image: CachedImageProvider( + widget.cover ?? comic.cover, + sourceKey: comic.sourceKey, + cid: comic.id, + ), + width: double.infinity, + height: double.infinity, + ), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SelectableText(comic.title, style: ts.s18), + if (comic.subTitle != null) + SelectableText(comic.subTitle!, style: ts.s14) + .paddingVertical(4), + Text( + (ComicSource.find(comic.sourceKey)?.name) ?? '', + style: ts.s12, ), ], ), - height: 144, - width: 144 * 0.72, - clipBehavior: Clip.antiAlias, - child: AnimatedImage( - image: CachedImageProvider( - widget.cover ?? comic.cover, - sourceKey: comic.sourceKey, - cid: comic.id, - ), - width: double.infinity, - height: double.infinity, - ), ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SelectableText(comic.title, style: ts.s18), - if (comic.subTitle != null) - SelectableText(comic.subTitle!, style: ts.s14) - .paddingVertical(4), - Text( - (ComicSource.find(comic.sourceKey)?.name) ?? '', - style: ts.s12, - ), - ], - ), - ), - ], - ).toSliver(); + ], + ), + ); } Widget buildActions() { bool isMobile = context.width < changePoint; bool hasHistory = history != null && (history!.ep > 1 || history!.page > 1); - return SliverToBoxAdapter( + return SliverLazyToBoxAdapter( child: Column( children: [ ListView( @@ -354,7 +356,7 @@ class _ComicPageState extends LoadingState if (comic.description == null || comic.description!.trim().isEmpty) { return const SliverPadding(padding: EdgeInsets.zero); } - return SliverToBoxAdapter( + return SliverLazyToBoxAdapter( child: Column( children: [ ListTile( @@ -482,7 +484,7 @@ class _ComicPageState extends LoadingState bool enableTranslation = App.locale.languageCode == 'zh' && comicSource.enableTagsTranslate; - return SliverToBoxAdapter( + return SliverLazyToBoxAdapter( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -1875,7 +1877,7 @@ class _CommentsPartState extends State<_CommentsPart> { Widget build(BuildContext context) { return MultiSliver( children: [ - SliverToBoxAdapter( + SliverLazyToBoxAdapter( child: ListTile( title: Text("Comments".tl), trailing: Row( From f0b1135eb73281785cc4fedbfa90f62dc43783bf Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 8 Feb 2025 18:23:49 +0800 Subject: [PATCH 09/18] Allow batch export. Close #179 --- assets/translation.json | 8 +- lib/components/message.dart | 121 +++++++++++++++--------- lib/pages/local_comics_page.dart | 154 ++++++++++++++++++------------- lib/utils/cbz.dart | 4 +- lib/utils/epub.dart | 27 +++--- lib/utils/pdf.dart | 10 +- 6 files changed, 189 insertions(+), 135 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 999477e..a279368 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -321,7 +321,9 @@ "Manage": "管理", "Verify": "验证", "Cloudflare verification required": "需要Cloudflare验证", - "Success": "成功" + "Success": "成功", + "Compressing": "压缩中", + "Exporting": "导出中" }, "zh_TW": { "Home": "首頁", @@ -645,6 +647,8 @@ "Manage": "管理", "Verify": "驗證", "Cloudflare verification required": "需要Cloudflare驗證", - "Success": "成功" + "Success": "成功", + "Compressing": "壓縮中", + "Exporting": "匯出中" } } \ No newline at end of file diff --git a/lib/components/message.dart b/lib/components/message.dart index 42b4374..777ec92 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -168,7 +168,15 @@ Future showConfirmDialog({ } class LoadingDialogController { - void Function()? closeDialog; + double? _progress; + + String? _message; + + void Function()? _closeDialog; + + void Function(double? value)? _serProgress; + + void Function(String message)? _setMessage; bool closed = false; @@ -177,63 +185,86 @@ class LoadingDialogController { return; } closed = true; - if (closeDialog == null) { - Future.microtask(closeDialog!); + if (_closeDialog == null) { + Future.microtask(_closeDialog!); } else { - closeDialog!(); + _closeDialog!(); } } + + void setProgress(double? value) { + if (closed) { + return; + } + _serProgress?.call(value); + } + + void setMessage(String message) { + if (closed) { + return; + } + _setMessage?.call(message); + } } -LoadingDialogController showLoadingDialog(BuildContext context, - {void Function()? onCancel, - bool barrierDismissible = true, - bool allowCancel = true, - String? message, - String cancelButtonText = "Cancel"}) { +LoadingDialogController showLoadingDialog( + BuildContext context, { + void Function()? onCancel, + bool barrierDismissible = true, + bool allowCancel = true, + String? message, + String cancelButtonText = "Cancel", + bool withProgress = false, +}) { var controller = LoadingDialogController(); + controller._message = message; + + if (withProgress) { + controller._progress = 0; + } var loadingDialogRoute = DialogRoute( - context: context, - barrierDismissible: barrierDismissible, - builder: (BuildContext context) { - return Dialog( - child: Container( - width: 100, - padding: const EdgeInsets.all(16.0), - child: Row( - children: [ - const SizedBox( - width: 30, - height: 30, - child: CircularProgressIndicator(), - ), - const SizedBox( - width: 16, - ), - Text( - message ?? 'Loading', - style: const TextStyle(fontSize: 16), - ), - const Spacer(), - if (allowCancel) - TextButton( - onPressed: () { - controller.close(); - onCancel?.call(); - }, - child: Text(cancelButtonText.tl)) - ], - ), - ), + context: context, + barrierDismissible: barrierDismissible, + builder: (BuildContext context) { + return StatefulBuilder(builder: (context, setState) { + controller._serProgress = (value) { + setState(() { + controller._progress = value; + }); + }; + controller._setMessage = (message) { + setState(() { + controller._message = message; + }); + }; + return ContentDialog( + title: controller._message ?? 'Loading', + content: LinearProgressIndicator( + value: controller._progress, + backgroundColor: context.colorScheme.surfaceContainer, + ).paddingHorizontal(16).paddingVertical(16), + actions: [ + FilledButton( + onPressed: allowCancel + ? () { + controller.close(); + onCancel?.call(); + } + : null, + child: Text(cancelButtonText.tl), + ) + ], ); }); + }, + ); var navigator = Navigator.of(context, rootNavigator: true); navigator.push(loadingDialogRoute).then((value) => controller.closed = true); - controller.closeDialog = () { + controller._closeDialog = () { navigator.removeRoute(loadingDialogRoute); }; @@ -444,9 +475,7 @@ Future showSelectDialog({ child: Text('Cancel'.tl), ), FilledButton( - onPressed: current == null - ? null - : context.pop, + onPressed: current == null ? null : context.pop, child: Text('Confirm'.tl), ), ], diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index a8a0794..cf5b6b6 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -12,6 +12,7 @@ import 'package:venera/utils/epub.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/translations.dart'; +import 'package:zip_flutter/zip_flutter.dart'; class LocalComicsPage extends StatefulWidget { const LocalComicsPage({super.key}); @@ -147,13 +148,13 @@ class _LocalComicsPageState extends State { text: "View Detail".tl, onClick: () { context.to(() => ComicPage( - id: selectedComics.keys.first.id, - sourceKey: selectedComics.keys.first.sourceKey, - )); + id: selectedComics.keys.first.id, + sourceKey: selectedComics.keys.first.sourceKey, + )); }, ), - if (selectedComics.length == 1) - ...exportActions(selectedComics.keys.first), + if (selectedComics.isNotEmpty) + ...exportActions(selectedComics.keys.toList()), ]); } @@ -322,7 +323,7 @@ class _LocalComicsPageState extends State { }); }, ), - ...exportActions(c as LocalComic), + ...exportActions([c as LocalComic]), ]; }, ), @@ -390,79 +391,102 @@ class _LocalComicsPageState extends State { return isDeleted; } - List exportActions(LocalComic c) { + List exportActions(List comics) { return [ MenuEntry( - icon: Icons.outbox_outlined, - text: "Export as cbz".tl, - onClick: () async { - var controller = showLoadingDialog( - context, - allowCancel: false, - ); - try { - var file = await CBZ.export(c); - await saveFile(filename: file.name, file: file); - await file.delete(); - } catch (e, s) { - context.showMessage(message: e.toString()); - Log.error("CBZ Export", e, s); - } - controller.close(); - }), + icon: Icons.outbox_outlined, + text: "Export as cbz".tl, + onClick: () { + exportComics(comics, CBZ.export, ".cbz"); + }, + ), MenuEntry( icon: Icons.picture_as_pdf_outlined, text: "Export as pdf".tl, onClick: () async { - var cache = FilePath.join(App.cachePath, 'temp.pdf'); - var controller = showLoadingDialog( - context, - allowCancel: false, - ); - try { - await createPdfFromComicIsolate( - comic: c, - savePath: cache, - ); - await saveFile( - file: File(cache), - filename: "${c.title}.pdf", - ); - } catch (e, s) { - Log.error("PDF Export", e, s); - context.showMessage(message: e.toString()); - } finally { - controller.close(); - File(cache).deleteIgnoreError(); - } + exportComics(comics, createPdfFromComicIsolate, ".pdf"); }, ), MenuEntry( icon: Icons.import_contacts_outlined, text: "Export as epub".tl, onClick: () async { - var controller = showLoadingDialog( - context, - allowCancel: false, - ); - File? file; - try { - file = await createEpubWithLocalComic( - c, - ); - await saveFile( - file: file, - filename: "${c.title}.epub", - ); - } catch (e, s) { - Log.error("EPUB Export", e, s); - context.showMessage(message: e.toString()); - } finally { - controller.close(); - file?.deleteIgnoreError(); - } + exportComics(comics, createEpubWithLocalComic, ".epub"); }, ) ]; } + + /// Export given comics to a file + void exportComics( + List comics, ExportComicFunc export, String ext) async { + var current = 0; + var cacheDir = FilePath.join(App.cachePath, 'comics_export'); + var outFile = FilePath.join(App.cachePath, 'comics_export.zip'); + bool canceled = false; + if (Directory(cacheDir).existsSync()) { + Directory(cacheDir).deleteSync(recursive: true); + } + Directory(cacheDir).createSync(); + var loadingController = showLoadingDialog( + context, + allowCancel: true, + message: "${"Exporting".tl} $current/${comics.length}", + withProgress: comics.length > 1, + onCancel: () { + canceled = true; + }, + ); + try { + var fileName = ""; + // For each comic, export it to a file + for (var comic in comics) { + fileName = FilePath.join(cacheDir, sanitizeFileName(comic.title) + ext); + await export(comic, fileName); + current++; + if (comics.length > 1) { + loadingController + .setMessage("${"Exporting".tl} $current/${comics.length}"); + loadingController.setProgress(current / comics.length); + } + if (canceled) { + return; + } + } + // For single comic, just save the file + if (comics.length == 1) { + await saveFile( + file: File(fileName), + filename: File(fileName).name, + ); + Directory(cacheDir).deleteSync(recursive: true); + loadingController.close(); + return; + } + // For multiple comics, compress the folder + loadingController.setProgress(null); + loadingController.setMessage("Compressing".tl); + await ZipFile.compressFolderAsync(cacheDir, outFile); + if (canceled) { + File(outFile).deleteIgnoreError(); + return; + } + } catch (e, s) { + Log.error("Export Comics", e, s); + context.showMessage(message: e.toString()); + loadingController.close(); + return; + } finally { + Directory(cacheDir).deleteIgnoreError(recursive: true); + } + await saveFile( + file: File(outFile), + filename: "comics_export.zip", + ); + loadingController.close(); + File(outFile).deleteIgnoreError(); + } } + +typedef ExportComicFunc = Future Function( + LocalComic comic, String outFilePath); diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 214c787..d36d54a 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -175,7 +175,7 @@ abstract class CBZ { return comic; } - static Future export(LocalComic comic) async { + static Future export(LocalComic comic, String outFilePath) async { var cache = Directory(FilePath.join(App.cachePath, 'cbz_export')); if (cache.existsSync()) cache.deleteSync(recursive: true); cache.createSync(); @@ -234,7 +234,7 @@ abstract class CBZ { ).toJson(), ), ); - var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz'))); + var cbz = File(outFilePath); if (cbz.existsSync()) cbz.deleteSync(); await _compress(cache.path, cbz.path); cache.deleteSync(recursive: true); diff --git a/lib/utils/epub.dart b/lib/utils/epub.dart index 7505f00..42764a1 100644 --- a/lib/utils/epub.dart +++ b/lib/utils/epub.dart @@ -24,7 +24,8 @@ class EpubData { }); } -Future createEpubComic(EpubData data, String cacheDir) async { +Future createEpubComic( + EpubData data, String cacheDir, String outFilePath) async { final workingDir = Directory(FilePath.join(cacheDir, 'epub')); if (workingDir.existsSync()) { workingDir.deleteSync(recursive: true); @@ -109,8 +110,7 @@ ${images.map((e) => ' $e').join('\n')} } // content.opf - final contentOpf = - File(FilePath.join(workingDir.path, 'content.opf')); + final contentOpf = File(FilePath.join(workingDir.path, 'content.opf')); final uuid = const Uuid().v4(); var spineStrBuilder = StringBuffer(); for (var i = 0; i < chapterIndex; i++) { @@ -171,16 +171,15 @@ ${navMapStrBuilder.toString()} '''); - // zip - final zipPath = FilePath.join(cacheDir, '${data.title}.epub'); - ZipFile.compressFolder(workingDir.path, zipPath); + ZipFile.compressFolder(workingDir.path, outFilePath); workingDir.deleteSync(recursive: true); - return File(zipPath); + return File(outFilePath); } -Future createEpubWithLocalComic(LocalComic comic) async { +Future createEpubWithLocalComic( + LocalComic comic, String outFilePath) async { var chapters = >{}; if (comic.chapters == null) { chapters[comic.title] = @@ -188,11 +187,11 @@ Future createEpubWithLocalComic(LocalComic comic) async { .map((e) => File(e)) .toList(); } else { - for (var chapter in comic.chapters!.keys) { - chapters[comic.chapters![chapter]!] = (await LocalManager() - .getImages(comic.id, comic.comicType, chapter)) - .map((e) => File(e)) - .toList(); + for (var chapter in comic.downloadedChapters) { + chapters[comic.chapters![chapter]!] = + (await LocalManager().getImages(comic.id, comic.comicType, chapter)) + .map((e) => File(e)) + .toList(); } } var data = EpubData( @@ -205,6 +204,6 @@ Future createEpubWithLocalComic(LocalComic comic) async { final cacheDir = App.cachePath; return Isolate.run(() => overrideIO(() async { - return createEpubComic(data, cacheDir); + return createEpubComic(data, cacheDir, outFilePath); })); } diff --git a/lib/utils/pdf.dart b/lib/utils/pdf.dart index 085ea1f..9ce0493 100644 --- a/lib/utils/pdf.dart +++ b/lib/utils/pdf.dart @@ -49,7 +49,7 @@ Future _createPdfFromComic({ images.add(file.path); } } else { - for (var chapter in comic.chapters!.keys) { + for (var chapter in comic.downloadedChapters) { var files = Directory(FilePath.join(baseDir, chapter)).listSync(); reorderFiles(files); for (var file in files) { @@ -112,10 +112,7 @@ Future _runIsolate( ); } -Future createPdfFromComicIsolate({ - required LocalComic comic, - required String savePath, -}) async { +Future createPdfFromComicIsolate(LocalComic comic, String savePath) async { var receivePort = ReceivePort(); SendPort? sendPort; Isolate? isolate; @@ -134,7 +131,8 @@ Future createPdfFromComicIsolate({ } }); isolate = await _runIsolate(comic, savePath, receivePort.sendPort); - return completer.future; + await completer.future; + return File(savePath); } class PdfGenerator { From ce50812857fefce27f0c1af08b85a00aa0df66a3 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 8 Feb 2025 19:37:04 +0800 Subject: [PATCH 10/18] Fix invalid image order when exporting comic as pdf. --- lib/utils/io.dart | 18 ++++++------------ lib/utils/pdf.dart | 6 +++--- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/utils/io.dart b/lib/utils/io.dart index dc6df55..d5ec62b 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -35,19 +35,9 @@ class FilePath { } extension FileSystemEntityExt on FileSystemEntity { + /// Get the base name of the file or directory. String get name { - var path = this.path; - if (path.endsWith('/') || path.endsWith('\\')) { - path = path.substring(0, path.length - 1); - } - - int i = path.length - 1; - - while (i >= 0 && path[i] != '\\' && path[i] != '/') { - i--; - } - - return path.substring(i + 1); + return p.basename(path); } Future deleteIgnoreError({bool recursive = false}) async { @@ -83,6 +73,10 @@ extension FileExtension on File { // Stream is not usable since [AndroidFile] does not support [openRead]. await newFile.writeAsBytes(await readAsBytes()); } + + String get basenameWithoutExt { + return p.basenameWithoutExtension(path); + } } extension DirectoryExtension on Directory { diff --git a/lib/utils/pdf.dart b/lib/utils/pdf.dart index 9ce0493..48da584 100644 --- a/lib/utils/pdf.dart +++ b/lib/utils/pdf.dart @@ -30,14 +30,14 @@ Future _createPdfFromComic({ files.removeWhere( (element) => element is! File || element.path.startsWith('cover')); files.sort((a, b) { - var aName = (a as File).name; - var bName = (b as File).name; + var aName = (a as File).basenameWithoutExt; + var bName = (b as File).basenameWithoutExt; var aNumber = int.tryParse(aName); var bNumber = int.tryParse(bName); if (aNumber != null && bNumber != null) { return aNumber.compareTo(bNumber); } - return aName.compareTo(bName); + return a.name.compareTo(b.name); }); } From 6be258092aa75bf59a78ae4ece3e1c00a40952b0 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 8 Feb 2025 20:40:45 +0800 Subject: [PATCH 11/18] Remove confirmation prompt from deb. Close #177 --- debian/debian.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/debian/debian.yaml b/debian/debian.yaml index 545a025..f4044f3 100644 --- a/debian/debian.yaml +++ b/debian/debian.yaml @@ -2,7 +2,7 @@ flutter_app: command: venera arch: x64 parent: /usr/local/lib - nonInteractive: false + nonInteractive: true control: Package: venera From 614c01872ba867c41ae7acd38701c926247cc3d0 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 8 Feb 2025 21:10:43 +0800 Subject: [PATCH 12/18] Fix auto language filter. Close #171 --- lib/pages/search_result_page.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/search_result_page.dart b/lib/pages/search_result_page.dart index b5beac7..f174c14 100644 --- a/lib/pages/search_result_page.dart +++ b/lib/pages/search_result_page.dart @@ -116,13 +116,13 @@ class _SearchResultPageState extends State { @override void initState() { sourceKey = widget.sourceKey; + text = checkAutoLanguage(widget.text); controller = SearchBarController( - currentText: checkAutoLanguage(widget.text), + currentText: text, onSearch: search, ); options = widget.options ?? const []; validateOptions(); - text = widget.text; appdata.addSearchHistory(text); suggestionsController = _SuggestionsController(controller); super.initState(); From 8ab4f7a34bf50fb62bb116655da61458b4824a24 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 9 Feb 2025 11:38:19 +0800 Subject: [PATCH 13/18] Fix the issue where cache files are not deleted. --- lib/utils/data_sync.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 7f6aebf..4ee6435 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -118,6 +118,7 @@ class DataSync with ChangeNotifier { await client.remove(files.first.name!); } await client.write(filename, await data.readAsBytes()); + data.deleteIgnoreError(); Log.info("Upload Data", "Data uploaded successfully"); return const Res(true); } catch (e, s) { From 591f2836d423f8f1cf3096382b8dba205fd1086c Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 9 Feb 2025 13:45:30 +0800 Subject: [PATCH 14/18] Improve windows build script. --- windows/build.iss | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/windows/build.iss b/windows/build.iss index 15cc4d6..5faf1a6 100644 --- a/windows/build.iss +++ b/windows/build.iss @@ -3,11 +3,36 @@ #define MyAppName "Venera" #define MyAppVersion "{{version}}" -#define MyAppPublisher "wgh136" +#define MyAppPublisher "nyne" #define MyAppURL "https://github.com/venera-app/venera" #define MyAppExeName "venera.exe" #define RootPath "{{root_path}}" +[Code] +procedure CurStepChanged(CurStep: TSetupStep); +var + OldVersionPath, ShortcutPath: string; +begin + if CurStep = ssInstall then + begin + OldVersionPath := 'C:\Program Files (x86)\Venera'; + if DirExists(OldVersionPath) then + begin + DelTree(OldVersionPath, True, True, True); + ShortcutPath := GetEnv('USERPROFILE') + '\Desktop\Venera.lnk'; + if FileExists(ShortcutPath) then + begin + DeleteFile(ShortcutPath); + end; + ShortcutPath := 'C:\Users\Public\Desktop\Venera.lnk'; + if FileExists(ShortcutPath) then + begin + DeleteFile(ShortcutPath); + end; + end; + end; +end; + [Setup] ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) @@ -30,6 +55,8 @@ SetupIconFile={#RootPath}\windows\runner\resources\app_icon.ico Compression=lzma SolidCompression=yes WizardStyle=modern +ArchitecturesInstallIn64BitMode=x64compatible +ArchitecturesAllowed=x64compatible [Languages] Name: "english"; MessagesFile: "compiler:Default.isl" From e55c45a589dc8bf6ad50daceb04ea1e362681618 Mon Sep 17 00:00:00 2001 From: nyne <67669799+wgh136@users.noreply.github.com> Date: Sun, 9 Feb 2025 15:11:46 +0800 Subject: [PATCH 15/18] Support Linux arm64. Close #176 --- .github/workflows/main.yml | 55 ++++++++++++++++++++++++++++++++------ debian/build.py | 11 +++++++- debian/debian.yaml | 4 +-- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3128d23..ec54973 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -39,12 +39,18 @@ jobs: ln -s /Applications dist/dmg_contents/Applications hdiutil create -volname "venera" -srcfolder dist/dmg_contents -ov -format UDZO "dist/venera.dmg" + - name: Add version to filename + run: | + APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + mkdir -p result + mv dist/venera.dmg result/venera-$APP_VERSION.dmg + # Step 4: Attach and upload artifacts (optional) - name: Upload DMG uses: actions/upload-artifact@v4 with: - name: venera.dmg - path: dist/venera.dmg + name: macos_build + path: result/ Build_IOS: runs-on: macos-15 steps: @@ -62,10 +68,15 @@ jobs: mv /Users/runner/work/venera/venera/build/ios/iphoneos/Runner.app /Users/runner/work/venera/venera/build/ios/iphoneos/Payload cd /Users/runner/work/venera/venera/build/ios/iphoneos/ zip -r venera-ios.ipa Payload + - name: Add version to filename + run: | + APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + mkdir -p result + mv build/ios/iphoneos/venera-ios.ipa result/venera-ios-$APP_VERSION.ipa - uses: actions/upload-artifact@v4 with: - name: app-ios.ipa - path: /Users/runner/work/venera/venera/build/ios/iphoneos/venera-ios.ipa + name: ios_build + path: result/ Build_Android: runs-on: ubuntu-latest steps: @@ -130,7 +141,7 @@ jobs: sudo apt-get update -y sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 dart pub global activate flutter_to_debian - - run: python3 debian/build.py + - run: python3 debian/build.py x64 - run: dart run flutter_to_arch - run: | sudo rm -rf build/linux/arch/app.tar.gz @@ -145,19 +156,43 @@ jobs: with: name: arch_build path: build/linux/arch/ + Build_Linux_ARM64: + runs-on: ubuntu-22.04-arm + steps: + - uses: actions/checkout@v4 + - name: Setup Flutter + run: | + FLUTTER_VERSION=$(grep " flutter:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + sudo apt-get update -y && sudo apt-get upgrade -y; + sudo apt-get install -y curl git unzip xz-utils zip libglu1-mesa clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev + git clone --depth 1 --branch $FLUTTER_VERSION https://github.com/flutter/flutter.git $RUNNER_TEMP/flutter + echo "$RUNNER_TEMP/flutter/bin" >> $GITHUB_PATH + - name: Install Flutter + run: flutter doctor + - name: Install dependencies + run: | + flutter pub get + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 + dart pub global activate flutter_to_debian + - run: python3 debian/build.py arm64 + - uses: actions/upload-artifact@v4 + with: + name: deb_arm64_build + path: build/linux/x64/release/debian # This is a bug related to flutter_to_debian, but it's not a big deal. Release: runs-on: ubuntu-latest - needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux] + needs: [Build_MacOS, Build_IOS, Build_Android, Build_Windows, Build_Linux, Build_Linux_ARM64] if: github.event_name == 'release' # 仅在 push 事件时执行 steps: - uses: actions/download-artifact@v4 with: - name: venera.dmg + name: macos_build path: outputs - uses: actions/download-artifact@v4 with: - name: app-ios.ipa + name: ios_build path: outputs - uses: actions/download-artifact@v4 with: @@ -175,6 +210,10 @@ jobs: with: name: arch_build path: outputs + - uses: actions/download-artifact@v4 + with: + name: deb_arm64_build + path: outputs - uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} diff --git a/debian/build.py b/debian/build.py index 0547d45..0932f2e 100644 --- a/debian/build.py +++ b/debian/build.py @@ -1,5 +1,7 @@ import subprocess +import sys +arch = sys.argv[1] debianContent = '' desktopContent = '' version = '' @@ -12,7 +14,14 @@ with open('pubspec.yaml', 'r') as f: version = str.split(str.split(f.read(), 'version: ')[1], '+')[0] with open('debian/debian.yaml', 'w') as f: - f.write(debianContent.replace('{{Version}}', version)) + content = debianContent.replace('{{Version}}', version) + if arch == 'x64': + content = content.replace('{{Arch}}', 'x64') + content = content.replace('{{Architecture}}', 'amd64') + elif arch == 'arm64': + content = content.replace('{{Arch}}', 'arm64') + content = content.replace('{{Architecture}}', 'arm64') + f.write(content) with open('debian/gui/venera.desktop', 'w') as f: f.write(desktopContent.replace('{{Version}}', version)) diff --git a/debian/debian.yaml b/debian/debian.yaml index f4044f3..a9d0869 100644 --- a/debian/debian.yaml +++ b/debian/debian.yaml @@ -1,13 +1,13 @@ flutter_app: command: venera - arch: x64 + arch: {{Arch}} parent: /usr/local/lib nonInteractive: true control: Package: venera Version: {{Version}} - Architecture: amd64 + Architecture: {{Architecture}} Priority: optional Depends: libwebkit2gtk-4.1-0, libgtk-3-0 Maintainer: nyne From 17ef17ca5bf0bdda7b545823ca9ce06094058716 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 9 Feb 2025 18:22:38 +0800 Subject: [PATCH 16/18] Add a button for managing network folders. --- lib/pages/favorites/favorites_page.dart | 1 + lib/pages/favorites/side_bar.dart | 45 +++++++++++++++++------- lib/pages/settings/explore_settings.dart | 28 ++++++++------- 3 files changed, 49 insertions(+), 25 deletions(-) diff --git a/lib/pages/favorites/favorites_page.dart b/lib/pages/favorites/favorites_page.dart index 80445b2..e9dc5aa 100644 --- a/lib/pages/favorites/favorites_page.dart +++ b/lib/pages/favorites/favorites_page.dart @@ -16,6 +16,7 @@ import 'package:venera/foundation/res.dart'; import 'package:venera/network/download.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/reader/reader.dart'; +import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; diff --git a/lib/pages/favorites/side_bar.dart b/lib/pages/favorites/side_bar.dart index 9246836..f2535bb 100644 --- a/lib/pages/favorites/side_bar.dart +++ b/lib/pages/favorites/side_bar.dart @@ -20,22 +20,35 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { var networkFolders = []; + void findNetworkFolders() { + networkFolders.clear(); + var all = ComicSource.all() + .where((e) => e.favoriteData != null) + .map((e) => e.favoriteData!.key) + .toList(); + var settings = appdata.settings['favorites'] as List; + for (var p in settings) { + if (all.contains(p) && !networkFolders.contains(p)) { + networkFolders.add(p); + } + } + } + @override void initState() { favPage = widget.favPage ?? context.findAncestorStateOfType<_FavoritesPageState>()!; favPage.folderList = this; folders = LocalFavoritesManager().folderNames; - networkFolders = ComicSource.all() - .where((e) => e.favoriteData != null && e.isLogged) - .map((e) => e.favoriteData!.key) - .toList(); + findNetworkFolders(); + appdata.settings.addListener(updateFolders); super.initState(); } @override void dispose() { super.dispose(); + appdata.settings.removeListener(updateFolders); } @override @@ -102,7 +115,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { onClick: () { newFolder().then((value) { setState(() { - folders = LocalFavoritesManager().folderNames; + folders = + LocalFavoritesManager().folderNames; }); }); }, @@ -113,7 +127,8 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { onClick: () { sortFolders().then((value) { setState(() { - folders = LocalFavoritesManager().folderNames; + folders = + LocalFavoritesManager().folderNames; }); }); }, @@ -143,15 +158,24 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { ), child: Row( children: [ - const SizedBox(width: 16), 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), ); } index--; @@ -241,10 +265,7 @@ class _LeftBarState extends State<_LeftBar> implements FolderList { if (!mounted) return; setState(() { folders = LocalFavoritesManager().folderNames; - networkFolders = ComicSource.all() - .where((e) => e.favoriteData != null) - .map((e) => e.favoriteData!.key) - .toList(); + findNetworkFolders(); }); } } diff --git a/lib/pages/settings/explore_settings.dart b/lib/pages/settings/explore_settings.dart index dc40018..d945d08 100644 --- a/lib/pages/settings/explore_settings.dart +++ b/lib/pages/settings/explore_settings.dart @@ -38,19 +38,7 @@ class _ExploreSettingsState extends State { ).toSliver(), _PopupWindowSetting( title: "Network Favorite Pages".tl, - builder: () { - var pages = {}; - for (var c in ComicSource.all()) { - if (c.favoriteData != null) { - pages[c.favoriteData!.key] = c.favoriteData!.title; - } - } - return _MultiPagesFilter( - title: "Network Favorite Pages".tl, - settingsIndex: "favorites", - pages: pages, - ); - }, + builder: setFavoritesPagesWidget, ).toSliver(), _SwitchSetting( title: "Show favorite status on comic tile".tl, @@ -208,4 +196,18 @@ Widget setCategoryPagesWidget() { settingsIndex: "categories", pages: pages, ); +} + +Widget setFavoritesPagesWidget() { + var pages = {}; + for (var c in ComicSource.all()) { + if (c.favoriteData != null) { + pages[c.favoriteData!.key] = c.favoriteData!.title; + } + } + return _MultiPagesFilter( + title: "Network Favorite Pages".tl, + settingsIndex: "favorites", + pages: pages, + ); } \ No newline at end of file From df4263f969c59492ba43c52db4d6a3876b7a56a8 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 9 Feb 2025 19:29:51 +0800 Subject: [PATCH 17/18] Add ability to manage search sources. Close #174 --- assets/translation.json | 6 +- lib/foundation/appdata.dart | 1 + lib/init.dart | 11 ++- lib/pages/aggregated_search_page.dart | 14 +++- lib/pages/search_page.dart | 95 +++++++++++++++++++----- lib/pages/settings/explore_settings.dart | 18 +++++ 6 files changed, 120 insertions(+), 25 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index a279368..73d62cc 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -323,7 +323,8 @@ "Cloudflare verification required": "需要Cloudflare验证", "Success": "成功", "Compressing": "压缩中", - "Exporting": "导出中" + "Exporting": "导出中", + "Search Sources": "搜索源" }, "zh_TW": { "Home": "首頁", @@ -649,6 +650,7 @@ "Cloudflare verification required": "需要Cloudflare驗證", "Success": "成功", "Compressing": "壓縮中", - "Exporting": "匯出中" + "Exporting": "匯出中", + "Search Sources": "搜索源" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 1e083df..65ec128 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -126,6 +126,7 @@ class _Settings with ChangeNotifier { 'explore_pages': [], 'categories': [], 'favorites': [], + 'searchSources': null, 'showFavoriteStatusOnTile': true, 'showHistoryStatusOnTile': false, 'blockedWords': [], diff --git a/lib/init.dart b/lib/init.dart index f3f4c0d..11c4bc3 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -42,11 +42,16 @@ Future init() async { await ComicSource.init().wait(); await LocalManager().init().wait(); CacheManager().setLimitSize(appdata.settings['cacheSize']); + if (appdata.settings['searchSources'] == null) { + appdata.settings['searchSources'] = ComicSource.all() + .where((e) => e.searchPageData != null) + .map((e) => e.key) + .toList(); + } if (App.isAndroid) { handleLinks(); } FlutterError.onError = (details) { - Log.error( - "Unhandled Exception", "${details.exception}\n${details.stack}"); + Log.error("Unhandled Exception", "${details.exception}\n${details.stack}"); }; -} \ No newline at end of file +} diff --git a/lib/pages/aggregated_search_page.dart b/lib/pages/aggregated_search_page.dart index 06093a4..48fc849 100644 --- a/lib/pages/aggregated_search_page.dart +++ b/lib/pages/aggregated_search_page.dart @@ -2,6 +2,7 @@ import "package:flutter/material.dart"; import 'package:shimmer_animation/shimmer_animation.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/pages/search_result_page.dart"; import "package:venera/utils/translations.dart"; @@ -24,7 +25,18 @@ class _AggregatedSearchPageState extends State { @override void initState() { - sources = ComicSource.all().where((e) => e.searchPageData != null).toList(); + var all = ComicSource.all() + .where((e) => e.searchPageData != null) + .map((e) => e.key) + .toList(); + var settings = appdata.settings['searchSources'] as List; + var sources = []; + for (var source in settings) { + if (all.contains(source)) { + sources.add(source); + } + } + this.sources = sources.map((e) => ComicSource.find(e)!).toList(); _keyword = widget.keyword; controller = SearchBarController( currentText: widget.keyword, diff --git a/lib/pages/search_page.dart b/lib/pages/search_page.dart index f3d6cbe..6258e0d 100644 --- a/lib/pages/search_page.dart +++ b/lib/pages/search_page.dart @@ -10,12 +10,14 @@ import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/state_controller.dart'; import 'package:venera/pages/aggregated_search_page.dart'; import 'package:venera/pages/search_result_page.dart'; +import 'package:venera/pages/settings/settings_page.dart'; import 'package:venera/utils/app_links.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/tags_translation.dart'; import 'package:venera/utils/translations.dart'; import 'comic_page.dart'; +import 'comic_source_page.dart'; class SearchPage extends StatefulWidget { const SearchPage({super.key}); @@ -27,8 +29,13 @@ class SearchPage extends StatefulWidget { class _SearchPageState extends State { late final SearchBarController controller; + late List searchSources; + String searchTarget = ""; + SearchPageData get currentSearchPageData => + ComicSource.find(searchTarget)!.searchPageData!; + bool aggregatedSearch = false; var focusNode = FocusNode(); @@ -139,31 +146,85 @@ class _SearchPageState extends State { @override void initState() { + findSearchSources(); var defaultSearchTarget = appdata.settings['defaultSearchTarget']; if (defaultSearchTarget == "_aggregated_") { aggregatedSearch = true; - searchTarget = ComicSource.all().where((e) => e.searchPageData != null) - .toList().first.key; } else if (defaultSearchTarget != null && - ComicSource.find(defaultSearchTarget) != null) { + searchSources.contains(defaultSearchTarget)) { searchTarget = defaultSearchTarget; - } else { - searchTarget = ComicSource.all().first.key; } controller = SearchBarController( onSearch: search, ); + appdata.settings.addListener(updateSearchSourcesIfNeeded); super.initState(); } @override void dispose() { focusNode.dispose(); + appdata.settings.removeListener(updateSearchSourcesIfNeeded); super.dispose(); } + void findSearchSources() { + var all = ComicSource.all() + .where((e) => e.searchPageData != null) + .map((e) => e.key) + .toList(); + var settings = appdata.settings['searchSources'] as List; + var sources = []; + for (var source in settings) { + if (all.contains(source)) { + sources.add(source); + } + } + searchSources = sources; + if (!searchSources.contains(searchTarget)) { + searchTarget = searchSources.firstOrNull ?? ""; + } + } + + void updateSearchSourcesIfNeeded() { + var old = searchSources; + findSearchSources(); + if (old.isEqualsTo(searchSources)) { + return; + } + setState(() {}); + } + + void manageSearchSources() { + showPopUpWidget(App.rootContext, setSearchSourcesWidget()); + } + + Widget buildEmpty() { + var msg = "No Search Sources".tl; + msg += '\n'; + VoidCallback onTap; + if (ComicSource.isEmpty) { + msg += "Please add some sources".tl; + onTap = () { + context.to(() => ComicSourcePage()); + }; + } else { + msg += "Please check your settings".tl; + onTap = manageSearchSources; + } + return NetworkError( + message: msg, + retry: onTap, + withAppbar: true, + buttonText: "Manage".tl, + ); + } + @override Widget build(BuildContext context) { + if (searchSources.isEmpty) { + return buildEmpty(); + } return Scaffold( body: SmoothCustomScrollView( slivers: buildSlivers().toList(), @@ -192,8 +253,7 @@ class _SearchPageState extends State { } Widget buildSearchTarget() { - var sources = - ComicSource.all().where((e) => e.searchPageData != null).toList(); + var sources = searchSources.map((e) => ComicSource.find(e)!).toList(); return SliverToBoxAdapter( child: Container( width: double.infinity, @@ -205,6 +265,10 @@ class _SearchPageState extends State { contentPadding: EdgeInsets.zero, leading: const Icon(Icons.search), title: Text("Search in".tl), + trailing: IconButton( + icon: const Icon(Icons.settings), + onPressed: manageSearchSources, + ), ), Wrap( spacing: 8, @@ -231,11 +295,6 @@ class _SearchPageState extends State { onChanged: (value) { setState(() { aggregatedSearch = value ?? false; - if (!aggregatedSearch && - appdata.settings['defaultSearchTarget'] == - "_aggregated_") { - searchTarget = sources.first.key; - } }); }, ), @@ -247,9 +306,7 @@ class _SearchPageState extends State { } void useDefaultOptions() { - final searchOptions = - ComicSource.find(searchTarget)!.searchPageData!.searchOptions ?? - []; + final searchOptions = currentSearchPageData.searchOptions ?? []; options = searchOptions.map((e) => e.defaultValue).toList(); } @@ -260,9 +317,7 @@ class _SearchPageState extends State { var children = []; - final searchOptions = - ComicSource.find(searchTarget)!.searchPageData!.searchOptions ?? - []; + final searchOptions = currentSearchPageData.searchOptions ?? []; if (searchOptions.length != options.length) { useDefaultOptions(); } @@ -396,7 +451,9 @@ class _SearchPageState extends State { Text( subTitle, style: TextStyle( - fontSize: 14, color: Theme.of(context).colorScheme.outline), + fontSize: 14, + color: Theme.of(context).colorScheme.outline, + ), ) ], ), diff --git a/lib/pages/settings/explore_settings.dart b/lib/pages/settings/explore_settings.dart index d945d08..957182e 100644 --- a/lib/pages/settings/explore_settings.dart +++ b/lib/pages/settings/explore_settings.dart @@ -40,6 +40,10 @@ class _ExploreSettingsState extends State { title: "Network Favorite Pages".tl, builder: setFavoritesPagesWidget, ).toSliver(), + _PopupWindowSetting( + title: "Search Sources".tl, + builder: setSearchSourcesWidget, + ).toSliver(), _SwitchSetting( title: "Show favorite status on comic tile".tl, settingKey: "showFavoriteStatusOnTile", @@ -210,4 +214,18 @@ Widget setFavoritesPagesWidget() { settingsIndex: "favorites", pages: pages, ); +} + +Widget setSearchSourcesWidget() { + var pages = {}; + for (var c in ComicSource.all()) { + if (c.searchPageData != null) { + pages[c.key] = c.name; + } + } + return _MultiPagesFilter( + title: "Search Sources".tl, + settingsIndex: "searchSources", + pages: pages, + ); } \ No newline at end of file From 0b65b4ab53e617b8e08fff6575633c3cdea44f17 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 9 Feb 2025 19:32:10 +0800 Subject: [PATCH 18/18] Update version code --- lib/foundation/app.dart | 2 +- pubspec.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index aecd9e5..56a99be 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -10,7 +10,7 @@ export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.2.3"; + final version = "1.2.4"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index b35e7fb..5f4e03e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.2.3+123 +version: 1.2.4+124 environment: sdk: '>=3.6.0 <4.0.0' @@ -82,7 +82,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^5.0.0 flutter_to_arch: ^1.0.1 - flutter_to_debian: + flutter_to_debian: ^2.0.2 flutter: uses-material-design: true