diff --git a/lib/features/player/view/player_when_expanded.dart b/lib/features/player/view/player_when_expanded.dart index 933121c..db8dcfc 100644 --- a/lib/features/player/view/player_when_expanded.dart +++ b/lib/features/player/view/player_when_expanded.dart @@ -1,17 +1,11 @@ -import 'package:duration_picker/duration_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:miniplayer/miniplayer.dart'; import 'package:vaani/constants/sizes.dart'; import 'package:vaani/features/player/providers/currently_playing_provider.dart'; import 'package:vaani/features/player/providers/player_form.dart'; import 'package:vaani/features/player/view/audiobook_player.dart'; -import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'; -import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' - show sleepTimerProvider; -import 'package:vaani/settings/app_settings_provider.dart'; -import 'package:vaani/shared/extensions/duration_format.dart'; +import 'package:vaani/features/sleep_timer/view/sleep_timer_button.dart'; import 'package:vaani/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/widgets/not_implemented.dart'; @@ -261,128 +255,3 @@ class PlayerWhenExpanded extends HookConsumerWidget { ); } } - -class SleepTimerButton extends HookConsumerWidget { - const SleepTimerButton({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sleepTimer = ref.watch(sleepTimerProvider); - // if sleep timer is not active, show the button with the sleep timer icon - // if the sleep timer is active, show the remaining time in a pill shaped container - return Tooltip( - message: 'Sleep Timer', - child: InkWell( - onTap: () async { - pendingPlayerModals++; - // show the sleep timer dialog - final resultingDuration = await showDurationPicker( - context: context, - initialTime: ref - .watch(appSettingsProvider) - .playerSettings - .sleepTimerSettings - .defaultDuration, - ); - pendingPlayerModals--; - if (resultingDuration != null) { - // if 0 is selected, cancel the timer - if (resultingDuration.inSeconds == 0) { - ref.read(sleepTimerProvider.notifier).cancelTimer(); - } else { - ref.read(sleepTimerProvider.notifier).setTimer(resultingDuration); - } - } - }, - child: sleepTimer == null - ? Icon( - Icons.timer_rounded, - color: Theme.of(context).colorScheme.onSurface, - ) - : RemainingSleepTimeDisplay( - timer: sleepTimer, - ), - ), - ); - } -} - -class SleepTimerDialog extends HookConsumerWidget { - const SleepTimerDialog({ - super.key, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final sleepTimer = ref.watch(sleepTimerProvider); - final sleepTimerSettings = - ref.watch(appSettingsProvider).playerSettings.sleepTimerSettings; - final timerDurationController = useTextEditingController( - text: sleepTimerSettings.defaultDuration.inMinutes.toString(), - ); - - return AlertDialog( - title: const Text('Sleep Timer'), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Set the duration for the sleep timer'), - TextField( - controller: timerDurationController, - keyboardType: TextInputType.number, - decoration: const InputDecoration( - labelText: 'Duration in minutes', - ), - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - // sleepTimer.setTimer( - // Duration( - // minutes: int.tryParse(timerDurationController.text) ?? 0, - // ), - // ); - Navigator.of(context).pop(); - }, - child: const Text('Set Timer'), - ), - ], - ); - } -} - -class RemainingSleepTimeDisplay extends HookConsumerWidget { - const RemainingSleepTimeDisplay({ - super.key, - required this.timer, - }); - - final SleepTimer timer; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final remainingTime = useStream(timer.remainingTimeStream).data; - return Container( - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(16), - ), - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), - child: Text( - timer.timer == null - ? timer.duration.smartBinaryFormat - : remainingTime?.smartBinaryFormat ?? '', - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.onPrimary, - ), - ), - ); - } -} diff --git a/lib/features/player/view/widgets/speed_selector.dart b/lib/features/player/view/widgets/speed_selector.dart index 987579a..65a3bfa 100644 --- a/lib/features/player/view/widgets/speed_selector.dart +++ b/lib/features/player/view/widgets/speed_selector.dart @@ -6,6 +6,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/shared/hooks.dart'; + +const double itemExtent = 25; class SpeedSelector extends HookConsumerWidget { const SpeedSelector({ @@ -34,11 +37,11 @@ class SpeedSelector extends HookConsumerWidget { // the speed options final minSpeed = min( - speeds.reduce((minSpeedSoFar, element) => min(minSpeedSoFar, element)), + speeds.reduce(min), playerSettings.minSpeed, ); final maxSpeed = max( - speeds.reduce((maxSpeedSoFar, element) => max(maxSpeedSoFar, element)), + speeds.reduce(max), playerSettings.maxSpeed, ); final speedIncrement = playerSettings.speedIncrement; @@ -53,10 +56,9 @@ class SpeedSelector extends HookConsumerWidget { }, ); - final scrollController = FixedExtentScrollController( + final scrollController = useFixedExtentScrollController( initialItem: availableSpeedsList.indexOf(currentSpeed), ); - const double itemExtent = 25; return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -89,7 +91,6 @@ class SpeedSelector extends HookConsumerWidget { availableSpeedsList: availableSpeedsList, speedState: speedState, scrollController: scrollController, - itemExtent: itemExtent, ), ), ), @@ -159,14 +160,12 @@ class SpeedWheel extends StatelessWidget { required this.availableSpeedsList, required this.speedState, required this.scrollController, - required this.itemExtent, this.showIncrementButtons = true, }); final List availableSpeedsList; final ValueNotifier speedState; final FixedExtentScrollController scrollController; - final double itemExtent; final bool showIncrementButtons; @override @@ -203,7 +202,7 @@ class SpeedWheel extends StatelessWidget { physics: const FixedExtentScrollPhysics(), children: availableSpeedsList .map( - (speed) => SpeedLine(itemExtent: itemExtent, speed: speed), + (speed) => SpeedLine(speed: speed), ) .toList(), onSelectedItemChanged: (index) { @@ -236,11 +235,9 @@ class SpeedWheel extends StatelessWidget { class SpeedLine extends StatelessWidget { const SpeedLine({ super.key, - required this.itemExtent, required this.speed, }); - final double itemExtent; final double speed; @override @@ -250,7 +247,6 @@ class SpeedLine extends StatelessWidget { // a vertical line Expanded( child: Container( - // height: itemExtent, // thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05 width: speed % 0.5 == 0 ? 3 diff --git a/lib/features/sleep_timer/core/sleep_timer.dart b/lib/features/sleep_timer/core/sleep_timer.dart index 215c264..9350436 100644 --- a/lib/features/sleep_timer/core/sleep_timer.dart +++ b/lib/features/sleep_timer/core/sleep_timer.dart @@ -19,7 +19,17 @@ class SleepTimer { set duration(Duration value) { _duration = value; - clearCountDownTimer(); + _logger.fine('duration set to $value'); + + /// if the timer is active, restart it with the new duration + /// if the timer is not active, do nothing + if (isActive && player.playing) { + _logger.fine('timer is active counting down with new duration'); + startCountDown(value); + } else { + _logger.fine('timer is not active'); + clearCountDownTimer(); + } } /// The player to be paused diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.dart index 92e5eb8..d9db355 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.dart @@ -36,10 +36,16 @@ class SleepTimer extends _$SleepTimer { return sleepTimer; } - void setTimer(Duration resultingDuration) { + void setTimer(Duration? resultingDuration, {bool notifyListeners = true}) { + if (resultingDuration == null || resultingDuration.inSeconds == 0) { + cancelTimer(); + return; + } if (state != null) { state!.duration = resultingDuration; - ref.notifyListeners(); + if (notifyListeners) { + ref.notifyListeners(); + } } else { final timer = core.SleepTimer( duration: resultingDuration, diff --git a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart index 6d820e4..b0bfd9c 100644 --- a/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart +++ b/lib/features/sleep_timer/providers/sleep_timer_provider.g.dart @@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$sleepTimerHash() => r'4f80bcc342e918c70c547b8b24790ccd88aba8c3'; +String _$sleepTimerHash() => r'722bebfb7367acb84aacd98da66c5858b7e446cd'; /// See also [SleepTimer]. @ProviderFor(SleepTimer) diff --git a/lib/features/sleep_timer/view/sleep_timer_button.dart b/lib/features/sleep_timer/view/sleep_timer_button.dart new file mode 100644 index 0000000..9b1a07d --- /dev/null +++ b/lib/features/sleep_timer/view/sleep_timer_button.dart @@ -0,0 +1,366 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart'; +import 'package:vaani/features/player/view/player_when_expanded.dart'; +import 'package:vaani/features/player/view/widgets/speed_selector.dart'; +import 'package:vaani/features/sleep_timer/core/sleep_timer.dart'; +import 'package:vaani/features/sleep_timer/providers/sleep_timer_provider.dart' + show sleepTimerProvider; +import 'package:vaani/main.dart'; +import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/shared/extensions/duration_format.dart'; +import 'package:vaani/shared/hooks.dart'; + +class SleepTimerButton extends HookConsumerWidget { + const SleepTimerButton({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sleepTimer = ref.watch(sleepTimerProvider); + final durationState = useState(sleepTimer?.duration); + + // if sleep timer is not active, show the button with the sleep timer icon + // if the sleep timer is active, show the remaining time in a pill shaped container + return Tooltip( + message: 'Sleep Timer', + child: InkWell( + onTap: () async { + appLogger.fine('Sleep Timer button pressed'); + pendingPlayerModals++; + // show the sleep timer dialog + await showModalBottomSheet( + context: context, + barrierLabel: 'Sleep Timer', + builder: (context) { + return SleepTimerBottomSheet( + onDurationSelected: (duration) { + durationState.value = duration; + // ref + // .read(sleepTimerProvider.notifier) + // .setTimer(duration, notifyListeners: false); + }, + ); + }, + ); + pendingPlayerModals--; + ref.read(sleepTimerProvider.notifier).setTimer(durationState.value); + appLogger + .fine('Sleep Timer dialog closed with ${durationState.value}'); + }, + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: sleepTimer == null + ? Icon( + Icons.timer_rounded, + color: Theme.of(context).colorScheme.onSurface, + ) + : RemainingSleepTimeDisplay( + timer: sleepTimer, + ), + ), + ), + ); + } +} + +class SleepTimerBottomSheet extends HookConsumerWidget { + const SleepTimerBottomSheet({ + super.key, + this.onDurationSelected, + }); + + final void Function(Duration?)? onDurationSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sleepTimer = ref.watch(sleepTimerProvider); + final sleepTimerSettings = + ref.watch(appSettingsProvider).playerSettings.sleepTimerSettings; + + final durationOptions = sleepTimerSettings.presetDurations; + final minDuration = Duration.zero; + final maxDuration = [ + ...durationOptions, + sleepTimerSettings.maxDuration, + ].reduce((a, b) => a > b ? a : b); + final incrementStep = Duration(minutes: 1); + final allPossibleDurations = [ + for (var i = minDuration; i <= maxDuration; i += incrementStep) i, + ]; + + final scrollController = useFixedExtentScrollController( + initialItem: + allPossibleDurations.indexOf(sleepTimer?.duration ?? minDuration), + ); + + final durationState = useState( + sleepTimer?.duration ?? minDuration, + ); + + // useEffect to rebuild the sleep timer when the duration changes + useEffect( + () { + onDurationSelected?.call(durationState.value); + return null; + }, + [durationState.value], + ); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // the title + Padding( + padding: const EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Center( + child: Text( + 'Sleep Timer', + style: Theme.of(context).textTheme.titleLarge, + ), + ), + ), + + // a inverted triangle to indicate the speed selector + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Icon( + Icons.arrow_drop_down, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + + // the speed selector + Padding( + padding: const EdgeInsets.only(bottom: 8.0, right: 8.0, left: 8.0), + child: SizedBox( + height: 80, + child: SleepTimerWheel( + durationState: durationState, + availableDurations: allPossibleDurations, + scrollController: scrollController, + ), + ), + ), + + // a cancel button to cancel the sleep timer + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: TextButton.icon( + onPressed: () { + ref.read(sleepTimerProvider.notifier).cancelTimer(); + onDurationSelected?.call(null); + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.cancel), + label: const Text('Cancel Sleep Timer'), + ), + ), + + // the speed buttons + Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 8.0, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: durationOptions + .map( + (timerDuration) => TextButton( + style: timerDuration == durationState.value + ? TextButton.styleFrom( + backgroundColor: + Theme.of(context).colorScheme.primaryContainer, + foregroundColor: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ) + // border if not selected + : TextButton.styleFrom( + side: BorderSide( + color: Theme.of(context) + .colorScheme + .primaryContainer, + ), + ), + onPressed: () async { + // animate the wheel to the selected speed + var index = allPossibleDurations.indexOf(timerDuration); + // if the speed is not in the list + if (index == -1) { + // find the nearest speed + final nearestDuration = allPossibleDurations.firstWhere( + (element) => element > timerDuration, + orElse: () => allPossibleDurations.last, + ); + index = allPossibleDurations.indexOf(nearestDuration); + } + await scrollController.animateToItem( + index, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + }, + child: Text(timerDuration.smartBinaryFormat), + ), + ) + .toList(), + ), + ), + ], + ); + } +} + +class RemainingSleepTimeDisplay extends HookConsumerWidget { + const RemainingSleepTimeDisplay({ + super.key, + required this.timer, + }); + + final SleepTimer timer; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final remainingTime = useStream(timer.remainingTimeStream).data; + return Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(16), + ), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + child: Text( + timer.timer == null + ? timer.duration.smartBinaryFormat + : remainingTime?.smartBinaryFormat ?? '', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onPrimary, + ), + ), + ); + } +} + +class SleepTimerWheel extends StatelessWidget { + const SleepTimerWheel({ + super.key, + required this.availableDurations, + required this.scrollController, + required this.durationState, + this.showIncrementButtons = true, + }); + + final List availableDurations; + final ValueNotifier durationState; + final FixedExtentScrollController scrollController; + final bool showIncrementButtons; + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // a minus button to decrease the speed + if (showIncrementButtons) + IconButton.filledTonal( + icon: const Icon(Icons.remove), + onPressed: () { + // animate to index - 1 + final index = availableDurations + .indexOf(durationState.value ?? Duration.zero); + if (index > 0) { + scrollController.animateToItem( + index - 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), + // the speed selector wheel + Flexible( + child: ListWheelScrollViewX( + controller: scrollController, + scrollDirection: Axis.horizontal, + itemExtent: itemExtent, + diameterRatio: 1.5, squeeze: 1.2, + // useMagnifier: true, + // magnification: 1.5, + physics: const FixedExtentScrollPhysics(), + children: availableDurations + .map( + (duration) => DurationLine(duration: duration), + ) + .toList(), + onSelectedItemChanged: (index) { + durationState.value = availableDurations[index]; + }, + ), + ), + + if (showIncrementButtons) + // a plus button to increase the speed + IconButton.filledTonal( + icon: const Icon(Icons.add), + onPressed: () { + // animate to index + 1 + final index = availableDurations + .indexOf(durationState.value ?? Duration.zero); + if (index < availableDurations.length - 1) { + scrollController.animateToItem( + index + 1, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }, + ), + ], + ); + } +} + +class DurationLine extends StatelessWidget { + const DurationLine({ + super.key, + required this.duration, + }); + + final Duration duration; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + // a vertical line + Expanded( + child: Container( + // thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05 + width: duration.inMinutes % 5 == 0 + ? 3 + : duration.inMinutes % 2.5 == 0 + ? 2 + : 0.5, + color: Theme.of(context).colorScheme.onSurface, + ), + ), + Opacity( + opacity: duration.inMinutes % 2.5 == 0 ? 1 : 0, + child: Text( + '${duration.inMinutes}m', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall?.fontSize, + ), + ), + ), + ], + ); + } +} diff --git a/lib/settings/models/app_settings.dart b/lib/settings/models/app_settings.dart index 7debe11..a5ce0a0 100644 --- a/lib/settings/models/app_settings.dart +++ b/lib/settings/models/app_settings.dart @@ -81,22 +81,17 @@ class MinimizedPlayerSettings with _$MinimizedPlayerSettings { _$MinimizedPlayerSettingsFromJson(json); } -enum SleepTimerShakeSenseMode { never, always, nearEnds } - @freezed class SleepTimerSettings with _$SleepTimerSettings { const factory SleepTimerSettings({ @Default(Duration(minutes: 15)) Duration defaultDuration, - @Default(SleepTimerShakeSenseMode.always) - SleepTimerShakeSenseMode shakeSenseMode, - - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] - @Default(Duration(seconds: 30)) Duration shakeSenseDuration, - @Default(true) bool vibrateWhenReset, - @Default(false) bool beepWhenReset, + @Default( + [Duration(minutes: 5), Duration(minutes: 15), Duration(minutes: 30)], + ) + List presetDurations, + @Default(Duration(minutes: 100)) Duration maxDuration, @Default(false) bool fadeOutAudio, - @Default(0.5) double shakeDetectThreshold, + @Default(Duration(seconds: 20)) Duration fadeOutDuration, /// if true, the player will automatically rewind the audio when the sleep timer is stopped @Default(false) bool autoRewindWhenStopped, diff --git a/lib/settings/models/app_settings.freezed.dart b/lib/settings/models/app_settings.freezed.dart index 4980b85..b53c4ce 100644 --- a/lib/settings/models/app_settings.freezed.dart +++ b/lib/settings/models/app_settings.freezed.dart @@ -1374,16 +1374,10 @@ SleepTimerSettings _$SleepTimerSettingsFromJson(Map json) { /// @nodoc mixin _$SleepTimerSettings { Duration get defaultDuration => throw _privateConstructorUsedError; - SleepTimerShakeSenseMode get shakeSenseMode => - throw _privateConstructorUsedError; - - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] - Duration get shakeSenseDuration => throw _privateConstructorUsedError; - bool get vibrateWhenReset => throw _privateConstructorUsedError; - bool get beepWhenReset => throw _privateConstructorUsedError; + List get presetDurations => throw _privateConstructorUsedError; + Duration get maxDuration => throw _privateConstructorUsedError; bool get fadeOutAudio => throw _privateConstructorUsedError; - double get shakeDetectThreshold => throw _privateConstructorUsedError; + Duration get fadeOutDuration => throw _privateConstructorUsedError; /// if true, the player will automatically rewind the audio when the sleep timer is stopped bool get autoRewindWhenStopped => throw _privateConstructorUsedError; @@ -1422,12 +1416,10 @@ abstract class $SleepTimerSettingsCopyWith<$Res> { @useResult $Res call( {Duration defaultDuration, - SleepTimerShakeSenseMode shakeSenseMode, - Duration shakeSenseDuration, - bool vibrateWhenReset, - bool beepWhenReset, + List presetDurations, + Duration maxDuration, bool fadeOutAudio, - double shakeDetectThreshold, + Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, @@ -1452,12 +1444,10 @@ class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings> @override $Res call({ Object? defaultDuration = null, - Object? shakeSenseMode = null, - Object? shakeSenseDuration = null, - Object? vibrateWhenReset = null, - Object? beepWhenReset = null, + Object? presetDurations = null, + Object? maxDuration = null, Object? fadeOutAudio = null, - Object? shakeDetectThreshold = null, + Object? fadeOutDuration = null, Object? autoRewindWhenStopped = null, Object? autoRewindDurations = null, Object? autoTurnOnTimer = null, @@ -1470,30 +1460,22 @@ class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings> ? _value.defaultDuration : defaultDuration // ignore: cast_nullable_to_non_nullable as Duration, - shakeSenseMode: null == shakeSenseMode - ? _value.shakeSenseMode - : shakeSenseMode // ignore: cast_nullable_to_non_nullable - as SleepTimerShakeSenseMode, - shakeSenseDuration: null == shakeSenseDuration - ? _value.shakeSenseDuration - : shakeSenseDuration // ignore: cast_nullable_to_non_nullable + presetDurations: null == presetDurations + ? _value.presetDurations + : presetDurations // ignore: cast_nullable_to_non_nullable + as List, + maxDuration: null == maxDuration + ? _value.maxDuration + : maxDuration // ignore: cast_nullable_to_non_nullable as Duration, - vibrateWhenReset: null == vibrateWhenReset - ? _value.vibrateWhenReset - : vibrateWhenReset // ignore: cast_nullable_to_non_nullable - as bool, - beepWhenReset: null == beepWhenReset - ? _value.beepWhenReset - : beepWhenReset // ignore: cast_nullable_to_non_nullable - as bool, fadeOutAudio: null == fadeOutAudio ? _value.fadeOutAudio : fadeOutAudio // ignore: cast_nullable_to_non_nullable as bool, - shakeDetectThreshold: null == shakeDetectThreshold - ? _value.shakeDetectThreshold - : shakeDetectThreshold // ignore: cast_nullable_to_non_nullable - as double, + fadeOutDuration: null == fadeOutDuration + ? _value.fadeOutDuration + : fadeOutDuration // ignore: cast_nullable_to_non_nullable + as Duration, autoRewindWhenStopped: null == autoRewindWhenStopped ? _value.autoRewindWhenStopped : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable @@ -1532,12 +1514,10 @@ abstract class _$$SleepTimerSettingsImplCopyWith<$Res> @useResult $Res call( {Duration defaultDuration, - SleepTimerShakeSenseMode shakeSenseMode, - Duration shakeSenseDuration, - bool vibrateWhenReset, - bool beepWhenReset, + List presetDurations, + Duration maxDuration, bool fadeOutAudio, - double shakeDetectThreshold, + Duration fadeOutDuration, bool autoRewindWhenStopped, Map autoRewindDurations, bool autoTurnOnTimer, @@ -1560,12 +1540,10 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res> @override $Res call({ Object? defaultDuration = null, - Object? shakeSenseMode = null, - Object? shakeSenseDuration = null, - Object? vibrateWhenReset = null, - Object? beepWhenReset = null, + Object? presetDurations = null, + Object? maxDuration = null, Object? fadeOutAudio = null, - Object? shakeDetectThreshold = null, + Object? fadeOutDuration = null, Object? autoRewindWhenStopped = null, Object? autoRewindDurations = null, Object? autoTurnOnTimer = null, @@ -1578,30 +1556,22 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res> ? _value.defaultDuration : defaultDuration // ignore: cast_nullable_to_non_nullable as Duration, - shakeSenseMode: null == shakeSenseMode - ? _value.shakeSenseMode - : shakeSenseMode // ignore: cast_nullable_to_non_nullable - as SleepTimerShakeSenseMode, - shakeSenseDuration: null == shakeSenseDuration - ? _value.shakeSenseDuration - : shakeSenseDuration // ignore: cast_nullable_to_non_nullable + presetDurations: null == presetDurations + ? _value._presetDurations + : presetDurations // ignore: cast_nullable_to_non_nullable + as List, + maxDuration: null == maxDuration + ? _value.maxDuration + : maxDuration // ignore: cast_nullable_to_non_nullable as Duration, - vibrateWhenReset: null == vibrateWhenReset - ? _value.vibrateWhenReset - : vibrateWhenReset // ignore: cast_nullable_to_non_nullable - as bool, - beepWhenReset: null == beepWhenReset - ? _value.beepWhenReset - : beepWhenReset // ignore: cast_nullable_to_non_nullable - as bool, fadeOutAudio: null == fadeOutAudio ? _value.fadeOutAudio : fadeOutAudio // ignore: cast_nullable_to_non_nullable as bool, - shakeDetectThreshold: null == shakeDetectThreshold - ? _value.shakeDetectThreshold - : shakeDetectThreshold // ignore: cast_nullable_to_non_nullable - as double, + fadeOutDuration: null == fadeOutDuration + ? _value.fadeOutDuration + : fadeOutDuration // ignore: cast_nullable_to_non_nullable + as Duration, autoRewindWhenStopped: null == autoRewindWhenStopped ? _value.autoRewindWhenStopped : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable @@ -1635,12 +1605,14 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res> class _$SleepTimerSettingsImpl implements _SleepTimerSettings { const _$SleepTimerSettingsImpl( {this.defaultDuration = const Duration(minutes: 15), - this.shakeSenseMode = SleepTimerShakeSenseMode.always, - this.shakeSenseDuration = const Duration(seconds: 30), - this.vibrateWhenReset = true, - this.beepWhenReset = false, + final List presetDurations = const [ + Duration(minutes: 5), + Duration(minutes: 15), + Duration(minutes: 30) + ], + this.maxDuration = const Duration(minutes: 100), this.fadeOutAudio = false, - this.shakeDetectThreshold = 0.5, + this.fadeOutDuration = const Duration(seconds: 20), this.autoRewindWhenStopped = false, final Map autoRewindDurations = const { 5: Duration(seconds: 10), @@ -1653,7 +1625,8 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings { this.alwaysAutoTurnOnTimer = true, this.autoTurnOnTime = const Duration(hours: 22, minutes: 0), this.autoTurnOffTime = const Duration(hours: 6, minutes: 0)}) - : _autoRewindDurations = autoRewindDurations; + : _presetDurations = presetDurations, + _autoRewindDurations = autoRewindDurations; factory _$SleepTimerSettingsImpl.fromJson(Map json) => _$$SleepTimerSettingsImplFromJson(json); @@ -1661,27 +1634,24 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings { @override @JsonKey() final Duration defaultDuration; + final List _presetDurations; @override @JsonKey() - final SleepTimerShakeSenseMode shakeSenseMode; + List get presetDurations { + if (_presetDurations is EqualUnmodifiableListView) return _presetDurations; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_presetDurations); + } - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] @override @JsonKey() - final Duration shakeSenseDuration; - @override - @JsonKey() - final bool vibrateWhenReset; - @override - @JsonKey() - final bool beepWhenReset; + final Duration maxDuration; @override @JsonKey() final bool fadeOutAudio; @override @JsonKey() - final double shakeDetectThreshold; + final Duration fadeOutDuration; /// if true, the player will automatically rewind the audio when the sleep timer is stopped @override @@ -1723,7 +1693,7 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings { @override String toString() { - return 'SleepTimerSettings(defaultDuration: $defaultDuration, shakeSenseMode: $shakeSenseMode, shakeSenseDuration: $shakeSenseDuration, vibrateWhenReset: $vibrateWhenReset, beepWhenReset: $beepWhenReset, fadeOutAudio: $fadeOutAudio, shakeDetectThreshold: $shakeDetectThreshold, autoRewindWhenStopped: $autoRewindWhenStopped, autoRewindDurations: $autoRewindDurations, autoTurnOnTimer: $autoTurnOnTimer, alwaysAutoTurnOnTimer: $alwaysAutoTurnOnTimer, autoTurnOnTime: $autoTurnOnTime, autoTurnOffTime: $autoTurnOffTime)'; + return 'SleepTimerSettings(defaultDuration: $defaultDuration, presetDurations: $presetDurations, maxDuration: $maxDuration, fadeOutAudio: $fadeOutAudio, fadeOutDuration: $fadeOutDuration, autoRewindWhenStopped: $autoRewindWhenStopped, autoRewindDurations: $autoRewindDurations, autoTurnOnTimer: $autoTurnOnTimer, alwaysAutoTurnOnTimer: $alwaysAutoTurnOnTimer, autoTurnOnTime: $autoTurnOnTime, autoTurnOffTime: $autoTurnOffTime)'; } @override @@ -1733,18 +1703,14 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings { other is _$SleepTimerSettingsImpl && (identical(other.defaultDuration, defaultDuration) || other.defaultDuration == defaultDuration) && - (identical(other.shakeSenseMode, shakeSenseMode) || - other.shakeSenseMode == shakeSenseMode) && - (identical(other.shakeSenseDuration, shakeSenseDuration) || - other.shakeSenseDuration == shakeSenseDuration) && - (identical(other.vibrateWhenReset, vibrateWhenReset) || - other.vibrateWhenReset == vibrateWhenReset) && - (identical(other.beepWhenReset, beepWhenReset) || - other.beepWhenReset == beepWhenReset) && + const DeepCollectionEquality() + .equals(other._presetDurations, _presetDurations) && + (identical(other.maxDuration, maxDuration) || + other.maxDuration == maxDuration) && (identical(other.fadeOutAudio, fadeOutAudio) || other.fadeOutAudio == fadeOutAudio) && - (identical(other.shakeDetectThreshold, shakeDetectThreshold) || - other.shakeDetectThreshold == shakeDetectThreshold) && + (identical(other.fadeOutDuration, fadeOutDuration) || + other.fadeOutDuration == fadeOutDuration) && (identical(other.autoRewindWhenStopped, autoRewindWhenStopped) || other.autoRewindWhenStopped == autoRewindWhenStopped) && const DeepCollectionEquality() @@ -1764,12 +1730,10 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings { int get hashCode => Object.hash( runtimeType, defaultDuration, - shakeSenseMode, - shakeSenseDuration, - vibrateWhenReset, - beepWhenReset, + const DeepCollectionEquality().hash(_presetDurations), + maxDuration, fadeOutAudio, - shakeDetectThreshold, + fadeOutDuration, autoRewindWhenStopped, const DeepCollectionEquality().hash(_autoRewindDurations), autoTurnOnTimer, @@ -1797,12 +1761,10 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings { abstract class _SleepTimerSettings implements SleepTimerSettings { const factory _SleepTimerSettings( {final Duration defaultDuration, - final SleepTimerShakeSenseMode shakeSenseMode, - final Duration shakeSenseDuration, - final bool vibrateWhenReset, - final bool beepWhenReset, + final List presetDurations, + final Duration maxDuration, final bool fadeOutAudio, - final double shakeDetectThreshold, + final Duration fadeOutDuration, final bool autoRewindWhenStopped, final Map autoRewindDurations, final bool autoTurnOnTimer, @@ -1816,20 +1778,13 @@ abstract class _SleepTimerSettings implements SleepTimerSettings { @override Duration get defaultDuration; @override - SleepTimerShakeSenseMode get shakeSenseMode; - - /// the duration in which the shake is detected before the end of the timer and after the timer ends - /// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] + List get presetDurations; @override - Duration get shakeSenseDuration; - @override - bool get vibrateWhenReset; - @override - bool get beepWhenReset; + Duration get maxDuration; @override bool get fadeOutAudio; @override - double get shakeDetectThreshold; + Duration get fadeOutDuration; /// if true, the player will automatically rewind the audio when the sleep timer is stopped @override diff --git a/lib/settings/models/app_settings.g.dart b/lib/settings/models/app_settings.g.dart index da9ca9c..0dfb5e9 100644 --- a/lib/settings/models/app_settings.g.dart +++ b/lib/settings/models/app_settings.g.dart @@ -150,17 +150,21 @@ _$SleepTimerSettingsImpl _$$SleepTimerSettingsImplFromJson( defaultDuration: json['defaultDuration'] == null ? const Duration(minutes: 15) : Duration(microseconds: (json['defaultDuration'] as num).toInt()), - shakeSenseMode: $enumDecodeNullable( - _$SleepTimerShakeSenseModeEnumMap, json['shakeSenseMode']) ?? - SleepTimerShakeSenseMode.always, - shakeSenseDuration: json['shakeSenseDuration'] == null - ? const Duration(seconds: 30) - : Duration(microseconds: (json['shakeSenseDuration'] as num).toInt()), - vibrateWhenReset: json['vibrateWhenReset'] as bool? ?? true, - beepWhenReset: json['beepWhenReset'] as bool? ?? false, + presetDurations: (json['presetDurations'] as List?) + ?.map((e) => Duration(microseconds: (e as num).toInt())) + .toList() ?? + const [ + Duration(minutes: 5), + Duration(minutes: 15), + Duration(minutes: 30) + ], + maxDuration: json['maxDuration'] == null + ? const Duration(minutes: 100) + : Duration(microseconds: (json['maxDuration'] as num).toInt()), fadeOutAudio: json['fadeOutAudio'] as bool? ?? false, - shakeDetectThreshold: - (json['shakeDetectThreshold'] as num?)?.toDouble() ?? 0.5, + fadeOutDuration: json['fadeOutDuration'] == null + ? const Duration(seconds: 20) + : Duration(microseconds: (json['fadeOutDuration'] as num).toInt()), autoRewindWhenStopped: json['autoRewindWhenStopped'] as bool? ?? false, autoRewindDurations: (json['autoRewindDurations'] as Map?)?.map( @@ -188,13 +192,11 @@ Map _$$SleepTimerSettingsImplToJson( _$SleepTimerSettingsImpl instance) => { 'defaultDuration': instance.defaultDuration.inMicroseconds, - 'shakeSenseMode': - _$SleepTimerShakeSenseModeEnumMap[instance.shakeSenseMode]!, - 'shakeSenseDuration': instance.shakeSenseDuration.inMicroseconds, - 'vibrateWhenReset': instance.vibrateWhenReset, - 'beepWhenReset': instance.beepWhenReset, + 'presetDurations': + instance.presetDurations.map((e) => e.inMicroseconds).toList(), + 'maxDuration': instance.maxDuration.inMicroseconds, 'fadeOutAudio': instance.fadeOutAudio, - 'shakeDetectThreshold': instance.shakeDetectThreshold, + 'fadeOutDuration': instance.fadeOutDuration.inMicroseconds, 'autoRewindWhenStopped': instance.autoRewindWhenStopped, 'autoRewindDurations': instance.autoRewindDurations .map((k, e) => MapEntry(k.toString(), e.inMicroseconds)), @@ -204,12 +206,6 @@ Map _$$SleepTimerSettingsImplToJson( 'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds, }; -const _$SleepTimerShakeSenseModeEnumMap = { - SleepTimerShakeSenseMode.never: 'never', - SleepTimerShakeSenseMode.always: 'always', - SleepTimerShakeSenseMode.nearEnds: 'nearEnds', -}; - _$DownloadSettingsImpl _$$DownloadSettingsImplFromJson( Map json) => _$DownloadSettingsImpl( diff --git a/lib/shared/extensions/duration_format.dart b/lib/shared/extensions/duration_format.dart index 09906cf..b492a37 100644 --- a/lib/shared/extensions/duration_format.dart +++ b/lib/shared/extensions/duration_format.dart @@ -9,14 +9,27 @@ extension DurationFormat on Duration { final minutes = inMinutes.remainder(60); final seconds = inSeconds.remainder(60); if (hours > 0) { - return '${hours}h ${minutes}m'; + // skip minutes if it's 0 + if (minutes == 0) { + return smartSingleFormat; + } + return '${Duration(hours: hours).smartBinaryFormat} ${Duration(minutes: minutes).smartSingleFormat}'; } else if (minutes > 0) { - return '${minutes}m ${seconds}s'; + if (seconds == 0) { + return smartSingleFormat; + } + return '${Duration(minutes: minutes).smartSingleFormat} ${Duration(seconds: seconds).smartSingleFormat}'; } else { - return '${seconds}s'; + return smartSingleFormat; } } + /// formats the duration using only 1 unit + /// if the duration is more than 1 hour, it will return `10h` + /// if the duration is less than 1 hour, it will return `30m` + /// if the duration is less than 1 minute, it will return `20s` + /// + /// rest of the duration will be ignored String get smartSingleFormat { if (inHours > 0) { return '${inHours}h'; diff --git a/lib/shared/hooks.dart b/lib/shared/hooks.dart index 8e27b3a..62c6116 100644 --- a/lib/shared/hooks.dart +++ b/lib/shared/hooks.dart @@ -28,3 +28,64 @@ void useTimer(VoidCallback callback, Duration delay) { [delay], ); } + +/// Creates [FixedExtentScrollController] that will be disposed automatically. +/// +/// See also: +/// - [FixedExtentScrollController] +FixedExtentScrollController useFixedExtentScrollController({ + String? debugLabel, + List? keys, + int initialItem = 0, + void Function(ScrollPosition)? onAttach, + void Function(ScrollPosition)? onDetach, +}) { + return use( + _FixedExtentScrollControllerHook( + debugLabel: debugLabel, + keys: keys, + initialItem: initialItem, + onAttach: onAttach, + onDetach: onDetach, + ), + ); +} + +class _FixedExtentScrollControllerHook + extends Hook { + const _FixedExtentScrollControllerHook({ + this.debugLabel, + super.keys, + required this.initialItem, + this.onAttach, + this.onDetach, + }); + + final int initialItem; + final void Function(ScrollPosition)? onAttach; + final void Function(ScrollPosition)? onDetach; + + final String? debugLabel; + + @override + HookState> + createState() => _FixedExtentScrollControllerHookState(); +} + +class _FixedExtentScrollControllerHookState extends HookState< + FixedExtentScrollController, _FixedExtentScrollControllerHook> { + late final controller = FixedExtentScrollController( + initialItem: hook.initialItem, + onAttach: hook.onAttach, + onDetach: hook.onDetach, + ); + + @override + FixedExtentScrollController build(BuildContext context) => controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'useFixedExtentScrollController'; +}