mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 13:38:13 -08:00
270 lines
12 KiB
Dart
270 lines
12 KiB
Dart
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<void> openBookViewer(
|
|
BuildContext context,
|
|
AutoDisposeStateNotifierProvider<BookDetailsProviderNotifier, BookProviderModel> 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<BookDetailsProviderNotifier, BookProviderModel> provider;
|
|
const BookViewerScreen({required this.provider, this.initialPage = 0, super.key});
|
|
|
|
@override
|
|
ConsumerState<ConsumerStatefulWidget> createState() => _BookViewerScreenState();
|
|
}
|
|
|
|
class _BookViewerScreenState extends ConsumerState<BookViewerScreen> {
|
|
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,
|
|
onPopInvoked: (didPop) 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<void> nextPage() async =>
|
|
throttler.run(() async => await extendedController.nextPage(duration: pageAnimDuration, curve: pageAnimCurve));
|
|
|
|
Future<void> previousPage() async => throttler
|
|
.run(() async => await extendedController.previousPage(duration: pageAnimDuration, curve: pageAnimCurve));
|
|
|
|
Future<void> loadNextBook(BookModel? book) async {
|
|
await ref.read(bookViewerProvider.notifier).fetchBook(book);
|
|
extendedController.jumpToPage(0);
|
|
return;
|
|
}
|
|
}
|