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_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,
),
),
);
}
}

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: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<double> availableSpeedsList;
final ValueNotifier<double> 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

View file

@ -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

View file

@ -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,

View file

@ -6,7 +6,7 @@ part of 'sleep_timer_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$sleepTimerHash() => r'4f80bcc342e918c70c547b8b24790ccd88aba8c3';
String _$sleepTimerHash() => r'722bebfb7367acb84aacd98da66c5858b7e446cd';
/// See also [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);
}
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<Duration> 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,

View file

@ -1374,16 +1374,10 @@ SleepTimerSettings _$SleepTimerSettingsFromJson(Map<String, dynamic> 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<Duration> 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<Duration> presetDurations,
Duration maxDuration,
bool fadeOutAudio,
double shakeDetectThreshold,
Duration fadeOutDuration,
bool autoRewindWhenStopped,
Map<int, Duration> 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<Duration>,
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<Duration> presetDurations,
Duration maxDuration,
bool fadeOutAudio,
double shakeDetectThreshold,
Duration fadeOutDuration,
bool autoRewindWhenStopped,
Map<int, Duration> 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<Duration>,
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<Duration> 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<int, Duration> 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<String, dynamic> json) =>
_$$SleepTimerSettingsImplFromJson(json);
@ -1661,27 +1634,24 @@ class _$SleepTimerSettingsImpl implements _SleepTimerSettings {
@override
@JsonKey()
final Duration defaultDuration;
final List<Duration> _presetDurations;
@override
@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
@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<Duration> presetDurations,
final Duration maxDuration,
final bool fadeOutAudio,
final double shakeDetectThreshold,
final Duration fadeOutDuration,
final bool autoRewindWhenStopped,
final Map<int, Duration> 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<Duration> 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

View file

@ -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<dynamic>?)
?.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<String, dynamic>?)?.map(
@ -188,13 +192,11 @@ Map<String, dynamic> _$$SleepTimerSettingsImplToJson(
_$SleepTimerSettingsImpl instance) =>
<String, dynamic>{
'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<String, dynamic> _$$SleepTimerSettingsImplToJson(
'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds,
};
const _$SleepTimerShakeSenseModeEnumMap = {
SleepTimerShakeSenseMode.never: 'never',
SleepTimerShakeSenseMode.always: 'always',
SleepTimerShakeSenseMode.nearEnds: 'nearEnds',
};
_$DownloadSettingsImpl _$$DownloadSettingsImplFromJson(
Map<String, dynamic> json) =>
_$DownloadSettingsImpl(

View file

@ -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';

View file

@ -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<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';
}