double columm LTR & RTL

This commit is contained in:
kodjomoustapha 2023-10-28 22:19:19 +01:00
parent 864d00417a
commit 7a966f20ff
4 changed files with 280 additions and 185 deletions

View file

@ -4,6 +4,14 @@
<h1 align="center"> Mangayomi </h1>
<p align="center">
[![CI](https://github.com/kodjodevf/mangayomi/actions/workflows/release.yml/badge.svg)](https://github.com/kodjodevf/mangayomi/actions/workflows/release.yml)
[![latest release](https://img.shields.io/github/release/kodjodevf/mangayomi.svg?maxAge=3600&label=download)](https://github.com/kodjodevf/mangayomi/releases)
[![Discord](https://img.shields.io/discord/1157628512077893666.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/Ae2S6dUhAY)
</p>
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

View file

@ -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);
}
}

View file

@ -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,

View file

@ -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<double>? _doubleClickAnimation;
late DoubleClickAnimationListener _doubleClickAnimationListener;
List<double> doubleTapScales = <double>[1.0, 2.0];
final StreamController<double> _rebuildDetail =
StreamController<double>.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<UChapDataPreload> _cropBorderCheckList = [];
final List<int> _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<T> extends StatelessWidget {