From e9675d45680d93038c7f3234f76be8f0c6774ed0 Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Wed, 17 Jun 2026 21:37:37 +0200 Subject: [PATCH 1/7] feat: add screen dimming to reader Adds a screen dimming overlay to reduce display brightness for improved reading comfort. Users can set a persistent dim level via a new option in the reader settings. The dim level can also be adjusted dynamically by vertically dragging on the left side of the screen while in the reader, with a visual indicator showing the current percentage. --- .../epub_reader/epub_reader_controls.dart | 16 ++++ .../image_reader/image_reader_controls.dart | 15 ++++ lib/pages/reader/overlay/reader_overlay.dart | 80 ++++++++++++++++++- .../pdf_reader/pdf_reader_controls.dart | 17 ++++ .../settings/reader_dim_settings.dart | 67 ++++++++++++++++ 5 files changed, 194 insertions(+), 1 deletion(-) create mode 100644 lib/riverpod/providers/settings/reader_dim_settings.dart diff --git a/lib/pages/reader/epub_reader/epub_reader_controls.dart b/lib/pages/reader/epub_reader/epub_reader_controls.dart index a6312980..14db1773 100644 --- a/lib/pages/reader/epub_reader/epub_reader_controls.dart +++ b/lib/pages/reader/epub_reader/epub_reader_controls.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:kover/models/read_direction.dart'; import 'package:kover/riverpod/providers/settings/epub_reader_settings.dart'; +import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/constants/kover_icons.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/widgets/settings/boolean_option.dart'; @@ -18,6 +19,9 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final provider = epubReaderSettingsProvider(seriesId: seriesId); + final dimLevel = + ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; + return Async( asyncValue: ref.watch(provider), data: (settings) { @@ -145,6 +149,18 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget { .setShowProgressBar(value); }, ), + NumericOption( + title: 'Screen Dimming', + icon: LucideIcons.sunMedium, + value: dimLevel * 100, + min: 0, + max: 90, + step: ReaderDimSettingsLimits.dimStep, + decimalPlaces: 0, + onChanged: (newValue) async => await ref + .read(readerDimSettingsProvider.notifier) + .setDimLevel(newValue / 100), + ), ], ), ), diff --git a/lib/pages/reader/image_reader/image_reader_controls.dart b/lib/pages/reader/image_reader/image_reader_controls.dart index 857d9532..6b259e35 100644 --- a/lib/pages/reader/image_reader/image_reader_controls.dart +++ b/lib/pages/reader/image_reader/image_reader_controls.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:kover/models/read_direction.dart'; import 'package:kover/riverpod/providers/breakpoints.dart'; import 'package:kover/riverpod/providers/settings/image_reader_settings.dart'; +import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/constants/kover_icons.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/widgets/settings/boolean_option.dart'; @@ -20,6 +21,8 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final provider = imageReaderSettingsProvider(seriesId: seriesId); final breakpoint = ref.watch(breakpointsProvider); + final dimLevel = + ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; return Async( asyncValue: ref.watch(provider), data: (settings) { @@ -202,6 +205,18 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget { .read(provider.notifier) .setShowProgressBar(newValue), ), + NumericOption( + title: 'Screen Dimming', + icon: LucideIcons.sunMedium, + value: dimLevel * 100, + min: 0, + max: 90, + step: ReaderDimSettingsLimits.dimStep, + decimalPlaces: 0, + onChanged: (newValue) async => await ref + .read(readerDimSettingsProvider.notifier) + .setDimLevel(newValue / 100), + ), ], ), ), diff --git a/lib/pages/reader/overlay/reader_overlay.dart b/lib/pages/reader/overlay/reader_overlay.dart index 75403ec3..c08c0a78 100644 --- a/lib/pages/reader/overlay/reader_overlay.dart +++ b/lib/pages/reader/overlay/reader_overlay.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -10,6 +12,7 @@ import 'package:kover/riverpod/providers/reader//reader.dart'; import 'package:kover/riverpod/providers/reader/epub_reader.dart'; import 'package:kover/riverpod/providers/reader/reader_navigation.dart'; import 'package:kover/riverpod/providers/router.dart'; +import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/utils/logging.dart'; import 'package:kover/widgets/util/async_value.dart'; @@ -63,6 +66,8 @@ class ReaderOverlay extends HookConsumerWidget { final uiVisible = useState(false); final snackbarDismissed = useState(false); final showSnackbar = useState(ShowSnackbar.none); + final isDimming = useState(false); + final dimHideTimer = useRef(null); final provider = readerProvider( seriesId: seriesId, chapterId: chapterId, @@ -95,6 +100,9 @@ class ReaderOverlay extends HookConsumerWidget { ), ); + final dimLevel = + ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; + ref.listen( readerNavigationProvider( seriesId: seriesId, @@ -167,14 +175,42 @@ class ReaderOverlay extends HookConsumerWidget { ], ), ), + if (dimLevel > 0) + Positioned.fill( + child: IgnorePointer( + child: ColoredBox( + color: Color.fromRGBO(0, 0, 0, dimLevel), + ), + ), + ), Positioned.fill( child: Row( children: [ Flexible( flex: 1, child: GestureDetector( - behavior: .translucent, + behavior: .opaque, onTap: onPreviousPage, + onVerticalDragStart: (_) { + dimHideTimer.value?.cancel(); + isDimming.value = true; + }, + onVerticalDragUpdate: (details) { + final screenHeight = + MediaQuery.sizeOf(context).height; + final delta = + (details.delta.dy / screenHeight) * 0.9; + ref + .read(readerDimSettingsProvider.notifier) + .adjustDimLevel(delta); + }, + onVerticalDragEnd: (_) { + dimHideTimer.value?.cancel(); + dimHideTimer.value = Timer( + const Duration(milliseconds: 600), + () => isDimming.value = false, + ); + }, ), ), Flexible( @@ -291,6 +327,48 @@ class ReaderOverlay extends HookConsumerWidget { .show(duration: 10.ms, maintain: false) .fade(duration: 100.ms), ), + IgnorePointer( + child: AnimatedOpacity( + opacity: isDimming.value ? 1.0 : 0.0, + duration: const Duration(milliseconds: 200), + child: Align( + alignment: .centerLeft, + child: Padding( + padding: const EdgeInsets.only( + left: LayoutConstants.largePadding, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: LayoutConstants.mediumPadding, + vertical: LayoutConstants.mediumPadding, + ), + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular( + LayoutConstants.mediumPadding, + ), + ), + child: Column( + mainAxisSize: .min, + children: [ + const Icon( + Icons.brightness_6, + color: Colors.white, + ), + Text( + '${(dimLevel * 100).round()}%', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + ), + ), + ], + ), + ), + ), + ), + ), + ), ], ), ), diff --git a/lib/pages/reader/pdf_reader/pdf_reader_controls.dart b/lib/pages/reader/pdf_reader/pdf_reader_controls.dart index 08b53895..fc831508 100644 --- a/lib/pages/reader/pdf_reader/pdf_reader_controls.dart +++ b/lib/pages/reader/pdf_reader/pdf_reader_controls.dart @@ -2,11 +2,14 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:kover/models/read_direction.dart'; import 'package:kover/riverpod/providers/settings/pdf_reader_settings.dart'; +import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/constants/kover_icons.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/widgets/settings/boolean_option.dart'; import 'package:kover/widgets/settings/choice_option.dart'; +import 'package:kover/widgets/settings/numeric_option.dart'; import 'package:kover/widgets/util/async_value.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; class PdfReaderSettingsBottomSheet extends ConsumerWidget { final int seriesId; @@ -15,6 +18,8 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final provider = pdfReaderSettingsProvider(seriesId: seriesId); + final dimLevel = + ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; return Async( asyncValue: ref.watch(provider), @@ -110,6 +115,18 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget { .setShowProgressBar(newValue); }, ), + NumericOption( + title: 'Screen Dimming', + icon: LucideIcons.sunMedium, + value: dimLevel * 100, + min: 0, + max: 90, + step: ReaderDimSettingsLimits.dimStep, + decimalPlaces: 0, + onChanged: (newValue) async => await ref + .read(readerDimSettingsProvider.notifier) + .setDimLevel(newValue / 100), + ), ], ), ), diff --git a/lib/riverpod/providers/settings/reader_dim_settings.dart b/lib/riverpod/providers/settings/reader_dim_settings.dart new file mode 100644 index 00000000..61dee949 --- /dev/null +++ b/lib/riverpod/providers/settings/reader_dim_settings.dart @@ -0,0 +1,67 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hooks_riverpod/experimental/persist.dart'; +import 'package:kover/riverpod/repository/storage_repository.dart'; +import 'package:kover/utils/logging.dart'; +import 'package:riverpod_annotation/experimental/json_persist.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'reader_dim_settings.freezed.dart'; +part 'reader_dim_settings.g.dart'; + +sealed class ReaderDimSettingsLimits { + static const double dimMin = 0.0; + static const double dimMax = 0.9; + static const double dimStep = 5.0; +} + +@freezed +sealed class ReaderDimSettingsState with _$ReaderDimSettingsState { + const ReaderDimSettingsState._(); + const factory ReaderDimSettingsState({ + @Default(0.0) double dimLevel, + }) = _ReaderDimSettingsState; + + factory ReaderDimSettingsState.fromJson(Map json) => + _$ReaderDimSettingsStateFromJson(json); +} + +@riverpod +@JsonPersist() +class ReaderDimSettings extends _$ReaderDimSettings { + @override + Future build() async { + await persist( + ref.watch(storageProvider.future), + options: const StorageOptions(cacheTime: StorageCacheTime.unsafe_forever), + ).future; + return state.value ?? const ReaderDimSettingsState(); + } + + Future adjustDimLevel(double delta) async { + final current = await future; + state = AsyncData( + current.copyWith( + dimLevel: (current.dimLevel + delta).clamp( + ReaderDimSettingsLimits.dimMin, + ReaderDimSettingsLimits.dimMax, + ), + ), + ); + } + + Future setDimLevel(double level) async { + final current = await future; + state = AsyncData( + current.copyWith( + dimLevel: level.clamp( + ReaderDimSettingsLimits.dimMin, + ReaderDimSettingsLimits.dimMax, + ), + ), + ); + log.info( + 'set dim level', + attributes: {'value': .double(level)}, + ); + } +} From d4221e743aabd88b9e7e3cafe7c5dd944c386b2b Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Wed, 17 Jun 2026 22:08:36 +0200 Subject: [PATCH 2/7] refactor: polish screen dimming feature - Fix timer leak: cancel dimHideTimer on widget dispose via useEffect - Revert GestureDetector to .translucent so epub scroll gestures pass through - Move progress bar above dim overlay in stack so it stays visible when dimmed - Extract shared DimOption widget to eliminate triplication across epub/image/pdf controls - Remove redundant async/await on void onChanged callbacks - Add missing log to adjustDimLevel for consistent observability - Replace magic fontSize 12 with LayoutConstants.smallerIcon Co-Authored-By: Claude Sonnet 4.6 --- .../epub_reader/epub_reader_controls.dart | 18 +----- .../image_reader/image_reader_controls.dart | 17 +---- lib/pages/reader/overlay/reader_overlay.dart | 62 ++++++++++--------- .../pdf_reader/pdf_reader_controls.dart | 19 +----- .../settings/reader_dim_settings.dart | 4 ++ lib/widgets/settings/dim_option.dart | 27 ++++++++ 6 files changed, 71 insertions(+), 76 deletions(-) create mode 100644 lib/widgets/settings/dim_option.dart diff --git a/lib/pages/reader/epub_reader/epub_reader_controls.dart b/lib/pages/reader/epub_reader/epub_reader_controls.dart index 14db1773..4037daaf 100644 --- a/lib/pages/reader/epub_reader/epub_reader_controls.dart +++ b/lib/pages/reader/epub_reader/epub_reader_controls.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:kover/models/read_direction.dart'; import 'package:kover/riverpod/providers/settings/epub_reader_settings.dart'; -import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/constants/kover_icons.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/widgets/settings/boolean_option.dart'; import 'package:kover/widgets/settings/choice_option.dart'; +import 'package:kover/widgets/settings/dim_option.dart'; import 'package:kover/widgets/settings/numeric_option.dart'; import 'package:kover/widgets/util/async_value.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; @@ -19,9 +19,6 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final provider = epubReaderSettingsProvider(seriesId: seriesId); - final dimLevel = - ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; - return Async( asyncValue: ref.watch(provider), data: (settings) { @@ -149,18 +146,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget { .setShowProgressBar(value); }, ), - NumericOption( - title: 'Screen Dimming', - icon: LucideIcons.sunMedium, - value: dimLevel * 100, - min: 0, - max: 90, - step: ReaderDimSettingsLimits.dimStep, - decimalPlaces: 0, - onChanged: (newValue) async => await ref - .read(readerDimSettingsProvider.notifier) - .setDimLevel(newValue / 100), - ), + const DimOption(), ], ), ), diff --git a/lib/pages/reader/image_reader/image_reader_controls.dart b/lib/pages/reader/image_reader/image_reader_controls.dart index 6b259e35..669aed8c 100644 --- a/lib/pages/reader/image_reader/image_reader_controls.dart +++ b/lib/pages/reader/image_reader/image_reader_controls.dart @@ -3,11 +3,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:kover/models/read_direction.dart'; import 'package:kover/riverpod/providers/breakpoints.dart'; import 'package:kover/riverpod/providers/settings/image_reader_settings.dart'; -import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/constants/kover_icons.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/widgets/settings/boolean_option.dart'; import 'package:kover/widgets/settings/choice_option.dart'; +import 'package:kover/widgets/settings/dim_option.dart'; import 'package:kover/widgets/settings/numeric_option.dart'; import 'package:kover/widgets/util/async_value.dart'; import 'package:lucide_icons_flutter/lucide_icons.dart'; @@ -21,8 +21,6 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final provider = imageReaderSettingsProvider(seriesId: seriesId); final breakpoint = ref.watch(breakpointsProvider); - final dimLevel = - ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; return Async( asyncValue: ref.watch(provider), data: (settings) { @@ -205,18 +203,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget { .read(provider.notifier) .setShowProgressBar(newValue), ), - NumericOption( - title: 'Screen Dimming', - icon: LucideIcons.sunMedium, - value: dimLevel * 100, - min: 0, - max: 90, - step: ReaderDimSettingsLimits.dimStep, - decimalPlaces: 0, - onChanged: (newValue) async => await ref - .read(readerDimSettingsProvider.notifier) - .setDimLevel(newValue / 100), - ), + const DimOption(), ], ), ), diff --git a/lib/pages/reader/overlay/reader_overlay.dart b/lib/pages/reader/overlay/reader_overlay.dart index c08c0a78..37b14604 100644 --- a/lib/pages/reader/overlay/reader_overlay.dart +++ b/lib/pages/reader/overlay/reader_overlay.dart @@ -68,6 +68,9 @@ class ReaderOverlay extends HookConsumerWidget { final showSnackbar = useState(ShowSnackbar.none); final isDimming = useState(false); final dimHideTimer = useRef(null); + useEffect(() { + return () => dimHideTimer.value?.cancel(); + }, const []); final provider = readerProvider( seriesId: seriesId, chapterId: chapterId, @@ -149,32 +152,7 @@ class ReaderOverlay extends HookConsumerWidget { }, child: Stack( children: [ - Positioned.fill( - child: Column( - mainAxisSize: .min, - children: [ - Expanded(child: child), - if (showProgressBar && state.series.format == .epub) - SubpageProgress( - seriesId: seriesId, - chapterId: chapterId, - ) - .animate( - target: uiVisible.value ? 0.0 : 1.0, - ) - .fadeIn(duration: 200.ms) - else if (showProgressBar) - ReaderProgress( - seriesId: seriesId, - chapterId: chapterId, - ) - .animate( - target: uiVisible.value ? 0.0 : 1.0, - ) - .fadeIn(duration: 200.ms), - ], - ), - ), + Positioned.fill(child: child), if (dimLevel > 0) Positioned.fill( child: IgnorePointer( @@ -183,13 +161,41 @@ class ReaderOverlay extends HookConsumerWidget { ), ), ), + if (showProgressBar && state.series.format == .epub) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: SubpageProgress( + seriesId: seriesId, + chapterId: chapterId, + ) + .animate( + target: uiVisible.value ? 0.0 : 1.0, + ) + .fadeIn(duration: 200.ms), + ) + else if (showProgressBar) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: ReaderProgress( + seriesId: seriesId, + chapterId: chapterId, + ) + .animate( + target: uiVisible.value ? 0.0 : 1.0, + ) + .fadeIn(duration: 200.ms), + ), Positioned.fill( child: Row( children: [ Flexible( flex: 1, child: GestureDetector( - behavior: .opaque, + behavior: .translucent, onTap: onPreviousPage, onVerticalDragStart: (_) { dimHideTimer.value?.cancel(); @@ -359,7 +365,7 @@ class ReaderOverlay extends HookConsumerWidget { '${(dimLevel * 100).round()}%', style: const TextStyle( color: Colors.white, - fontSize: 12, + fontSize: LayoutConstants.smallerIcon, ), ), ], diff --git a/lib/pages/reader/pdf_reader/pdf_reader_controls.dart b/lib/pages/reader/pdf_reader/pdf_reader_controls.dart index fc831508..9e6d3e42 100644 --- a/lib/pages/reader/pdf_reader/pdf_reader_controls.dart +++ b/lib/pages/reader/pdf_reader/pdf_reader_controls.dart @@ -2,14 +2,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:kover/models/read_direction.dart'; import 'package:kover/riverpod/providers/settings/pdf_reader_settings.dart'; -import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; import 'package:kover/utils/constants/kover_icons.dart'; import 'package:kover/utils/layout_constants.dart'; import 'package:kover/widgets/settings/boolean_option.dart'; import 'package:kover/widgets/settings/choice_option.dart'; -import 'package:kover/widgets/settings/numeric_option.dart'; +import 'package:kover/widgets/settings/dim_option.dart'; import 'package:kover/widgets/util/async_value.dart'; -import 'package:lucide_icons_flutter/lucide_icons.dart'; class PdfReaderSettingsBottomSheet extends ConsumerWidget { final int seriesId; @@ -18,8 +16,6 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final provider = pdfReaderSettingsProvider(seriesId: seriesId); - final dimLevel = - ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; return Async( asyncValue: ref.watch(provider), @@ -115,18 +111,7 @@ class PdfReaderSettingsBottomSheet extends ConsumerWidget { .setShowProgressBar(newValue); }, ), - NumericOption( - title: 'Screen Dimming', - icon: LucideIcons.sunMedium, - value: dimLevel * 100, - min: 0, - max: 90, - step: ReaderDimSettingsLimits.dimStep, - decimalPlaces: 0, - onChanged: (newValue) async => await ref - .read(readerDimSettingsProvider.notifier) - .setDimLevel(newValue / 100), - ), + const DimOption(), ], ), ), diff --git a/lib/riverpod/providers/settings/reader_dim_settings.dart b/lib/riverpod/providers/settings/reader_dim_settings.dart index 61dee949..f314d77b 100644 --- a/lib/riverpod/providers/settings/reader_dim_settings.dart +++ b/lib/riverpod/providers/settings/reader_dim_settings.dart @@ -47,6 +47,10 @@ class ReaderDimSettings extends _$ReaderDimSettings { ), ), ); + log.info( + 'adjust dim level', + attributes: {'value': .double(state.value!.dimLevel)}, + ); } Future setDimLevel(double level) async { diff --git a/lib/widgets/settings/dim_option.dart b/lib/widgets/settings/dim_option.dart new file mode 100644 index 00000000..9dd82609 --- /dev/null +++ b/lib/widgets/settings/dim_option.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:kover/riverpod/providers/settings/reader_dim_settings.dart'; +import 'package:kover/widgets/settings/numeric_option.dart'; +import 'package:lucide_icons_flutter/lucide_icons.dart'; + +class DimOption extends ConsumerWidget { + const DimOption({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dimLevel = + ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; + + return NumericOption( + title: 'Screen Dimming', + icon: LucideIcons.sunMedium, + value: dimLevel * 100, + min: 0, + max: 90, + step: ReaderDimSettingsLimits.dimStep, + decimalPlaces: 0, + onChanged: (newValue) => + ref.read(readerDimSettingsProvider.notifier).setDimLevel(newValue / 100), + ); + } +} From c8bd1058ee979d1ce7fbf9c75040f001a1ca138b Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Thu, 18 Jun 2026 10:04:02 +0200 Subject: [PATCH 3/7] refactor: robustify reader dim level retrieval The `readerDimSettingsProvider` returns an `AsyncValue`. Switching from `valueOrNull` to `maybeWhen` provides a more explicit and safer way to extract the `dimLevel` only when the provider is in a `data` state, falling back to a default value for `loading` or `error` states. This enhances the reliability of the dimming feature. Updates `workmanager_apple` dependency in `Podfile.lock`. --- ios/Podfile.lock | 2 +- lib/pages/reader/overlay/reader_overlay.dart | 5 ++++- lib/widgets/settings/dim_option.dart | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 588f1162..59b82ca6 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -15,7 +15,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 + workmanager_apple: 7bac258335c310689a641e2d66e88d4845d372e9 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e diff --git a/lib/pages/reader/overlay/reader_overlay.dart b/lib/pages/reader/overlay/reader_overlay.dart index 37b14604..ab5303c1 100644 --- a/lib/pages/reader/overlay/reader_overlay.dart +++ b/lib/pages/reader/overlay/reader_overlay.dart @@ -104,7 +104,10 @@ class ReaderOverlay extends HookConsumerWidget { ); final dimLevel = - ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; + ref.watch(readerDimSettingsProvider).maybeWhen( + data: (state) => state.dimLevel, + orElse: () => 0.0, + ); ref.listen( readerNavigationProvider( diff --git a/lib/widgets/settings/dim_option.dart b/lib/widgets/settings/dim_option.dart index 9dd82609..2269472b 100644 --- a/lib/widgets/settings/dim_option.dart +++ b/lib/widgets/settings/dim_option.dart @@ -10,7 +10,10 @@ class DimOption extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final dimLevel = - ref.watch(readerDimSettingsProvider).valueOrNull?.dimLevel ?? 0.0; + ref.watch(readerDimSettingsProvider).maybeWhen( + data: (state) => state.dimLevel, + orElse: () => 0.0, + ); return NumericOption( title: 'Screen Dimming', From be64f6751c70489ce86f0f4e5cbd6bc6de56aa58 Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Thu, 18 Jun 2026 21:30:33 +0200 Subject: [PATCH 4/7] Remove vertical drag dimming gesture from reader overlay - Remove vertical drag handlers from left tap zone in reader overlay - Keep dimming adjustment available through settings menu - Preserve visual dimming overlay effect when settings are changed - Clean up unused state variables (isDimming, dimHideTimer) - Remove on-screen dimming indicator that appeared during dragging Users can still control screen dimming via the DimOption widget in reader settings, but won't accidentally trigger it while navigating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/pages/reader/overlay/reader_overlay.dart | 69 -------------------- 1 file changed, 69 deletions(-) diff --git a/lib/pages/reader/overlay/reader_overlay.dart b/lib/pages/reader/overlay/reader_overlay.dart index ab5303c1..514cda83 100644 --- a/lib/pages/reader/overlay/reader_overlay.dart +++ b/lib/pages/reader/overlay/reader_overlay.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -66,11 +64,6 @@ class ReaderOverlay extends HookConsumerWidget { final uiVisible = useState(false); final snackbarDismissed = useState(false); final showSnackbar = useState(ShowSnackbar.none); - final isDimming = useState(false); - final dimHideTimer = useRef(null); - useEffect(() { - return () => dimHideTimer.value?.cancel(); - }, const []); final provider = readerProvider( seriesId: seriesId, chapterId: chapterId, @@ -200,26 +193,6 @@ class ReaderOverlay extends HookConsumerWidget { child: GestureDetector( behavior: .translucent, onTap: onPreviousPage, - onVerticalDragStart: (_) { - dimHideTimer.value?.cancel(); - isDimming.value = true; - }, - onVerticalDragUpdate: (details) { - final screenHeight = - MediaQuery.sizeOf(context).height; - final delta = - (details.delta.dy / screenHeight) * 0.9; - ref - .read(readerDimSettingsProvider.notifier) - .adjustDimLevel(delta); - }, - onVerticalDragEnd: (_) { - dimHideTimer.value?.cancel(); - dimHideTimer.value = Timer( - const Duration(milliseconds: 600), - () => isDimming.value = false, - ); - }, ), ), Flexible( @@ -336,48 +309,6 @@ class ReaderOverlay extends HookConsumerWidget { .show(duration: 10.ms, maintain: false) .fade(duration: 100.ms), ), - IgnorePointer( - child: AnimatedOpacity( - opacity: isDimming.value ? 1.0 : 0.0, - duration: const Duration(milliseconds: 200), - child: Align( - alignment: .centerLeft, - child: Padding( - padding: const EdgeInsets.only( - left: LayoutConstants.largePadding, - ), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: LayoutConstants.mediumPadding, - vertical: LayoutConstants.mediumPadding, - ), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular( - LayoutConstants.mediumPadding, - ), - ), - child: Column( - mainAxisSize: .min, - children: [ - const Icon( - Icons.brightness_6, - color: Colors.white, - ), - Text( - '${(dimLevel * 100).round()}%', - style: const TextStyle( - color: Colors.white, - fontSize: LayoutConstants.smallerIcon, - ), - ), - ], - ), - ), - ), - ), - ), - ), ], ), ), From f87e725d1820c74481135bf7c69bd638114fe438 Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Thu, 18 Jun 2026 21:32:52 +0200 Subject: [PATCH 5/7] Reorder Screen Dimming slider to appear below Margins - Move DimOption widget to appear directly after Margins slider - Applied to both ePub reader and Image reader settings drawers - Improves logical grouping of reader customization options Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- lib/pages/reader/epub_reader/epub_reader_controls.dart | 2 +- lib/pages/reader/image_reader/image_reader_controls.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pages/reader/epub_reader/epub_reader_controls.dart b/lib/pages/reader/epub_reader/epub_reader_controls.dart index 4037daaf..6f1f216d 100644 --- a/lib/pages/reader/epub_reader/epub_reader_controls.dart +++ b/lib/pages/reader/epub_reader/epub_reader_controls.dart @@ -92,6 +92,7 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget { .read(provider.notifier) .setMarginSize(newValue), ), + const DimOption(), NumericOption( title: 'Line Height', @@ -146,7 +147,6 @@ class EpubReaderSettingsBottomSheet extends ConsumerWidget { .setShowProgressBar(value); }, ), - const DimOption(), ], ), ), diff --git a/lib/pages/reader/image_reader/image_reader_controls.dart b/lib/pages/reader/image_reader/image_reader_controls.dart index 669aed8c..7e1eac04 100644 --- a/lib/pages/reader/image_reader/image_reader_controls.dart +++ b/lib/pages/reader/image_reader/image_reader_controls.dart @@ -151,6 +151,7 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget { .read(provider.notifier) .setVerticalReaderPadding(newValue), ), + const DimOption(), NumericOption( title: 'Vertical Gap', icon: LucideIcons.unfoldVertical, @@ -203,7 +204,6 @@ class ImageReaderSettingsBottomSheet extends ConsumerWidget { .read(provider.notifier) .setShowProgressBar(newValue), ), - const DimOption(), ], ), ), From 2461e2f712a68a9d7244462617c56c27b673d3cf Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Thu, 18 Jun 2026 21:35:33 +0200 Subject: [PATCH 6/7] revert podfile lock --- ios/Podfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 59b82ca6..588f1162 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -15,7 +15,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 - workmanager_apple: 7bac258335c310689a641e2d66e88d4845d372e9 + workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778 PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e From 1dcb0aa6a1ed1da0fea1f478a36d58696f61c1f5 Mon Sep 17 00:00:00 2001 From: Gideon Botha Date: Thu, 18 Jun 2026 21:44:58 +0200 Subject: [PATCH 7/7] refactor: apply consistent code formatting - Format enum definitions with trailing newlines before semicolons. - Break long chained method calls into multiple lines for improved readability. --- lib/pages/reader/overlay/reader_overlay.dart | 39 +++++++++++--------- lib/widgets/settings/dim_option.dart | 10 +++-- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/lib/pages/reader/overlay/reader_overlay.dart b/lib/pages/reader/overlay/reader_overlay.dart index 514cda83..58da208d 100644 --- a/lib/pages/reader/overlay/reader_overlay.dart +++ b/lib/pages/reader/overlay/reader_overlay.dart @@ -96,8 +96,9 @@ class ReaderOverlay extends HookConsumerWidget { ), ); - final dimLevel = - ref.watch(readerDimSettingsProvider).maybeWhen( + final dimLevel = ref + .watch(readerDimSettingsProvider) + .maybeWhen( data: (state) => state.dimLevel, orElse: () => 0.0, ); @@ -162,28 +163,30 @@ class ReaderOverlay extends HookConsumerWidget { bottom: 0, left: 0, right: 0, - child: SubpageProgress( - seriesId: seriesId, - chapterId: chapterId, - ) - .animate( - target: uiVisible.value ? 0.0 : 1.0, - ) - .fadeIn(duration: 200.ms), + child: + SubpageProgress( + seriesId: seriesId, + chapterId: chapterId, + ) + .animate( + target: uiVisible.value ? 0.0 : 1.0, + ) + .fadeIn(duration: 200.ms), ) else if (showProgressBar) Positioned( bottom: 0, left: 0, right: 0, - child: ReaderProgress( - seriesId: seriesId, - chapterId: chapterId, - ) - .animate( - target: uiVisible.value ? 0.0 : 1.0, - ) - .fadeIn(duration: 200.ms), + child: + ReaderProgress( + seriesId: seriesId, + chapterId: chapterId, + ) + .animate( + target: uiVisible.value ? 0.0 : 1.0, + ) + .fadeIn(duration: 200.ms), ), Positioned.fill( child: Row( diff --git a/lib/widgets/settings/dim_option.dart b/lib/widgets/settings/dim_option.dart index 2269472b..ccd4e0dc 100644 --- a/lib/widgets/settings/dim_option.dart +++ b/lib/widgets/settings/dim_option.dart @@ -9,8 +9,9 @@ class DimOption extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final dimLevel = - ref.watch(readerDimSettingsProvider).maybeWhen( + final dimLevel = ref + .watch(readerDimSettingsProvider) + .maybeWhen( data: (state) => state.dimLevel, orElse: () => 0.0, ); @@ -23,8 +24,9 @@ class DimOption extends ConsumerWidget { max: 90, step: ReaderDimSettingsLimits.dimStep, decimalPlaces: 0, - onChanged: (newValue) => - ref.read(readerDimSettingsProvider.notifier).setDimLevel(newValue / 100), + onChanged: (newValue) => ref + .read(readerDimSettingsProvider.notifier) + .setDimLevel(newValue / 100), ); } }