import 'package:extended_image/extended_image.dart'; import 'package:fladder/models/book_model.dart'; import 'package:fladder/providers/book_viewer_provider.dart'; import 'package:fladder/providers/items/book_details_provider.dart'; import 'package:fladder/providers/settings/book_viewer_settings_provider.dart'; import 'package:fladder/screens/book_viewer/book_viewer_controls.dart'; import 'package:fladder/screens/book_viewer/book_viewer_reader.dart' if (dart.library.html) 'package:fladder/screens/book_viewer/book_viewer_reader_web.dart'; import 'package:fladder/util/themes_data.dart'; import 'package:fladder/util/throttler.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; Future openBookViewer( BuildContext context, AutoDisposeStateNotifierProvider provider, { int? initialPage, }) async { return showDialog( context: context, useRootNavigator: true, useSafeArea: false, builder: (context) => Dialog.fullscreen( child: BookViewerScreen( initialPage: initialPage ?? 0, provider: provider, ), ), ); } class BookViewerScreen extends ConsumerStatefulWidget { final int initialPage; final AutoDisposeStateNotifierProvider provider; const BookViewerScreen({required this.provider, this.initialPage = 0, super.key}); @override ConsumerState createState() => _BookViewerScreenState(); } class _BookViewerScreenState extends ConsumerState { final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130)); final Duration pageAnimDuration = const Duration(milliseconds: 125); final Curve pageAnimCurve = Curves.easeInCubic; late final ExtendedPageController extendedController = ExtendedPageController(initialPage: widget.initialPage); late final BookViewController viewController = BookViewController(); bool outOfRange = false; @override void initState() { super.initState(); Future.microtask(() => ref.read(bookViewerSettingsProvider.notifier).setSavedBrightness()); } late double lastScale = switch (ref.read(bookViewerSettingsProvider).initZoomState) { InitZoomState.contained => 1.0, InitZoomState.covered => 1.75, }; late double lastPosition = 0.0; @override Widget build(BuildContext context) { final bookViewerDetails = ref.watch(bookViewerProvider); final loading = bookViewerDetails.loading; final pages = bookViewerDetails.pages; final book = bookViewerDetails.book; final bookViewSettings = ref.watch(bookViewerSettingsProvider); ref.listen( bookViewerProvider.select((value) => value.loading), (previous, next) { if (previous == true && next == false) { ref.read(bookViewerProvider.notifier).updatePlayback((widget.initialPage.toDouble()).toInt()); } }, ); return Theme( data: ThemesData.of(context).dark, child: PopScope( canPop: true, onPopInvokedWithResult: (didPop, result) async { await ref.read(bookViewerProvider.notifier).stopPlayback(); }, child: Scaffold( backgroundColor: Colors.black, body: Stack( fit: StackFit.expand, children: [ if (!loading) ExtendedImageGesturePageView.builder( itemCount: pages.length + 2, controller: extendedController, canScrollPage: (gestureDetails) { return bookViewSettings.disableScrollOnZoom ? gestureDetails != null ? !(gestureDetails.totalScale! > 1.0) : true : true; }, onPageChanged: (value) { final newRange = pages.length + 1 == value || value == 0; if (outOfRange != newRange) { viewController.toggleControls(value: newRange); outOfRange = newRange; } ref.read(bookViewerProvider.notifier).updatePlayback(value); }, reverse: bookViewSettings.readDirection == ReadDirection.rightToLeft, itemBuilder: (context, index) { if (pages.length + 1 == index || index == 0) { final atEnd = index >= pages.length; final details = ref.read(widget.provider); return Padding( padding: const EdgeInsets.all(16.0), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: bookViewSettings.readDirection != ReadDirection.leftToRight ? CrossAxisAlignment.start : CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ if (atEnd) ...{ Flexible( child: Text( "End: \n${book?.name}", textAlign: TextAlign.start, style: Theme.of(context).textTheme.titleLarge, ), ), if (details.nextChapter(bookViewerDetails.book) != null) ...{ const SizedBox(height: 32), Flexible( child: Text( "Next: ", textAlign: TextAlign.start, style: Theme.of(context).textTheme.titleLarge, ), ), Flexible( child: FilledButton( style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)), onPressed: () async => await loadNextBook(details.nextChapter(bookViewerDetails.book)), child: Row( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.keyboard_arrow_left_rounded), Text( details.nextChapter(bookViewerDetails.book)!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onPrimary), ), ], ), ), ), } else ...{ const SizedBox(height: 32), const Card( child: Padding( padding: EdgeInsets.all(16), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_rounded), SizedBox(width: 16), Text("No next chapter"), ], ), ), ) } } else ...{ Flexible( child: Text( "Start: \n${book?.name}", textAlign: TextAlign.start, style: Theme.of(context).textTheme.titleLarge, ), ), if (details.previousChapter(bookViewerDetails.book) != null) ...{ const SizedBox(height: 32), Flexible( child: Text( "Previous:", textAlign: TextAlign.start, style: Theme.of(context).textTheme.titleLarge, ), ), Flexible( child: FilledButton( style: FilledButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 8)), onPressed: () async => await loadNextBook(details.previousChapter(bookViewerDetails.book)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Text( details.previousChapter(bookViewerDetails.book)!.name, style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.bold, color: Theme.of(context).colorScheme.onPrimary), ), const Icon(Icons.keyboard_arrow_right_rounded), ], ), ), ), } else ...{ const SizedBox(height: 32), const Card( child: Padding( padding: EdgeInsets.all(16), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.info_rounded), SizedBox(width: 16), Text("First chapter"), ], ), ), ) } }, ], ), ), ); } else { return BookViewerReader( index: index, pages: pages, bookViewSettings: bookViewSettings, previousPage: previousPage, nextPage: nextPage, viewController: viewController, lastScale: lastScale, newScale: (value) => lastScale = value, ); } }, ), BookViewerControls( provider: widget.provider, viewController: viewController, controller: extendedController, ) ], ), ), ), ); } Future nextPage() async => throttler.run(() async => await extendedController.nextPage(duration: pageAnimDuration, curve: pageAnimCurve)); Future previousPage() async => throttler .run(() async => await extendedController.previousPage(duration: pageAnimDuration, curve: pageAnimCurve)); Future loadNextBook(BookModel? book) async { await ref.read(bookViewerProvider.notifier).fetchBook(book); extendedController.jumpToPage(0); return; } }