refactor: enhance sleep timer functionality and improve duration formatting

This commit is contained in:
Dr-Blank 2024-10-02 06:05:10 -04:00
parent 4c5dc53169
commit ff41bf2b4d
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
11 changed files with 565 additions and 298 deletions

View file

@ -1,17 +1,11 @@
import 'package:duration_picker/duration_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:miniplayer/miniplayer.dart'; import 'package:miniplayer/miniplayer.dart';
import 'package:vaani/constants/sizes.dart'; import 'package:vaani/constants/sizes.dart';
import 'package:vaani/features/player/providers/currently_playing_provider.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/providers/player_form.dart';
import 'package:vaani/features/player/view/audiobook_player.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/view/sleep_timer_button.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/shared/extensions/inverse_lerp.dart'; import 'package:vaani/shared/extensions/inverse_lerp.dart';
import 'package:vaani/shared/widgets/not_implemented.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,
),
),
);
}
}

View file

@ -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:list_wheel_scroll_view_nls/list_wheel_scroll_view_nls.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/hooks.dart';
const double itemExtent = 25;
class SpeedSelector extends HookConsumerWidget { class SpeedSelector extends HookConsumerWidget {
const SpeedSelector({ const SpeedSelector({
@ -34,11 +37,11 @@ class SpeedSelector extends HookConsumerWidget {
// the speed options // the speed options
final minSpeed = min( final minSpeed = min(
speeds.reduce((minSpeedSoFar, element) => min(minSpeedSoFar, element)), speeds.reduce(min),
playerSettings.minSpeed, playerSettings.minSpeed,
); );
final maxSpeed = max( final maxSpeed = max(
speeds.reduce((maxSpeedSoFar, element) => max(maxSpeedSoFar, element)), speeds.reduce(max),
playerSettings.maxSpeed, playerSettings.maxSpeed,
); );
final speedIncrement = playerSettings.speedIncrement; final speedIncrement = playerSettings.speedIncrement;
@ -53,10 +56,9 @@ class SpeedSelector extends HookConsumerWidget {
}, },
); );
final scrollController = FixedExtentScrollController( final scrollController = useFixedExtentScrollController(
initialItem: availableSpeedsList.indexOf(currentSpeed), initialItem: availableSpeedsList.indexOf(currentSpeed),
); );
const double itemExtent = 25;
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@ -89,7 +91,6 @@ class SpeedSelector extends HookConsumerWidget {
availableSpeedsList: availableSpeedsList, availableSpeedsList: availableSpeedsList,
speedState: speedState, speedState: speedState,
scrollController: scrollController, scrollController: scrollController,
itemExtent: itemExtent,
), ),
), ),
), ),
@ -159,14 +160,12 @@ class SpeedWheel extends StatelessWidget {
required this.availableSpeedsList, required this.availableSpeedsList,
required this.speedState, required this.speedState,
required this.scrollController, required this.scrollController,
required this.itemExtent,
this.showIncrementButtons = true, this.showIncrementButtons = true,
}); });
final List<double> availableSpeedsList; final List<double> availableSpeedsList;
final ValueNotifier<double> speedState; final ValueNotifier<double> speedState;
final FixedExtentScrollController scrollController; final FixedExtentScrollController scrollController;
final double itemExtent;
final bool showIncrementButtons; final bool showIncrementButtons;
@override @override
@ -203,7 +202,7 @@ class SpeedWheel extends StatelessWidget {
physics: const FixedExtentScrollPhysics(), physics: const FixedExtentScrollPhysics(),
children: availableSpeedsList children: availableSpeedsList
.map( .map(
(speed) => SpeedLine(itemExtent: itemExtent, speed: speed), (speed) => SpeedLine(speed: speed),
) )
.toList(), .toList(),
onSelectedItemChanged: (index) { onSelectedItemChanged: (index) {
@ -236,11 +235,9 @@ class SpeedWheel extends StatelessWidget {
class SpeedLine extends StatelessWidget { class SpeedLine extends StatelessWidget {
const SpeedLine({ const SpeedLine({
super.key, super.key,
required this.itemExtent,
required this.speed, required this.speed,
}); });
final double itemExtent;
final double speed; final double speed;
@override @override
@ -250,7 +247,6 @@ class SpeedLine extends StatelessWidget {
// a vertical line // a vertical line
Expanded( Expanded(
child: Container( child: Container(
// height: itemExtent,
// thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05 // thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05
width: speed % 0.5 == 0 width: speed % 0.5 == 0
? 3 ? 3

View file

@ -19,7 +19,17 @@ class SleepTimer {
set duration(Duration value) { set duration(Duration value) {
_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 /// The player to be paused

View file

@ -36,10 +36,16 @@ class SleepTimer extends _$SleepTimer {
return 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) { if (state != null) {
state!.duration = resultingDuration; state!.duration = resultingDuration;
ref.notifyListeners(); if (notifyListeners) {
ref.notifyListeners();
}
} else { } else {
final timer = core.SleepTimer( final timer = core.SleepTimer(
duration: resultingDuration, duration: resultingDuration,

View file

@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$sleepTimerHash() => r'4f80bcc342e918c70c547b8b24790ccd88aba8c3'; String _$sleepTimerHash() => r'722bebfb7367acb84aacd98da66c5858b7e446cd';
/// See also [SleepTimer]. /// See also [SleepTimer].
@ProviderFor(SleepTimer) @ProviderFor(SleepTimer)

View file

@ -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<Duration?>(
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 = <Duration>[
...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<Duration>(
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<Duration> availableDurations;
final ValueNotifier<Duration?> 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,
),
),
),
],
);
}
}

View file

@ -81,22 +81,17 @@ class MinimizedPlayerSettings with _$MinimizedPlayerSettings {
_$MinimizedPlayerSettingsFromJson(json); _$MinimizedPlayerSettingsFromJson(json);
} }
enum SleepTimerShakeSenseMode { never, always, nearEnds }
@freezed @freezed
class SleepTimerSettings with _$SleepTimerSettings { class SleepTimerSettings with _$SleepTimerSettings {
const factory SleepTimerSettings({ const factory SleepTimerSettings({
@Default(Duration(minutes: 15)) Duration defaultDuration, @Default(Duration(minutes: 15)) Duration defaultDuration,
@Default(SleepTimerShakeSenseMode.always) @Default(
SleepTimerShakeSenseMode shakeSenseMode, [Duration(minutes: 5), Duration(minutes: 15), Duration(minutes: 30)],
)
/// the duration in which the shake is detected before the end of the timer and after the timer ends List<Duration> presetDurations,
/// only used if [shakeSenseMode] is [SleepTimerShakeSenseMode.nearEnds] @Default(Duration(minutes: 100)) Duration maxDuration,
@Default(Duration(seconds: 30)) Duration shakeSenseDuration,
@Default(true) bool vibrateWhenReset,
@Default(false) bool beepWhenReset,
@Default(false) bool fadeOutAudio, @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 /// if true, the player will automatically rewind the audio when the sleep timer is stopped
@Default(false) bool autoRewindWhenStopped, @Default(false) bool autoRewindWhenStopped,

View file

@ -1374,16 +1374,10 @@ SleepTimerSettings _$SleepTimerSettingsFromJson(Map<String, dynamic> json) {
/// @nodoc /// @nodoc
mixin _$SleepTimerSettings { mixin _$SleepTimerSettings {
Duration get defaultDuration => throw _privateConstructorUsedError; Duration get defaultDuration => throw _privateConstructorUsedError;
SleepTimerShakeSenseMode get shakeSenseMode => List<Duration> get presetDurations => throw _privateConstructorUsedError;
throw _privateConstructorUsedError; Duration get maxDuration => 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;
bool get fadeOutAudio => 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 /// if true, the player will automatically rewind the audio when the sleep timer is stopped
bool get autoRewindWhenStopped => throw _privateConstructorUsedError; bool get autoRewindWhenStopped => throw _privateConstructorUsedError;
@ -1422,12 +1416,10 @@ abstract class $SleepTimerSettingsCopyWith<$Res> {
@useResult @useResult
$Res call( $Res call(
{Duration defaultDuration, {Duration defaultDuration,
SleepTimerShakeSenseMode shakeSenseMode, List<Duration> presetDurations,
Duration shakeSenseDuration, Duration maxDuration,
bool vibrateWhenReset,
bool beepWhenReset,
bool fadeOutAudio, bool fadeOutAudio,
double shakeDetectThreshold, Duration fadeOutDuration,
bool autoRewindWhenStopped, bool autoRewindWhenStopped,
Map<int, Duration> autoRewindDurations, Map<int, Duration> autoRewindDurations,
bool autoTurnOnTimer, bool autoTurnOnTimer,
@ -1452,12 +1444,10 @@ class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings>
@override @override
$Res call({ $Res call({
Object? defaultDuration = null, Object? defaultDuration = null,
Object? shakeSenseMode = null, Object? presetDurations = null,
Object? shakeSenseDuration = null, Object? maxDuration = null,
Object? vibrateWhenReset = null,
Object? beepWhenReset = null,
Object? fadeOutAudio = null, Object? fadeOutAudio = null,
Object? shakeDetectThreshold = null, Object? fadeOutDuration = null,
Object? autoRewindWhenStopped = null, Object? autoRewindWhenStopped = null,
Object? autoRewindDurations = null, Object? autoRewindDurations = null,
Object? autoTurnOnTimer = null, Object? autoTurnOnTimer = null,
@ -1470,30 +1460,22 @@ class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings>
? _value.defaultDuration ? _value.defaultDuration
: defaultDuration // ignore: cast_nullable_to_non_nullable : defaultDuration // ignore: cast_nullable_to_non_nullable
as Duration, as Duration,
shakeSenseMode: null == shakeSenseMode presetDurations: null == presetDurations
? _value.shakeSenseMode ? _value.presetDurations
: shakeSenseMode // ignore: cast_nullable_to_non_nullable : presetDurations // ignore: cast_nullable_to_non_nullable
as SleepTimerShakeSenseMode, as List<Duration>,
shakeSenseDuration: null == shakeSenseDuration maxDuration: null == maxDuration
? _value.shakeSenseDuration ? _value.maxDuration
: shakeSenseDuration // ignore: cast_nullable_to_non_nullable : maxDuration // ignore: cast_nullable_to_non_nullable
as Duration, 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 fadeOutAudio: null == fadeOutAudio
? _value.fadeOutAudio ? _value.fadeOutAudio
: fadeOutAudio // ignore: cast_nullable_to_non_nullable : fadeOutAudio // ignore: cast_nullable_to_non_nullable
as bool, as bool,
shakeDetectThreshold: null == shakeDetectThreshold fadeOutDuration: null == fadeOutDuration
? _value.shakeDetectThreshold ? _value.fadeOutDuration
: shakeDetectThreshold // ignore: cast_nullable_to_non_nullable : fadeOutDuration // ignore: cast_nullable_to_non_nullable
as double, as Duration,
autoRewindWhenStopped: null == autoRewindWhenStopped autoRewindWhenStopped: null == autoRewindWhenStopped
? _value.autoRewindWhenStopped ? _value.autoRewindWhenStopped
: autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable
@ -1532,12 +1514,10 @@ abstract class _$$SleepTimerSettingsImplCopyWith<$Res>
@useResult @useResult
$Res call( $Res call(
{Duration defaultDuration, {Duration defaultDuration,
SleepTimerShakeSenseMode shakeSenseMode, List<Duration> presetDurations,
Duration shakeSenseDuration, Duration maxDuration,
bool vibrateWhenReset,
bool beepWhenReset,
bool fadeOutAudio, bool fadeOutAudio,
double shakeDetectThreshold, Duration fadeOutDuration,
bool autoRewindWhenStopped, bool autoRewindWhenStopped,
Map<int, Duration> autoRewindDurations, Map<int, Duration> autoRewindDurations,
bool autoTurnOnTimer, bool autoTurnOnTimer,
@ -1560,12 +1540,10 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? defaultDuration = null, Object? defaultDuration = null,
Object? shakeSenseMode = null, Object? presetDurations = null,
Object? shakeSenseDuration = null, Object? maxDuration = null,
Object? vibrateWhenReset = null,
Object? beepWhenReset = null,
Object? fadeOutAudio = null, Object? fadeOutAudio = null,
Object? shakeDetectThreshold = null, Object? fadeOutDuration = null,
Object? autoRewindWhenStopped = null, Object? autoRewindWhenStopped = null,
Object? autoRewindDurations = null, Object? autoRewindDurations = null,
Object? autoTurnOnTimer = null, Object? autoTurnOnTimer = null,
@ -1578,30 +1556,22 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res>
? _value.defaultDuration ? _value.defaultDuration
: defaultDuration // ignore: cast_nullable_to_non_nullable : defaultDuration // ignore: cast_nullable_to_non_nullable
as Duration, as Duration,
shakeSenseMode: null == shakeSenseMode presetDurations: null == presetDurations
? _value.shakeSenseMode ? _value._presetDurations
: shakeSenseMode // ignore: cast_nullable_to_non_nullable : presetDurations // ignore: cast_nullable_to_non_nullable
as SleepTimerShakeSenseMode, as List<Duration>,
shakeSenseDuration: null == shakeSenseDuration maxDuration: null == maxDuration
? _value.shakeSenseDuration ? _value.maxDuration
: shakeSenseDuration // ignore: cast_nullable_to_non_nullable : maxDuration // ignore: cast_nullable_to_non_nullable
as Duration, 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 fadeOutAudio: null == fadeOutAudio
? _value.fadeOutAudio ? _value.fadeOutAudio
: fadeOutAudio // ignore: cast_nullable_to_non_nullable : fadeOutAudio // ignore: cast_nullable_to_non_nullable
as bool, as bool,
shakeDetectThreshold: null == shakeDetectThreshold fadeOutDuration: null == fadeOutDuration
? _value.shakeDetectThreshold ? _value.fadeOutDuration
: shakeDetectThreshold // ignore: cast_nullable_to_non_nullable : fadeOutDuration // ignore: cast_nullable_to_non_nullable
as double, as Duration,
autoRewindWhenStopped: null == autoRewindWhenStopped autoRewindWhenStopped: null == autoRewindWhenStopped
? _value.autoRewindWhenStopped ? _value.autoRewindWhenStopped
: autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable : autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable
@ -1635,12 +1605,14 @@ class __$$SleepTimerSettingsImplCopyWithImpl<$Res>
class _$SleepTimerSettingsImpl implements _SleepTimerSettings { class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
const _$SleepTimerSettingsImpl( const _$SleepTimerSettingsImpl(
{this.defaultDuration = const Duration(minutes: 15), {this.defaultDuration = const Duration(minutes: 15),
this.shakeSenseMode = SleepTimerShakeSenseMode.always, final List<Duration> presetDurations = const [
this.shakeSenseDuration = const Duration(seconds: 30), Duration(minutes: 5),
this.vibrateWhenReset = true, Duration(minutes: 15),
this.beepWhenReset = false, Duration(minutes: 30)
],
this.maxDuration = const Duration(minutes: 100),
this.fadeOutAudio = false, this.fadeOutAudio = false,
this.shakeDetectThreshold = 0.5, this.fadeOutDuration = const Duration(seconds: 20),
this.autoRewindWhenStopped = false, this.autoRewindWhenStopped = false,
final Map<int, Duration> autoRewindDurations = const { final Map<int, Duration> autoRewindDurations = const {
5: Duration(seconds: 10), 5: Duration(seconds: 10),
@ -1653,7 +1625,8 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
this.alwaysAutoTurnOnTimer = true, this.alwaysAutoTurnOnTimer = true,
this.autoTurnOnTime = const Duration(hours: 22, minutes: 0), this.autoTurnOnTime = const Duration(hours: 22, minutes: 0),
this.autoTurnOffTime = const Duration(hours: 6, minutes: 0)}) this.autoTurnOffTime = const Duration(hours: 6, minutes: 0)})
: _autoRewindDurations = autoRewindDurations; : _presetDurations = presetDurations,
_autoRewindDurations = autoRewindDurations;
factory _$SleepTimerSettingsImpl.fromJson(Map<String, dynamic> json) => factory _$SleepTimerSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$SleepTimerSettingsImplFromJson(json); _$$SleepTimerSettingsImplFromJson(json);
@ -1661,27 +1634,24 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
@override @override
@JsonKey() @JsonKey()
final Duration defaultDuration; final Duration defaultDuration;
final List<Duration> _presetDurations;
@override @override
@JsonKey() @JsonKey()
final SleepTimerShakeSenseMode shakeSenseMode; List<Duration> 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 @override
@JsonKey() @JsonKey()
final Duration shakeSenseDuration; final Duration maxDuration;
@override
@JsonKey()
final bool vibrateWhenReset;
@override
@JsonKey()
final bool beepWhenReset;
@override @override
@JsonKey() @JsonKey()
final bool fadeOutAudio; final bool fadeOutAudio;
@override @override
@JsonKey() @JsonKey()
final double shakeDetectThreshold; final Duration fadeOutDuration;
/// if true, the player will automatically rewind the audio when the sleep timer is stopped /// if true, the player will automatically rewind the audio when the sleep timer is stopped
@override @override
@ -1723,7 +1693,7 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
@override @override
String toString() { 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 @override
@ -1733,18 +1703,14 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
other is _$SleepTimerSettingsImpl && other is _$SleepTimerSettingsImpl &&
(identical(other.defaultDuration, defaultDuration) || (identical(other.defaultDuration, defaultDuration) ||
other.defaultDuration == defaultDuration) && other.defaultDuration == defaultDuration) &&
(identical(other.shakeSenseMode, shakeSenseMode) || const DeepCollectionEquality()
other.shakeSenseMode == shakeSenseMode) && .equals(other._presetDurations, _presetDurations) &&
(identical(other.shakeSenseDuration, shakeSenseDuration) || (identical(other.maxDuration, maxDuration) ||
other.shakeSenseDuration == shakeSenseDuration) && other.maxDuration == maxDuration) &&
(identical(other.vibrateWhenReset, vibrateWhenReset) ||
other.vibrateWhenReset == vibrateWhenReset) &&
(identical(other.beepWhenReset, beepWhenReset) ||
other.beepWhenReset == beepWhenReset) &&
(identical(other.fadeOutAudio, fadeOutAudio) || (identical(other.fadeOutAudio, fadeOutAudio) ||
other.fadeOutAudio == fadeOutAudio) && other.fadeOutAudio == fadeOutAudio) &&
(identical(other.shakeDetectThreshold, shakeDetectThreshold) || (identical(other.fadeOutDuration, fadeOutDuration) ||
other.shakeDetectThreshold == shakeDetectThreshold) && other.fadeOutDuration == fadeOutDuration) &&
(identical(other.autoRewindWhenStopped, autoRewindWhenStopped) || (identical(other.autoRewindWhenStopped, autoRewindWhenStopped) ||
other.autoRewindWhenStopped == autoRewindWhenStopped) && other.autoRewindWhenStopped == autoRewindWhenStopped) &&
const DeepCollectionEquality() const DeepCollectionEquality()
@ -1764,12 +1730,10 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
defaultDuration, defaultDuration,
shakeSenseMode, const DeepCollectionEquality().hash(_presetDurations),
shakeSenseDuration, maxDuration,
vibrateWhenReset,
beepWhenReset,
fadeOutAudio, fadeOutAudio,
shakeDetectThreshold, fadeOutDuration,
autoRewindWhenStopped, autoRewindWhenStopped,
const DeepCollectionEquality().hash(_autoRewindDurations), const DeepCollectionEquality().hash(_autoRewindDurations),
autoTurnOnTimer, autoTurnOnTimer,
@ -1797,12 +1761,10 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
abstract class _SleepTimerSettings implements SleepTimerSettings { abstract class _SleepTimerSettings implements SleepTimerSettings {
const factory _SleepTimerSettings( const factory _SleepTimerSettings(
{final Duration defaultDuration, {final Duration defaultDuration,
final SleepTimerShakeSenseMode shakeSenseMode, final List<Duration> presetDurations,
final Duration shakeSenseDuration, final Duration maxDuration,
final bool vibrateWhenReset,
final bool beepWhenReset,
final bool fadeOutAudio, final bool fadeOutAudio,
final double shakeDetectThreshold, final Duration fadeOutDuration,
final bool autoRewindWhenStopped, final bool autoRewindWhenStopped,
final Map<int, Duration> autoRewindDurations, final Map<int, Duration> autoRewindDurations,
final bool autoTurnOnTimer, final bool autoTurnOnTimer,
@ -1816,20 +1778,13 @@ abstract class _SleepTimerSettings implements SleepTimerSettings {
@override @override
Duration get defaultDuration; Duration get defaultDuration;
@override @override
SleepTimerShakeSenseMode get shakeSenseMode; List<Duration> get 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 @override
Duration get shakeSenseDuration; Duration get maxDuration;
@override
bool get vibrateWhenReset;
@override
bool get beepWhenReset;
@override @override
bool get fadeOutAudio; bool get fadeOutAudio;
@override @override
double get shakeDetectThreshold; Duration get fadeOutDuration;
/// if true, the player will automatically rewind the audio when the sleep timer is stopped /// if true, the player will automatically rewind the audio when the sleep timer is stopped
@override @override

View file

@ -150,17 +150,21 @@ _$SleepTimerSettingsImpl _$$SleepTimerSettingsImplFromJson(
defaultDuration: json['defaultDuration'] == null defaultDuration: json['defaultDuration'] == null
? const Duration(minutes: 15) ? const Duration(minutes: 15)
: Duration(microseconds: (json['defaultDuration'] as num).toInt()), : Duration(microseconds: (json['defaultDuration'] as num).toInt()),
shakeSenseMode: $enumDecodeNullable( presetDurations: (json['presetDurations'] as List<dynamic>?)
_$SleepTimerShakeSenseModeEnumMap, json['shakeSenseMode']) ?? ?.map((e) => Duration(microseconds: (e as num).toInt()))
SleepTimerShakeSenseMode.always, .toList() ??
shakeSenseDuration: json['shakeSenseDuration'] == null const [
? const Duration(seconds: 30) Duration(minutes: 5),
: Duration(microseconds: (json['shakeSenseDuration'] as num).toInt()), Duration(minutes: 15),
vibrateWhenReset: json['vibrateWhenReset'] as bool? ?? true, Duration(minutes: 30)
beepWhenReset: json['beepWhenReset'] as bool? ?? false, ],
maxDuration: json['maxDuration'] == null
? const Duration(minutes: 100)
: Duration(microseconds: (json['maxDuration'] as num).toInt()),
fadeOutAudio: json['fadeOutAudio'] as bool? ?? false, fadeOutAudio: json['fadeOutAudio'] as bool? ?? false,
shakeDetectThreshold: fadeOutDuration: json['fadeOutDuration'] == null
(json['shakeDetectThreshold'] as num?)?.toDouble() ?? 0.5, ? const Duration(seconds: 20)
: Duration(microseconds: (json['fadeOutDuration'] as num).toInt()),
autoRewindWhenStopped: json['autoRewindWhenStopped'] as bool? ?? false, autoRewindWhenStopped: json['autoRewindWhenStopped'] as bool? ?? false,
autoRewindDurations: autoRewindDurations:
(json['autoRewindDurations'] as Map<String, dynamic>?)?.map( (json['autoRewindDurations'] as Map<String, dynamic>?)?.map(
@ -188,13 +192,11 @@ Map<String, dynamic> _$$SleepTimerSettingsImplToJson(
_$SleepTimerSettingsImpl instance) => _$SleepTimerSettingsImpl instance) =>
<String, dynamic>{ <String, dynamic>{
'defaultDuration': instance.defaultDuration.inMicroseconds, 'defaultDuration': instance.defaultDuration.inMicroseconds,
'shakeSenseMode': 'presetDurations':
_$SleepTimerShakeSenseModeEnumMap[instance.shakeSenseMode]!, instance.presetDurations.map((e) => e.inMicroseconds).toList(),
'shakeSenseDuration': instance.shakeSenseDuration.inMicroseconds, 'maxDuration': instance.maxDuration.inMicroseconds,
'vibrateWhenReset': instance.vibrateWhenReset,
'beepWhenReset': instance.beepWhenReset,
'fadeOutAudio': instance.fadeOutAudio, 'fadeOutAudio': instance.fadeOutAudio,
'shakeDetectThreshold': instance.shakeDetectThreshold, 'fadeOutDuration': instance.fadeOutDuration.inMicroseconds,
'autoRewindWhenStopped': instance.autoRewindWhenStopped, 'autoRewindWhenStopped': instance.autoRewindWhenStopped,
'autoRewindDurations': instance.autoRewindDurations 'autoRewindDurations': instance.autoRewindDurations
.map((k, e) => MapEntry(k.toString(), e.inMicroseconds)), .map((k, e) => MapEntry(k.toString(), e.inMicroseconds)),
@ -204,12 +206,6 @@ Map<String, dynamic> _$$SleepTimerSettingsImplToJson(
'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds, 'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds,
}; };
const _$SleepTimerShakeSenseModeEnumMap = {
SleepTimerShakeSenseMode.never: 'never',
SleepTimerShakeSenseMode.always: 'always',
SleepTimerShakeSenseMode.nearEnds: 'nearEnds',
};
_$DownloadSettingsImpl _$$DownloadSettingsImplFromJson( _$DownloadSettingsImpl _$$DownloadSettingsImplFromJson(
Map<String, dynamic> json) => Map<String, dynamic> json) =>
_$DownloadSettingsImpl( _$DownloadSettingsImpl(

View file

@ -9,14 +9,27 @@ extension DurationFormat on Duration {
final minutes = inMinutes.remainder(60); final minutes = inMinutes.remainder(60);
final seconds = inSeconds.remainder(60); final seconds = inSeconds.remainder(60);
if (hours > 0) { 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) { } else if (minutes > 0) {
return '${minutes}m ${seconds}s'; if (seconds == 0) {
return smartSingleFormat;
}
return '${Duration(minutes: minutes).smartSingleFormat} ${Duration(seconds: seconds).smartSingleFormat}';
} else { } 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 { String get smartSingleFormat {
if (inHours > 0) { if (inHours > 0) {
return '${inHours}h'; return '${inHours}h';

View file

@ -28,3 +28,64 @@ void useTimer(VoidCallback callback, Duration delay) {
[delay], [delay],
); );
} }
/// Creates [FixedExtentScrollController] that will be disposed automatically.
///
/// See also:
/// - [FixedExtentScrollController]
FixedExtentScrollController useFixedExtentScrollController({
String? debugLabel,
List<Object?>? 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<FixedExtentScrollController> {
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<FixedExtentScrollController, Hook<FixedExtentScrollController>>
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';
}