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/settings.dart'; import 'package:mangayomi/modules/anime/widgets/desktop.dart'; import 'package:mangayomi/modules/library/providers/local_archive.dart'; import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart'; import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.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/manga/reader/widgets/custom_popup_menu_button.dart'; import 'package:mangayomi/modules/manga/reader/widgets/custom_value_indicator_shape.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/extensions/string_extensions.dart'; import 'package:mangayomi/utils/riverpod.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/providers/reader_controller_provider.dart'; import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicator_animate_rotate.dart'; import 'package:mangayomi/modules/manga/reader/widgets/transition_view_paged.dart'; import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; import 'package:mangayomi/modules/manga/reader/providers/manga_reader_provider.dart'; import 'package:mangayomi/modules/manga/reader/image_view_webtoon.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; import 'package:photo_view/photo_view.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:share_plus/share_plus.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; import 'package:window_manager/window_manager.dart'; import 'package:path/path.dart' as p; typedef DoubleClickAnimationListener = void Function(); class MangaReaderView extends ConsumerStatefulWidget { final int chapterId; const MangaReaderView({super.key, required this.chapterId}); @override ConsumerState createState() => _MangaReaderViewState(); } class _MangaReaderViewState extends ConsumerState { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { ref.invalidate(mangaReaderProvider(widget.chapterId)); }); } @override Widget build(BuildContext context) { final chapterData = ref.watch(mangaReaderProvider(widget.chapterId)); return chapterData.when( loading: () => scaffoldWith(context, const ProgressCenter()), error: (error, _) => scaffoldWith(context, Center(child: Text(error.toString()))), data: (data) { final chapter = data.chapter; final model = data.pages; if (model.pageUrls.isEmpty && !(chapter.manga.value?.isLocalArchive ?? false)) { return scaffoldWith( context, const Center(child: Text('Error: no pages available')), restoreUi: true, ); } return MangaChapterPageGallery( chapter: chapter, chapterUrlModel: model, ); }, ); } Widget scaffoldWith( BuildContext context, Widget body, { bool restoreUi = false, }) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: const Text(''), leading: BackButton( onPressed: () { if (restoreUi) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: SystemUiOverlay.values, ); } Navigator.of(context).pop(); }, ), ), body: body, ); } } 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, WidgetsBindingObserver { 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() { WidgetsBinding.instance.removeObserver(this); _readerController.setMangaHistoryUpdate(); final index = _uChapDataPreload[_currentIndex!].index; if (index != null) { _readerController.setPageIndex(_geCurrentIndex(index), true); } _rebuildDetail.close(); _doubleClickAnimationController.dispose(); _scaleAnimationController.dispose(); _failedToLoadImage.dispose(); _autoScroll.value = false; _autoScroll.dispose(); _autoScrollPage.dispose(); _itemPositionsListener.itemPositions.removeListener(_readProgressListener); _photoViewController.dispose(); _photoViewScaleStateController.dispose(); _extendedController.dispose(); clearGestureDetailsCache(); if (isDesktop) { setFullScreen(value: false); } else { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: SystemUiOverlay.values, ); } discordRpc?.showIdleText(); _readerController.keepAliveLink?.close(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused || state == AppLifecycleState.detached) { final index = _uChapDataPreload[_currentIndex!].index; if (index != null) { _readerController.setPageIndex(_geCurrentIndex(index), true); } } } late final _autoScroll = ValueNotifier( _readerController.autoScrollValues().$1, ); late final _autoScrollPage = ValueNotifier(_autoScroll.value); late GetChapterPagesModel _chapterUrlModel = widget.chapterUrlModel; late Chapter chapter = widget.chapter; final 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() { super.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(); discordRpc?.showChapterDetails(ref, chapter); WidgetsBinding.instance.addObserver(this); } // final double _horizontalScaleValue = 1.0; bool _isNextChapterPreloading = false; late int pagePreloadAmount = ref.read(pagePreloadAmountStateProvider); late bool _isBookmarked = _readerController.getChapterBookmarked(); bool _isLastPageTransition = false; final _currentReaderMode = StateProvider(() => 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( // () => _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 SuperListView( 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.getCoverImage ..updatedAt = DateTime.now() .millisecondsSinceEpoch, ); }); 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 { if (context.mounted) { final box = context.findRenderObject() as RenderBox?; await SharePlus.instance.share( ShareParams( files: [ XFile.fromData( imageBytes, name: name, mimeType: 'image/png', ), ], sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size, ), ); } }, ), 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 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() ? ImageViewWebtoon( pages: _uChapDataPreload, itemScrollController: _itemScrollController, scrollOffsetController: _pageOffsetController, itemPositionsListener: _itemPositionsListener, scrollDirection: isHorizontalContinuaous ? Axis.horizontal : Axis.vertical, minCacheExtent: pagePreloadAmount * context.height(1), initialScrollIndex: _readerController .getPageIndex(), physics: const ClampingScrollPhysics(), onLongPressData: (data) => _onLongPressImageDialog(data, context), onFailedToLoadImage: (value) { // Handle failed image loading if (_failedToLoadImage.value != value && context.mounted) { _failedToLoadImage.value = value; } }, backgroundColor: backgroundColor, isDoublePageMode: _pageMode == PageMode.doublePage && !isHorizontalContinuaous, isHorizontalContinuous: isHorizontalContinuaous, readerMode: ref.watch(_currentReaderMode)!, photoViewController: _photoViewController, photoViewScaleStateController: _photoViewScaleStateController, scalePosition: _scalePosition, onScaleEnd: (details) => _onScaleEnd( context, details, _photoViewController.value, ), onDoubleTapDown: (offset) => _toggleScale(offset), onDoubleTap: () {}, ) : 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 true; }, itemBuilder: (context, index) { if (index < _uChapDataPreload.length && _uChapDataPreload[index] .isTransitionPage) { return TransitionViewPaged( data: _uChapDataPreload[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 true; }, itemBuilder: (BuildContext context, int index) { if (_uChapDataPreload[index] .isTransitionPage) { return TransitionViewPaged( data: _uChapDataPreload[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, ), ), Consumer( builder: (context, ref, child) { final usePageTapZones = ref.watch( usePageTapZonesStateProvider, ); return _gestureRightLeft( failedToLoadImage, usePageTapZones, ); }, ), Consumer( builder: (context, ref, child) { final usePageTapZones = ref.watch( usePageTapZonesStateProvider, ); return _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() async { final itemPositions = _itemPositionsListener.itemPositions.value; if (itemPositions.isNotEmpty) { _currentIndex = itemPositions.first.index; int pagesLength = (_pageMode == PageMode.doublePage && !(ref.watch(_currentReaderMode) == ReaderMode.horizontalContinuous)) ? (_uChapDataPreload.length / 2).ceil() + 1 : _uChapDataPreload.length; if (_currentIndex! >= 0 && _currentIndex! < pagesLength) { try { final idx = _uChapDataPreload[_currentIndex!].index; if (idx != null) { _readerController.setPageIndex(_geCurrentIndex(idx), false); } } catch (_) {} if (_readerController.chapter.id != _uChapDataPreload[_currentIndex!].chapter!.id) { if (mounted) { setState(() { _readerController = ref.read( readerControllerProvider( chapter: _uChapDataPreload[_currentIndex!].chapter!, ).notifier, ); chapter = _uChapDataPreload[_currentIndex!].chapter!; final chapterUrlModel = _uChapDataPreload[_currentIndex!].chapterUrlModel; if (chapterUrlModel != null) { _chapterUrlModel = chapterUrlModel; } _isBookmarked = _readerController.getChapterBookmarked(); }); } } if ((itemPositions.last.index == pagesLength - 1) && !_isLastPageTransition) { if (_isNextChapterPreloading) return; try { _isNextChapterPreloading = true; if (!mounted) return; final value = await ref.read( getChapterPagesProvider( chapter: _readerController.getNextChapter(), ).future, ); if (mounted) { _preloadNextChapter(value, chapter); _isNextChapterPreloading = false; } } on RangeError { _isNextChapterPreloading = false; _addLastPageTransition(chapter); } } final idx = _uChapDataPreload[_currentIndex!].index; if (idx != null) { ref.read(currentIndexProvider(chapter).notifier).setCurrentIndex(idx); } } } } void _addLastPageTransition(Chapter chap) { if (_isLastPageTransition) return; try { if (!mounted || (_uChapDataPreload.last.isLastChapter ?? false)) return; final currentLength = _uChapDataPreload.length; final transitionPage = UChapDataPreload.transition( currentChapter: chap, nextChapter: null, mangaName: chap.manga.value?.name ?? '', isLastChapter: true, pageIndex: currentLength, ); if (mounted) { setState(() { _uChapDataPreload.add(transitionPage); _isLastPageTransition = true; }); } } catch (_) {} } void _preloadNextChapter(GetChapterPagesModel chapterData, Chapter chap) { try { if (chapterData.uChapDataPreload.isEmpty || !mounted) return; final firstChapter = chapterData.uChapDataPreload.first.chapter; if (firstChapter == null) return; if (_isChapterAlreadyLoaded(firstChapter)) return; final currentLength = _uChapDataPreload.length; final transitionPage = UChapDataPreload.transition( currentChapter: chap, nextChapter: firstChapter, mangaName: chap.manga.value?.name ?? '', pageIndex: currentLength, ); final newPages = chapterData.uChapDataPreload .asMap() .entries .map( (entry) => entry.value..pageIndex = currentLength + 1 + entry.key, ) .toList(); if (mounted) { setState(() { _uChapDataPreload.add(transitionPage); _uChapDataPreload.addAll(newPages); }); } } catch (_) {} } bool _isChapterAlreadyLoaded(Chapter chapter) { final existingIdentifiers = _uChapDataPreload .map((item) => item.chapter) .where((ch) => ch != null) .map((ch) => _getChapterIdentifier(ch!)) .toSet(); return existingIdentifiers.contains(_getChapterIdentifier(chapter)); } String _getChapterIdentifier(Chapter chapter) { final url = chapter.url?.trim() ?? ''; final archivePath = chapter.archivePath?.trim() ?? ''; if (url.isNotEmpty) return 'url:$url'; if (archivePath.isNotEmpty) return 'archive:$archivePath'; return 'id:${chapter.id}'; } 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); } } Future _onPageChanged(int index) async { final cropBorders = ref.watch(cropBordersStateProvider); if (cropBorders) { _processCropBordersByIndex(index); } for (var i = 1; i < pagePreloadAmount + 1; i++) { _precacheImages(index + i); _precacheImages(index - i); } final idx = _uChapDataPreload[_currentIndex!].index; if (idx != null) { _readerController.setPageIndex(_geCurrentIndex(idx), false); } if (_readerController.chapter.id != _uChapDataPreload[index].chapter!.id) { if (mounted) { setState(() { _readerController = ref.read( readerControllerProvider( chapter: _uChapDataPreload[_currentIndex!].chapter!, ).notifier, ); chapter = _uChapDataPreload[_currentIndex!].chapter!; final chapterUrlModel = _uChapDataPreload[index].chapterUrlModel; if (chapterUrlModel != null) { _chapterUrlModel = chapterUrlModel; } _isBookmarked = _readerController.getChapterBookmarked(); }); } } _currentIndex = index; if (_uChapDataPreload[index].index != null) { ref .read(currentIndexProvider(chapter).notifier) .setCurrentIndex(_uChapDataPreload[index].index!); } if ((_uChapDataPreload[index].pageIndex! == _uChapDataPreload.length - 1) && !_isLastPageTransition) { if (_isNextChapterPreloading) return; try { _isNextChapterPreloading = true; if (!mounted) return; final value = await ref.watch( getChapterPagesProvider( chapter: _readerController.getNextChapter(), ).future, ); if (mounted) { _preloadNextChapter(value, chapter); _isNextChapterPreloading = false; } } on RangeError { _isNextChapterPreloading = false; _addLastPageTransition(chapter); } } } 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); if (!mounted) return; final value = await ref.read( cropBordersProvider( data: _uChapDataPreload[index], cropBorder: true, ).future, ); if (mounted) { setState(() { _uChapDataPreload[index] = _uChapDataPreload[index] ..cropImage = value; }); } } } void _processCropBorders() async { if (_cropBorderCheckList.length == _uChapDataPreload.length) return; for (var i = 0; i < _uChapDataPreload.length; i++) { if (!_cropBorderCheckList.contains(i)) { _cropBorderCheckList.add(i); if (!mounted) return; final value = await ref.read( cropBordersProvider( data: _uChapDataPreload[i], cropBorder: true, ).future, ); if (mounted) { setState(() { _uChapDataPreload[i] = _uChapDataPreload[i]..cropImage = value; }); } } } } void _goBack(BuildContext context) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: SystemUiOverlay.values, ); Navigator.pop(context); } Widget _appBar() { 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: 300), 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!, manga.sourceId, )!; final url = "${source.baseUrl}${chapter.url!.getUrlWithoutDomain}"; 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() { 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( valueIndicatorShape: CustomValueIndicatorShape( tranform: _isReverseHorizontal, ), 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)); final showPagesNumber = ref.watch(showPagesNumberStateProvider); return _isView ? const SizedBox.shrink() : 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 (context.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 { bool autoScrollAreadyFalse = _autoScroll.value == false; if (!autoScrollAreadyFalse) { _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 showPagesNumber = ref.watch(showPagesNumberStateProvider); 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: showPagesNumber, 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).set(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 (!autoScrollAreadyFalse || _autoScroll.value) { if (_autoScrollPage.value) { _autoPagescroll(); _autoScroll.value = true; } } } }