From ef8dc9e8d49c046a7f65a49d7536c56ad1753a06 Mon Sep 17 00:00:00 2001 From: nyne Date: Sun, 26 Jan 2025 18:36:35 +0800 Subject: [PATCH 01/11] fix #158 --- lib/pages/settings/settings_page.dart | 62 ++++++++++++++------------- pubspec.lock | 6 +-- pubspec.yaml | 1 + 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart index b08de03..cb79750 100644 --- a/lib/pages/settings/settings_page.dart +++ b/lib/pages/settings/settings_page.dart @@ -206,37 +206,41 @@ class _SettingsPageState extends State implements PopEntry { ], ); } else { - return Stack( - children: [ - Positioned.fill(child: buildLeft()), - Positioned( - left: offset, - width: MediaQuery.of(context).size.width, - top: 0, - bottom: 0, - child: Listener( - onPointerDown: handlePointerDown, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - switchInCurve: Curves.fastOutSlowIn, - switchOutCurve: Curves.fastOutSlowIn, - transitionBuilder: (child, animation) { - var tween = Tween( - begin: const Offset(1, 0), end: const Offset(0, 0)); + return LayoutBuilder( + builder: (context, constrains) { + return Stack( + children: [ + Positioned.fill(child: buildLeft()), + Positioned( + left: offset, + width: constrains.maxWidth, + top: 0, + bottom: 0, + child: Listener( + onPointerDown: handlePointerDown, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + switchInCurve: Curves.fastOutSlowIn, + switchOutCurve: Curves.fastOutSlowIn, + transitionBuilder: (child, animation) { + var tween = Tween( + begin: const Offset(1, 0), end: const Offset(0, 0)); - return SlideTransition( - position: tween.animate(animation), - child: child, - ); - }, - child: Material( - key: ValueKey(currentPage), - child: buildRight(), + return SlideTransition( + position: tween.animate(animation), + child: child, + ); + }, + child: Material( + key: ValueKey(currentPage), + child: buildRight(), + ), + ), ), - ), - ), - ) - ], + ) + ], + ); + }, ); } } diff --git a/pubspec.lock b/pubspec.lock index d3e9212..368074e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1083,13 +1083,13 @@ packages: source: hosted version: "6.5.0" yaml: - dependency: transitive + dependency: "direct main" description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" zip_flutter: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b723d14..79dd7be 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -75,6 +75,7 @@ dependencies: flex_seed_scheme: ^3.5.0 flutter_localizations: sdk: flutter + yaml: ^3.1.3 dev_dependencies: flutter_test: From d99a30b7d883741e66696ee45e573f02f9004db9 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 30 Jan 2025 17:49:01 +0800 Subject: [PATCH 02/11] Update desktop file --- debian/gui/venera.desktop | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/gui/venera.desktop b/debian/gui/venera.desktop index e11ee22..fd7ce9d 100644 --- a/debian/gui/venera.desktop +++ b/debian/gui/venera.desktop @@ -5,4 +5,5 @@ Comment=venera Terminal=false Type=Application Categories=Utility -Keywords=Flutter;comic;images; \ No newline at end of file +Keywords=Flutter;comic;images; +Icon=venera \ No newline at end of file From d675af3fb4271de5427a5a8470a2376c47169c67 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 31 Jan 2025 10:46:24 +0800 Subject: [PATCH 03/11] fix cloudflare verification --- assets/translation.json | 8 +++++-- lib/components/loading.dart | 28 ++++++++-------------- lib/network/cloudflare.dart | 47 +++++++++++++++++++++++-------------- 3 files changed, 45 insertions(+), 38 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index 4362b1b..f9f3955 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -318,7 +318,9 @@ "Deselect All": "取消全选", "Add keyword": "添加关键词", "Keyword": "关键词", - "Manage": "管理" + "Manage": "管理", + "Verify": "验证", + "Cloudflare verification required": "需要Cloudflare验证" }, "zh_TW": { "Home": "首頁", @@ -639,6 +641,8 @@ "Deselect All": "取消全選", "Add keyword": "添加關鍵詞", "Keyword": "關鍵詞", - "Manage": "管理" + "Manage": "管理", + "Verify": "驗證", + "Cloudflare verification required": "需要Cloudflare驗證" } } \ No newline at end of file diff --git a/lib/components/loading.dart b/lib/components/loading.dart index 10f8224..234d0ef 100644 --- a/lib/components/loading.dart +++ b/lib/components/loading.dart @@ -57,7 +57,9 @@ class NetworkError extends StatelessWidget { if (cfe != null) FilledButton( onPressed: () => passCloudflare( - CloudflareException.fromString(message)!, retry!), + CloudflareException.fromString(message)!, + retry!, + ), child: Text('Verify'.tl), ) else @@ -130,7 +132,7 @@ abstract class LoadingState if (res.success) { return res; } else { - if(!mounted) return res; + if (!mounted) return res; if (retry >= 3) { return res; } @@ -188,7 +190,7 @@ abstract class LoadingState isLoading = true; Future.microtask(() { loadDataWithRetry().then((value) async { - if(!mounted) return; + if (!mounted) return; if (value.success) { data = value.data; await onDataLoaded(); @@ -321,21 +323,11 @@ abstract class MultiPageLoadingState } Widget buildError(BuildContext context, String error) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(error, maxLines: 3), - const SizedBox(height: 12), - Button.outlined( - onPressed: () { - reset(); - }, - child: const Text("Retry"), - ) - ], - ), - ).paddingHorizontal(16); + return NetworkError( + withAppbar: false, + message: error, + retry: reset, + ); } @override diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index ff22041..fdbbf30 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -1,6 +1,7 @@ import 'dart:io' as io; import 'package:dio/dio.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/consts.dart'; @@ -58,7 +59,7 @@ class CloudflareException implements DioException { class CloudflareInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - if(options.headers['cookie'].toString().contains('cf_clearance')) { + if (options.headers['cookie'].toString().contains('cf_clearance')) { options.headers['user-agent'] = appdata.implicitData['ua'] ?? webUA; } handler.next(options); @@ -129,7 +130,7 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { appdata.writeImplicitData(); } var cookiesMap = await controller.getCookies(url); - if(cookiesMap['cf_clearance'] == null) { + if (cookiesMap['cf_clearance'] == null) { return; } saveCookies(cookiesMap); @@ -137,30 +138,40 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { onFinished(); } }, + onClose: onFinished, ); webview.open(); } else { + void check(InAppWebViewController controller) async { + var res = await controller.platform.evaluateJavascript( + source: + "document.head.innerHTML.includes('#challenge-success-text')"); + if (res == false) { + var ua = await controller.getUA(); + if (ua != null) { + appdata.implicitData['ua'] = ua; + appdata.writeImplicitData(); + } + var cookies = await controller.getCookies(url) ?? []; + if (cookies.firstWhereOrNull( + (element) => element.name == 'cf_clearance') == + null) { + return; + } + SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies); + App.rootPop(); + } + } + await App.rootContext.to( () => AppWebview( initialUrl: url, singlePage: true, + onTitleChange: (title, controller) async { + check(controller); + }, onLoadStop: (controller) async { - var res = await controller.platform.evaluateJavascript( - source: - "document.head.innerHTML.includes('#challenge-success-text')"); - if (res == false) { - var ua = await controller.getUA(); - if (ua != null) { - appdata.implicitData['ua'] = ua; - appdata.writeImplicitData(); - } - var cookies = await controller.getCookies(url) ?? []; - if(cookies.firstWhereOrNull((element) => element.name == 'cf_clearance') == null) { - return; - } - SingleInstanceCookieJar.instance?.saveFromResponse(uri, cookies); - App.rootPop(); - } + check(controller); }, onStarted: (controller) async { var ua = await controller.getUA(); From 9ea749a84a3b0631860d4080b471938dfe1230e0 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 31 Jan 2025 11:53:06 +0800 Subject: [PATCH 04/11] login with webview on windows and linux. fix #162, fix #141 --- lib/pages/accounts_page.dart | 66 ++++++++++++++++++++++++++++++++++-- lib/pages/webview.dart | 5 ++- 2 files changed, 67 insertions(+), 4 deletions(-) diff --git a/lib/pages/accounts_page.dart b/lib/pages/accounts_page.dart index 121ad37..b569b19 100644 --- a/lib/pages/accounts_page.dart +++ b/lib/pages/accounts_page.dart @@ -9,6 +9,7 @@ import 'package:venera/foundation/state_controller.dart'; import 'package:venera/network/cookie_jar.dart'; import 'package:venera/pages/webview.dart'; import 'package:venera/utils/translations.dart'; +import 'dart:io' as io; class AccountsPageLogic extends StateController { final _reLogin = {}; @@ -236,7 +237,13 @@ class _LoginPageState extends State<_LoginPage> { const SizedBox(height: 24), if (widget.config.loginWebsite != null) TextButton( - onPressed: loginWithWebview, + onPressed: () { + if (App.isWindows || App.isLinux) { + loginWithWebview2(); + } else { + loginWithWebview(); + } + }, child: Text("Login with webview".tl), ), const SizedBox(height: 8), @@ -313,8 +320,8 @@ class _LoginPageState extends State<_LoginPage> { bool success = false; void validate(InAppWebViewController c) async { - if (widget.config.checkLoginStatus != null - && widget.config.checkLoginStatus!(url, title)) { + if (widget.config.checkLoginStatus != null && + widget.config.checkLoginStatus!(url, title)) { var cookies = (await c.getCookies(url)) ?? []; SingleInstanceCookieJar.instance?.saveFromResponse( Uri.parse(url), @@ -346,4 +353,57 @@ class _LoginPageState extends State<_LoginPage> { context.pop(); } } + + // for windows and linux + void loginWithWebview2() async { + if (!await DesktopWebview.isAvailable()) { + context.showMessage(message: "Webview is not available".tl); + } + + var url = widget.config.loginWebsite!; + var title = ''; + bool success = false; + + void onClose() { + if (success) { + widget.source.data['account'] = 'ok'; + widget.source.saveData(); + context.pop(); + } + } + + void validate(DesktopWebview webview) async { + if (widget.config.checkLoginStatus != null && + widget.config.checkLoginStatus!(url, title)) { + var cookiesMap = await webview.getCookies(url); + var cookies = []; + cookiesMap.forEach((key, value) { + cookies.add(io.Cookie(key, value)); + }); + SingleInstanceCookieJar.instance?.saveFromResponse( + Uri.parse(url), + cookies, + ); + success = true; + widget.config.onLoginWithWebviewSuccess?.call(); + webview.close(); + onClose(); + } + } + + var webview = DesktopWebview( + initialUrl: widget.config.loginWebsite!, + onTitleChange: (t, webview) { + title = t; + validate(webview); + }, + onNavigation: (u, webview) { + url = u; + validate(webview); + }, + onClose: onClose, + ); + + webview.open(); + } } diff --git a/lib/pages/webview.dart b/lib/pages/webview.dart index e6a4320..70655f6 100644 --- a/lib/pages/webview.dart +++ b/lib/pages/webview.dart @@ -303,7 +303,10 @@ class DesktopWebview { proxy: AppDio.proxy, )); _webview!.addOnWebMessageReceivedCallback(onMessage); - _webview!.setOnNavigation((s) => onNavigation?.call(s, this)); + _webview!.setOnNavigation((s) { + s = s.substring(1, s.length - 1); + return onNavigation?.call(s, this); + }); _webview!.launch(initialUrl, triggerOnUrlRequestEvent: false); _runTimer(); _webview!.onClose.then((value) { From 0b9f0b7d35db59982fd06821f9803deef7189e6c Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 31 Jan 2025 13:08:24 +0800 Subject: [PATCH 05/11] Improve downloading message. Close #165 --- lib/network/download.dart | 36 ++++++++++++++++++++++++--------- lib/pages/downloading_page.dart | 22 ++++++++++++++++---- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/lib/network/download.dart b/lib/network/download.dart index 5227b0d..058b93e 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -59,6 +59,16 @@ abstract class DownloadTask with ChangeNotifier { return null; } } + + @override + bool operator ==(Object other) { + return other is DownloadTask && + other.id == id && + other.comicType == comicType; + } + + @override + int get hashCode => Object.hash(id, comicType); } class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { @@ -220,7 +230,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { runRecorder(); if (comic == null) { - var res = await runWithRetry(() async { + _message = "Fetching comic info..."; + notifyListeners(); + var res = await _runWithRetry(() async { var r = await source.loadComicInfo!(comicId); if (r.error) { throw r.errorMessage!; @@ -260,7 +272,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { await LocalManager().saveCurrentDownloadingTasks(); if (_cover == null) { - var res = await runWithRetry(() async { + _message = "Downloading cover..."; + notifyListeners(); + var res = await _runWithRetry(() async { Uint8List? data; await for (var progress in ImageDownloader.loadThumbnail(comic!.cover, source.key)) { @@ -272,8 +286,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { throw "Failed to download cover"; } var fileType = detectFileType(data); - var file = - File(FilePath.join(path!, "cover${fileType.ext}")); + var file = File(FilePath.join(path!, "cover${fileType.ext}")); file.writeAsBytesSync(data); return "file://${file.path}"; }); @@ -290,7 +303,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { if (_images == null) { if (comic!.chapters == null) { - var res = await runWithRetry(() async { + _message = "Fetching image list..."; + notifyListeners(); + var res = await _runWithRetry(() async { var r = await source.loadComicPages!(comicId, null); if (r.error) { throw r.errorMessage!; @@ -312,6 +327,8 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { } else { _images = {}; _totalCount = 0; + int cpCount = 0; + int totalCpCount = chapters?.length ?? comic!.chapters!.length; for (var i in comic!.chapters!.keys) { if (chapters != null && !chapters!.contains(i)) { continue; @@ -320,7 +337,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { _totalCount += _images![i]!.length; continue; } - var res = await runWithRetry(() async { + _message = "Fetching image list ($cpCount/$totalCpCount)..."; + notifyListeners(); + var res = await _runWithRetry(() async { var r = await source.loadComicPages!(comicId, i); if (r.error) { throw r.errorMessage!; @@ -458,8 +477,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { }).toList(), directory: Directory(path!).name, chapters: comic!.chapters, - cover: - File(_cover!.split("file://").last).name, + cover: File(_cover!.split("file://").last).name, comicType: ComicType(source.key.hashCode), downloadedChapters: chapters ?? [], createdAt: DateTime.now(), @@ -478,7 +496,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { int get hashCode => Object.hash(comicId, source.key); } -Future> runWithRetry(Future Function() task, +Future> _runWithRetry(Future Function() task, {int retry = 3}) async { for (var i = 0; i < retry; i++) { try { diff --git a/lib/pages/downloading_page.dart b/lib/pages/downloading_page.dart index 790e482..795d6e0 100644 --- a/lib/pages/downloading_page.dart +++ b/lib/pages/downloading_page.dart @@ -46,6 +46,7 @@ class _DownloadingPageState extends State { i--; return _DownloadTaskTile( + key: ValueKey(LocalManager().downloadingTasks[i]), task: LocalManager().downloadingTasks[i], ); }, @@ -120,7 +121,7 @@ class _DownloadingPageState extends State { } class _DownloadTaskTile extends StatefulWidget { - const _DownloadTaskTile({required this.task}); + const _DownloadTaskTile({required this.task, super.key}); final DownloadTask task; @@ -129,20 +130,33 @@ class _DownloadTaskTile extends StatefulWidget { } class _DownloadTaskTileState extends State<_DownloadTaskTile> { + late DownloadTask task; + @override void initState() { - widget.task.addListener(update); + task = widget.task; + task.addListener(update); super.initState(); } @override void dispose() { - widget.task.removeListener(update); + task.removeListener(update); super.dispose(); } + @override + void didUpdateWidget(covariant _DownloadTaskTile oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.task != widget.task) { + task.removeListener(update); + task = widget.task; + task.addListener(update); + } + } + void update() { - context.findAncestorStateOfType<_DownloadingPageState>()?.update(); + setState(() {}); } @override From e2c69d882f39e20ffda29d7893abfae5cac9118e Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 31 Jan 2025 13:11:04 +0800 Subject: [PATCH 06/11] Fix image order. Close #159 --- lib/pages/reader/images.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 11ef91a..69690c0 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -228,6 +228,8 @@ class _GalleryModeState extends State<_GalleryMode> ? Axis.vertical : Axis.horizontal; + bool reverse = reader.mode == ReaderMode.galleryRightToLeft; + List imageWidgets = images.map((imageKey) { ImageProvider imageProvider = _createImageProviderFromKey(imageKey, context); @@ -239,6 +241,10 @@ class _GalleryModeState extends State<_GalleryMode> ); }).toList(); + if (reverse) { + imageWidgets = imageWidgets.reversed.toList(); + } + return axis == Axis.vertical ? Column(children: imageWidgets) : Row(children: imageWidgets); From 8c5dae1e5939aaf3593c29b9da1a88a6f7b54fc1 Mon Sep 17 00:00:00 2001 From: nyne Date: Fri, 31 Jan 2025 13:27:22 +0800 Subject: [PATCH 07/11] Fix empty page. Close #160 --- lib/pages/reader/images.dart | 4 +--- lib/pages/reader/reader.dart | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 69690c0..5bb1212 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -111,9 +111,7 @@ class _GalleryModeState extends State<_GalleryMode> late _ReaderState reader; - int get totalPages => ((reader.images!.length + reader.imagesPerPage - 1) / - reader.imagesPerPage) - .ceil(); + int get totalPages => (reader.images!.length / reader.imagesPerPage).ceil(); @override void initState() { diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index c897f70..e38407b 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -98,8 +98,7 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } @override - int get maxPage => - ((images?.length ?? 1) + imagesPerPage - 1) ~/ imagesPerPage; + int get maxPage => ((images?.length ?? 1) / imagesPerPage).ceil(); ComicType get type => widget.type; From 739685f60fef90982ba35bb4c114cdfc0ee978d7 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 1 Feb 2025 10:11:34 +0800 Subject: [PATCH 08/11] Fix crash when using cbz export on iOS and macOS. Close #164 --- pubspec.lock | 4 ++-- pubspec.yaml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 368074e..25c9852 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1094,10 +1094,10 @@ packages: dependency: "direct main" description: name: zip_flutter - sha256: fe63ef9098bb2426b001adba2e28029820d71ce80cce957a36676bd6b3227245 + sha256: bbf3160062610a43901b7ebbc6f6dd46519540f03a84027dc7b1fff399dda1ac url: "https://pub.dev" source: hosted - version: "0.0.9" + version: "0.0.10" sdks: dart: ">=3.6.0 <4.0.0" flutter: ">=3.27.3" diff --git a/pubspec.yaml b/pubspec.yaml index 79dd7be..4b72bac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,7 +48,7 @@ dependencies: sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 - zip_flutter: ^0.0.9 + zip_flutter: ^0.0.10 lodepng_flutter: git: url: https://github.com/venera-app/lodepng_flutter From 4e6f71ef36fee23f86f6c4ddb42e05dd0c08daa2 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 1 Feb 2025 15:54:52 +0800 Subject: [PATCH 09/11] Merge account page and comic source page. --- assets/translation.json | 6 +- lib/pages/accounts_page.dart | 409 ----------------- lib/pages/comic_source_page.dart | 748 ++++++++++++++++++++++++------- lib/pages/home_page.dart | 111 ----- 4 files changed, 586 insertions(+), 688 deletions(-) delete mode 100644 lib/pages/accounts_page.dart diff --git a/assets/translation.json b/assets/translation.json index f9f3955..999477e 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -320,7 +320,8 @@ "Keyword": "关键词", "Manage": "管理", "Verify": "验证", - "Cloudflare verification required": "需要Cloudflare验证" + "Cloudflare verification required": "需要Cloudflare验证", + "Success": "成功" }, "zh_TW": { "Home": "首頁", @@ -643,6 +644,7 @@ "Keyword": "關鍵詞", "Manage": "管理", "Verify": "驗證", - "Cloudflare verification required": "需要Cloudflare驗證" + "Cloudflare verification required": "需要Cloudflare驗證", + "Success": "成功" } } \ No newline at end of file diff --git a/lib/pages/accounts_page.dart b/lib/pages/accounts_page.dart deleted file mode 100644 index b569b19..0000000 --- a/lib/pages/accounts_page.dart +++ /dev/null @@ -1,409 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:venera/components/components.dart'; -import 'package:venera/foundation/app.dart'; -import 'package:venera/foundation/comic_source/comic_source.dart'; -import 'package:venera/foundation/state_controller.dart'; -import 'package:venera/network/cookie_jar.dart'; -import 'package:venera/pages/webview.dart'; -import 'package:venera/utils/translations.dart'; -import 'dart:io' as io; - -class AccountsPageLogic extends StateController { - final _reLogin = {}; -} - -class AccountsPage extends StatelessWidget { - const AccountsPage({super.key}); - - AccountsPageLogic get logic => StateController.find(); - - @override - Widget build(BuildContext context) { - var body = StateBuilder( - init: AccountsPageLogic(), - builder: (logic) { - return CustomScrollView( - slivers: [ - SliverAppbar(title: Text("Accounts".tl)), - SliverList( - delegate: SliverChildListDelegate( - buildContent(context).toList(), - ), - ), - SliverPadding( - padding: EdgeInsets.only(bottom: context.padding.bottom), - ) - ], - ); - }, - ); - - return Scaffold( - body: body, - ); - } - - Iterable buildContent(BuildContext context) sync* { - var sources = ComicSource.all().where((element) => element.account != null); - if (sources.isEmpty) return; - - for (var element in sources) { - final bool logged = element.isLogged; - yield Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), - child: Text( - element.name, - style: const TextStyle(fontSize: 16), - ), - ); - if (!logged) { - yield ListTile( - title: Text("Log in".tl), - trailing: const Icon(Icons.arrow_right), - onTap: () async { - await context.to( - () => _LoginPage( - config: element.account!, - source: element, - ), - ); - element.saveData(); - ComicSource.notifyListeners(); - logic.update(); - }, - ); - } - if (logged) { - for (var item in element.account!.infoItems) { - if (item.builder != null) { - yield item.builder!(context); - } else { - yield ListTile( - title: Text(item.title.tl), - subtitle: item.data == null ? null : Text(item.data!()), - onTap: item.onTap, - ); - } - } - if (element.data["account"] is List) { - bool loading = logic._reLogin[element.key] == true; - yield ListTile( - title: Text("Re-login".tl), - subtitle: Text("Click if login expired".tl), - onTap: () async { - if (element.data["account"] == null) { - context.showMessage(message: "No data".tl); - return; - } - logic._reLogin[element.key] = true; - logic.update(); - final List account = element.data["account"]; - var res = await element.account!.login!(account[0], account[1]); - if (res.error) { - context.showMessage(message: res.errorMessage!); - } else { - context.showMessage(message: "Success".tl); - } - logic._reLogin[element.key] = false; - logic.update(); - }, - trailing: loading - ? const SizedBox.square( - dimension: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : const Icon(Icons.refresh), - ); - } - yield ListTile( - title: Text("Log out".tl), - onTap: () { - element.data["account"] = null; - element.account?.logout(); - element.saveData(); - ComicSource.notifyListeners(); - logic.update(); - }, - trailing: const Icon(Icons.logout), - ); - } - yield const Divider(thickness: 0.6); - } - } - - void setClipboard(String text) { - Clipboard.setData(ClipboardData(text: text)); - showToast( - message: "Copied".tl, - icon: const Icon(Icons.check), - context: App.rootContext, - ); - } -} - -class _LoginPage extends StatefulWidget { - const _LoginPage({required this.config, required this.source}); - - final AccountConfig config; - - final ComicSource source; - - @override - State<_LoginPage> createState() => _LoginPageState(); -} - -class _LoginPageState extends State<_LoginPage> { - String username = ""; - String password = ""; - bool loading = false; - - final Map _cookies = {}; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: const Appbar( - title: Text(''), - ), - body: Center( - child: Container( - padding: const EdgeInsets.all(16), - constraints: const BoxConstraints(maxWidth: 400), - child: AutofillGroup( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text("Login".tl, style: const TextStyle(fontSize: 24)), - const SizedBox(height: 32), - if (widget.config.cookieFields == null) - TextField( - decoration: InputDecoration( - labelText: "Username".tl, - border: const OutlineInputBorder(), - ), - enabled: widget.config.login != null, - onChanged: (s) { - username = s; - }, - autofillHints: const [AutofillHints.username], - ).paddingBottom(16), - if (widget.config.cookieFields == null) - TextField( - decoration: InputDecoration( - labelText: "Password".tl, - border: const OutlineInputBorder(), - ), - obscureText: true, - enabled: widget.config.login != null, - onChanged: (s) { - password = s; - }, - onSubmitted: (s) => login(), - autofillHints: const [AutofillHints.password], - ).paddingBottom(16), - for (var field in widget.config.cookieFields ?? []) - TextField( - decoration: InputDecoration( - labelText: field, - border: const OutlineInputBorder(), - ), - obscureText: true, - enabled: widget.config.validateCookies != null, - onChanged: (s) { - _cookies[field] = s; - }, - ).paddingBottom(16), - if (widget.config.login == null && - widget.config.cookieFields == null) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.error_outline), - const SizedBox(width: 8), - Text("Login with password is disabled".tl), - ], - ) - else - Button.filled( - isLoading: loading, - onPressed: login, - child: Text("Continue".tl), - ), - const SizedBox(height: 24), - if (widget.config.loginWebsite != null) - TextButton( - onPressed: () { - if (App.isWindows || App.isLinux) { - loginWithWebview2(); - } else { - loginWithWebview(); - } - }, - child: Text("Login with webview".tl), - ), - const SizedBox(height: 8), - if (widget.config.registerWebsite != null) - TextButton( - onPressed: () => - launchUrlString(widget.config.registerWebsite!), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.link), - const SizedBox(width: 8), - Text("Create Account".tl), - ], - ), - ), - ], - ), - ), - ), - ), - ); - } - - void login() { - if (widget.config.login != null) { - if (username.isEmpty || password.isEmpty) { - showToast( - message: "Cannot be empty".tl, - icon: const Icon(Icons.error_outline), - context: context, - ); - return; - } - setState(() { - loading = true; - }); - widget.config.login!(username, password).then((value) { - if (value.error) { - context.showMessage(message: value.errorMessage!); - setState(() { - loading = false; - }); - } else { - if (mounted) { - context.pop(); - } - } - }); - } else if (widget.config.validateCookies != null) { - setState(() { - loading = true; - }); - var cookies = - widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList(); - widget.config.validateCookies!(cookies).then((value) { - if (value) { - widget.source.data['account'] = 'ok'; - widget.source.saveData(); - context.pop(); - } else { - context.showMessage(message: "Invalid cookies".tl); - setState(() { - loading = false; - }); - } - }); - } - } - - void loginWithWebview() async { - var url = widget.config.loginWebsite!; - var title = ''; - bool success = false; - - void validate(InAppWebViewController c) async { - if (widget.config.checkLoginStatus != null && - widget.config.checkLoginStatus!(url, title)) { - var cookies = (await c.getCookies(url)) ?? []; - SingleInstanceCookieJar.instance?.saveFromResponse( - Uri.parse(url), - cookies, - ); - success = true; - widget.config.onLoginWithWebviewSuccess?.call(); - App.mainNavigatorKey?.currentContext?.pop(); - } - } - - await context.to( - () => AppWebview( - initialUrl: widget.config.loginWebsite!, - onNavigation: (u, c) { - url = u; - validate(c); - return false; - }, - onTitleChange: (t, c) { - title = t; - validate(c); - }, - ), - ); - if (success) { - widget.source.data['account'] = 'ok'; - widget.source.saveData(); - context.pop(); - } - } - - // for windows and linux - void loginWithWebview2() async { - if (!await DesktopWebview.isAvailable()) { - context.showMessage(message: "Webview is not available".tl); - } - - var url = widget.config.loginWebsite!; - var title = ''; - bool success = false; - - void onClose() { - if (success) { - widget.source.data['account'] = 'ok'; - widget.source.saveData(); - context.pop(); - } - } - - void validate(DesktopWebview webview) async { - if (widget.config.checkLoginStatus != null && - widget.config.checkLoginStatus!(url, title)) { - var cookiesMap = await webview.getCookies(url); - var cookies = []; - cookiesMap.forEach((key, value) { - cookies.add(io.Cookie(key, value)); - }); - SingleInstanceCookieJar.instance?.saveFromResponse( - Uri.parse(url), - cookies, - ); - success = true; - widget.config.onLoginWithWebviewSuccess?.call(); - webview.close(); - onClose(); - } - } - - var webview = DesktopWebview( - initialUrl: widget.config.loginWebsite!, - onTitleChange: (t, webview) { - title = t; - validate(webview); - }, - onNavigation: (u, webview) { - url = u; - validate(webview); - }, - onClose: onClose, - ); - - webview.open(); - } -} diff --git a/lib/pages/comic_source_page.dart b/lib/pages/comic_source_page.dart index bdbb94c..55298d0 100644 --- a/lib/pages/comic_source_page.dart +++ b/lib/pages/comic_source_page.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'dart:io' as io; import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; @@ -7,11 +9,13 @@ import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/app_dio.dart'; +import 'package:venera/network/cookie_jar.dart'; +import 'package:venera/pages/webview.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/translations.dart'; -class ComicSourcePage extends StatefulWidget { +class ComicSourcePage extends StatelessWidget { const ComicSourcePage({super.key}); static Future checkComicSourceUpdate() async { @@ -44,11 +48,6 @@ class ComicSourcePage extends StatefulWidget { return shouldUpdate.length; } - @override - State createState() => _ComicSourcePageState(); -} - -class _ComicSourcePageState extends State { @override Widget build(BuildContext context) { return Scaffold( @@ -92,167 +91,19 @@ class _BodyState extends State<_Body> { style: AppbarStyle.shadow, ), buildCard(context), - for (var source in ComicSource.all()) buildSource(context, source), + for (var source in ComicSource.all()) + _SliverComicSource( + key: ValueKey(source.key), + source: source, + edit: edit, + update: update, + delete: delete, + ), SliverPadding(padding: EdgeInsets.only(bottom: context.padding.bottom)), ], ); } - Widget buildSource(BuildContext context, ComicSource source) { - var newVersion = ComicSource.availableUpdates[source.key]; - bool hasUpdate = - newVersion != null && compareSemVer(newVersion, source.version); - return SliverToBoxAdapter( - child: Column( - children: [ - const Divider(), - ListTile( - title: Row( - children: [ - Text(source.name), - const SizedBox(width: 6), - if (hasUpdate) - Tooltip( - message: newVersion, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: context.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text( - "New Version".tl, - style: const TextStyle(fontSize: 13), - ), - ), - ) - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Tooltip( - message: "Edit".tl, - child: IconButton( - onPressed: () => edit(source), - icon: const Icon(Icons.edit_note)), - ), - Tooltip( - message: "Update".tl, - child: IconButton( - onPressed: () => update(source), - icon: const Icon(Icons.update)), - ), - Tooltip( - message: "Delete".tl, - child: IconButton( - onPressed: () => delete(source), - icon: const Icon(Icons.delete)), - ), - ], - ), - ), - ListTile( - title: const Text("Version"), - subtitle: Text(source.version), - ), - ...buildSourceSettings(source), - ], - ), - ); - } - - Iterable buildSourceSettings(ComicSource source) sync* { - if (source.settings == null) { - return; - } else if (source.data['settings'] == null) { - source.data['settings'] = {}; - } - for (var item in source.settings!.entries) { - var key = item.key; - String type = item.value['type']; - try { - if (type == "select") { - var current = source.data['settings'][key]; - if (current == null) { - var d = item.value['default']; - for (var option in item.value['options']) { - if (option['value'] == d) { - current = option['text'] ?? option['value']; - break; - } - } - } else { - current = item.value['options'] - .firstWhere((e) => e['value'] == current)['text'] ?? - current; - } - yield ListTile( - title: Text((item.value['title'] as String).ts(source.key)), - trailing: Select( - current: (current as String).ts(source.key), - values: (item.value['options'] as List) - .map((e) => - ((e['text'] ?? e['value']) as String).ts(source.key)) - .toList(), - onTap: (i) { - source.data['settings'][key] = - item.value['options'][i]['value']; - source.saveData(); - setState(() {}); - }, - ), - ); - } else if (type == "switch") { - var current = source.data['settings'][key] ?? item.value['default']; - yield ListTile( - title: Text((item.value['title'] as String).ts(source.key)), - trailing: Switch( - value: current, - onChanged: (v) { - source.data['settings'][key] = v; - source.saveData(); - setState(() {}); - }, - ), - ); - } else if (type == "input") { - var current = - source.data['settings'][key] ?? item.value['default'] ?? ''; - yield ListTile( - title: Text((item.value['title'] as String).ts(source.key)), - subtitle: - Text(current, maxLines: 1, overflow: TextOverflow.ellipsis), - trailing: IconButton( - icon: const Icon(Icons.edit), - onPressed: () { - showInputDialog( - context: context, - title: (item.value['title'] as String).ts(source.key), - initialValue: current, - inputValidator: item.value['validator'] == null - ? null - : RegExp(item.value['validator']), - onConfirm: (value) { - source.data['settings'][key] = value; - source.saveData(); - setState(() {}); - return null; - }, - ); - }, - ), - ); - } else if (type == "callback") { - yield _CallbackSetting(setting: item, sourceKey: source.key); - } - } catch (e, s) { - Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); - } - } - } - void delete(ComicSource source) { showConfirmDialog( context: App.rootContext, @@ -297,10 +148,12 @@ class _BodyState extends State<_Body> { // } } - context.to(() => _EditFilePage(source.filePath, () async { - await ComicSource.reload(); - setState(() {}); - })); + context.to( + () => _EditFilePage(source.filePath, () async { + await ComicSource.reload(); + setState(() {}); + }), + ); } static Future update(ComicSource source) async { @@ -764,3 +617,566 @@ class _CallbackSettingState extends State<_CallbackSetting> { ); } } + +class _SliverComicSource extends StatefulWidget { + const _SliverComicSource({ + super.key, + required this.source, + required this.edit, + required this.update, + required this.delete, + }); + + final ComicSource source; + + final void Function(ComicSource source) edit; + final void Function(ComicSource source) update; + final void Function(ComicSource source) delete; + + @override + State<_SliverComicSource> createState() => _SliverComicSourceState(); +} + +class _SliverComicSourceState extends State<_SliverComicSource> { + ComicSource get source => widget.source; + + @override + Widget build(BuildContext context) { + var newVersion = ComicSource.availableUpdates[source.key]; + bool hasUpdate = + newVersion != null && compareSemVer(newVersion, source.version); + + return SliverMainAxisGroup( + slivers: [ + SliverPadding(padding: const EdgeInsets.only(top: 16)), + SliverToBoxAdapter( + child: ListTile( + title: Row( + children: [ + Text( + source.name, + style: ts.s18, + ), + const SizedBox(width: 6), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: context.colorScheme.surfaceContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + source.version, + style: const TextStyle(fontSize: 13), + ), + ), + if (hasUpdate) + Tooltip( + message: newVersion, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: context.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + "New Version".tl, + style: const TextStyle(fontSize: 13), + ), + ), + ).paddingLeft(4) + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Tooltip( + message: "Edit".tl, + child: IconButton( + onPressed: () => widget.edit(source), + icon: const Icon(Icons.edit_note), + ), + ), + Tooltip( + message: "Update".tl, + child: IconButton( + onPressed: () => widget.update(source), + icon: const Icon(Icons.update), + ), + ), + Tooltip( + message: "Delete".tl, + child: IconButton( + onPressed: () => widget.delete(source), + icon: const Icon(Icons.delete), + ), + ), + ], + ), + ), + ), + SliverToBoxAdapter( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: context.colorScheme.outlineVariant, + width: 0.6, + ), + ), + ), + ), + ), + SliverToBoxAdapter( + child: Column( + children: buildSourceSettings().toList(), + ), + ), + SliverToBoxAdapter( + child: Column( + children: _buildAccount().toList(), + ), + ), + ], + ); + } + + Iterable buildSourceSettings() sync* { + if (source.settings == null) { + return; + } else if (source.data['settings'] == null) { + source.data['settings'] = {}; + } + for (var item in source.settings!.entries) { + var key = item.key; + String type = item.value['type']; + try { + if (type == "select") { + var current = source.data['settings'][key]; + if (current == null) { + var d = item.value['default']; + for (var option in item.value['options']) { + if (option['value'] == d) { + current = option['text'] ?? option['value']; + break; + } + } + } else { + current = item.value['options'] + .firstWhere((e) => e['value'] == current)['text'] ?? + current; + } + yield ListTile( + title: Text((item.value['title'] as String).ts(source.key)), + trailing: Select( + current: (current as String).ts(source.key), + values: (item.value['options'] as List) + .map((e) => + ((e['text'] ?? e['value']) as String).ts(source.key)) + .toList(), + onTap: (i) { + source.data['settings'][key] = + item.value['options'][i]['value']; + source.saveData(); + setState(() {}); + }, + ), + ); + } else if (type == "switch") { + var current = source.data['settings'][key] ?? item.value['default']; + yield ListTile( + title: Text((item.value['title'] as String).ts(source.key)), + trailing: Switch( + value: current, + onChanged: (v) { + source.data['settings'][key] = v; + source.saveData(); + setState(() {}); + }, + ), + ); + } else if (type == "input") { + var current = + source.data['settings'][key] ?? item.value['default'] ?? ''; + yield ListTile( + title: Text((item.value['title'] as String).ts(source.key)), + subtitle: + Text(current, maxLines: 1, overflow: TextOverflow.ellipsis), + trailing: IconButton( + icon: const Icon(Icons.edit), + onPressed: () { + showInputDialog( + context: context, + title: (item.value['title'] as String).ts(source.key), + initialValue: current, + inputValidator: item.value['validator'] == null + ? null + : RegExp(item.value['validator']), + onConfirm: (value) { + source.data['settings'][key] = value; + source.saveData(); + setState(() {}); + return null; + }, + ); + }, + ), + ); + } else if (type == "callback") { + yield _CallbackSetting(setting: item, sourceKey: source.key); + } + } catch (e, s) { + Log.error("ComicSourcePage", "Failed to build a setting\n$e\n$s"); + } + } + } + + final _reLogin = {}; + + Iterable _buildAccount() sync* { + if (source.account == null) return; + final bool logged = source.isLogged; + if (!logged) { + yield ListTile( + title: Text("Log in".tl), + trailing: const Icon(Icons.arrow_right), + onTap: () async { + await context.to( + () => _LoginPage( + config: source.account!, + source: source, + ), + ); + source.saveData(); + setState(() {}); + }, + ); + } + if (logged) { + for (var item in source.account!.infoItems) { + if (item.builder != null) { + yield item.builder!(context); + } else { + yield ListTile( + title: Text(item.title.tl), + subtitle: item.data == null ? null : Text(item.data!()), + onTap: item.onTap, + ); + } + } + if (source.data["account"] is List) { + bool loading = _reLogin[source.key] == true; + yield ListTile( + title: Text("Re-login".tl), + subtitle: Text("Click if login expired".tl), + onTap: () async { + if (source.data["account"] == null) { + context.showMessage(message: "No data".tl); + return; + } + setState(() { + _reLogin[source.key] = true; + }); + final List account = source.data["account"]; + var res = await source.account!.login!(account[0], account[1]); + if (res.error) { + context.showMessage(message: res.errorMessage!); + } else { + context.showMessage(message: "Success".tl); + } + setState(() { + _reLogin[source.key] = false; + }); + }, + trailing: loading + ? const SizedBox.square( + dimension: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : const Icon(Icons.refresh), + ); + } + yield ListTile( + title: Text("Log out".tl), + onTap: () { + source.data["account"] = null; + source.account?.logout(); + source.saveData(); + ComicSource.notifyListeners(); + setState(() {}); + }, + trailing: const Icon(Icons.logout), + ); + } + } +} + +class _LoginPage extends StatefulWidget { + const _LoginPage({required this.config, required this.source}); + + final AccountConfig config; + + final ComicSource source; + + @override + State<_LoginPage> createState() => _LoginPageState(); +} + +class _LoginPageState extends State<_LoginPage> { + String username = ""; + String password = ""; + bool loading = false; + + final Map _cookies = {}; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const Appbar( + title: Text(''), + ), + body: Center( + child: Container( + padding: const EdgeInsets.all(16), + constraints: const BoxConstraints(maxWidth: 400), + child: AutofillGroup( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Login".tl, style: const TextStyle(fontSize: 24)), + const SizedBox(height: 32), + if (widget.config.cookieFields == null) + TextField( + decoration: InputDecoration( + labelText: "Username".tl, + border: const OutlineInputBorder(), + ), + enabled: widget.config.login != null, + onChanged: (s) { + username = s; + }, + autofillHints: const [AutofillHints.username], + ).paddingBottom(16), + if (widget.config.cookieFields == null) + TextField( + decoration: InputDecoration( + labelText: "Password".tl, + border: const OutlineInputBorder(), + ), + obscureText: true, + enabled: widget.config.login != null, + onChanged: (s) { + password = s; + }, + onSubmitted: (s) => login(), + autofillHints: const [AutofillHints.password], + ).paddingBottom(16), + for (var field in widget.config.cookieFields ?? []) + TextField( + decoration: InputDecoration( + labelText: field, + border: const OutlineInputBorder(), + ), + obscureText: true, + enabled: widget.config.validateCookies != null, + onChanged: (s) { + _cookies[field] = s; + }, + ).paddingBottom(16), + if (widget.config.login == null && + widget.config.cookieFields == null) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.error_outline), + const SizedBox(width: 8), + Text("Login with password is disabled".tl), + ], + ) + else + Button.filled( + isLoading: loading, + onPressed: login, + child: Text("Continue".tl), + ), + const SizedBox(height: 24), + if (widget.config.loginWebsite != null) + TextButton( + onPressed: () { + if (App.isWindows || App.isLinux) { + loginWithWebview2(); + } else { + loginWithWebview(); + } + }, + child: Text("Login with webview".tl), + ), + const SizedBox(height: 8), + if (widget.config.registerWebsite != null) + TextButton( + onPressed: () => + launchUrlString(widget.config.registerWebsite!), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.link), + const SizedBox(width: 8), + Text("Create Account".tl), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + void login() { + if (widget.config.login != null) { + if (username.isEmpty || password.isEmpty) { + showToast( + message: "Cannot be empty".tl, + icon: const Icon(Icons.error_outline), + context: context, + ); + return; + } + setState(() { + loading = true; + }); + widget.config.login!(username, password).then((value) { + if (value.error) { + context.showMessage(message: value.errorMessage!); + setState(() { + loading = false; + }); + } else { + if (mounted) { + context.pop(); + } + } + }); + } else if (widget.config.validateCookies != null) { + setState(() { + loading = true; + }); + var cookies = + widget.config.cookieFields!.map((e) => _cookies[e] ?? '').toList(); + widget.config.validateCookies!(cookies).then((value) { + if (value) { + widget.source.data['account'] = 'ok'; + widget.source.saveData(); + context.pop(); + } else { + context.showMessage(message: "Invalid cookies".tl); + setState(() { + loading = false; + }); + } + }); + } + } + + void loginWithWebview() async { + var url = widget.config.loginWebsite!; + var title = ''; + bool success = false; + + void validate(InAppWebViewController c) async { + if (widget.config.checkLoginStatus != null && + widget.config.checkLoginStatus!(url, title)) { + var cookies = (await c.getCookies(url)) ?? []; + SingleInstanceCookieJar.instance?.saveFromResponse( + Uri.parse(url), + cookies, + ); + success = true; + widget.config.onLoginWithWebviewSuccess?.call(); + App.mainNavigatorKey?.currentContext?.pop(); + } + } + + await context.to( + () => AppWebview( + initialUrl: widget.config.loginWebsite!, + onNavigation: (u, c) { + url = u; + validate(c); + return false; + }, + onTitleChange: (t, c) { + title = t; + validate(c); + }, + ), + ); + if (success) { + widget.source.data['account'] = 'ok'; + widget.source.saveData(); + context.pop(); + } + } + + // for windows and linux + void loginWithWebview2() async { + if (!await DesktopWebview.isAvailable()) { + context.showMessage(message: "Webview is not available".tl); + } + + var url = widget.config.loginWebsite!; + var title = ''; + bool success = false; + + void onClose() { + if (success) { + widget.source.data['account'] = 'ok'; + widget.source.saveData(); + context.pop(); + } + } + + void validate(DesktopWebview webview) async { + if (widget.config.checkLoginStatus != null && + widget.config.checkLoginStatus!(url, title)) { + var cookiesMap = await webview.getCookies(url); + var cookies = []; + cookiesMap.forEach((key, value) { + cookies.add(io.Cookie(key, value)); + }); + SingleInstanceCookieJar.instance?.saveFromResponse( + Uri.parse(url), + cookies, + ); + success = true; + widget.config.onLoginWithWebviewSuccess?.call(); + webview.close(); + onClose(); + } + } + + var webview = DesktopWebview( + initialUrl: widget.config.loginWebsite!, + onTitleChange: (t, webview) { + title = t; + validate(webview); + }, + onNavigation: (u, webview) { + url = u; + validate(webview); + }, + onClose: onClose, + ); + + webview.open(); + } +} diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index ffd8318..a550dbf 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; -import 'package:venera/pages/accounts_page.dart'; import 'package:venera/pages/comic_page.dart'; import 'package:venera/pages/comic_source_page.dart'; import 'package:venera/pages/downloading_page.dart'; @@ -36,7 +35,6 @@ class HomePage extends StatelessWidget { const _History(), const _Local(), const _ComicSourceWidget(), - const _AccountsWidget(), const ImageFavorites(), SliverPadding(padding: EdgeInsets.only(top: context.padding.bottom)), ], @@ -698,115 +696,6 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { } } -class _AccountsWidget extends StatefulWidget { - const _AccountsWidget(); - - @override - State<_AccountsWidget> createState() => _AccountsWidgetState(); -} - -class _AccountsWidgetState extends State<_AccountsWidget> { - late List accounts; - - void onComicSourceChange() { - setState(() { - accounts.clear(); - for (var c in ComicSource.all()) { - if (c.isLogged) { - accounts.add(c.name); - } - } - }); - } - - @override - void initState() { - accounts = []; - for (var c in ComicSource.all()) { - if (c.isLogged) { - accounts.add(c.name); - } - } - ComicSource.addListener(onComicSourceChange); - super.initState(); - } - - @override - void dispose() { - ComicSource.removeListener(onComicSourceChange); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return SliverToBoxAdapter( - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 8), - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - width: 0.6, - ), - borderRadius: BorderRadius.circular(8), - ), - child: InkWell( - borderRadius: BorderRadius.circular(8), - onTap: () { - context.to(() => const AccountsPage()); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 56, - child: Row( - children: [ - Center( - child: Text('Accounts'.tl, style: ts.s18), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 8), - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(accounts.length.toString(), style: ts.s12), - ), - const Spacer(), - const Icon(Icons.arrow_right), - ], - ), - ).paddingHorizontal(16), - SizedBox( - width: double.infinity, - child: Wrap( - runSpacing: 8, - spacing: 8, - children: accounts.map((e) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 2, - ), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(8), - ), - child: Text(e), - ); - }).toList(), - ).paddingHorizontal(16).paddingBottom(16), - ), - ], - ), - ), - ), - ); - } -} - class _AnimatedDownloadingIcon extends StatefulWidget { const _AnimatedDownloadingIcon(); From 28a56b46124f25f89ed926a3a75bd920e3914082 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 1 Feb 2025 15:56:57 +0800 Subject: [PATCH 10/11] Update version code --- lib/foundation/app.dart | 2 +- pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index ecdf6eb..9ed44e8 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.2"; + final version = "1.2.3"; bool get isAndroid => Platform.isAndroid; diff --git a/pubspec.yaml b/pubspec.yaml index 4b72bac..0624920 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.2.2+122 +version: 1.2.3+123 environment: sdk: '>=3.6.0 <4.0.0' From 340496da303521f03ca46d98b866c1bf411430f2 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 1 Feb 2025 16:24:43 +0800 Subject: [PATCH 11/11] Fix cloudflare bypass --- lib/network/cloudflare.dart | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index fdbbf30..cf61de1 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -5,6 +5,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/consts.dart'; +import 'package:venera/foundation/log.dart'; import 'package:venera/pages/webview.dart'; import 'package:venera/utils/ext.dart'; @@ -121,9 +122,18 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { var webview = DesktopWebview( initialUrl: url, onTitleChange: (title, controller) async { - var res = await controller.evaluateJavascript( - "document.head.innerHTML.includes('#challenge-success-text')"); - if (res == 'false') { + var head = + await controller.evaluateJavascript("document.head.innerHTML") ?? + ""; + Log.info("Cloudflare", "Checking head: $head"); + var isChallenging = head.contains('#challenge-success-text') || + head.contains("#challenge-error-text") || + head.contains("#challenge-form"); + if (!isChallenging) { + Log.info( + "Cloudflare", + "Cloudflare is passed due to there is no challenge css", + ); var ua = controller.userAgent; if (ua != null) { appdata.implicitData['ua'] = ua; @@ -143,10 +153,17 @@ void passCloudflare(CloudflareException e, void Function() onFinished) async { webview.open(); } else { void check(InAppWebViewController controller) async { - var res = await controller.platform.evaluateJavascript( - source: - "document.head.innerHTML.includes('#challenge-success-text')"); - if (res == false) { + var head = await controller.evaluateJavascript( + source: "document.head.innerHTML") as String; + Log.info("Cloudflare", "Checking head: $head"); + var isChallenging = head.contains('#challenge-success-text') || + head.contains("#challenge-error-text") || + head.contains("#challenge-form"); + if (!isChallenging) { + Log.info( + "Cloudflare", + "Cloudflare is passed due to there is no challenge css", + ); var ua = await controller.getUA(); if (ua != null) { appdata.implicitData['ua'] = ua;