From 7a966f20ff6a209d01aa333327422a4ddf598187 Mon Sep 17 00:00:00 2001
From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com>
Date: Sat, 28 Oct 2023 22:19:19 +0100
Subject: [PATCH] double columm LTR & RTL
---
README.md | 8 +
.../manga/reader/image_view_center.dart | 50 ++-
.../manga/reader/image_view_vertical.dart | 23 +-
lib/modules/manga/reader/reader_view.dart | 384 +++++++++++-------
4 files changed, 280 insertions(+), 185 deletions(-)
diff --git a/README.md b/README.md
index 34b9c8d3..a8807f24 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,14 @@
Mangayomi
+
+
+ [](https://github.com/kodjodevf/mangayomi/actions/workflows/release.yml)
+ [](https://github.com/kodjodevf/mangayomi/releases)
+ [](https://discord.gg/Ae2S6dUhAY)
+
+
+
Mangayomi is free an open source manga reader and anime streaming cross-plateform app inspired by Tachiyomi made with Flutter. It allows users to read manga and watch anime from a variety of sources.
## Features
diff --git a/lib/modules/manga/reader/image_view_center.dart b/lib/modules/manga/reader/image_view_center.dart
index 208a9346..20180225 100644
--- a/lib/modules/manga/reader/image_view_center.dart
+++ b/lib/modules/manga/reader/image_view_center.dart
@@ -3,7 +3,6 @@ import 'dart:typed_data';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
@@ -14,50 +13,46 @@ class ImageViewCenter extends ConsumerWidget {
final UChapDataPreload datas;
final bool cropBorders;
final Widget? Function(ExtendedImageState state) loadStateChanged;
- const ImageViewCenter(
- {super.key,
- required this.datas,
- required this.cropBorders,
- required this.loadStateChanged});
+ final Function(ExtendedImageGestureState state)? onDoubleTap;
+ final GestureConfig Function(ExtendedImageState state)?
+ initGestureConfigHandler;
+ const ImageViewCenter({
+ super.key,
+ required this.datas,
+ required this.cropBorders,
+ required this.loadStateChanged,
+ this.onDoubleTap,
+ this.initGestureConfigHandler,
+ });
@override
Widget build(BuildContext context, WidgetRef ref) {
- final image =
- ref.watch(cropBordersProvider(datas: datas, cropBorder: cropBorders));
- final defaultWidget = _imageView(datas.isLocale!, datas.archiveImage, ref);
- return image.when(
- data: (data) {
- // if (data == null && !datas.isLocale!) {
- // ref.invalidate(cropBordersProvider(datas: datas, cropBorder: true));
- // }
- return _imageView(data != null ? true : datas.isLocale!,
- data ?? datas.archiveImage, ref);
- },
- error: (_, __) => defaultWidget,
- loading: () => defaultWidget,
- );
+ final cropImageExist = cropBorders && datas.cropImage != null;
+
+ return _imageView(cropImageExist ? true : datas.isLocale!,
+ cropImageExist ? datas.cropImage : datas.archiveImage, ref);
}
Widget _imageView(bool isLocale, Uint8List? archiveImage, WidgetRef ref) {
final scaleType = ref.watch(scaleTypeStateProvider);
return isLocale
? archiveImage != null
- ? ExtendedImage.memory(
- archiveImage,
+ ? ExtendedImage.memory(archiveImage,
fit: getBoxFit(scaleType),
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
loadStateChanged: loadStateChanged,
- )
+ initGestureConfigHandler: initGestureConfigHandler,
+ onDoubleTap: onDoubleTap)
: ExtendedImage.file(
File("${datas.path!.path}" "${padIndex(datas.index! + 1)}.jpg"),
fit: getBoxFit(scaleType),
clearMemoryCacheWhenDispose: true,
enableMemoryCache: false,
loadStateChanged: loadStateChanged,
- )
- : ExtendedImage.network(
- datas.url!.trim().trimLeft().trimRight(),
+ initGestureConfigHandler: initGestureConfigHandler,
+ onDoubleTap: onDoubleTap)
+ : ExtendedImage.network(datas.url!.trim().trimLeft().trimRight(),
fit: getBoxFit(scaleType),
headers: ref.watch(headersProvider(
source: datas.chapter!.manga.value!.source!,
@@ -67,6 +62,7 @@ class ImageViewCenter extends ConsumerWidget {
cacheMaxAge: const Duration(days: 7),
handleLoadingProgress: true,
loadStateChanged: loadStateChanged,
- );
+ initGestureConfigHandler: initGestureConfigHandler,
+ onDoubleTap: onDoubleTap);
}
}
diff --git a/lib/modules/manga/reader/image_view_vertical.dart b/lib/modules/manga/reader/image_view_vertical.dart
index 2f821ec7..f3f38452 100644
--- a/lib/modules/manga/reader/image_view_vertical.dart
+++ b/lib/modules/manga/reader/image_view_vertical.dart
@@ -3,7 +3,6 @@ import 'dart:typed_data';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/manga/reader/reader_view.dart';
import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart';
@@ -28,24 +27,10 @@ class ImageViewVertical extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- final image =
- ref.watch(cropBordersProvider(datas: datas, cropBorder: cropBorders));
- final defaultWidget =
- _imageView(datas.isLocale!, datas.archiveImage, context, ref);
- return Container(
- color: Colors.black,
- child: image.when(
- data: (data) {
- // if (data == null && !datas.isLocale!) {
- // ref.invalidate(
- // cropBordersProvider(datas: datas, cropBorder: true));
- // }
- return _imageView(data != null ? true : datas.isLocale!,
- data ?? datas.archiveImage, context, ref);
- },
- error: (_, __) => defaultWidget,
- loading: () => defaultWidget,
- ));
+ final cropImageExist = cropBorders && datas.cropImage != null;
+
+ return _imageView(cropImageExist ? true : datas.isLocale!,
+ cropImageExist ? datas.cropImage : datas.archiveImage, context, ref);
}
Widget _imageView(bool isLocale, Uint8List? archiveImage,
diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart
index 49f9baec..fc7b1d54 100644
--- a/lib/modules/manga/reader/reader_view.dart
+++ b/lib/modules/manga/reader/reader_view.dart
@@ -29,6 +29,7 @@ import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provi
import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart';
import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart';
import 'package:mangayomi/modules/widgets/progress_center.dart';
+import 'package:mangayomi/utils/reg_exp_matcher.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:rinf/rinf.dart';
@@ -166,6 +167,9 @@ class _MangaChapterPageGalleryState
_readerController.setPageIndex(
_geCurrentIndex(_uChapDataPreload[_currentIndex!].index!));
Rinf.ensureFinalized();
+ _rebuildDetail.close();
+ _doubleClickAnimationController.dispose();
+ clearGestureDetailsCache();
super.dispose();
}
@@ -185,8 +189,18 @@ class _MangaChapterPageGalleryState
final ItemPositionsListener _itemPositionsListener =
ItemPositionsListener.create();
+ late AnimationController _doubleClickAnimationController;
+
+ Animation? _doubleClickAnimation;
+ late DoubleClickAnimationListener _doubleClickAnimationListener;
+ List doubleTapScales = [1.0, 2.0];
+ final StreamController _rebuildDetail =
+ StreamController.broadcast();
+ final double _imageDetailY = 0;
@override
void initState() {
+ _doubleClickAnimationController = AnimationController(
+ duration: _doubleTapAnimationDuration(), vsync: this);
_scaleAnimationController = AnimationController(
duration: _doubleTapAnimationDuration(), vsync: this);
_animation = Tween(begin: 1.0, end: 2.0).animate(
@@ -198,7 +212,7 @@ class _MangaChapterPageGalleryState
super.initState();
}
- double _horizontalScaleValue = 1.0;
+ final double _horizontalScaleValue = 1.0;
late int pagePreloadAmount = ref.watch(pagePreloadAmountStateProvider);
late bool _isBookmarked = _readerController.getChapterBookmarked();
@@ -238,7 +252,7 @@ class _MangaChapterPageGalleryState
Color _backgroundColor(BuildContext context) =>
Theme.of(context).scaffoldBackgroundColor.withOpacity(0.9);
- final List _cropBorderCheckList = [];
+ final List _cropBorderCheckList = [];
@override
Widget build(BuildContext context) {
@@ -318,20 +332,22 @@ class _MangaChapterPageGalleryState
itemCount: 1,
builder: (_, __) =>
PhotoViewGalleryPageOptions.customChild(
- controller: _photoViewController,
- scaleStateController:
- _photoViewScaleStateController,
- basePosition: _scalePosition,
- onScaleEnd: _onScaleEnd,
- child: pageMode == PageMode.doubleColumm
- ? ScrollablePositionedList.separated(
+ controller: _photoViewController,
+ scaleStateController:
+ _photoViewScaleStateController,
+ basePosition: _scalePosition,
+ onScaleEnd: _onScaleEnd,
+ child: ScrollablePositionedList.separated(
minCacheExtent: pagePreloadAmount *
mediaHeight(context, 1),
initialScrollIndex:
_readerController.getPageIndex(),
- itemCount: (_uChapDataPreload.length / 2)
- .ceil() +
- 1,
+ itemCount:
+ pageMode == PageMode.doubleColumm
+ ? (_uChapDataPreload.length / 2)
+ .ceil() +
+ 1
+ : _uChapDataPreload.length,
physics: const ClampingScrollPhysics(),
itemScrollController:
_itemScrollController,
@@ -348,28 +364,42 @@ class _MangaChapterPageGalleryState
details.globalPosition);
},
onDoubleTap: () {},
- child: DoubleColummVerticalView(
- datas: index == 0
- ? [_uChapDataPreload[0], null]
- : [
- index1 <
- _uChapDataPreload
- .length
- ? _uChapDataPreload[
- index1]
- : null,
- index2 <
- _uChapDataPreload
- .length
- ? _uChapDataPreload[
- index2]
- : null,
- ],
- scale: (a) {},
- backgroundColor: backgroundColor,
- isFailedToLoadImage: (val) {},
- cropBorders: cropBorders,
- ),
+ child: pageMode ==
+ PageMode.doubleColumm
+ ? DoubleColummVerticalView(
+ datas: index == 0
+ ? [
+ _uChapDataPreload[0],
+ null
+ ]
+ : [
+ index1 <
+ _uChapDataPreload
+ .length
+ ? _uChapDataPreload[
+ index1]
+ : null,
+ index2 <
+ _uChapDataPreload
+ .length
+ ? _uChapDataPreload[
+ index2]
+ : null,
+ ],
+ scale: (a) {},
+ backgroundColor:
+ backgroundColor,
+ isFailedToLoadImage: (val) {},
+ cropBorders: cropBorders,
+ )
+ : ImageViewVertical(
+ datas:
+ _uChapDataPreload[index],
+ failedToLoadImage: (value) {
+ // _failedToLoadImage.value = value;
+ },
+ cropBorders: cropBorders,
+ ),
);
},
separatorBuilder: (_, __) => Divider(
@@ -380,53 +410,13 @@ class _MangaChapterPageGalleryState
ReaderMode.webtoon
? 0
: 6),
- )
- : ScrollablePositionedList.separated(
- minCacheExtent: pagePreloadAmount *
- mediaHeight(context, 1),
- initialScrollIndex:
- _readerController.getPageIndex(),
- itemCount: _uChapDataPreload.length,
- physics: const ClampingScrollPhysics(),
- itemScrollController:
- _itemScrollController,
- itemPositionsListener:
- _itemPositionsListener,
- itemBuilder: (context, index) {
- return GestureDetector(
- behavior: HitTestBehavior.translucent,
- onDoubleTapDown:
- (TapDownDetails details) {
- _toggleScale(
- details.globalPosition);
- },
- onDoubleTap: () {},
- child: ImageViewVertical(
- datas: _uChapDataPreload[index],
- failedToLoadImage: (value) {
- // _failedToLoadImage.value = value;
- },
- cropBorders: cropBorders,
- ),
- );
- },
- separatorBuilder: (_, __) => Divider(
- color: getBackgroundColor(
- backgroundColor),
- height:
- ref.watch(_currentReaderMode) ==
- ReaderMode.webtoon
- ? 0
- : 6),
- ),
- ),
+ )),
)
- : pageMode == PageMode.doubleColumm
- ? Material(
- color: getBackgroundColor(backgroundColor),
- shadowColor:
- getBackgroundColor(backgroundColor),
- child: ExtendedImageGesturePageView.builder(
+ : Material(
+ color: getBackgroundColor(backgroundColor),
+ shadowColor: getBackgroundColor(backgroundColor),
+ child: pageMode == PageMode.doubleColumm
+ ? ExtendedImageGesturePageView.builder(
controller: _extendedController,
scrollDirection: _scrollDirection,
reverse: _isReverseHorizontal,
@@ -437,20 +427,20 @@ class _MangaChapterPageGalleryState
itemBuilder: (context, index) {
int index1 = index * 2 - 1;
int index2 = index1 + 1;
-
+ final pageList = (index == 0
+ ? [_uChapDataPreload[0], null]
+ : [
+ index1 < _uChapDataPreload.length
+ ? _uChapDataPreload[index1]
+ : null,
+ index2 < _uChapDataPreload.length
+ ? _uChapDataPreload[index2]
+ : null,
+ ]);
return DoubleColummView(
- datas: index == 0
- ? [_uChapDataPreload[0], null]
- : [
- index1 <
- _uChapDataPreload.length
- ? _uChapDataPreload[index1]
- : null,
- index2 <
- _uChapDataPreload.length
- ? _uChapDataPreload[index2]
- : null,
- ],
+ datas: _isReverseHorizontal
+ ? pageList.reversed.toList()
+ : pageList,
scale: (a) {},
backgroundColor: backgroundColor,
isFailedToLoadImage: (val) {
@@ -464,23 +454,23 @@ class _MangaChapterPageGalleryState
itemCount:
(_uChapDataPreload.length / 2).ceil() +
1,
- onPageChanged: _onPageChanged))
- : Material(
- color: getBackgroundColor(backgroundColor),
- shadowColor:
- getBackgroundColor(backgroundColor),
- child: ExtendedImageGesturePageView.builder(
+ onPageChanged: _onPageChanged)
+ : ExtendedImageGesturePageView.builder(
controller: _extendedController,
scrollDirection: _scrollDirection,
reverse: _isReverseHorizontal,
physics: const ClampingScrollPhysics(),
- canScrollPage: (_) {
- return _horizontalScaleValue == 1.0;
+ canScrollPage: (gestureDetails) {
+ return gestureDetails != null
+ ? !(gestureDetails.totalScale! > 1.0)
+ : true;
},
- itemBuilder: (context, index) {
+ itemBuilder:
+ (BuildContext context, int index) {
return ImageViewCenter(
datas: _uChapDataPreload[index],
- loadStateChanged: (state) {
+ loadStateChanged:
+ (ExtendedImageState state) {
if (state.extendedImageLoadState ==
LoadState.loading) {
final ImageChunkEvent?
@@ -509,11 +499,28 @@ class _MangaChapterPageGalleryState
true) {
_failedToLoadImage.value = false;
}
- return ViewPage(
- imageProvider:
- state.imageProvider,
- scale: (scale) =>
- _horizontalScaleValue = scale,
+ return StreamBuilder(
+ builder: (context, data) {
+ return ExtendedImageGesture(
+ state,
+ canScaleImage: (_) =>
+ _imageDetailY == 0,
+ imageBuilder: (image) {
+ return Stack(
+ children: [
+ Positioned.fill(
+ top: _imageDetailY,
+ bottom:
+ -_imageDetailY,
+ child: image,
+ ),
+ ],
+ );
+ },
+ );
+ },
+ initialData: _imageDetailY,
+ stream: _rebuildDetail.stream,
);
}
if (state.extendedImageLoadState ==
@@ -579,7 +586,64 @@ class _MangaChapterPageGalleryState
],
));
}
- return null;
+ return Container();
+ },
+ initGestureConfigHandler: (state) {
+ return GestureConfig(
+ inertialSpeed: 200,
+ inPageView: true,
+ maxScale: 8,
+ animationMaxScale: 8,
+ cacheGesture: true,
+ hitTestBehavior:
+ HitTestBehavior.translucent,
+ );
+ },
+ onDoubleTap: (state) {
+ final Offset? pointerDownPosition =
+ state.pointerDownPosition;
+ final double? begin =
+ state.gestureDetails!.totalScale;
+ double end;
+
+ //remove old
+ _doubleClickAnimation?.removeListener(
+ _doubleClickAnimationListener);
+
+ //stop pre
+ _doubleClickAnimationController
+ .stop();
+
+ //reset to use
+ _doubleClickAnimationController
+ .reset();
+
+ if (begin == doubleTapScales[0]) {
+ end = doubleTapScales[1];
+ } else {
+ end = doubleTapScales[0];
+ }
+
+ _doubleClickAnimationListener = () {
+ state.handleDoubleTap(
+ scale: _doubleClickAnimation!
+ .value,
+ doubleTapPosition:
+ pointerDownPosition);
+ };
+
+ _doubleClickAnimation = Tween(
+ begin: begin, end: end)
+ .animate(CurvedAnimation(
+ curve: Curves.ease,
+ parent:
+ _doubleClickAnimationController));
+
+ _doubleClickAnimation!.addListener(
+ _doubleClickAnimationListener);
+
+ _doubleClickAnimationController
+ .forward();
},
cropBorders: cropBorders,
);
@@ -598,7 +662,7 @@ class _MangaChapterPageGalleryState
);
}
- void _precacheNetworkImages(int index) {
+ void _precacheImages(int index) {
try {
if (0 <= index && index < _uChapDataPreload.length) {
if (!_uChapDataPreload[index].isLocale!) {
@@ -612,6 +676,26 @@ class _MangaChapterPageGalleryState
lang: chapter.manga.value!.lang!)),
),
context);
+ } else {
+ final archiveImage = (_uChapDataPreload[index].archiveImage);
+
+ if (archiveImage != null) {
+ precacheImage(
+ ExtendedMemoryImageProvider(
+ (_uChapDataPreload[index].archiveImage)!),
+ context);
+ } else {
+ precacheImage(
+ ExtendedFileImageProvider(File(
+ "${_uChapDataPreload[index].path!.path}${padIndex(_uChapDataPreload[index].index! + 1)}.jpg")),
+ context);
+ }
+ }
+ if (_uChapDataPreload[index].cropImage != null) {
+ precacheImage(
+ ExtendedMemoryImageProvider(
+ (_uChapDataPreload[index].cropImage)!),
+ context);
}
}
} catch (_) {}
@@ -630,7 +714,11 @@ class _MangaChapterPageGalleryState
void _readProgressListener() {
_currentIndex = _itemPositionsListener.itemPositions.value.first.index;
- if (_currentIndex! >= 0 && _currentIndex! < _uChapDataPreload.length) {
+
+ int pagesLength = ref.watch(_pageMode) == PageMode.doubleColumm
+ ? (_uChapDataPreload.length / 2).ceil() + 1
+ : _uChapDataPreload.length;
+ if (_currentIndex! >= 0 && _currentIndex! < pagesLength) {
if (_readerController.chapter.id !=
_uChapDataPreload[_currentIndex!].chapter!.id) {
if (mounted) {
@@ -649,7 +737,7 @@ class _MangaChapterPageGalleryState
);
}
if (_itemPositionsListener.itemPositions.value.last.index ==
- _uChapDataPreload.length - 1) {
+ pagesLength - 1) {
try {
bool hasNextChapter = _readerController.getChapterIndex() != 0;
final chapter =
@@ -730,16 +818,17 @@ class _MangaChapterPageGalleryState
);
if (!(_isVerticalContinous())) {
for (var i = 1; i < pagePreloadAmount + 1; i++) {
- _precacheNetworkImages(_currentIndex! + i);
- _precacheNetworkImages(_currentIndex! - i);
+ _precacheImages(_currentIndex! + i);
+ _precacheImages(_currentIndex! - i);
}
}
}
void _onPageChanged(int index) {
+ _processCropBordersByIndex(index);
for (var i = 1; i < pagePreloadAmount + 1; i++) {
- _precacheNetworkImages(index + i);
- _precacheNetworkImages(index - i);
+ _precacheImages(index + i);
+ _precacheImages(index - i);
}
if (_readerController.chapter.id != _uChapDataPreload[index].chapter!.id) {
@@ -934,19 +1023,42 @@ class _MangaChapterPageGalleryState
}
void _processCropBorders() async {
- for (var datas in _uChapDataPreload) {
- if (!_cropBorderCheckList.contains(datas)) {
- _cropBorderCheckList.add(datas);
- ref.watch(cropBordersProvider(datas: datas, cropBorder: true));
- ref.watch(cropBordersProvider(datas: datas, cropBorder: false));
- } else {
- // if (!datas.isLocale!) {
- // final res = await ref.watch(
- // cropBordersProvider(datas: datas, cropBorder: true).future);
- // if (res == null) {
- // ref.invalidate(cropBordersProvider(datas: datas, cropBorder: true));
- // }
- // }
+ for (var i = 0; i < _uChapDataPreload.length; i++) {
+ if (!_cropBorderCheckList.contains(i)) {
+ _cropBorderCheckList.add(i);
+ ref
+ .watch(cropBordersProvider(
+ datas: _uChapDataPreload[i], cropBorder: true)
+ .future)
+ .then((value) {
+ _uChapDataPreload[i] = _uChapDataPreload[i]..cropImage = value;
+ });
+ }
+ }
+ }
+
+ void _processCropBordersByIndex(int index) async {
+ if (!_cropBorderCheckList.contains(index)) {
+ _cropBorderCheckList.add(index);
+ ref
+ .watch(cropBordersProvider(
+ datas: _uChapDataPreload[index], cropBorder: true)
+ .future)
+ .then((value) {
+ _uChapDataPreload[index] = _uChapDataPreload[index]
+ ..cropImage = value;
+ });
+ } else {
+ if (_uChapDataPreload[index].isLocale! &&
+ _uChapDataPreload[index].cropImage == null) {
+ ref
+ .watch(cropBordersProvider(
+ datas: _uChapDataPreload[index], cropBorder: true)
+ .future)
+ .then((value) {
+ _uChapDataPreload[index] = _uChapDataPreload[index]
+ ..cropImage = value;
+ });
}
}
}
@@ -1781,16 +1893,10 @@ class UChapDataPreload {
int? index;
GetChapterUrlModel? chapterUrlModel;
int? pageIndex;
- UChapDataPreload(
- this.chapter,
- this.path,
- this.url,
- this.isLocale,
- this.archiveImage,
- this.index,
- this.chapterUrlModel,
- this.pageIndex,
- );
+ Uint8List? cropImage;
+ UChapDataPreload(this.chapter, this.path, this.url, this.isLocale,
+ this.archiveImage, this.index, this.chapterUrlModel, this.pageIndex,
+ {this.cropImage});
}
class CustomPopupMenuButton extends StatelessWidget {