From 282d9e8b7eeac77e35c102a7be78bbe40bd3cdb9 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 14 Jan 2024 02:16:45 +0800 Subject: [PATCH 1/4] Update comic_reader_content.dart --- .../reader/comic/comic_reader_content.dart | 103 ++++++++++-------- 1 file changed, 58 insertions(+), 45 deletions(-) diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index fa08729a..e70b5907 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -124,51 +124,64 @@ class _ComicReaderContentState extends State { //zooming is inspired by: https://github.com/flutter/flutter/issues/86531 if (Platform.isAndroid) { return InteractiveViewer( - minScale: minScaleValue, - transformationController: transformationController, - onInteractionEnd: (ScaleEndDetails endDetails) { - setState(() { - isZoomed = false; - }); - }, - onInteractionUpdate: (x) { - double correctScaleValue = - transformationController.value.getMaxScaleOnAxis(); - if (x.scale == correctScaleValue) { - setState(() { - isZoomed = false; - }); - } - setState(() { - isZoomed = true; - }); - debugPrint("${x.scale}"); - }, - child: ScrollablePositionedList.builder( - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - initialScrollIndex: cuurentPage, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - scrollOffsetListener: _c.scrolloffsetListener, - physics: isZoomed - ? const NeverScrollableScrollPhysics() - : const ScrollPhysics(), - itemBuilder: (context, index) { - final url = images[index]; - return Obx( - () => CacheNetWorkImagePic( - url, - fit: BoxFit.cover, - headers: _c.watchData.value?.headers, - ), - ); - }, - itemCount: images.length, - ), - ); + constrained: false, + minScale: 0.5, + child: Column( + children: [ + for (int col = 0; col < images.length; col++) + CacheNetWorkImagePic( + images[col], + fit: BoxFit.cover, + headers: _c.watchData.value?.headers, + ) + ], + )); + // InteractiveViewer( + // minScale: minScaleValue, + // transformationController: transformationController, + // onInteractionEnd: (ScaleEndDetails endDetails) { + // setState(() { + // isZoomed = false; + // }); + // }, + // onInteractionUpdate: (x) { + // double correctScaleValue = + // transformationController.value.getMaxScaleOnAxis(); + // if (x.scale == correctScaleValue) { + // setState(() { + // isZoomed = false; + // }); + // } + // setState(() { + // isZoomed = true; + // }); + // debugPrint("${x.scale}"); + // }, + // child: ScrollablePositionedList.builder( + // padding: EdgeInsets.symmetric( + // horizontal: viewPadding, + // ), + // initialScrollIndex: cuurentPage, + // itemScrollController: _c.itemScrollController, + // itemPositionsListener: _c.itemPositionsListener, + // scrollOffsetController: _c.scrollOffsetController, + // scrollOffsetListener: _c.scrolloffsetListener, + // physics: isZoomed + // ? const NeverScrollableScrollPhysics() + // : const ScrollPhysics(), + // itemBuilder: (context, index) { + // final url = images[index]; + // return Obx( + // () => CacheNetWorkImagePic( + // url, + // fit: BoxFit.cover, + // headers: _c.watchData.value?.headers, + // ), + // ); + // }, + // itemCount: images.length, + // ), + // ); } return ScrollablePositionedList.builder( padding: EdgeInsets.symmetric( From 033f7d12b4a7a3efb4e9651eb4bf1bc24f38ac12 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 14 Jan 2024 03:01:34 +0800 Subject: [PATCH 2/4] fix: page indicator --- .../reader/comic/comic_reader_content.dart | 50 +++++++++++++++++-- pubspec.lock | 8 +++ pubspec.yaml | 1 + 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index e70b5907..cc920b32 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -13,6 +13,7 @@ import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; +import 'package:visibility_detector/visibility_detector.dart'; class ComicReaderContent extends StatefulWidget { const ComicReaderContent(this.tag, {super.key}); @@ -23,6 +24,15 @@ class ComicReaderContent extends StatefulWidget { } class _ComicReaderContentState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + // This code will be executed after the widget has been built and rendered. + debugPrint('Column has been rendered'); + }); + } + late final _c = Get.find(tag: widget.tag); final zoomScale = 1.0.obs; bool isZoomed = false; @@ -129,11 +139,21 @@ class _ComicReaderContentState extends State { child: Column( children: [ for (int col = 0; col < images.length; col++) - CacheNetWorkImagePic( - images[col], - fit: BoxFit.cover, - headers: _c.watchData.value?.headers, - ) + VisibilityDetector( + key: Key(col.toString()), + child: CacheNetWorkImagePic( + images[col], + fit: BoxFit.cover, + headers: _c.watchData.value?.headers, + ), + onVisibilityChanged: (info) { + final visiblePercentage = + info.visibleFraction * 100; + debugPrint("$col, $visiblePercentage"); + if (visiblePercentage > 50) { + _c.currentPage.value = col; + } + }) ], )); // InteractiveViewer( @@ -250,3 +270,23 @@ class _ComicReaderContentState extends State { ); } } + +class RenderNotifier extends StatelessWidget { + RenderNotifier( + {super.key, + required this.index, + required this.onBuild, + required this.child}); + final int index; + final VoidCallback onBuild; + final Widget child; + + @override + Widget build(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + onBuild(); + }); + + return child; + } +} diff --git a/pubspec.lock b/pubspec.lock index cb03d0aa..1ef13f80 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1387,6 +1387,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 + url: "https://pub.dev" + source: hosted + version: "0.4.0+2" volume_controller: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 33b1ec62..3749954f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,6 +53,7 @@ dependencies: url: https://github.com/MiaoMint/flutter_windows_webview ref: master webview_cookie_manager: ^2.0.6 + visibility_detector: ^0.4.0+2 http_parser: ^4.0.2 html: ^0.15.4 xpath_selector_html_parser: ^3.0.1 From 335ad4ad45fef339d39cdcac7ddb48c75959b089 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:53:19 +0800 Subject: [PATCH 3/4] fix:scaling mode in android webtoon mode --- lib/controllers/watch/comic_controller.dart | 128 ++++++++++++---- .../reader/comic/comic_reader_content.dart | 143 ++++++------------ 2 files changed, 147 insertions(+), 124 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 0fb49b10..eefee892 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -26,23 +26,42 @@ class ComicController extends ReaderController { }; final String setting = MiruStorage.getSetting(SettingKey.readingMode); final readType = MangaReadMode.standard.obs; - + final currentScale = 1.0.obs; // MangaReadMode // 当前页码 final currentPage = 0.obs; bool timerCancel = false; + Rx? animationController; final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); final scrollOffsetController = ScrollOffsetController(); final scrolloffsetListener = ScrollOffsetListener.create(); + final transformationController = TransformationController(); + List columnKeys = [GlobalKey()]; // 是否已经恢复上次阅读 final isRecover = false.obs; - + double yPos = 0.0; @override void onInit() { _initSetting(); + transformationController.addListener(() { + if (columnKeys[0].currentWidget == null) return; + final matrix = transformationController.value; + yPos = matrix.getTranslation().y; + final scale = matrix.getMaxScaleOnAxis(); + final page = getYPage(columnKeys, yPos, scale); + currentPage.value = page; + // debugPrint("$page $yPos"); + }); + // transformationcontroller.addListener(() { + // final matrix = transformationcontroller.value; + // final scale = matrix.getMaxScaleOnAxis(); + // currentScale.value = scale; + // final y_dir = matrix.getTranslation().y; + // // print('Current scale: $scale y_dir: $y_dir'); + // }); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -50,6 +69,7 @@ class ComicController extends ReaderController { final pos = itemPositionsListener.itemPositions.value.first; currentPage.value = pos.index; }); + ever(readType, (callback) { _jumpPage(currentPage.value); // 保存设置 @@ -64,12 +84,15 @@ class ComicController extends ReaderController { if (isRecover.value || callback == null) { return; } + columnKeys = + List.generate(watchData.value!.urls.length, (index) => GlobalKey()); isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( super.runtime.extension.package, super.detailUrl, ); + if (history == null || history.progress.isEmpty || episodeGroupId != history.episodeGroupId || @@ -88,18 +111,45 @@ class ComicController extends ReaderController { super.detailUrl, readType.value); } - // double mapValue(double value) { - // double mappedValue = ((value - 0) * (1 - (-1))) / (2.5 - 0) + (-1); - // return mappedValue; - // } + //get the postion of children border + List childSepHeight(List columnKeys) { + { + final List heights = []; + double seperator = 0; + for (int i = 0; i < columnKeys.length; i++) { + final RenderBox renderBox = + columnKeys[i].currentContext.findRenderObject(); + seperator -= renderBox.size.height; + heights.add(seperator); + } + return heights; + } + } + + int getYPage(List columnKeys, double yPos, double scale) { + double seperator = -1.0; + int val = 0; + for (int i = 0; i < columnKeys.length; i++) { + final RenderBox renderBox = + columnKeys[i].currentContext.findRenderObject(); - _jumpPage(int page) { - if (readType.value == MangaReadMode.webTonn) { - if (itemScrollController.isAttached) { - itemScrollController.jumpTo( - index: page, - ); + if (seperator * scale < yPos) { + val = i; + break; } + seperator -= renderBox.size.height; + } + debugPrint("$yPos $val $seperator $scale"); + return val; + } + + _jumpPage(int page) async { + if (readType.value == MangaReadMode.webTonn) { + await Future.delayed(const Duration(milliseconds: 500)); + if (columnKeys[0].currentWidget == null) return; + final renderHeight = childSepHeight(columnKeys); + transformationController.value = Matrix4.identity() + ..translate(0.0, renderHeight[page - 1]); // translate(x,y); return; } if (pageController.value.hasClients) { @@ -118,14 +168,37 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 100), - curve: Curves.ease, - offset: 200.0, - ); + if (currentPage.value == columnKeys.length - 1) return; + final offset = columnKeys[currentPage.value] + .currentContext + .findRenderObject() + .size + .height; + webtoonJumpWithOffset(offset); } } + void webtoonJumpWithOffset(double yOffset) { + double endYPos = yPos - yOffset; + Tween(begin: yPos, end: endYPos) + .animate( + CurvedAnimation( + parent: animationController!.value, + curve: Curves.ease, + ), + ) + .addListener(() { + transformationController.value = Matrix4.identity() + ..translate(0.0, endYPos); // translate(x,y); + }); + animationController!.value.addStatusListener((status) { + if (status == AnimationStatus.completed) { + animationController!.value.reset(); + } + }); + animationController!.value.forward(); + } + // 上一页 @override void previousPage() { @@ -135,21 +208,16 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 10), - curve: Curves.linear, - offset: -200.0, - ); + if (currentPage.value == 0) return; + final offset = columnKeys[currentPage.value - 1] + .currentContext + .findRenderObject() + .size + .height; + webtoonJumpWithOffset(-offset); } } - // void scrollWithOffset(double offset) { - // scrollOffsetController.animateScroll( - // duration: const Duration(milliseconds: 100), - // curve: Curves.ease, - // offset: offset); - // } - @override void onClose() { if (super.watchData.value != null) { @@ -161,6 +229,8 @@ class ComicController extends ReaderController { ); } pageController.value.dispose(); + animationController?.value.dispose(); + transformationController.dispose(); if (MiruStorage.getSetting(SettingKey.autoTracking) && anilistID != "") { AniListProvider.editList( status: AnilistMediaListStatus.current, diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index cc920b32..70fe7133 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -13,7 +13,6 @@ import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; -import 'package:visibility_detector/visibility_detector.dart'; class ComicReaderContent extends StatefulWidget { const ComicReaderContent(this.tag, {super.key}); @@ -23,22 +22,25 @@ class ComicReaderContent extends StatefulWidget { State createState() => _ComicReaderContentState(); } -class _ComicReaderContentState extends State { +class _ComicReaderContentState extends State + with SingleTickerProviderStateMixin { @override void initState() { + _c.animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 700), + ).obs; + super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - // This code will be executed after the widget has been built and rendered. - debugPrint('Column has been rendered'); - }); } late final _c = Get.find(tag: widget.tag); final zoomScale = 1.0.obs; bool isZoomed = false; + // List? _columnKeys; - TransformationController transformationController = - TransformationController(); + // TransformationController transformationController = + // TransformationController(); final double minScaleValue = 1.0; _buildDisplay(Widget child) { @@ -131,77 +133,24 @@ class _ComicReaderContentState extends State { final cuurentPage = _c.currentPage.value; if (readerType == MangaReadMode.webTonn) { - //zooming is inspired by: https://github.com/flutter/flutter/issues/86531 + final width = MediaQuery.of(context).size.width; if (Platform.isAndroid) { - return InteractiveViewer( - constrained: false, - minScale: 0.5, - child: Column( - children: [ - for (int col = 0; col < images.length; col++) - VisibilityDetector( - key: Key(col.toString()), - child: CacheNetWorkImagePic( + return InteractiveViewer.builder( + transformationController: _c.transformationController, + minScale: 1, + builder: (context, quad) => SizedBox( + width: width, + child: Column( + children: [ + for (int col = 0; col < images.length; col++) + CacheNetWorkImagePic( images[col], + key: _c.columnKeys[col], fit: BoxFit.cover, headers: _c.watchData.value?.headers, - ), - onVisibilityChanged: (info) { - final visiblePercentage = - info.visibleFraction * 100; - debugPrint("$col, $visiblePercentage"); - if (visiblePercentage > 50) { - _c.currentPage.value = col; - } - }) - ], - )); - // InteractiveViewer( - // minScale: minScaleValue, - // transformationController: transformationController, - // onInteractionEnd: (ScaleEndDetails endDetails) { - // setState(() { - // isZoomed = false; - // }); - // }, - // onInteractionUpdate: (x) { - // double correctScaleValue = - // transformationController.value.getMaxScaleOnAxis(); - // if (x.scale == correctScaleValue) { - // setState(() { - // isZoomed = false; - // }); - // } - // setState(() { - // isZoomed = true; - // }); - // debugPrint("${x.scale}"); - // }, - // child: ScrollablePositionedList.builder( - // padding: EdgeInsets.symmetric( - // horizontal: viewPadding, - // ), - // initialScrollIndex: cuurentPage, - // itemScrollController: _c.itemScrollController, - // itemPositionsListener: _c.itemPositionsListener, - // scrollOffsetController: _c.scrollOffsetController, - // scrollOffsetListener: _c.scrolloffsetListener, - // physics: isZoomed - // ? const NeverScrollableScrollPhysics() - // : const ScrollPhysics(), - // itemBuilder: (context, index) { - // final url = images[index]; - // return Obx( - // () => CacheNetWorkImagePic( - // url, - // fit: BoxFit.cover, - // headers: _c.watchData.value?.headers, - // ), - // ); - // }, - // itemCount: images.length, - // ), - // ); + ) + ], + ))); } return ScrollablePositionedList.builder( padding: EdgeInsets.symmetric( @@ -221,6 +170,30 @@ class _ComicReaderContentState extends State { }, itemCount: images.length, ); + // return InteractiveViewer.builder( + // scaleEnabled: false, + // transformationController: _c.transformationController, + // minScale: 1, + // onInteractionUpdate: (details) { + // debugPrint( + // "${details.scale} ${_c.yPos} ${details.pointerCount}"); + + // // _c.transformationController.value = Matrix4.identity() + // // ..translate(0, _c.yPos - 100); + // }, + // builder: (context, quad) => SizedBox( + // width: width, + // child: Column( + // children: [ + // for (int col = 0; col < images.length; col++) + // CacheNetWorkImagePic( + // images[col], + // key: _c.columnKeys[col], + // fit: BoxFit.cover, + // headers: _c.watchData.value?.headers, + // ) + // ], + // ))); } //common mode and left to right mode return ExtendedImageGesturePageView.builder( @@ -270,23 +243,3 @@ class _ComicReaderContentState extends State { ); } } - -class RenderNotifier extends StatelessWidget { - RenderNotifier( - {super.key, - required this.index, - required this.onBuild, - required this.child}); - final int index; - final VoidCallback onBuild; - final Widget child; - - @override - Widget build(BuildContext context) { - WidgetsBinding.instance.addPostFrameCallback((_) { - onBuild(); - }); - - return child; - } -} From 666c0029415639bd8af81da1af0868fc23d4066d Mon Sep 17 00:00:00 2001 From: MiaoMint Date: Fri, 19 Jan 2024 00:02:06 +0800 Subject: [PATCH 4/4] fix: webtoon scaling --- lib/controllers/watch/comic_controller.dart | 152 ++++------ .../reader/comic/comic_reader_content.dart | 269 ++++++++---------- lib/views/widgets/cache_network_image.dart | 3 + 3 files changed, 175 insertions(+), 249 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index eefee892..fb6bfc41 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:miru_app/data/providers/anilist_provider.dart'; import 'package:miru_app/models/index.dart'; @@ -25,43 +26,29 @@ class ComicController extends ReaderController { 'webTonn': MangaReadMode.webTonn, }; final String setting = MiruStorage.getSetting(SettingKey.readingMode); + final readType = MangaReadMode.standard.obs; + final currentScale = 1.0.obs; // MangaReadMode // 当前页码 final currentPage = 0.obs; - bool timerCancel = false; - Rx? animationController; + final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); final scrollOffsetController = ScrollOffsetController(); - final scrolloffsetListener = ScrollOffsetListener.create(); - final transformationController = TransformationController(); - List columnKeys = [GlobalKey()]; // 是否已经恢复上次阅读 final isRecover = false.obs; - double yPos = 0.0; + + // 是否按下 ctrl + + final isZoom = false.obs; + @override void onInit() { _initSetting(); - transformationController.addListener(() { - if (columnKeys[0].currentWidget == null) return; - final matrix = transformationController.value; - yPos = matrix.getTranslation().y; - final scale = matrix.getMaxScaleOnAxis(); - final page = getYPage(columnKeys, yPos, scale); - currentPage.value = page; - // debugPrint("$page $yPos"); - }); - // transformationcontroller.addListener(() { - // final matrix = transformationcontroller.value; - // final scale = matrix.getMaxScaleOnAxis(); - // currentScale.value = scale; - // final y_dir = matrix.getTranslation().y; - // // print('Current scale: $scale y_dir: $y_dir'); - // }); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -84,8 +71,7 @@ class ComicController extends ReaderController { if (isRecover.value || callback == null) { return; } - columnKeys = - List.generate(watchData.value!.urls.length, (index) => GlobalKey()); + isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( @@ -105,51 +91,51 @@ class ComicController extends ReaderController { super.onInit(); } - _initSetting() async { - readType.value = readmode[setting] ?? MangaReadMode.standard; - readType.value = await DatabaseService.getMnagaReaderType( - super.detailUrl, readType.value); - } - - //get the postion of children border - List childSepHeight(List columnKeys) { - { - final List heights = []; - double seperator = 0; - for (int i = 0; i < columnKeys.length; i++) { - final RenderBox renderBox = - columnKeys[i].currentContext.findRenderObject(); - seperator -= renderBox.size.height; - heights.add(seperator); + onKey(RawKeyEvent event) { + // 按下 ctrl + isZoom.value = event.isControlPressed; + // 上下 + if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) { + if (readType.value == MangaReadMode.webTonn) { + return previousPage(); + } + } + if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) { + if (readType.value == MangaReadMode.webTonn) { + return nextPage(); } - return heights; } - } - int getYPage(List columnKeys, double yPos, double scale) { - double seperator = -1.0; - int val = 0; - for (int i = 0; i < columnKeys.length; i++) { - final RenderBox renderBox = - columnKeys[i].currentContext.findRenderObject(); + if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) { + if (readType.value == MangaReadMode.rightToLeft) { + return nextPage(); + } + previousPage(); + } - if (seperator * scale < yPos) { - val = i; - break; + if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) { + if (readType.value == MangaReadMode.rightToLeft) { + return previousPage(); } - seperator -= renderBox.size.height; + nextPage(); } - debugPrint("$yPos $val $seperator $scale"); - return val; + } + + _initSetting() async { + readType.value = readmode[setting] ?? MangaReadMode.standard; + readType.value = await DatabaseService.getMnagaReaderType( + super.detailUrl, + readType.value, + ); } _jumpPage(int page) async { if (readType.value == MangaReadMode.webTonn) { - await Future.delayed(const Duration(milliseconds: 500)); - if (columnKeys[0].currentWidget == null) return; - final renderHeight = childSepHeight(columnKeys); - transformationController.value = Matrix4.identity() - ..translate(0.0, renderHeight[page - 1]); // translate(x,y); + if (itemScrollController.isAttached) { + itemScrollController.jumpTo( + index: page, + ); + } return; } if (pageController.value.hasClients) { @@ -168,35 +154,12 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - if (currentPage.value == columnKeys.length - 1) return; - final offset = columnKeys[currentPage.value] - .currentContext - .findRenderObject() - .size - .height; - webtoonJumpWithOffset(offset); - } - } - - void webtoonJumpWithOffset(double yOffset) { - double endYPos = yPos - yOffset; - Tween(begin: yPos, end: endYPos) - .animate( - CurvedAnimation( - parent: animationController!.value, + scrollOffsetController.animateScroll( + duration: const Duration(milliseconds: 100), curve: Curves.ease, - ), - ) - .addListener(() { - transformationController.value = Matrix4.identity() - ..translate(0.0, endYPos); // translate(x,y); - }); - animationController!.value.addStatusListener((status) { - if (status == AnimationStatus.completed) { - animationController!.value.reset(); - } - }); - animationController!.value.forward(); + offset: 200.0, + ); + } } // 上一页 @@ -208,13 +171,11 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - if (currentPage.value == 0) return; - final offset = columnKeys[currentPage.value - 1] - .currentContext - .findRenderObject() - .size - .height; - webtoonJumpWithOffset(-offset); + scrollOffsetController.animateScroll( + duration: const Duration(milliseconds: 100), + curve: Curves.ease, + offset: -200.0, + ); } } @@ -228,9 +189,6 @@ class ComicController extends ReaderController { pages.toString(), ); } - pageController.value.dispose(); - animationController?.value.dispose(); - transformationController.dispose(); if (MiruStorage.getSetting(SettingKey.autoTracking) && anilistID != "") { AniListProvider.editList( status: AnilistMediaListStatus.current, diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 70fe7133..e45523b5 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:miru_app/models/index.dart'; @@ -22,26 +21,30 @@ class ComicReaderContent extends StatefulWidget { State createState() => _ComicReaderContentState(); } -class _ComicReaderContentState extends State - with SingleTickerProviderStateMixin { +class _ComicReaderContentState extends State { @override void initState() { - _c.animationController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 700), - ).obs; - super.initState(); } late final _c = Get.find(tag: widget.tag); - final zoomScale = 1.0.obs; - bool isZoomed = false; - // List? _columnKeys; - // TransformationController transformationController = - // TransformationController(); - final double minScaleValue = 1.0; + // 按下数量 + final List _pointer = []; + + _buildPlaceholder(BuildContext context) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return SizedBox( + width: width, + height: height, + child: const Center( + child: Center( + child: ProgressRing(), + ), + ), + ); + } _buildDisplay(Widget child) { return Stack( @@ -74,154 +77,116 @@ class _ComicReaderContentState extends State return RawKeyboardListener( focusNode: FocusNode(), autofocus: true, - onKey: (event) { - // 上下 - if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) { - if (_c.readType.value == MangaReadMode.webTonn) { - return _c.previousPage(); - } - } - if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) { - if (_c.readType.value == MangaReadMode.webTonn) { - return _c.nextPage(); - } - } - - if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) { - if (_c.readType.value == MangaReadMode.rightToLeft) { - return _c.nextPage(); - } - _c.previousPage(); - } - - if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) { - if (_c.readType.value == MangaReadMode.rightToLeft) { - return _c.previousPage(); - } - _c.nextPage(); - } - }, + onKey: _c.onKey, child: Container( color: backgroundColor, width: double.infinity, - child: LayoutBuilder(builder: ((context, constraints) { - final maxWidth = constraints.maxWidth; - return Obx(() { - if (_c.error.value.isNotEmpty) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(_c.error.value), - PlatformButton( - child: Text('common.retry'.i18n), - onPressed: () { - _c.getContent(); - }, - ) - ], - ); - } - - // 加载中 - if (_c.watchData.value == null) { - return const Center(child: ProgressRing()); - } + child: LayoutBuilder( + builder: ((context, constraints) { + final maxWidth = constraints.maxWidth; + return Obx(() { + if (_c.error.value.isNotEmpty) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_c.error.value), + PlatformButton( + child: Text('common.retry'.i18n), + onPressed: () { + _c.getContent(); + }, + ) + ], + ); + } - final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; - final images = _c.watchData.value!.urls; - final readerType = _c.readType.value; - final cuurentPage = _c.currentPage.value; + // 加载中 + if (_c.watchData.value == null) { + return const Center(child: ProgressRing()); + } - if (readerType == MangaReadMode.webTonn) { - final width = MediaQuery.of(context).size.width; - if (Platform.isAndroid) { - return InteractiveViewer.builder( - transformationController: _c.transformationController, - minScale: 1, - builder: (context, quad) => SizedBox( - width: width, - child: Column( - children: [ - for (int col = 0; col < images.length; col++) - CacheNetWorkImagePic( - images[col], - key: _c.columnKeys[col], - fit: BoxFit.cover, - headers: _c.watchData.value?.headers, - ) - ], - ))); + final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; + final images = _c.watchData.value!.urls; + final readerType = _c.readType.value; + final cuurentPage = _c.currentPage.value; + + if (readerType == MangaReadMode.webTonn) { + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return SizedBox( + width: width, + height: height, + child: Listener( + onPointerDown: (event) { + _pointer.add(event.pointer); + if (_pointer.length == 2) { + _c.isZoom.value = true; + } + }, + onPointerUp: (event) { + _pointer.remove(event.pointer); + if (_pointer.length == 1) { + _c.isZoom.value = false; + } + }, + child: InteractiveViewer( + scaleEnabled: _c.isZoom.value, + child: ScrollablePositionedList.builder( + physics: _c.isZoom.value + ? const NeverScrollableScrollPhysics() + : null, + padding: EdgeInsets.symmetric( + horizontal: viewPadding, + ), + initialScrollIndex: cuurentPage, + itemScrollController: _c.itemScrollController, + itemPositionsListener: _c.itemPositionsListener, + scrollOffsetController: _c.scrollOffsetController, + itemBuilder: (context, index) { + final url = images[index]; + return CacheNetWorkImagePic( + url, + fit: BoxFit.fitWidth, + placeholder: _buildPlaceholder(context), + headers: _c.watchData.value?.headers, + ); + }, + itemCount: images.length, + ), + ), + ), + ); } - return ScrollablePositionedList.builder( - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - initialScrollIndex: cuurentPage, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - itemBuilder: (context, index) { + + //common mode and left to right mode + return ExtendedImageGesturePageView.builder( + itemCount: images.length, + reverse: readerType == MangaReadMode.rightToLeft, + onPageChanged: (index) { + _c.currentPage.value = index; + }, + scrollDirection: Axis.horizontal, + controller: _c.pageController.value, + itemBuilder: (BuildContext context, int index) { final url = images[index]; - return CacheNetWorkImagePic( - url, - fit: BoxFit.fitWidth, - headers: _c.watchData.value?.headers, + return Container( + padding: EdgeInsets.symmetric( + horizontal: viewPadding, + ), + child: CacheNetWorkImagePic( + url, + mode: ExtendedImageMode.gesture, + key: ValueKey(url), + fit: BoxFit.contain, + placeholder: _buildPlaceholder(context), + headers: _c.watchData.value?.headers, + ), ); }, - itemCount: images.length, ); - // return InteractiveViewer.builder( - // scaleEnabled: false, - // transformationController: _c.transformationController, - // minScale: 1, - // onInteractionUpdate: (details) { - // debugPrint( - // "${details.scale} ${_c.yPos} ${details.pointerCount}"); - - // // _c.transformationController.value = Matrix4.identity() - // // ..translate(0, _c.yPos - 100); - // }, - // builder: (context, quad) => SizedBox( - // width: width, - // child: Column( - // children: [ - // for (int col = 0; col < images.length; col++) - // CacheNetWorkImagePic( - // images[col], - // key: _c.columnKeys[col], - // fit: BoxFit.cover, - // headers: _c.watchData.value?.headers, - // ) - // ], - // ))); - } - //common mode and left to right mode - return ExtendedImageGesturePageView.builder( - itemCount: images.length, - reverse: readerType == MangaReadMode.rightToLeft, - onPageChanged: (index) { - _c.currentPage.value = index; - }, - scrollDirection: Axis.horizontal, - controller: _c.pageController.value, - itemBuilder: (BuildContext context, int index) { - final url = images[index]; - return Container( - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - child: ExtendedImage.network( - url, - mode: ExtendedImageMode.gesture, - key: ValueKey(url), - fit: BoxFit.contain, - headers: _c.watchData.value?.headers, - ), - ); - }, - ); - }); - })), + }); + }), + ), ), ); } diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 3a22c020..b13ad99e 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -23,6 +23,7 @@ class CacheNetWorkImagePic extends StatelessWidget { this.headers, this.placeholder, this.canFullScreen = false, + this.mode = ExtendedImageMode.none, }); final String url; final BoxFit fit; @@ -32,6 +33,7 @@ class CacheNetWorkImagePic extends StatelessWidget { final Map? headers; final bool canFullScreen; final Widget? placeholder; + final ExtendedImageMode mode; _errorBuild() { if (fallback != null) { @@ -49,6 +51,7 @@ class CacheNetWorkImagePic extends StatelessWidget { width: width, height: height, cache: true, + mode: mode, loadStateChanged: (state) { switch (state.extendedImageLoadState) { case LoadState.loading: