import 'dart:async'; import 'dart:math'; import 'dart:io'; import 'package:extended_image/extended_image.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/page.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/modules/anime/widgets/desktop.dart'; import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart'; import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart'; import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart'; import 'package:mangayomi/modules/manga/reader/double_columm_view_center.dart'; import 'package:mangayomi/modules/manga/reader/providers/color_filter_provider.dart'; import 'package:mangayomi/modules/manga/reader/widgets/color_filter_widget.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/providers/storage_provider.dart'; import 'package:mangayomi/utils/utils.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; import 'package:mangayomi/services/get_chapter_pages.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/utils/extensions/others.dart'; import 'package:mangayomi/utils/global_style.dart'; import 'package:mangayomi/modules/manga/reader/image_view_paged.dart'; import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart'; import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; 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:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:share_plus/share_plus.dart'; import 'package:window_manager/window_manager.dart'; import 'package:path/path.dart' as p; typedef DoubleClickAnimationListener = void Function(); class MangaReaderView extends ConsumerWidget { final Chapter chapter; const MangaReaderView({ super.key, required this.chapter, }); @override Widget build(BuildContext context, WidgetRef ref) { final chapterData = ref.watch(getChapterPagesProvider( chapter: chapter, )); return chapterData.when( data: (data) { if (data.pageUrls.isEmpty && (chapter.manga.value!.isLocalArchive ?? false) == false) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: const Text(''), leading: BackButton( onPressed: () { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Navigator.pop(context); }, ), ), body: const Center( child: Text("Error"), ), ); } return MangaChapterPageGallery(chapter: chapter, chapterUrlModel: data); }, error: (error, stackTrace) => Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: const Text(''), leading: BackButton( onPressed: () { Navigator.pop(context); }, ), ), body: Center( child: Text(error.toString()), ), ), loading: () { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: const Text(''), leading: BackButton( onPressed: () { Navigator.pop(context); }, ), ), body: const ProgressCenter(), ); }, ); } } class MangaChapterPageGallery extends ConsumerStatefulWidget { const MangaChapterPageGallery({ super.key, required this.chapter, required this.chapterUrlModel, }); final GetChapterPagesModel chapterUrlModel; final Chapter chapter; @override ConsumerState createState() { return _MangaChapterPageGalleryState(); } } class _MangaChapterPageGalleryState extends ConsumerState with TickerProviderStateMixin { late AnimationController _scaleAnimationController; late Animation _animation; late ReaderController _readerController = ref.read(readerControllerProvider(chapter: chapter).notifier); bool isDesktop = Platform.isMacOS || Platform.isLinux || Platform.isWindows; @override void dispose() { _readerController.setMangaHistoryUpdate(); _readerController.checkAndSyncProgress(); _readerController.setPageIndex(_geCurrentIndex(_uChapDataPreload[_currentIndex!].index!), true); _rebuildDetail.close(); _doubleClickAnimationController.dispose(); _autoScroll.value = false; clearGestureDetailsCache(); if (isDesktop) { setFullScreen(value: false); } else { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); } super.dispose(); } late final _autoScroll = ValueNotifier(_readerController.autoScrollValues().$1); late final _autoScrollPage = ValueNotifier(_autoScroll.value); late GetChapterPagesModel _chapterUrlModel = widget.chapterUrlModel; late Chapter chapter = widget.chapter; List _uChapDataPreload = []; final _failedToLoadImage = ValueNotifier(false); late int? _currentIndex = _readerController.getPageIndex(); late final ItemScrollController _itemScrollController = ItemScrollController(); final ScrollOffsetController _pageOffsetController = ScrollOffsetController(); final ItemPositionsListener _itemPositionsListener = ItemPositionsListener.create(); late AnimationController _doubleClickAnimationController; Animation? _doubleClickAnimation; late DoubleClickAnimationListener _doubleClickAnimationListener; List doubleTapScales = [1.0, 2.0]; final StreamController _rebuildDetail = StreamController.broadcast(); @override void initState() { _doubleClickAnimationController = AnimationController(duration: _doubleTapAnimationDuration(), vsync: this); _scaleAnimationController = AnimationController(duration: _doubleTapAnimationDuration(), vsync: this); _animation = Tween(begin: 1.0, end: 2.0).animate(CurvedAnimation(curve: Curves.ease, parent: _scaleAnimationController)); _animation.addListener(() => _photoViewController.scale = _animation.value); _itemPositionsListener.itemPositions.addListener(_readProgressListener); _initCurrentIndex(); super.initState(); } final double _horizontalScaleValue = 1.0; late int pagePreloadAmount = ref.watch(pagePreloadAmountStateProvider); late bool _isBookmarked = _readerController.getChapterBookmarked(); final _currentReaderMode = StateProvider((ref) => null); PageMode? _pageMode; bool _isView = false; Alignment _scalePosition = Alignment.center; final PhotoViewController _photoViewController = PhotoViewController(); final PhotoViewScaleStateController _photoViewScaleStateController = PhotoViewScaleStateController(); final List _cropBorderCheckList = []; void _onScaleEnd(BuildContext context, ScaleEndDetails details, PhotoViewControllerValue controllerValue) { if (controllerValue.scale! < 1) { _photoViewScaleStateController.reset(); } } late final _extendedController = ExtendedPageController(initialPage: _currentIndex!); double get pixelRatio => View.of(context).devicePixelRatio; Size get size => View.of(context).physicalSize / pixelRatio; Alignment _computeAlignmentByTapOffset(Offset offset) { return Alignment( (offset.dx - size.width / 2) / (size.width / 2), (offset.dy - size.height / 2) / (size.height / 2)); } Axis _scrollDirection = Axis.vertical; bool _isReverseHorizontal = false; late final _showPagesNumber = StateProvider((ref) => _readerController.getShowPageNumber()); Color _backgroundColor(BuildContext context) => Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0.9); void _setFullScreen({bool? value}) async { if (isDesktop) { value = await windowManager.isFullScreen(); setFullScreen(value: !value); } ref.read(fullScreenReaderStateProvider.notifier).set(!value!); } void _onLongPressImageDialog(UChapDataPreload datas, BuildContext context) async { Widget button(String label, IconData icon, Function() onPressed) => Expanded( child: Padding( padding: const EdgeInsets.all(15), child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.transparent, elevation: 0, shadowColor: Colors.transparent), onPressed: onPressed, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( padding: const EdgeInsets.all(4), child: Icon(icon), ), Text(label) ], )), ), ); final imageBytes = await datas.getImageBytes; if (imageBytes != null && context.mounted) { final name = "${widget.chapter.manga.value!.name} ${widget.chapter.name} - ${datas.pageIndex}" .replaceAll(RegExp(r'[^a-zA-Z0-9 .()\-\s]'), '_'); showModalBottomSheet( context: context, constraints: BoxConstraints( maxWidth: context.width(1), ), builder: (context) { return ListView( shrinkWrap: true, children: [ Container( decoration: BoxDecoration( borderRadius: const BorderRadius.only(topLeft: Radius.circular(20), topRight: Radius.circular(20)), color: context.themeData.scaffoldBackgroundColor), child: Column( children: [ Padding( padding: const EdgeInsets.all(8.0), child: Container( height: 7, width: 35, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: context.secondaryColor.withValues(alpha: 0.4)), ), ), Row( children: [ button(context.l10n.set_as_cover, Icons.image_outlined, () async { final res = await showDialog( context: context, builder: (context) { return AlertDialog( content: Text(context.l10n.use_this_as_cover_art), actions: [ Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { Navigator.pop(context); }, child: Text(context.l10n.cancel)), const SizedBox( width: 15, ), TextButton( onPressed: () { final manga = widget.chapter.manga.value!; isar.writeTxnSync(() { isar.mangas.putSync(manga..customCoverImage = imageBytes); }); if (mounted) { Navigator.pop(context, "ok"); } }, child: Text(context.l10n.ok)), ], ) ], ); }); if (res != null && res == "ok" && context.mounted) { Navigator.pop(context); botToast(context.l10n.cover_updated, second: 3); } }), button(context.l10n.share, Icons.share_outlined, () async { await Share.shareXFiles([XFile.fromData(imageBytes, name: name, mimeType: 'image/png')]); }), button(context.l10n.save, Icons.save_outlined, () async { final dir = await StorageProvider().getGalleryDirectory(); final file = File(p.join(dir!.path, "$name.png")); file.writeAsBytesSync(imageBytes); if (context.mounted) { botToast(context.l10n.picture_saved, second: 3); } }), ], ), ], ), ), ], ); }, ); } } @override Widget build(BuildContext context) { final backgroundColor = ref.watch(backgroundColorStateProvider); final fullScreenReader = ref.watch(fullScreenReaderStateProvider); final cropBorders = ref.watch(cropBordersStateProvider); final bool isHorizontalContinuaous = ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous; if (cropBorders) { _processCropBorders(); } final usePageTapZones = ref.watch(usePageTapZonesStateProvider); final l10n = l10nLocalizations(context)!; return KeyboardListener( autofocus: true, focusNode: FocusNode(), onKeyEvent: (event) { bool isLogicalKeyPressed(LogicalKeyboardKey key) => HardwareKeyboard.instance.isLogicalKeyPressed(key); bool hasNextChapter = _readerController.getChapterIndex().$1 != 0; bool hasPrevChapter = _readerController.getChapterIndex().$1 + 1 != _readerController.getChaptersLength(_readerController.getChapterIndex().$2); final action = switch (event.logicalKey) { LogicalKeyboardKey.f11 => (!isLogicalKeyPressed(LogicalKeyboardKey.f11)) ? _setFullScreen() : null, LogicalKeyboardKey.escape => (!isLogicalKeyPressed(LogicalKeyboardKey.escape)) ? _goBack(context) : null, LogicalKeyboardKey.backspace => (!isLogicalKeyPressed(LogicalKeyboardKey.backspace)) ? _goBack(context) : null, LogicalKeyboardKey.arrowUp => (!isLogicalKeyPressed(LogicalKeyboardKey.arrowUp)) ? _onBtnTapped(_currentIndex! - 1, true) : null, LogicalKeyboardKey.arrowLeft => (!isLogicalKeyPressed(LogicalKeyboardKey.arrowLeft)) ? _isReverseHorizontal ? _onBtnTapped(_currentIndex! + 1, false) : _onBtnTapped(_currentIndex! - 1, true) : null, LogicalKeyboardKey.arrowRight => (!isLogicalKeyPressed(LogicalKeyboardKey.arrowRight)) ? _isReverseHorizontal ? _onBtnTapped(_currentIndex! - 1, true) : _onBtnTapped(_currentIndex! + 1, false) : null, LogicalKeyboardKey.arrowDown => (!isLogicalKeyPressed(LogicalKeyboardKey.arrowDown)) ? _onBtnTapped(_currentIndex! + 1, true) : null, LogicalKeyboardKey.keyN || LogicalKeyboardKey.pageDown => ((!isLogicalKeyPressed(LogicalKeyboardKey.keyN) || !isLogicalKeyPressed(LogicalKeyboardKey.pageDown))) ? switch (hasNextChapter) { true => pushReplacementMangaReaderView( context: context, chapter: _readerController.getNextChapter(), ), _ => null } : null, LogicalKeyboardKey.keyP || LogicalKeyboardKey.pageUp => ((!isLogicalKeyPressed(LogicalKeyboardKey.keyP) || !isLogicalKeyPressed(LogicalKeyboardKey.pageUp))) ? switch (hasPrevChapter) { true => pushReplacementMangaReaderView(context: context, chapter: _readerController.getPrevChapter()), _ => null } : null, _ => null }; action; }, child: NotificationListener( onNotification: (notification) { if (notification.direction == ScrollDirection.idle) { if (_isView) { _isViewFunction(); } } return true; }, child: Material( child: SafeArea( top: !fullScreenReader, bottom: false, child: ValueListenableBuilder( valueListenable: _failedToLoadImage, builder: (context, failedToLoadImage, child) { return Stack( children: [ _isVerticalOrHorizontalContinous() ? PhotoViewGallery.builder( itemCount: 1, builder: (_, __) => PhotoViewGalleryPageOptions.customChild( controller: _photoViewController, scaleStateController: _photoViewScaleStateController, basePosition: _scalePosition, onScaleEnd: _onScaleEnd, child: ScrollablePositionedList.separated( scrollDirection: isHorizontalContinuaous ? Axis.horizontal : Axis.vertical, minCacheExtent: pagePreloadAmount * context.height(1), initialScrollIndex: _readerController.getPageIndex(), itemCount: (_pageMode == PageMode.doublePage && !isHorizontalContinuaous) ? (_uChapDataPreload.length / 2).ceil() + 1 : _uChapDataPreload.length, physics: const ClampingScrollPhysics(), itemScrollController: _itemScrollController, scrollOffsetController: _pageOffsetController, itemPositionsListener: _itemPositionsListener, itemBuilder: (context, index) { int index1 = index * 2 - 1; int index2 = index1 + 1; return GestureDetector( behavior: HitTestBehavior.translucent, onDoubleTapDown: (details) { _toggleScale(details.globalPosition); }, onDoubleTap: () {}, child: (_pageMode == PageMode.doublePage && !isHorizontalContinuaous) ? DoubleColummVerticalView( datas: index == 0 ? [_uChapDataPreload[0], null] : [ index1 < _uChapDataPreload.length ? _uChapDataPreload[index1] : null, index2 < _uChapDataPreload.length ? _uChapDataPreload[index2] : null, ], backgroundColor: backgroundColor, isFailedToLoadImage: (val) {}, onLongPressData: (datas) { _onLongPressImageDialog(datas, context); }, ) : ImageViewVertical( data: _uChapDataPreload[index], failedToLoadImage: (value) { // _failedToLoadImage.value = value; }, onLongPressData: (datas) { _onLongPressImageDialog(datas, context); }, isHorizontal: ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous, ), ); }, separatorBuilder: (_, __) => ref.watch(_currentReaderMode) == ReaderMode.webtoon ? const SizedBox.shrink() : ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous ? VerticalDivider(color: getBackgroundColor(backgroundColor), width: 6) : Divider(color: getBackgroundColor(backgroundColor), height: 6), )), ) : Material( color: getBackgroundColor(backgroundColor), shadowColor: getBackgroundColor(backgroundColor), child: (_pageMode == PageMode.doublePage && !isHorizontalContinuaous) ? ExtendedImageGesturePageView.builder( controller: _extendedController, scrollDirection: _scrollDirection, reverse: _isReverseHorizontal, physics: const ClampingScrollPhysics(), canScrollPage: (_) { return _horizontalScaleValue == 1.0; }, 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: _isReverseHorizontal ? pageList.reversed.toList() : pageList, backgroundColor: backgroundColor, isFailedToLoadImage: (val) { if (_failedToLoadImage.value != val && mounted) { _failedToLoadImage.value = val; } }, onLongPressData: (datas) { _onLongPressImageDialog(datas, context); }, ); }, itemCount: (_uChapDataPreload.length / 2).ceil() + 1, onPageChanged: _onPageChanged) : ExtendedImageGesturePageView.builder( controller: _extendedController, scrollDirection: _scrollDirection, reverse: _isReverseHorizontal, physics: const ClampingScrollPhysics(), canScrollPage: (gestureDetails) { return gestureDetails != null ? !(gestureDetails.totalScale! > 1.0) : true; }, itemBuilder: (BuildContext context, int index) { return ImageViewPaged( data: _uChapDataPreload[index], loadStateChanged: (state) { if (state.extendedImageLoadState == LoadState.loading) { final ImageChunkEvent? loadingProgress = state.loadingProgress; final double progress = loadingProgress?.expectedTotalBytes != null ? loadingProgress!.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : 0; return Container( color: getBackgroundColor(backgroundColor), height: context.height(0.8), child: CircularProgressIndicatorAnimateRotate(progress: progress), ); } if (state.extendedImageLoadState == LoadState.completed) { if (_failedToLoadImage.value == true) { Future.delayed(const Duration(milliseconds: 10)) .then((value) => _failedToLoadImage.value = false); } return ExtendedImageGesture( state, canScaleImage: (_) => true, imageBuilder: (Widget image, {ExtendedImageGestureState? imageGestureState}) { return image; }, ); } if (state.extendedImageLoadState == LoadState.failed) { if (_failedToLoadImage.value == false) { Future.delayed(const Duration(milliseconds: 10)) .then((value) => _failedToLoadImage.value = true); } return Container( color: getBackgroundColor(backgroundColor), height: context.height(0.8), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( l10n.image_loading_error, style: TextStyle(color: Colors.white.withValues(alpha: 0.7)), ), Padding( padding: const EdgeInsets.all(8.0), child: GestureDetector( onLongPress: () { state.reLoadImage(); _failedToLoadImage.value = false; }, onTap: () { state.reLoadImage(); _failedToLoadImage.value = false; }, child: Container( decoration: BoxDecoration( color: context.primaryColor, borderRadius: BorderRadius.circular(30)), child: Padding( padding: const EdgeInsets.symmetric( vertical: 8, horizontal: 16), child: Text( l10n.retry, ), ), )), ), ], )); } return const SizedBox.shrink(); }, 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(); }, onLongPressData: (datas) { _onLongPressImageDialog(datas, context); }, ); }, itemCount: _uChapDataPreload.length, onPageChanged: _onPageChanged)), _gestureRightLeft(failedToLoadImage, usePageTapZones), _gestureTopBottom(failedToLoadImage, usePageTapZones), _appBar(), _bottomBar(), _showPage(), _autoScrollPlayPauseBtn() ], ); }), ), ), ), ); } Future _precacheImages(int index) async { try { if (0 <= index && index < _uChapDataPreload.length) { await precacheImage(_uChapDataPreload[index].getImageProvider(ref, false), context); } } catch (_) {} } Duration? _doubleTapAnimationDuration() { int doubleTapAnimationValue = isar.settings.getSync(227)!.doubleTapAnimationSpeed!; if (doubleTapAnimationValue == 0) { return const Duration(milliseconds: 10); } else if (doubleTapAnimationValue == 1) { return const Duration(milliseconds: 800); } return const Duration(milliseconds: 200); } void _readProgressListener() { _currentIndex = _itemPositionsListener.itemPositions.value.first.index; int pagesLength = (_pageMode == PageMode.doublePage && !(ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous)) ? (_uChapDataPreload.length / 2).ceil() + 1 : _uChapDataPreload.length; if (_currentIndex! >= 0 && _currentIndex! < pagesLength) { if (_readerController.chapter.id != _uChapDataPreload[_currentIndex!].chapter!.id) { _readerController.setPageIndex(_geCurrentIndex(_uChapDataPreload[_currentIndex! - 1].index!), false); if (mounted) { setState(() { _readerController = ref.read(readerControllerProvider(chapter: _uChapDataPreload[_currentIndex!].chapter!).notifier); chapter = _uChapDataPreload[_currentIndex!].chapter!; _chapterUrlModel = _uChapDataPreload[_currentIndex!].chapterUrlModel!; _isBookmarked = _readerController.getChapterBookmarked(); }); } } if (_itemPositionsListener.itemPositions.value.last.index == pagesLength - 1) { try { ref .watch(getChapterPagesProvider( chapter: _readerController.getNextChapter(), ).future) .then((value) => _preloadNextChapter(value, chapter)); } catch (_) {} } ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex( _uChapDataPreload[_currentIndex!].index!, ); } } void _preloadNextChapter(GetChapterPagesModel chapterData, Chapter chap) { try { int length = 0; bool isExist = false; List uChapDataPreloadP = []; List uChapDataPreloadL = _uChapDataPreload; List preChap = []; final uIsNotEmpty = chapterData.uChapDataPreload.first.chapter!.url!.isNotEmpty; final aIsNotEmpty = chapterData.uChapDataPreload.first.chapter!.archivePath!.isNotEmpty; for (var chp in _uChapDataPreload) { final cuIsNotEmpty = chp.chapter!.url!.isNotEmpty; final caIsNotEmpty = chp.chapter!.archivePath!.isNotEmpty; if (uIsNotEmpty && cuIsNotEmpty && chapterData.uChapDataPreload.first.chapter!.url == chp.chapter!.url || aIsNotEmpty && caIsNotEmpty && chapterData.uChapDataPreload.first.chapter!.archivePath == chp.chapter!.archivePath) { isExist = true; } } if (!isExist) { for (var ch in chapterData.uChapDataPreload) { preChap.add(ch); } } if (preChap.isNotEmpty) { length = _uChapDataPreload.length; for (var i = 0; i < preChap.length; i++) { int index = i + length; final dataPreload = preChap[i]; uChapDataPreloadP.add(dataPreload..pageIndex = index); } if (mounted) { uChapDataPreloadL.addAll(uChapDataPreloadP); if (mounted) { setState(() { _uChapDataPreload = uChapDataPreloadL; }); } } } } catch (_) {} } void _initCurrentIndex() async { final readerMode = _readerController.getReaderMode(); _uChapDataPreload.addAll(_chapterUrlModel.uChapDataPreload); _readerController.setMangaHistoryUpdate(); await Future.delayed(const Duration(milliseconds: 1)); final fullScreenReader = ref.watch(fullScreenReaderStateProvider); if (fullScreenReader) { if (isDesktop) { setFullScreen(value: true); } else { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); } } ref.read(_currentReaderMode.notifier).state = readerMode; if (mounted) { setState(() { _pageMode = _readerController.getPageMode(); }); } _setReaderMode(readerMode, ref); ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex( _uChapDataPreload[_currentIndex!].index!, ); if (!(_isVerticalOrHorizontalContinous())) { for (var i = 1; i < pagePreloadAmount + 1; i++) { _precacheImages(_currentIndex! + i); _precacheImages(_currentIndex! - i); } } if (readerMode != ReaderMode.verticalContinuous && readerMode != ReaderMode.webtoon) { _autoScroll.value = false; } _autoPagescroll(); if (_readerController.getPageLength(_chapterUrlModel.pageUrls) == 1 && (readerMode == ReaderMode.ltr || readerMode == ReaderMode.rtl || readerMode == ReaderMode.vertical)) { _onPageChanged(0); } } void _onPageChanged(int index) { final cropBorders = ref.watch(cropBordersStateProvider); if (cropBorders) { _processCropBordersByIndex(index); } for (var i = 1; i < pagePreloadAmount + 1; i++) { _precacheImages(index + i); _precacheImages(index - i); } if (_readerController.chapter.id != _uChapDataPreload[index].chapter!.id) { _readerController.setPageIndex(_geCurrentIndex(_uChapDataPreload[_currentIndex!].index!), false); if (mounted) { setState(() { _readerController = ref.read(readerControllerProvider(chapter: _uChapDataPreload[_currentIndex!].chapter!).notifier); chapter = _uChapDataPreload[_currentIndex!].chapter!; _chapterUrlModel = _uChapDataPreload[index].chapterUrlModel!; _isBookmarked = _readerController.getChapterBookmarked(); }); } } _currentIndex = index; ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex(_uChapDataPreload[index].index!); if (_uChapDataPreload[index].pageIndex! == _uChapDataPreload.length - 1) { try { ref .watch(getChapterPagesProvider( chapter: _readerController.getNextChapter(), ).future) .then((value) => _preloadNextChapter(value, chapter)); } catch (_) {} } } late final _pageOffset = ValueNotifier(_readerController.autoScrollValues().$2); void _autoPagescroll() async { if (_isVerticalOrHorizontalContinous()) { for (int i = 0; i < 1; i++) { await Future.delayed(const Duration(milliseconds: 100)); if (!_autoScroll.value) { return; } _pageOffsetController.animateScroll(offset: _pageOffset.value, duration: const Duration(milliseconds: 100)); } _autoPagescroll(); } } void _onBtnTapped(int index, bool isPrev, {bool isSlide = false}) { if (_isView && !isSlide) { _isViewFunction(); } final readerMode = ref.watch(_currentReaderMode); final animatePageTransitions = ref.watch(animatePageTransitionsStateProvider); if (isPrev) { if (readerMode == ReaderMode.verticalContinuous || readerMode == ReaderMode.webtoon || readerMode == ReaderMode.horizontalContinuous) { if (index != -1) { if (isSlide) { _itemScrollController.jumpTo( index: index, ); } else { animatePageTransitions ? _itemScrollController.scrollTo( curve: Curves.ease, index: index, duration: const Duration(milliseconds: 150)) : _itemScrollController.jumpTo( index: index, ); } } } else { if (index != -1) { if (_extendedController.hasClients) { if (isSlide) { _extendedController.jumpToPage(index); } else { animatePageTransitions ? _extendedController.animateToPage(index, duration: const Duration(milliseconds: 150), curve: Curves.ease) : _extendedController.jumpToPage(index); } } } } } else { if (readerMode == ReaderMode.verticalContinuous || readerMode == ReaderMode.webtoon || readerMode == ReaderMode.horizontalContinuous) { if (isSlide) { _itemScrollController.jumpTo( index: index, ); } else { animatePageTransitions ? _itemScrollController.scrollTo( curve: Curves.ease, index: index, duration: const Duration(milliseconds: 150)) : _itemScrollController.jumpTo( index: index, ); } } else { if (_extendedController.hasClients) { if (isSlide) { _itemScrollController.jumpTo( index: index, ); } else { animatePageTransitions ? _extendedController.animateToPage(index, duration: const Duration(milliseconds: 150), curve: Curves.ease) : _extendedController.jumpToPage(index); } } } } } void _toggleScale(Offset tapPosition) { if (mounted) { setState(() { if (_scaleAnimationController.isAnimating) { return; } if (_photoViewController.scale == 1.0) { _scalePosition = _computeAlignmentByTapOffset(tapPosition); if (_scaleAnimationController.isCompleted) { _scaleAnimationController.reset(); } _scaleAnimationController.forward(); return; } if (_photoViewController.scale == 2.0) { _scaleAnimationController.reverse(); return; } _photoViewScaleStateController.reset(); }); } } void _setReaderMode(ReaderMode value, WidgetRef ref) async { if (value != ReaderMode.verticalContinuous && value != ReaderMode.webtoon) { _autoScroll.value = false; } else { if (_autoScrollPage.value) { _autoPagescroll(); _autoScroll.value = true; } } _failedToLoadImage.value = false; _readerController.setReaderMode(value); int index = (_pageMode == PageMode.doublePage && !(ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous)) ? (_currentIndex! / 2).ceil() : _currentIndex!; ref.read(_currentReaderMode.notifier).state = value; if (value == ReaderMode.vertical) { if (mounted) { setState(() { _scrollDirection = Axis.vertical; _isReverseHorizontal = false; }); await Future.delayed(const Duration(milliseconds: 30)); _extendedController.jumpToPage(index); } } else if (value == ReaderMode.ltr || value == ReaderMode.rtl) { if (mounted) { setState(() { if (value == ReaderMode.rtl) { _isReverseHorizontal = true; } else { _isReverseHorizontal = false; } _scrollDirection = Axis.horizontal; }); await Future.delayed(const Duration(milliseconds: 30)); _extendedController.jumpToPage(index); } } else { if (mounted) { setState(() { _isReverseHorizontal = false; }); await Future.delayed(const Duration(milliseconds: 30)); _itemScrollController.scrollTo(index: index, duration: const Duration(milliseconds: 1), curve: Curves.ease); } } } void _processCropBordersByIndex(int index) async { if (!_cropBorderCheckList.contains(index)) { _cropBorderCheckList.add(index); ref.watch(cropBordersProvider(data: _uChapDataPreload[index], cropBorder: true).future).then((value) { _uChapDataPreload[index] = _uChapDataPreload[index]..cropImage = value; }); if (mounted) { setState(() {}); } } } void _processCropBorders() async { for (var i = 0; i < _uChapDataPreload.length; i++) { if (!_cropBorderCheckList.contains(i)) { _cropBorderCheckList.add(i); ref.watch(cropBordersProvider(data: _uChapDataPreload[i], cropBorder: true).future).then((value) { _uChapDataPreload[i] = _uChapDataPreload[i]..cropImage = value; if (mounted) { setState(() {}); } }); } } } void _goBack(BuildContext context) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Navigator.pop(context); } Widget _appBar() { if (!_isView && Platform.isIOS) { return const SizedBox.shrink(); } final fullScreenReader = ref.watch(fullScreenReaderStateProvider); double height = _isView ? Platform.isIOS ? 120 : !fullScreenReader && !isDesktop ? 55 : 80 : 0; return Positioned( top: 0, child: AnimatedContainer( width: context.width(1), height: height, curve: Curves.ease, duration: const Duration(milliseconds: 200), child: PreferredSize( preferredSize: Size.fromHeight(height), child: AppBar( centerTitle: false, automaticallyImplyLeading: false, titleSpacing: 0, leading: BackButton( onPressed: () { Navigator.pop(context); }, ), title: ListTile( dense: true, title: SizedBox( width: context.width(0.8), child: Text( '${_readerController.getMangaName()} ', style: const TextStyle(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), ), subtitle: SizedBox( width: context.width(0.8), child: Text( _readerController.getChapterTitle(), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.w400, ), overflow: TextOverflow.ellipsis, ), ), ), actions: [ btnToShowChapterListDialog(context, context.l10n.chapters, widget.chapter), IconButton( onPressed: () { _readerController.setChapterBookmarked(); setState(() { _isBookmarked = !_isBookmarked; }); }, icon: Icon(_isBookmarked ? Icons.bookmark : Icons.bookmark_border_outlined)), if ((chapter.manga.value!.isLocalArchive ?? false) == false) IconButton( onPressed: () async { final manga = chapter.manga.value!; final source = getSource(manga.lang!, manga.source!)!; String url = chapter.url!.startsWith('/') ? "${source.baseUrl}/${chapter.url!}" : chapter.url!; Map data = { 'url': url, 'sourceId': source.id.toString(), 'title': chapter.name! }; context.push("/mangawebview", extra: data); }, icon: const Icon(Icons.public)), ], backgroundColor: _backgroundColor(context), ), ), ), ); } Widget _autoScrollPlayPauseBtn() { return _isVerticalOrHorizontalContinous() ? Positioned( bottom: 0, right: 0, child: !_isView ? ValueListenableBuilder( valueListenable: _autoScrollPage, builder: (context, valueT, child) => valueT ? ValueListenableBuilder( valueListenable: _autoScroll, builder: (context, value, child) => IconButton( onPressed: () { _autoPagescroll(); _autoScroll.value = !value; }, icon: Icon(value ? Icons.pause_circle : Icons.play_circle)), ) : const SizedBox.shrink(), ) : const SizedBox.shrink()) : const SizedBox.shrink(); } Widget _bottomBar() { if (!_isView && Platform.isIOS) { return const SizedBox.shrink(); } bool hasPrevChapter = _readerController.getChapterIndex().$1 + 1 != _readerController.getChaptersLength(_readerController.getChapterIndex().$2); bool hasNextChapter = _readerController.getChapterIndex().$1 != 0; final readerMode = ref.watch(_currentReaderMode); return Positioned( bottom: 0, child: AnimatedContainer( curve: Curves.ease, duration: const Duration(milliseconds: 300), width: context.width(1), height: (_isView ? 130 : 0), child: Column( children: [ Flexible( child: Transform.scale( scaleX: !_isReverseHorizontal ? 1 : -1, child: Row( children: [ Padding( padding: const EdgeInsets.all(8.0), child: CircleAvatar( radius: 23, backgroundColor: _backgroundColor(context), child: IconButton( onPressed: hasPrevChapter ? () { pushReplacementMangaReaderView( context: context, chapter: _readerController.getPrevChapter()); } : null, icon: Transform.scale( scaleX: 1, child: Icon(Icons.skip_previous_rounded, color: hasPrevChapter ? Theme.of(context).textTheme.bodyLarge!.color : Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.4)), )), ), ), Flexible( child: Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: Container( height: 70, decoration: BoxDecoration(color: _backgroundColor(context), borderRadius: BorderRadius.circular(25)), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Transform.scale( scaleX: !_isReverseHorizontal ? 1 : -1, child: SizedBox( width: 55, child: Center( child: Consumer(builder: (context, ref, child) { final currentIndex = ref.watch(currentIndexProvider(chapter)); return Text( _currentIndexLabel(currentIndex), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ); }), ), ), ), if (_isView) Flexible( flex: 14, child: Consumer(builder: (context, ref, child) { final currentIndex = ref.watch(currentIndexProvider(chapter)); return SliderTheme( data: SliderTheme.of(context).copyWith( overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0), ), child: Slider( onChanged: (value) { ref .read(currentIndexProvider(chapter).notifier) .setCurrentIndex(value.toInt()); }, onChangeEnd: (newValue) { try { final index = _uChapDataPreload .firstWhere((element) => element.chapter == chapter && element.index == newValue.toInt()) .pageIndex; _onBtnTapped( index!, true, isSlide: true, ); } catch (_) {} }, divisions: _readerController.getPageLength(_chapterUrlModel.pageUrls) == 1 ? null : _pageMode == PageMode.doublePage ? ((_readerController.getPageLength(_chapterUrlModel.pageUrls)) / 2) .ceil() + 1 : _readerController.getPageLength(_chapterUrlModel.pageUrls) - 1, value: min( (currentIndex).toDouble(), (_pageMode == PageMode.doublePage && !(ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous)) ? ((_readerController.getPageLength(_chapterUrlModel.pageUrls)) / 2) .ceil() + 1 : (_readerController .getPageLength(_chapterUrlModel.pageUrls) .toDouble())), label: _currentIndexLabel(currentIndex), min: 0, max: (_pageMode == PageMode.doublePage && !(ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous)) ? (((_readerController.getPageLength(_chapterUrlModel.pageUrls)) / 2) .ceil() + 1) .toDouble() : (_readerController.getPageLength(_chapterUrlModel.pageUrls) - 1) .toDouble(), ), ); }), ), Transform.scale( scaleX: !_isReverseHorizontal ? 1 : -1, child: SizedBox( width: 55, child: Center( child: Text( "${_readerController.getPageLength(_chapterUrlModel.pageUrls)}", style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ), ), ], ), ), ), ), Padding( padding: const EdgeInsets.all(8.0), child: CircleAvatar( radius: 23, backgroundColor: _backgroundColor(context), child: IconButton( onPressed: hasNextChapter ? () { pushReplacementMangaReaderView( context: context, chapter: _readerController.getNextChapter(), ); } : null, icon: Transform.scale( scaleX: 1, child: Icon( Icons.skip_next_rounded, color: hasNextChapter ? Theme.of(context).textTheme.bodyLarge!.color : Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.4), // size: 17, ), ), ), ), ), ], ), ), ), Flexible( child: Container( height: 65, color: _backgroundColor(context), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ PopupMenuButton( popUpAnimationStyle: popupAnimationStyle, color: Colors.black, child: const Icon( Icons.app_settings_alt_outlined, ), onSelected: (value) { ref.read(_currentReaderMode.notifier).state = value; _setReaderMode(value, ref); }, itemBuilder: (context) => [ for (var mode in ReaderMode.values) PopupMenuItem( value: mode, child: Row( children: [ Icon( Icons.check, color: readerMode == mode ? Colors.white : Colors.transparent, ), const SizedBox( width: 7, ), Text( getReaderModeName(mode, context), style: const TextStyle( color: Colors.white, fontSize: 12, ), ), ], )), ], ), Consumer(builder: (context, ref, child) { final cropBorders = ref.watch(cropBordersStateProvider); return IconButton( onPressed: () { ref.read(cropBordersStateProvider.notifier).set(!cropBorders); }, icon: Stack( children: [ const Icon( Icons.crop_rounded, ), if (!cropBorders) Positioned( right: 8, child: Transform.scale( scaleX: 2.5, child: const Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( '\\', style: TextStyle(fontSize: 17), ), ], ), ), ), ], ), ); }), IconButton( onPressed: () async { if (!(readerMode == ReaderMode.horizontalContinuous)) { PageMode newPageMode; _onBtnTapped( _pageMode == PageMode.onePage ? (_geCurrentIndex(_uChapDataPreload[_currentIndex!].index!) / 2).ceil() : _geCurrentIndex(_uChapDataPreload[_currentIndex!].index!), true, isSlide: true, ); newPageMode = _pageMode == PageMode.onePage ? PageMode.doublePage : PageMode.onePage; _readerController.setPageMode(newPageMode); if (mounted) { setState(() { _pageMode = newPageMode; }); } } }, icon: Icon( _pageMode == PageMode.doublePage ? CupertinoIcons.book_solid : CupertinoIcons.book, ), ), IconButton( onPressed: () { _showModalSettings(); }, icon: const Icon( Icons.settings_rounded, ), ), ], ), ), ), ], ), ), ); } Widget _showPage() { return Consumer(builder: (context, ref, child) { final currentIndex = ref.watch(currentIndexProvider(chapter)); return _isView ? const SizedBox.shrink() : ref.watch(_showPagesNumber) ? Align( alignment: Alignment.bottomCenter, child: Text( '${_currentIndexLabel(currentIndex)} / ${_readerController.getPageLength(_chapterUrlModel.pageUrls)}', style: const TextStyle( color: Colors.white, fontSize: 20.0, shadows: [ Shadow(offset: Offset(-1, -1), blurRadius: 1), Shadow(offset: Offset(1, -1), blurRadius: 1), Shadow(offset: Offset(1, 1), blurRadius: 1), Shadow(offset: Offset(-1, 1), blurRadius: 1) ], ), textAlign: TextAlign.center, )) : const SizedBox.shrink(); }); } void _isViewFunction() { final fullScreenReader = ref.watch(fullScreenReaderStateProvider); if (mounted) { setState(() { _isView = !_isView; }); } if (fullScreenReader) { if (_isView) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); } else { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); } } } String _currentIndexLabel(int index) { if (_pageMode != PageMode.doublePage) { return "${index + 1}"; } if (index == 0) { return "1"; } int pageLength = _readerController.getPageLength(_chapterUrlModel.pageUrls); int index1 = index * 2; int index2 = index1 + 1; return !(index * 2 < pageLength) ? "$pageLength" : "$index1-$index2"; } int _geCurrentIndex(int index) { if (_pageMode != PageMode.doublePage || index == 0) { return index; } int pageLength = _readerController.getPageLength(_chapterUrlModel.pageUrls); int index1 = index * 2; return !(index * 2 < pageLength) ? pageLength - 1 : index1 - 1; } Widget _gestureRightLeft(bool failedToLoadImage, bool usePageTapZones) { return Consumer( builder: (context, ref, child) { return Row( children: [ /// left region Expanded( flex: 2, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { if (usePageTapZones) { if (_isReverseHorizontal) { _onBtnTapped(_currentIndex! + 1, false); } else { _onBtnTapped(_currentIndex! - 1, true); } } else { _isViewFunction(); } }, onDoubleTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onSecondaryTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onDoubleTap: _isVerticalOrHorizontalContinous() ? () {} : null, onSecondaryTap: _isVerticalOrHorizontalContinous() ? () {} : null, ), ), /// center region Expanded( flex: 2, child: failedToLoadImage ? SizedBox( width: context.width(1), height: context.height(0.7), ) : GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { _isViewFunction(); }, onDoubleTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onSecondaryTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onDoubleTap: _isVerticalOrHorizontalContinous() ? () {} : null, onSecondaryTap: _isVerticalOrHorizontalContinous() ? () {} : null, ), ), /// right region Expanded( flex: 2, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { if (usePageTapZones) { if (_isReverseHorizontal) { _onBtnTapped(_currentIndex! - 1, true); } else { _onBtnTapped(_currentIndex! + 1, false); } } else { _isViewFunction(); } }, onDoubleTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onSecondaryTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onDoubleTap: _isVerticalOrHorizontalContinous() ? () {} : null, onSecondaryTap: _isVerticalOrHorizontalContinous() ? () {} : null, ), ), ], ); }, ); } Widget _gestureTopBottom(bool failedToLoadImage, bool usePageTapZones) { return Consumer( builder: (context, ref, child) { return Column( children: [ /// top region Expanded( flex: 2, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { failedToLoadImage ? _isViewFunction() : usePageTapZones ? _onBtnTapped(_currentIndex! - 1, true) : _isViewFunction(); }, onDoubleTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onSecondaryTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onDoubleTap: _isVerticalOrHorizontalContinous() ? () {} : null, onSecondaryTap: _isVerticalOrHorizontalContinous() ? () {} : null, ), ), /// center region const Expanded(flex: 5, child: SizedBox.shrink()), /// bottom region Expanded( flex: 2, child: GestureDetector( behavior: HitTestBehavior.translucent, onTap: () { failedToLoadImage ? _isViewFunction() : usePageTapZones ? _onBtnTapped(_currentIndex! + 1, false) : _isViewFunction(); }, onDoubleTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onSecondaryTapDown: _isVerticalOrHorizontalContinous() ? (details) { _toggleScale(details.globalPosition); } : null, onDoubleTap: _isVerticalOrHorizontalContinous() ? () {} : null, onSecondaryTap: _isVerticalOrHorizontalContinous() ? () {} : null, ), ), ], ); }, ); } bool _isVerticalOrHorizontalContinous() { final readerMode = ref.watch(_currentReaderMode); return readerMode == ReaderMode.verticalContinuous || readerMode == ReaderMode.webtoon || readerMode == ReaderMode.horizontalContinuous; } void _showModalSettings() async { _autoScroll.value = false; final l10n = l10nLocalizations(context)!; await customDraggableTabBar(tabs: [ Tab(text: l10n.reading_mode), Tab(text: l10n.general), Tab(text: l10n.custom_filter), ], children: [ Consumer(builder: (context, ref, chil) { final readerMode = ref.watch(_currentReaderMode); final usePageTapZones = ref.watch(usePageTapZonesStateProvider); final cropBorders = ref.watch(cropBordersStateProvider); return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( children: [ CustomPopupMenuButton( label: l10n.reading_mode, title: getReaderModeName(readerMode!, context), onSelected: (value) { ref.read(_currentReaderMode.notifier).state = value; _setReaderMode(value, ref); }, value: readerMode, list: ReaderMode.values, itemText: (mode) { return getReaderModeName(mode, context); }, ), SwitchListTile( value: cropBorders, title: Text( l10n.crop_borders, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14), ), onChanged: (value) { ref.read(cropBordersStateProvider.notifier).set(value); }), SwitchListTile( value: usePageTapZones, title: Text(l10n.use_page_tap_zones, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14)), onChanged: (value) { ref.read(usePageTapZonesStateProvider.notifier).set(value); }), if (readerMode == ReaderMode.verticalContinuous || readerMode == ReaderMode.webtoon || readerMode == ReaderMode.horizontalContinuous) ValueListenableBuilder( valueListenable: _autoScrollPage, builder: (context, valueT, child) { return Column( children: [ SwitchListTile( secondary: Icon(valueT ? Icons.timer : Icons.timer_outlined), value: valueT, title: Text(context.l10n.auto_scroll, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14)), onChanged: (val) { _readerController.setAutoScroll(val, _pageOffset.value); _autoScrollPage.value = val; _autoScroll.value = val; }), if (valueT) ValueListenableBuilder( valueListenable: _pageOffset, builder: (context, value, child) => Slider( min: 2.0, max: 30.0, divisions: max(28, 3), value: value, onChanged: (val) { _pageOffset.value = val; }, onChangeEnd: (val) { _readerController.setAutoScroll(valueT, val); }), ), ], ); }, ), ], ), ), ); }), Consumer(builder: (context, ref, chil) { final showPageNumber = ref.watch(_showPagesNumber); final animatePageTransitions = ref.watch(animatePageTransitionsStateProvider); final scaleType = ref.watch(scaleTypeStateProvider); final fullScreenReader = ref.watch(fullScreenReaderStateProvider); final backgroundColor = ref.watch(backgroundColorStateProvider); return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CustomPopupMenuButton( label: l10n.background_color, title: getBackgroundColorName(backgroundColor, context), onSelected: (value) { ref.read(backgroundColorStateProvider.notifier).set(value); }, value: backgroundColor, list: BackgroundColor.values, itemText: (color) { return getBackgroundColorName(color, context); }, ), CustomPopupMenuButton( label: l10n.scale_type, title: getScaleTypeNames(context)[scaleType.index], onSelected: (value) { ref.read(scaleTypeStateProvider.notifier).set(ScaleType.values[value.index]); }, value: scaleType, list: ScaleType.values.where((scale) { try { return getScaleTypeNames(context).contains(getScaleTypeNames(context)[scale.index]); } catch (_) { return false; } }).toList(), itemText: (scale) { return getScaleTypeNames(context)[scale.index]; }, ), SwitchListTile( value: fullScreenReader, title: Text( l10n.fullscreen, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14), ), onChanged: (value) { _setFullScreen(value: value); }), SwitchListTile( value: showPageNumber, title: Text( l10n.show_page_number, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14), ), onChanged: (value) { ref.read(_showPagesNumber.notifier).state = value; _readerController.setShowPageNumber(value); }), SwitchListTile( value: animatePageTransitions, title: Text( l10n.animate_page_transitions, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14), ), onChanged: (value) { ref.read(animatePageTransitionsStateProvider.notifier).set(value); }), ], ), ), ); }), Consumer(builder: (context, ref, chil) { final customColorFilter = ref.watch(customColorFilterStateProvider); final enableCustomColorFilter = ref.watch(enableCustomColorFilterStateProvider); int r = customColorFilter?.r ?? 0; int g = customColorFilter?.g ?? 0; int b = customColorFilter?.b ?? 0; int a = customColorFilter?.a ?? 0; final colorFilterBlendMode = ref.watch(colorFilterBlendModeStateProvider); return SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ SwitchListTile( value: enableCustomColorFilter, title: Text( l10n.custom_color_filter, style: TextStyle( color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9), fontSize: 14), ), onChanged: (value) { ref.read(enableCustomColorFilterStateProvider.notifier).set(value); }), if (enableCustomColorFilter) ...[ rgbaFilterWidget(a, r, g, b, (val) { if (val.$3 == "r") { ref.read(customColorFilterStateProvider.notifier).set(a, val.$1.toInt(), g, b, val.$2); } else if (val.$3 == "g") { ref.read(customColorFilterStateProvider.notifier).set(a, r, val.$1.toInt(), b, val.$2); } else if (val.$3 == "b") { ref.read(customColorFilterStateProvider.notifier).set(a, r, g, val.$1.toInt(), val.$2); } else { ref.read(customColorFilterStateProvider.notifier).set(val.$1.toInt(), r, g, b, val.$2); } }, context), CustomPopupMenuButton( label: l10n.color_filter_blend_mode, title: getColorFilterBlendModeName(colorFilterBlendMode, context), onSelected: (value) { ref.read(colorFilterBlendModeStateProvider.notifier).set(value); }, value: colorFilterBlendMode, list: ColorFilterBlendMode.values, itemText: (va) { return getColorFilterBlendModeName(va, context); }, ), ] ], ), ), ); }), ], context: context, vsync: this, fullWidth: true); if (_autoScrollPage.value) { _autoPagescroll(); _autoScroll.value = true; } } } class UChapDataPreload { Chapter? chapter; Directory? directory; PageUrl? pageUrl; bool? isLocale; Uint8List? archiveImage; int? index; GetChapterPagesModel? chapterUrlModel; int? pageIndex; Uint8List? cropImage; UChapDataPreload(this.chapter, this.directory, this.pageUrl, this.isLocale, this.archiveImage, this.index, this.chapterUrlModel, this.pageIndex, {this.cropImage}); } class CustomPopupMenuButton extends StatelessWidget { final String label; final String title; final ValueChanged onSelected; final T value; final List list; final String Function(T) itemText; const CustomPopupMenuButton( {super.key, required this.label, required this.title, required this.onSelected, required this.value, required this.list, required this.itemText}); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 10), child: PopupMenuButton( popUpAnimationStyle: popupAnimationStyle, tooltip: "", offset: Offset.fromDirection(1), color: Colors.black, onSelected: onSelected, itemBuilder: (context) => [ for (var d in list) PopupMenuItem( value: d, child: Row( children: [ Icon( Icons.check, color: d == value ? Colors.white : Colors.transparent, ), const SizedBox(width: 7), Text( itemText(d), style: const TextStyle(color: Colors.white), ), ], )), ], child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( child: Text( label, style: TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color!.withValues(alpha: 0.9)), overflow: TextOverflow.ellipsis, maxLines: 1, ), ), Row( children: [Text(title), const SizedBox(width: 20), const Icon(Icons.keyboard_arrow_down_outlined)], ), ], ), ), ), ); } }