Init repo

This commit is contained in:
PartyDonut 2024-09-15 14:12:28 +02:00
commit 764b6034e3
566 changed files with 212335 additions and 0 deletions

View file

@ -0,0 +1,118 @@
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/util/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<void> showBookViewerChapters(
BuildContext context, AutoDisposeStateNotifierProvider<BookDetailsProviderNotifier, BookProviderModel> provider,
{Function(BookModel book)? onPressed}) async {
if (AdaptiveLayout.of(context).isDesktop) {
return showModalSideSheet(context,
content: BookViewerChapters(
provider: provider,
onPressed: onPressed,
));
} else {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
showDragHandle: true,
useSafeArea: true,
builder: (context) => BookViewerChapters(
provider: provider,
onPressed: onPressed,
),
);
}
}
class BookViewerChapters extends ConsumerWidget {
final AutoDisposeStateNotifierProvider<BookDetailsProviderNotifier, BookProviderModel> provider;
final Function(BookModel book)? onPressed;
const BookViewerChapters({required this.provider, this.onPressed, super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final currentBook = ref.watch(bookViewerProvider.select((value) => value.book));
final chapters = ref.watch(provider.select((value) => value.chapters));
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"Chapters",
style: Theme.of(context).textTheme.titleLarge,
),
),
),
const Divider(),
Flexible(
child: ListView(
shrinkWrap: true,
children: [
...chapters.map(
(book) {
final bool current = currentBook == book;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Card(
elevation: current ? 10 : 3,
child: Container(
constraints: const BoxConstraints(minHeight: 80),
alignment: Alignment.center,
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
leading: AspectRatio(
aspectRatio: 1,
child: Card(
child: FladderImage(
image: book.getPosters?.primary,
),
),
),
title: Text(book.name),
trailing: current
? Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
child: Icon(
Icons.visibility_rounded,
color: Theme.of(context).colorScheme.primary,
),
)
: FilledButton(
onPressed: () => onPressed?.call(book),
style: ElevatedButton.styleFrom(
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
child: const Icon(Icons.read_more_rounded),
),
),
),
),
),
);
},
),
SizedBox(
height: MediaQuery.of(context).padding.bottom,
),
],
),
),
],
);
}
}

View file

@ -0,0 +1,398 @@
import 'package:extended_image/extended_image.dart';
import 'package:ficonsax/ficonsax.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_chapters.dart';
import 'package:fladder/screens/book_viewer/book_viewer_settings.dart';
import 'package:fladder/screens/shared/default_titlebar.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/throttler.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
class BookViewController {
bool controlsVisible = true;
late ValueNotifier<bool> visibilityChanged = ValueNotifier(controlsVisible);
void toggleControls({bool? value}) {
controlsVisible = value ?? !controlsVisible;
visibilityChanged.value = controlsVisible;
}
}
class BookViewerControls extends ConsumerStatefulWidget {
final AutoDisposeStateNotifierProvider<BookDetailsProviderNotifier, BookProviderModel> provider;
final BookViewController viewController;
final ExtendedPageController controller;
const BookViewerControls({
required this.provider,
required this.controller,
required this.viewController,
super.key,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _BookViewerControlsState();
}
class _BookViewerControlsState extends ConsumerState<BookViewerControls> {
final FocusNode focusNode = FocusNode();
final Throttler throttler = Throttler(duration: const Duration(milliseconds: 130));
final Duration pageAnimDuration = const Duration(milliseconds: 125);
final Curve pageAnimCurve = Curves.easeInCubic;
late final BookViewController viewController = widget.viewController;
late final double topPadding = MediaQuery.of(context).viewPadding.top;
late final double bottomPadding = MediaQuery.of(context).viewPadding.bottom;
bool showControls = true;
void toggleControls({bool? value}) {
setState(() {
showControls = value ?? !showControls;
});
SystemChrome.setEnabledSystemUIMode(!showControls ? SystemUiMode.leanBack : SystemUiMode.edgeToEdge, overlays: []);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
));
}
@override
void initState() {
super.initState();
WakelockPlus.enable();
viewController.visibilityChanged.addListener(() {
toggleControls(value: viewController.controlsVisible);
});
}
@override
void dispose() {
super.dispose();
WakelockPlus.disable();
ScreenBrightness().resetScreenBrightness();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge, overlays: []);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
));
}
@override
Widget build(BuildContext context) {
final details = ref.watch(widget.provider);
final bookViewerSettings = ref.watch(bookViewerSettingsProvider);
final chapters = details.chapters;
final bookViewerDetails = ref.watch(bookViewerProvider);
final currentPage = bookViewerDetails.currentPage;
const overlayColor = Colors.black;
final previousChapter = details.previousChapter(bookViewerDetails.book);
final nextChapter = details.nextChapter(bookViewerDetails.book);
if (AdaptiveLayout.of(context).isDesktop) {
FocusScope.of(context).requestFocus(focusNode);
}
return MediaQuery.removePadding(
context: context,
child: KeyboardListener(
focusNode: focusNode,
autofocus: AdaptiveLayout.of(context).isDesktop,
onKeyEvent: (value) {
if (value is KeyDownEvent) {
if (value.logicalKey == LogicalKeyboardKey.arrowLeft || value.logicalKey == LogicalKeyboardKey.keyA) {
bookViewerSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage();
}
if (value.logicalKey == LogicalKeyboardKey.arrowRight || value.logicalKey == LogicalKeyboardKey.keyD) {
bookViewerSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage();
}
if (value.logicalKey == LogicalKeyboardKey.space) {
toggleControls();
}
}
},
child: Stack(
children: [
IgnorePointer(
ignoring: !showControls,
child: AnimatedOpacity(
duration: const Duration(milliseconds: 500),
opacity: showControls ? 1 : 0,
child: Stack(
children: [
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(1),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(0),
],
),
),
child: Padding(
padding: EdgeInsets.only(top: topPadding).copyWith(bottom: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (AdaptiveLayout.of(context).isDesktop)
const Flexible(
child: DefaultTitleBar(
height: 50,
brightness: Brightness.dark,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const BackButton(),
const SizedBox(
width: 16,
),
Flexible(
child: Text(
bookViewerDetails.book?.name ?? "None",
style: Theme.of(context).textTheme.titleLarge,
),
)
],
),
const SizedBox(height: 16),
],
),
),
),
if (!bookViewerDetails.loading) ...{
if (bookViewerDetails.book != null && bookViewerDetails.pages.isNotEmpty) ...{
Align(
alignment: Alignment.bottomCenter,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
overlayColor.withOpacity(0),
overlayColor.withOpacity(0.65),
overlayColor.withOpacity(1),
],
),
),
child: Padding(
padding: EdgeInsets.only(bottom: bottomPadding).copyWith(top: 16, bottom: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 30),
Row(
children: [
const SizedBox(width: 8),
Tooltip(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter?.name != null
? "Load ${previousChapter?.name}"
: ""
: nextChapter?.name != null
? "Load ${nextChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? previousChapter != null
? () async => await loadNextBook(previousChapter)
: null
: nextChapter != null
? () async => await loadNextBook(nextChapter)
: null,
icon: const Icon(IconsaxOutline.backward),
),
),
const SizedBox(width: 8),
Flexible(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.7),
borderRadius: BorderRadius.circular(60),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Row(
children: [
if (bookViewerSettings.readDirection == ReadDirection.leftToRight)
...controls(currentPage, bookViewerSettings, bookViewerDetails)
else
...controls(currentPage, bookViewerSettings, bookViewerDetails)
.reversed,
],
),
),
),
),
const SizedBox(width: 8),
Tooltip(
message: bookViewerSettings.readDirection == ReadDirection.leftToRight
? nextChapter?.name != null
? "Load ${nextChapter?.name}"
: ""
: previousChapter?.name != null
? "Load ${previousChapter?.name}"
: "",
child: IconButton.filled(
onPressed: bookViewerSettings.readDirection == ReadDirection.leftToRight
? nextChapter != null
? () async => await loadNextBook(nextChapter)
: null
: previousChapter != null
? () async => await loadNextBook(previousChapter)
: null,
icon: const Icon(IconsaxOutline.forward),
),
),
const SizedBox(width: 8),
],
),
const SizedBox(height: 16),
Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Transform.flip(
flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft,
child: IconButton(
onPressed: () => widget.controller
.animateToPage(1, duration: pageAnimDuration, curve: pageAnimCurve),
icon: const Icon(IconsaxOutline.backward)),
),
IconButton(
onPressed: () {
showBookViewerSettings(context);
},
icon: const Icon(IconsaxOutline.setting_2),
),
IconButton(
onPressed: chapters.length > 1
? () {
showBookViewerChapters(
context,
widget.provider,
onPressed: (book) async {
Navigator.of(context).pop();
loadNextBook(book);
},
);
}
: () => fladderSnackbar(context, title: "No other chapters"),
icon: const Icon(IconsaxOutline.bookmark_2),
)
],
),
],
),
),
),
),
} else
const Center(
child: Card(
child: Padding(
padding: EdgeInsets.all(8.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.menu_book_rounded),
SizedBox(width: 8),
Text("Unable to load book"),
],
),
),
),
)
},
],
),
),
),
if (bookViewerDetails.loading)
Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (bookViewerDetails.book != null) ...{
Flexible(
child: Text("Loading ${bookViewerDetails.book?.name}",
style: Theme.of(context).textTheme.titleMedium),
),
const SizedBox(width: 16),
},
const CircularProgressIndicator.adaptive(strokeCap: StrokeCap.round),
],
),
),
),
)
],
),
),
);
}
List<Widget> controls(int currentPage, BookViewerSettingsModel bookViewerSettings, BookViewerModel details) {
final clampedCurrentPage = currentPage.clamp(1, details.pages.length);
return [
const SizedBox(width: 6),
Text(
(currentPage.clamp(1, details.pages.length)).toInt().toString().padLeft(1).padRight(1),
style: Theme.of(context).textTheme.titleMedium,
),
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Transform.flip(
flipX: bookViewerSettings.readDirection == ReadDirection.rightToLeft,
child: SizedBox(
height: 40,
child: FladderSlider(
value: clampedCurrentPage.toDouble(),
divisions: details.pages.length - 1,
min: 1,
max: details.pages.length.toDouble(),
onChangeEnd: (value) => widget.controller.jumpToPage(value.toInt()),
onChanged: (value) => ref.read(bookViewerProvider.notifier).setPage(value),
),
),
),
),
),
Text(
details.pages.length.toString().padLeft(1).padRight(1),
style: Theme.of(context).textTheme.titleMedium,
),
];
}
Future<void> loadNextBook(BookModel? book) async {
await ref.read(bookViewerProvider.notifier).fetchBook(book);
widget.controller.jumpToPage(0);
return;
}
Future<void> nextPage() async =>
throttler.run(() async => await widget.controller.nextPage(duration: pageAnimDuration, curve: pageAnimCurve));
Future<void> previousPage() async =>
throttler.run(() async => await widget.controller.previousPage(duration: pageAnimDuration, curve: pageAnimCurve));
}

View file

@ -0,0 +1,119 @@
import 'dart:io';
import 'dart:math' as math;
import 'package:extended_image/extended_image.dart';
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
import 'package:fladder/screens/book_viewer/book_viewer_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BookViewerReader extends ConsumerWidget {
final int index;
final List<String> pages;
final BookViewerSettingsModel bookViewSettings;
final Function() previousPage;
final Function() nextPage;
final BookViewController viewController;
final double lastScale;
final Function(double value) newScale;
const BookViewerReader({
required this.index,
required this.pages,
required this.bookViewSettings,
required this.previousPage,
required this.nextPage,
required this.viewController,
required this.lastScale,
required this.newScale,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
double? initScale({
required Size imageSize,
required Size size,
double? initialScale,
}) {
final double n1 = imageSize.height / imageSize.width;
final double n2 = size.height / size.width;
if (n1 > n2) {
final FittedSizes fittedSizes = applyBoxFit(BoxFit.cover, imageSize, size);
//final Size sourceSize = fittedSizes.source;
final Size destinationSize = fittedSizes.destination;
return size.width / destinationSize.width;
} else if (n1 / n2 < 1 / 4) {
final FittedSizes fittedSizes = applyBoxFit(BoxFit.cover, imageSize, size);
//final Size sourceSize = fittedSizes.source;
final Size destinationSize = fittedSizes.destination;
return size.height / destinationSize.height;
}
return initialScale;
}
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTapUp: (tapDetails) {
double screenWidth = MediaQuery.of(context).size.width;
double tapPosition = tapDetails.globalPosition.dx;
double tapPercentage = tapPosition / screenWidth;
if (tapPercentage < 0.22) {
bookViewSettings.readDirection == ReadDirection.leftToRight ? previousPage() : nextPage();
} else if (tapPercentage < 0.88) {
viewController.toggleControls();
} else {
bookViewSettings.readDirection == ReadDirection.leftToRight ? nextPage() : previousPage();
}
},
child: ExtendedImage.file(
fit: BoxFit.contain,
imageCacheName: pages[index - 1],
mode: ExtendedImageMode.gesture,
initGestureConfigHandler: (state) {
double? initialScale = !bookViewSettings.keepPageZoom
? switch (bookViewSettings.initZoomState) {
InitZoomState.contained => 1.0,
InitZoomState.covered => 1.75,
}
: lastScale;
if (state.extendedImageInfo != null) {
initialScale = initScale(
size: MediaQuery.sizeOf(context),
initialScale: initialScale,
imageSize: Size(
state.extendedImageInfo!.image.width.toDouble(), state.extendedImageInfo!.image.height.toDouble()));
}
return GestureConfig(
inertialSpeed: 300,
inPageView: true,
initialScale: initialScale!,
initialAlignment: bookViewSettings.initZoomState == InitZoomState.contained && initialScale == 1.0
? InitialAlignment.center
: switch (bookViewSettings.readDirection) {
ReadDirection.rightToLeft => InitialAlignment.topRight,
ReadDirection.leftToRight => InitialAlignment.topLeft,
},
reverseMousePointerScrollDirection: true,
maxScale: math.max(initialScale, 5.0),
minScale: math.min(initialScale, 1),
animationMaxScale: math.max(initialScale, 5.0),
gestureDetailsIsChanged: (details) {
if (bookViewSettings.keepPageZoom) {
if (lastScale != (details?.totalScale ?? initialScale)) {
newScale(details?.totalScale ?? 1.0);
}
}
},
cacheGesture: bookViewSettings.cachePageZoom,
hitTestBehavior: HitTestBehavior.translucent,
);
},
File(pages[index - 1]),
enableMemoryCache: true,
),
);
}
}

View file

@ -0,0 +1,33 @@
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
import 'package:fladder/screens/book_viewer/book_viewer_controls.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class BookViewerReader extends ConsumerWidget {
final int index;
final List<String> pages;
final BookViewerSettingsModel bookViewSettings;
final Function() previousPage;
final Function() nextPage;
final BookViewController viewController;
final double lastScale;
final Function(double value) newScale;
const BookViewerReader({
required this.index,
required this.pages,
required this.bookViewSettings,
required this.previousPage,
required this.nextPage,
required this.viewController,
required this.lastScale,
required this.newScale,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Container(
child: Text("Web not supported."),
);
}
}

View file

@ -0,0 +1,270 @@
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),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info_rounded),
const 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),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.info_rounded),
const 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;
}
}

View file

@ -0,0 +1,187 @@
import 'package:fladder/providers/settings/book_viewer_settings_provider.dart';
import 'package:fladder/util/adaptive_layout.dart';
import 'package:fladder/util/string_extensions.dart';
import 'package:fladder/widgets/shared/enum_selection.dart';
import 'package:fladder/widgets/shared/fladder_slider.dart';
import 'package:fladder/widgets/shared/modal_side_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Future<void> showBookViewerSettings(
BuildContext context,
) async {
if (AdaptiveLayout.of(context).isDesktop) {
return showModalSideSheet(context, content: const BookViewerSettingsScreen());
} else {
return showModalBottomSheet(
context: context,
showDragHandle: true,
builder: (context) => const BookViewerSettingsScreen(),
);
}
}
class BookViewerSettingsScreen extends ConsumerWidget {
const BookViewerSettingsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(bookViewerSettingsProvider);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"Reader settings",
style: Theme.of(context).textTheme.titleLarge,
),
),
),
const Divider(),
if (!AdaptiveLayout.of(context).isDesktop) ...{
ListTile(
title: Row(
children: [
const Text("Screen Brightness"),
Flexible(
child: Opacity(
opacity: settings.screenBrightness == null ? 0.5 : 1,
child: FladderSlider(
value: settings.screenBrightness ?? 1.0,
min: 0,
max: 1,
onChanged: (value) => ref.read(bookViewerSettingsProvider.notifier).setScreenBrightness(value),
),
),
),
IconButton(
onPressed: () => ref.read(bookViewerSettingsProvider.notifier).setScreenBrightness(null),
icon: Opacity(
opacity: settings.screenBrightness != null ? 0.5 : 1,
child: Icon(
Icons.brightness_auto_rounded,
color: Theme.of(context).colorScheme.primary,
),
),
)
],
),
),
},
ListTile(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: EnumSelection(
label: const Text("Read direction"),
current: settings.readDirection.name.toUpperCaseSplit(),
itemBuilder: (context) => ReadDirection.values
.map((value) => PopupMenuItem(
value: value,
child: Text(value.name.toUpperCaseSplit()),
onTap: () => ref
.read(bookViewerSettingsProvider.notifier)
.update((state) => state.copyWith(readDirection: value)),
))
.toList(),
),
),
],
),
),
ListTile(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: EnumSelection(
label: const Text("Init zoom"),
current: settings.initZoomState.name.toUpperCaseSplit(),
itemBuilder: (context) => InitZoomState.values
.map((value) => PopupMenuItem(
value: value,
child: Text(value.name.toUpperCaseSplit()),
onTap: () => ref
.read(bookViewerSettingsProvider.notifier)
.update((state) => state.copyWith(initZoomState: value)),
))
.toList(),
),
),
],
),
),
ListTile(
onTap: () => ref
.read(bookViewerSettingsProvider.notifier)
.update((state) => state.copyWith(disableScrollOnZoom: !settings.disableScrollOnZoom)),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Expanded(
flex: 3,
child: Text("Disable slide page gestures when zoomed"),
),
const Spacer(),
Switch.adaptive(
value: settings.disableScrollOnZoom,
onChanged: (value) => ref
.read(bookViewerSettingsProvider.notifier)
.update((state) => state.copyWith(disableScrollOnZoom: value)),
)
],
),
),
ListTile(
onTap: () => ref
.read(bookViewerSettingsProvider.notifier)
.update((state) => state.copyWith(cachePageZoom: !settings.cachePageZoom)),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Expanded(
flex: 3,
child: Text("Cache page zoom state"),
),
const Spacer(),
Switch.adaptive(
value: settings.cachePageZoom,
onChanged: (value) => ref
.read(bookViewerSettingsProvider.notifier)
.update((incoming) => incoming.copyWith(cachePageZoom: value)),
)
],
),
),
ListTile(
onTap: () => ref
.read(bookViewerSettingsProvider.notifier)
.update((state) => state.copyWith(keepPageZoom: !settings.keepPageZoom)),
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Expanded(
flex: 3,
child: Text("Keep page zoom"),
),
const Spacer(),
Switch.adaptive(
value: settings.keepPageZoom,
onChanged: (value) => ref
.read(bookViewerSettingsProvider.notifier)
.update((incoming) => incoming.copyWith(keepPageZoom: value)),
)
],
),
),
SizedBox(
height: MediaQuery.of(context).padding.bottom,
)
],
);
}
}