sleeptimer

This commit is contained in:
Dr-Blank 2024-06-06 15:35:30 -04:00
parent d372a6b096
commit b98188d7fb
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
17 changed files with 1262 additions and 363 deletions

View file

@ -1,8 +1,9 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/api/api_provider.dart';
import 'package:whispering_pages/features/player/core/audiobook_player.dart' as abp;
import 'package:whispering_pages/features/player/core/audiobook_player.dart'
as abp;
part 'audiobook_player_provider.g.dart';
part 'audiobook_player.g.dart';
// @Riverpod(keepAlive: true)
// abp.AudiobookPlayer audiobookPlayer(
@ -19,12 +20,23 @@ part 'audiobook_player_provider.g.dart';
const playerId = 'audiobook_player';
@Riverpod(keepAlive: true)
class AudiobookPlayer extends _$AudiobookPlayer {
class SimpleAudiobookPlayer extends _$SimpleAudiobookPlayer {
@override
abp.AudiobookPlayer build() {
final api = ref.watch(authenticatedApiProvider);
final player =
abp.AudiobookPlayer(api.token!, api.baseUrl);
final player = abp.AudiobookPlayer(api.token!, api.baseUrl);
ref.onDispose(player.dispose);
return player;
}
}
@Riverpod(keepAlive: true)
class AudiobookPlayer extends _$AudiobookPlayer {
@override
abp.AudiobookPlayer build() {
final player = ref.watch(simpleAudiobookPlayerProvider);
ref.onDispose(player.dispose);
@ -40,7 +52,7 @@ class AudiobookPlayer extends _$AudiobookPlayer {
ref.notifyListeners();
}
Future<void> setSpeed(double speed) async {
Future<void> setSpeed(double speed) async {
await state.setSpeed(speed);
notifyListeners();
}

View file

@ -6,7 +6,24 @@ part of 'audiobook_player.dart';
// RiverpodGenerator
// **************************************************************************
String _$audiobookPlayerHash() => r'a636d5e8e73dc6bbf7b3f47f83884bb3af3b9370';
String _$simpleAudiobookPlayerHash() =>
r'b65e6d779476a2c1fa38f617771bf997acb4f5b8';
/// See also [SimpleAudiobookPlayer].
@ProviderFor(SimpleAudiobookPlayer)
final simpleAudiobookPlayerProvider =
NotifierProvider<SimpleAudiobookPlayer, abp.AudiobookPlayer>.internal(
SimpleAudiobookPlayer.new,
name: r'simpleAudiobookPlayerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$simpleAudiobookPlayerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SimpleAudiobookPlayer = Notifier<abp.AudiobookPlayer>;
String _$audiobookPlayerHash() => r'38042d0c93034e6907677fdb614a9af1b9d636af';
/// See also [AudiobookPlayer].
@ProviderFor(AudiobookPlayer)

View file

@ -1,15 +1,19 @@
import 'dart:async';
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:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/constants/sizes.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/player/providers/currently_playing_provider.dart';
import 'package:whispering_pages/features/player/view/audiobook_player.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
import 'package:whispering_pages/features/sleep_timer/core/sleep_timer.dart';
import 'package:whispering_pages/features/sleep_timer/providers/sleep_timer_provider.dart';
import 'package:whispering_pages/shared/extensions/inverse_lerp.dart';
import 'widgets/audiobook_player_seek_button.dart';
import 'widgets/audiobook_player_seek_chapter_button.dart';
import 'widgets/player_speed_adjust_button.dart';
class PlayerWhenExpanded extends HookConsumerWidget {
const PlayerWhenExpanded({
super.key,
@ -127,7 +131,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context)
.colorScheme
.onBackground
.onSurface
.withOpacity(0.7),
),
maxLines: 1,
@ -193,10 +197,7 @@ class PlayerWhenExpanded extends HookConsumerWidget {
// speed control
const PlayerSpeedAdjustButton(),
// sleep timer
IconButton(
icon: const Icon(Icons.timer),
onPressed: () {},
),
const SleepTimerButton(),
// chapter list
IconButton(
icon: const Icon(Icons.menu_book_rounded),
@ -216,334 +217,85 @@ class PlayerWhenExpanded extends HookConsumerWidget {
}
}
class PlayerSpeedAdjustButton extends HookConsumerWidget {
const PlayerSpeedAdjustButton({
class SleepTimerButton extends HookConsumerWidget {
const SleepTimerButton({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final notifier = ref.watch(audiobookPlayerProvider.notifier);
return TextButton(
child: Text('${player.speed}x'),
onPressed: () {
showModalBottomSheet(
context: context,
barrierLabel: 'Select Speed',
constraints: const BoxConstraints(
maxHeight: 225,
),
builder: (context) {
return SpeedSelector(
onSpeedSelected: (speed) {
notifier.setSpeed(speed);
},
);
},
);
},
);
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 sleepTimer == null
? IconButton(
color: sleepTimer != null
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
icon: const Icon(Icons.timer_rounded),
onPressed: () {},
)
: RemainingSleepTimeDisplay(
timer: sleepTimer,
);
}
}
class SpeedSelector extends HookConsumerWidget {
const SpeedSelector({
class RemainingSleepTimeDisplay extends HookConsumerWidget {
const RemainingSleepTimeDisplay({
super.key,
required this.onSpeedSelected,
required this.timer,
});
final void Function(double speed) onSpeedSelected;
final SleepTimer timer;
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final speeds = appSettings.playerSettings.speedOptions;
final currentSpeed = ref.watch(audiobookPlayerProvider).speed;
final speedState = useState(currentSpeed);
// hook the onSpeedSelected function to the state
useEffect(
() {
onSpeedSelected(speedState.value);
return null;
},
[speedState.value],
);
// the speed options
const minSpeed = 0.1;
const maxSpeed = 4.0;
const speedIncrement = 0.05;
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil();
final availableSpeedsList = List.generate(
availableSpeeds,
(index) {
// need to round to 2 decimal place to avoid floating point errors
return double.parse(
(minSpeed + index * speedIncrement).toStringAsFixed(2),
);
},
);
final scrollController = FixedExtentScrollController(
initialItem: availableSpeedsList.indexOf(currentSpeed),
);
const double itemExtent = 25;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
'Playback Speed: ${speedState.value}x',
style: Theme.of(context).textTheme.titleLarge,
),
),
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.formatSingleLargestUnit()
: remainingTime?.formatSingleLargestUnit() ?? '',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
),
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// a minus button to decrease the speed
IconButton.filledTonal(
icon: const Icon(Icons.remove),
onPressed: () {
// animate to index - 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index > 0) {
scrollController.animateToItem(
index - 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
Expanded(
child: ListWheelScrollViewX(
controller: scrollController,
scrollDirection: Axis.horizontal,
itemExtent: itemExtent,
diameterRatio: 1.5, squeeze: 1.2,
// useMagnifier: true,
// magnification: 1.5,
physics: const FixedExtentScrollPhysics(),
children: availableSpeedsList
.map(
(speed) => Column(
children: [
// a vertical line
Container(
height: itemExtent * 2,
// thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05
width: speed % 0.5 == 0
? 3
: speed % 0.25 == 0
? 2
: 0.5,
color:
Theme.of(context).colorScheme.onBackground,
),
// the speed text but only at .5 increments of speed
if (speed % 0.25 == 0)
Text(
speed.toString(),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
),
),
],
),
)
.toList(),
onSelectedItemChanged: (index) {
speedState.value = availableSpeedsList[index];
// onSpeedSelected(availableSpeedsList[index]);
// call after 500ms to avoid the scrollview from scrolling to the selected speed
// Future.delayed(
// const Duration(milliseconds: 100),
// () => onSpeedSelected(availableSpeedsList[index]),
// );
},
),
),
// a plus button to increase the speed
IconButton.filledTonal(
icon: const Icon(Icons.add),
onPressed: () {
// animate to index + 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index < availableSpeedsList.length - 1) {
scrollController.animateToItem(
index + 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: speeds
.map(
(speed) => Flexible(
// the text button should be highlighted if the speed is selected
child: TextButton(
style: speed == speedState.value
? TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
)
: null,
onPressed: () async {
// animate the wheel to the selected speed
var index = availableSpeedsList.indexOf(speed);
// if the speed is not in the list
if (index == -1) {
// find the nearest speed
final nearestSpeed = availableSpeedsList.firstWhere(
(element) => element > speed,
orElse: () => availableSpeedsList.last,
);
index = availableSpeedsList.indexOf(nearestSpeed);
}
await scrollController.animateToItem(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
// call the onSpeedSelected function
speedState.value = speed;
},
child: Text('$speed'),
),
),
)
.toList(),
),
const SizedBox(
height: 8,
),
],
),
);
}
}
class AudiobookPlayerSeekButton extends HookConsumerWidget {
const AudiobookPlayerSeekButton({
super.key,
required this.isForward,
});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
return IconButton(
icon: Icon(
isForward ? Icons.forward_30 : Icons.replay_30,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {
if (isForward) {
player.seek(player.positionInBook + const Duration(seconds: 30));
} else {
player.seek(player.positionInBook - const Duration(seconds: 30));
}
},
);
}
}
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton({
super.key,
required this.isForward,
});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
// add a small offset so the display does not show the previous chapter for a split second
const offset = Duration(milliseconds: 10);
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter
const doNotSeekBackIfLessThan = Duration(seconds: 5);
/// seek forward to the next chapter
void seekForward() {
final index = player.book!.chapters.indexOf(player.currentChapter!);
if (index < player.book!.chapters.length - 1) {
player.seek(
player.book!.chapters[index + 1].start + offset,
);
} else {
player.seek(player.currentChapter!.end);
}
extension DurationFormat on Duration {
/// will return a number followed by h, m, or s depending on the duration
/// only the largest unit will be shown
String formatSingleLargestUnit() {
if (inHours > 0) {
return '${inHours}h';
} else if (inMinutes > 0) {
return '${inMinutes}m';
} else {
return '${inSeconds}s';
}
/// seek backward to the previous chapter or the start of the current chapter
void seekBackward() {
final currentPlayingChapterIndex =
player.book!.chapters.indexOf(player.currentChapter!);
final chapterPosition =
player.positionInBook - player.currentChapter!.start;
BookChapter chapterToSeekTo;
// if player position is less than 5 seconds into the chapter, go to the previous chapter
if (chapterPosition < doNotSeekBackIfLessThan &&
currentPlayingChapterIndex > 0) {
chapterToSeekTo = player.book!.chapters[currentPlayingChapterIndex - 1];
} else {
chapterToSeekTo = player.currentChapter!;
}
player.seek(
chapterToSeekTo.start + offset,
);
}
return IconButton(
icon: Icon(
isForward ? Icons.skip_next : Icons.skip_previous,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {
if (player.book == null) {
return;
}
// if chapter does not exist, go to the start or end of the book
if (player.currentChapter == null) {
player.seek(isForward ? player.book!.duration : Duration.zero);
return;
}
if (isForward) {
seekForward();
} else {
seekBackward();
}
},
);
}
}
void useInterval(VoidCallback callback, Duration delay) {
final savedCallback = useRef(callback);
savedCallback.value = callback;
useEffect(
() {
final timer = Timer.periodic(delay, (_) => savedCallback.value());
return timer.cancel;
},
[delay],
);
}

View file

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/constants/sizes.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
class AudiobookPlayerSeekButton extends HookConsumerWidget {
const AudiobookPlayerSeekButton({
super.key,
required this.isForward,
});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
return IconButton(
icon: Icon(
isForward ? Icons.forward_30 : Icons.replay_30,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {
if (isForward) {
player.seek(player.positionInBook + const Duration(seconds: 30));
} else {
player.seek(player.positionInBook - const Duration(seconds: 30));
}
},
);
}
}

View file

@ -0,0 +1,79 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:whispering_pages/constants/sizes.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
class AudiobookPlayerSeekChapterButton extends HookConsumerWidget {
const AudiobookPlayerSeekChapterButton({
super.key,
required this.isForward,
});
/// if true, the button seeks forward, else it seeks backwards
final bool isForward;
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
// add a small offset so the display does not show the previous chapter for a split second
const offset = Duration(milliseconds: 10);
/// time into the current chapter to determine if we should go to the previous chapter or the start of the current chapter
const doNotSeekBackIfLessThan = Duration(seconds: 5);
/// seek forward to the next chapter
void seekForward() {
final index = player.book!.chapters.indexOf(player.currentChapter!);
if (index < player.book!.chapters.length - 1) {
player.seek(
player.book!.chapters[index + 1].start + offset,
);
} else {
player.seek(player.currentChapter!.end);
}
}
/// seek backward to the previous chapter or the start of the current chapter
void seekBackward() {
final currentPlayingChapterIndex =
player.book!.chapters.indexOf(player.currentChapter!);
final chapterPosition =
player.positionInBook - player.currentChapter!.start;
BookChapter chapterToSeekTo;
// if player position is less than 5 seconds into the chapter, go to the previous chapter
if (chapterPosition < doNotSeekBackIfLessThan &&
currentPlayingChapterIndex > 0) {
chapterToSeekTo = player.book!.chapters[currentPlayingChapterIndex - 1];
} else {
chapterToSeekTo = player.currentChapter!;
}
player.seek(
chapterToSeekTo.start + offset,
);
}
return IconButton(
icon: Icon(
isForward ? Icons.skip_next : Icons.skip_previous,
size: AppElementSizes.iconSizeSmall,
),
onPressed: () {
if (player.book == null) {
return;
}
// if chapter does not exist, go to the start or end of the book
if (player.currentChapter == null) {
player.seek(isForward ? player.book!.duration : Duration.zero);
return;
}
if (isForward) {
seekForward();
} else {
seekBackward();
}
},
);
}
}

View file

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/player/view/widgets/speed_selector.dart';
class PlayerSpeedAdjustButton extends HookConsumerWidget {
const PlayerSpeedAdjustButton({
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final player = ref.watch(audiobookPlayerProvider);
final notifier = ref.watch(audiobookPlayerProvider.notifier);
return TextButton(
child: Text('${player.speed}x'),
onPressed: () {
showModalBottomSheet(
context: context,
barrierLabel: 'Select Speed',
constraints: const BoxConstraints(
maxHeight: 225,
),
builder: (context) {
return SpeedSelector(
onSpeedSelected: (speed) {
notifier.setSpeed(speed);
},
);
},
);
},
);
}
}

View file

@ -0,0 +1,205 @@
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:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
class SpeedSelector extends HookConsumerWidget {
const SpeedSelector({
super.key,
required this.onSpeedSelected,
});
final void Function(double speed) onSpeedSelected;
@override
Widget build(BuildContext context, WidgetRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final speeds = appSettings.playerSettings.speedOptions;
final currentSpeed = ref.watch(audiobookPlayerProvider).speed;
final speedState = useState(currentSpeed);
// hook the onSpeedSelected function to the state
useEffect(
() {
onSpeedSelected(speedState.value);
return null;
},
[speedState.value],
);
// the speed options
const minSpeed = 0.1;
const maxSpeed = 4.0;
const speedIncrement = 0.05;
final availableSpeeds = ((maxSpeed - minSpeed) / speedIncrement).ceil();
final availableSpeedsList = List.generate(
availableSpeeds,
(index) {
// need to round to 2 decimal place to avoid floating point errors
return double.parse(
(minSpeed + index * speedIncrement).toStringAsFixed(2),
);
},
);
final scrollController = FixedExtentScrollController(
initialItem: availableSpeedsList.indexOf(currentSpeed),
);
const double itemExtent = 25;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Text(
'Playback Speed: ${speedState.value}x',
style: Theme.of(context).textTheme.titleLarge,
),
),
),
),
Flexible(
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// a minus button to decrease the speed
IconButton.filledTonal(
icon: const Icon(Icons.remove),
onPressed: () {
// animate to index - 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index > 0) {
scrollController.animateToItem(
index - 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
Expanded(
child: ListWheelScrollViewX(
controller: scrollController,
scrollDirection: Axis.horizontal,
itemExtent: itemExtent,
diameterRatio: 1.5, squeeze: 1.2,
// useMagnifier: true,
// magnification: 1.5,
physics: const FixedExtentScrollPhysics(),
children: availableSpeedsList
.map(
(speed) => Column(
children: [
// a vertical line
Container(
height: itemExtent * 2,
// thick if multiple of 1, thin if multiple of 0.5 and transparent if multiple of 0.05
width: speed % 0.5 == 0
? 3
: speed % 0.25 == 0
? 2
: 0.5,
color:
Theme.of(context).colorScheme.onBackground,
),
// the speed text but only at .5 increments of speed
if (speed % 0.25 == 0)
Text(
speed.toString(),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onBackground,
),
),
],
),
)
.toList(),
onSelectedItemChanged: (index) {
speedState.value = availableSpeedsList[index];
// onSpeedSelected(availableSpeedsList[index]);
// call after 500ms to avoid the scrollview from scrolling to the selected speed
// Future.delayed(
// const Duration(milliseconds: 100),
// () => onSpeedSelected(availableSpeedsList[index]),
// );
},
),
),
// a plus button to increase the speed
IconButton.filledTonal(
icon: const Icon(Icons.add),
onPressed: () {
// animate to index + 1
final index = availableSpeedsList.indexOf(speedState.value);
if (index < availableSpeedsList.length - 1) {
scrollController.animateToItem(
index + 1,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
},
),
],
),
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: speeds
.map(
(speed) => Flexible(
// the text button should be highlighted if the speed is selected
child: TextButton(
style: speed == speedState.value
? TextButton.styleFrom(
backgroundColor: Theme.of(context)
.colorScheme
.primaryContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onPrimaryContainer,
)
: null,
onPressed: () async {
// animate the wheel to the selected speed
var index = availableSpeedsList.indexOf(speed);
// if the speed is not in the list
if (index == -1) {
// find the nearest speed
final nearestSpeed = availableSpeedsList.firstWhere(
(element) => element > speed,
orElse: () => availableSpeedsList.last,
);
index = availableSpeedsList.indexOf(nearestSpeed);
}
await scrollController.animateToItem(
index,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
// call the onSpeedSelected function
speedState.value = speed;
},
child: Text('$speed'),
),
),
)
.toList(),
),
const SizedBox(
height: 8,
),
],
),
);
}
}

View file

@ -0,0 +1,89 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.dart';
import 'package:just_audio/just_audio.dart';
import 'package:whispering_pages/features/player/core/audiobook_player.dart';
/// this timer pauses the music player after a certain duration
///
/// watches the state of the music player and pauses it when the timer is up
/// timer is cancelled when the music player is paused or stopped
class SleepTimer {
/// The duration after which the music player will be paused
final Duration duration;
/// The player to be paused
final AudiobookPlayer player;
/// The timer that will pause the player
Timer? timer;
/// for internal use only
/// when the timer was started
DateTime? startedAt;
SleepTimer({required this.duration, required this.player}) {
player.playbackEventStream.listen((event) {
if (event.processingState == ProcessingState.completed ||
event.processingState == ProcessingState.idle) {
reset();
}
});
/// pause the player when the timer is up
player.playerStateStream.listen((state) {
if (state.playing && timer == null) {
startTimer();
} else if (!state.playing) {
reset();
}
});
debugPrint('SleepTimer created with duration: $duration');
}
/// resets the timer
void reset() {
if (timer != null) {
timer!.cancel();
debugPrint(
'SleepTimer cancelled timer, remaining time: $remainingTime, duration: $duration',
);
timer = null;
}
}
/// starts the timer
void startTimer() {
reset();
timer = Timer(duration, () {
player.pause();
reset();
debugPrint('SleepTimer paused player after $duration');
});
startedAt = DateTime.now();
debugPrint('SleepTimer started for $duration at $startedAt');
}
Duration get remainingTime {
if (timer == null) {
return Duration.zero;
}
final elapsed = DateTime.now().difference(startedAt!);
return duration - elapsed;
}
/// a stream that emits the remaining time every second
Stream<Duration> get remainingTimeStream async* {
while (timer != null) {
yield remainingTime;
await Future.delayed(0.5.seconds);
}
}
/// dispose the timer
void dispose() {
reset();
debugPrint('SleepTimer disposed');
}
}

View file

@ -0,0 +1,19 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/sleep_timer/core/sleep_timer.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
part 'sleep_timer_provider.g.dart';
@Riverpod(keepAlive: true)
SleepTimer? sleepTimer(SleepTimerRef ref) {
final appSettings = ref.watch(appSettingsProvider);
final sleepTimerSettings = appSettings.playerSettings.sleepTimerSettings;
var sleepTimer = SleepTimer(
// duration: sleepTimerSettings.defaultDuration,
duration: const Duration(seconds: 5),
player: ref.watch(simpleAudiobookPlayerProvider),
);
ref.onDispose(sleepTimer.dispose);
return sleepTimer;
}

View file

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sleep_timer_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$sleepTimerHash() => r'79646b12412f3300166db29328664a5e58e405bd';
/// See also [sleepTimer].
@ProviderFor(sleepTimer)
final sleepTimerProvider = Provider<SleepTimer?>.internal(
sleepTimer,
name: r'sleepTimerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$sleepTimerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SleepTimerRef = ProviderRef<SleepTimer?>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -7,6 +7,8 @@ import 'package:just_audio_media_kit/just_audio_media_kit.dart'
show JustAudioMediaKit;
import 'package:whispering_pages/api/server_provider.dart';
import 'package:whispering_pages/db/storage.dart';
import 'package:whispering_pages/features/player/providers/audiobook_player.dart';
import 'package:whispering_pages/features/sleep_timer/providers/sleep_timer_provider.dart';
import 'package:whispering_pages/router/router.dart';
import 'package:whispering_pages/settings/api_settings_provider.dart';
import 'package:whispering_pages/settings/app_settings_provider.dart';
@ -56,14 +58,36 @@ class MyApp extends ConsumerWidget {
routerConfig.goNamed(Routes.onboarding.name);
}
return MaterialApp.router(
// debugShowCheckedModeBanner: false,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ref.watch(appSettingsProvider).isDarkMode
? ThemeMode.dark
: ThemeMode.light,
routerConfig: routerConfig,
return _EagerInitialization(
child: MaterialApp.router(
// debugShowCheckedModeBanner: false,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: ref.watch(appSettingsProvider).isDarkMode
? ThemeMode.dark
: ThemeMode.light,
routerConfig: routerConfig,
),
);
}
}
// https://riverpod.dev/docs/essentials/eager_initialization
// Eagerly initialize providers by watching them.
class _EagerInitialization extends ConsumerWidget {
const _EagerInitialization({required this.child});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Eagerly initialize providers by watching them.
// By using "watch", the provider will stay alive and not be disposed.
try {
ref.watch(simpleAudiobookPlayerProvider);
ref.watch(sleepTimerProvider);
} catch (e) {
debugPrintStack(stackTrace: StackTrace.current, label: e.toString());
}
return child;
}
}

View file

@ -2,14 +2,14 @@
import 'package:flutter/material.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
import 'package:whispering_pages/db/available_boxes.dart';
import 'package:whispering_pages/settings/models/app_settings.dart' as model;
part 'app_settings_provider.g.dart';
final _box = AvailableHiveBoxes.userPrefsBox;
@riverpod
@Riverpod(keepAlive: true)
class AppSettings extends _$AppSettings {
@override
model.AppSettings build() {

View file

@ -6,12 +6,12 @@ part of 'app_settings_provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$appSettingsHash() => r'da2cd1bb0da6e136e906bc61f29da89d0c5f53fb';
String _$appSettingsHash() => r'6716bc568850ffd373fd8572c5781beefafbb9ee';
/// See also [AppSettings].
@ProviderFor(AppSettings)
final appSettingsProvider =
AutoDisposeNotifierProvider<AppSettings, model.AppSettings>.internal(
NotifierProvider<AppSettings, model.AppSettings>.internal(
AppSettings.new,
name: r'appSettingsProvider',
debugGetCreateSourceHash:
@ -20,6 +20,6 @@ final appSettingsProvider =
allTransitiveDependencies: null,
);
typedef _$AppSettings = AutoDisposeNotifier<model.AppSettings>;
typedef _$AppSettings = Notifier<model.AppSettings>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -30,7 +30,7 @@ class PlayerSettings with _$PlayerSettings {
@Default(1) double preferredDefaultVolume,
@Default(1) double preferredDefaultSpeed,
@Default([0.75, 1, 1.25, 1.5, 1.75, 2]) List<double> speedOptions,
@Default(Duration(minutes: 15)) Duration sleepTimer,
@Default(SleepTimerSettings()) SleepTimerSettings sleepTimerSettings,
}) = _PlayerSettings;
factory PlayerSettings.fromJson(Map<String, dynamic> json) =>
@ -57,3 +57,50 @@ class MinimizedPlayerSettings with _$MinimizedPlayerSettings {
factory MinimizedPlayerSettings.fromJson(Map<String, dynamic> json) =>
_$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(false) bool fadeOutAudio,
@Default(0.5) double shakeDetectThreshold,
/// if true, the player will automatically rewind the audio when the sleep timer is stopped
@Default(false) bool autoRewindWhenStopped,
/// the key is the duration in minutes
@Default({
5: Duration(seconds: 10),
15: Duration(seconds: 30),
45: Duration(seconds: 45),
60: Duration(minutes: 1),
120: Duration(minutes: 2),
})
Map<int, Duration> autoRewindDurations,
/// auto turn on timer settings
@Default(false) bool autoTurnOnTimer,
/// always auto turn on timer settings or during specific times
@Default(true) bool alwaysAutoTurnOnTimer,
/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false
///
/// duration is the time from 00:00
@Default(Duration(hours: 22, minutes: 0)) Duration autoTurnOnTime,
@Default(Duration(hours: 6, minutes: 0)) Duration autoTurnOffTime,
}) = _SleepTimerSettings;
factory SleepTimerSettings.fromJson(Map<String, dynamic> json) =>
_$SleepTimerSettingsFromJson(json);
}

View file

@ -229,7 +229,8 @@ mixin _$PlayerSettings {
double get preferredDefaultVolume => throw _privateConstructorUsedError;
double get preferredDefaultSpeed => throw _privateConstructorUsedError;
List<double> get speedOptions => throw _privateConstructorUsedError;
Duration get sleepTimer => throw _privateConstructorUsedError;
SleepTimerSettings get sleepTimerSettings =>
throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -249,10 +250,11 @@ abstract class $PlayerSettingsCopyWith<$Res> {
double preferredDefaultVolume,
double preferredDefaultSpeed,
List<double> speedOptions,
Duration sleepTimer});
SleepTimerSettings sleepTimerSettings});
$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings;
$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings;
$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings;
}
/// @nodoc
@ -273,7 +275,7 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
Object? preferredDefaultVolume = null,
Object? preferredDefaultSpeed = null,
Object? speedOptions = null,
Object? sleepTimer = null,
Object? sleepTimerSettings = null,
}) {
return _then(_value.copyWith(
miniPlayerSettings: null == miniPlayerSettings
@ -296,10 +298,10 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
? _value.speedOptions
: speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>,
sleepTimer: null == sleepTimer
? _value.sleepTimer
: sleepTimer // ignore: cast_nullable_to_non_nullable
as Duration,
sleepTimerSettings: null == sleepTimerSettings
? _value.sleepTimerSettings
: sleepTimerSettings // ignore: cast_nullable_to_non_nullable
as SleepTimerSettings,
) as $Val);
}
@ -320,6 +322,15 @@ class _$PlayerSettingsCopyWithImpl<$Res, $Val extends PlayerSettings>
return _then(_value.copyWith(expandedPlayerSettings: value) as $Val);
});
}
@override
@pragma('vm:prefer-inline')
$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings {
return $SleepTimerSettingsCopyWith<$Res>(_value.sleepTimerSettings,
(value) {
return _then(_value.copyWith(sleepTimerSettings: value) as $Val);
});
}
}
/// @nodoc
@ -336,12 +347,14 @@ abstract class _$$PlayerSettingsImplCopyWith<$Res>
double preferredDefaultVolume,
double preferredDefaultSpeed,
List<double> speedOptions,
Duration sleepTimer});
SleepTimerSettings sleepTimerSettings});
@override
$MinimizedPlayerSettingsCopyWith<$Res> get miniPlayerSettings;
@override
$ExpandedPlayerSettingsCopyWith<$Res> get expandedPlayerSettings;
@override
$SleepTimerSettingsCopyWith<$Res> get sleepTimerSettings;
}
/// @nodoc
@ -360,7 +373,7 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
Object? preferredDefaultVolume = null,
Object? preferredDefaultSpeed = null,
Object? speedOptions = null,
Object? sleepTimer = null,
Object? sleepTimerSettings = null,
}) {
return _then(_$PlayerSettingsImpl(
miniPlayerSettings: null == miniPlayerSettings
@ -383,10 +396,10 @@ class __$$PlayerSettingsImplCopyWithImpl<$Res>
? _value._speedOptions
: speedOptions // ignore: cast_nullable_to_non_nullable
as List<double>,
sleepTimer: null == sleepTimer
? _value.sleepTimer
: sleepTimer // ignore: cast_nullable_to_non_nullable
as Duration,
sleepTimerSettings: null == sleepTimerSettings
? _value.sleepTimerSettings
: sleepTimerSettings // ignore: cast_nullable_to_non_nullable
as SleepTimerSettings,
));
}
}
@ -400,7 +413,7 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
this.preferredDefaultVolume = 1,
this.preferredDefaultSpeed = 1,
final List<double> speedOptions = const [0.75, 1, 1.25, 1.5, 1.75, 2],
this.sleepTimer = const Duration(minutes: 15)})
this.sleepTimerSettings = const SleepTimerSettings()})
: _speedOptions = speedOptions;
factory _$PlayerSettingsImpl.fromJson(Map<String, dynamic> json) =>
@ -429,11 +442,11 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
@override
@JsonKey()
final Duration sleepTimer;
final SleepTimerSettings sleepTimerSettings;
@override
String toString() {
return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimer: $sleepTimer)';
return 'PlayerSettings(miniPlayerSettings: $miniPlayerSettings, expandedPlayerSettings: $expandedPlayerSettings, preferredDefaultVolume: $preferredDefaultVolume, preferredDefaultSpeed: $preferredDefaultSpeed, speedOptions: $speedOptions, sleepTimerSettings: $sleepTimerSettings)';
}
@override
@ -451,8 +464,8 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
other.preferredDefaultSpeed == preferredDefaultSpeed) &&
const DeepCollectionEquality()
.equals(other._speedOptions, _speedOptions) &&
(identical(other.sleepTimer, sleepTimer) ||
other.sleepTimer == sleepTimer));
(identical(other.sleepTimerSettings, sleepTimerSettings) ||
other.sleepTimerSettings == sleepTimerSettings));
}
@JsonKey(ignore: true)
@ -464,7 +477,7 @@ class _$PlayerSettingsImpl implements _PlayerSettings {
preferredDefaultVolume,
preferredDefaultSpeed,
const DeepCollectionEquality().hash(_speedOptions),
sleepTimer);
sleepTimerSettings);
@JsonKey(ignore: true)
@override
@ -488,7 +501,7 @@ abstract class _PlayerSettings implements PlayerSettings {
final double preferredDefaultVolume,
final double preferredDefaultSpeed,
final List<double> speedOptions,
final Duration sleepTimer}) = _$PlayerSettingsImpl;
final SleepTimerSettings sleepTimerSettings}) = _$PlayerSettingsImpl;
factory _PlayerSettings.fromJson(Map<String, dynamic> json) =
_$PlayerSettingsImpl.fromJson;
@ -504,7 +517,7 @@ abstract class _PlayerSettings implements PlayerSettings {
@override
List<double> get speedOptions;
@override
Duration get sleepTimer;
SleepTimerSettings get sleepTimerSettings;
@override
@JsonKey(ignore: true)
_$$PlayerSettingsImplCopyWith<_$PlayerSettingsImpl> get copyWith =>
@ -821,3 +834,486 @@ abstract class _MinimizedPlayerSettings implements MinimizedPlayerSettings {
_$$MinimizedPlayerSettingsImplCopyWith<_$MinimizedPlayerSettingsImpl>
get copyWith => throw _privateConstructorUsedError;
}
SleepTimerSettings _$SleepTimerSettingsFromJson(Map<String, dynamic> json) {
return _SleepTimerSettings.fromJson(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;
bool get fadeOutAudio => throw _privateConstructorUsedError;
double get shakeDetectThreshold => throw _privateConstructorUsedError;
/// if true, the player will automatically rewind the audio when the sleep timer is stopped
bool get autoRewindWhenStopped => throw _privateConstructorUsedError;
/// the key is the duration in minutes
Map<int, Duration> get autoRewindDurations =>
throw _privateConstructorUsedError;
/// auto turn on timer settings
bool get autoTurnOnTimer => throw _privateConstructorUsedError;
/// always auto turn on timer settings or during specific times
bool get alwaysAutoTurnOnTimer => throw _privateConstructorUsedError;
/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false
///
/// duration is the time from 00:00
Duration get autoTurnOnTime => throw _privateConstructorUsedError;
Duration get autoTurnOffTime => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$SleepTimerSettingsCopyWith<SleepTimerSettings> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $SleepTimerSettingsCopyWith<$Res> {
factory $SleepTimerSettingsCopyWith(
SleepTimerSettings value, $Res Function(SleepTimerSettings) then) =
_$SleepTimerSettingsCopyWithImpl<$Res, SleepTimerSettings>;
@useResult
$Res call(
{Duration defaultDuration,
SleepTimerShakeSenseMode shakeSenseMode,
Duration shakeSenseDuration,
bool vibrateWhenReset,
bool beepWhenReset,
bool fadeOutAudio,
double shakeDetectThreshold,
bool autoRewindWhenStopped,
Map<int, Duration> autoRewindDurations,
bool autoTurnOnTimer,
bool alwaysAutoTurnOnTimer,
Duration autoTurnOnTime,
Duration autoTurnOffTime});
}
/// @nodoc
class _$SleepTimerSettingsCopyWithImpl<$Res, $Val extends SleepTimerSettings>
implements $SleepTimerSettingsCopyWith<$Res> {
_$SleepTimerSettingsCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? defaultDuration = null,
Object? shakeSenseMode = null,
Object? shakeSenseDuration = null,
Object? vibrateWhenReset = null,
Object? beepWhenReset = null,
Object? fadeOutAudio = null,
Object? shakeDetectThreshold = null,
Object? autoRewindWhenStopped = null,
Object? autoRewindDurations = null,
Object? autoTurnOnTimer = null,
Object? alwaysAutoTurnOnTimer = null,
Object? autoTurnOnTime = null,
Object? autoTurnOffTime = null,
}) {
return _then(_value.copyWith(
defaultDuration: null == defaultDuration
? _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
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,
autoRewindWhenStopped: null == autoRewindWhenStopped
? _value.autoRewindWhenStopped
: autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable
as bool,
autoRewindDurations: null == autoRewindDurations
? _value.autoRewindDurations
: autoRewindDurations // ignore: cast_nullable_to_non_nullable
as Map<int, Duration>,
autoTurnOnTimer: null == autoTurnOnTimer
? _value.autoTurnOnTimer
: autoTurnOnTimer // ignore: cast_nullable_to_non_nullable
as bool,
alwaysAutoTurnOnTimer: null == alwaysAutoTurnOnTimer
? _value.alwaysAutoTurnOnTimer
: alwaysAutoTurnOnTimer // ignore: cast_nullable_to_non_nullable
as bool,
autoTurnOnTime: null == autoTurnOnTime
? _value.autoTurnOnTime
: autoTurnOnTime // ignore: cast_nullable_to_non_nullable
as Duration,
autoTurnOffTime: null == autoTurnOffTime
? _value.autoTurnOffTime
: autoTurnOffTime // ignore: cast_nullable_to_non_nullable
as Duration,
) as $Val);
}
}
/// @nodoc
abstract class _$$SleepTimerSettingsImplCopyWith<$Res>
implements $SleepTimerSettingsCopyWith<$Res> {
factory _$$SleepTimerSettingsImplCopyWith(_$SleepTimerSettingsImpl value,
$Res Function(_$SleepTimerSettingsImpl) then) =
__$$SleepTimerSettingsImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Duration defaultDuration,
SleepTimerShakeSenseMode shakeSenseMode,
Duration shakeSenseDuration,
bool vibrateWhenReset,
bool beepWhenReset,
bool fadeOutAudio,
double shakeDetectThreshold,
bool autoRewindWhenStopped,
Map<int, Duration> autoRewindDurations,
bool autoTurnOnTimer,
bool alwaysAutoTurnOnTimer,
Duration autoTurnOnTime,
Duration autoTurnOffTime});
}
/// @nodoc
class __$$SleepTimerSettingsImplCopyWithImpl<$Res>
extends _$SleepTimerSettingsCopyWithImpl<$Res, _$SleepTimerSettingsImpl>
implements _$$SleepTimerSettingsImplCopyWith<$Res> {
__$$SleepTimerSettingsImplCopyWithImpl(_$SleepTimerSettingsImpl _value,
$Res Function(_$SleepTimerSettingsImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? defaultDuration = null,
Object? shakeSenseMode = null,
Object? shakeSenseDuration = null,
Object? vibrateWhenReset = null,
Object? beepWhenReset = null,
Object? fadeOutAudio = null,
Object? shakeDetectThreshold = null,
Object? autoRewindWhenStopped = null,
Object? autoRewindDurations = null,
Object? autoTurnOnTimer = null,
Object? alwaysAutoTurnOnTimer = null,
Object? autoTurnOnTime = null,
Object? autoTurnOffTime = null,
}) {
return _then(_$SleepTimerSettingsImpl(
defaultDuration: null == defaultDuration
? _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
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,
autoRewindWhenStopped: null == autoRewindWhenStopped
? _value.autoRewindWhenStopped
: autoRewindWhenStopped // ignore: cast_nullable_to_non_nullable
as bool,
autoRewindDurations: null == autoRewindDurations
? _value._autoRewindDurations
: autoRewindDurations // ignore: cast_nullable_to_non_nullable
as Map<int, Duration>,
autoTurnOnTimer: null == autoTurnOnTimer
? _value.autoTurnOnTimer
: autoTurnOnTimer // ignore: cast_nullable_to_non_nullable
as bool,
alwaysAutoTurnOnTimer: null == alwaysAutoTurnOnTimer
? _value.alwaysAutoTurnOnTimer
: alwaysAutoTurnOnTimer // ignore: cast_nullable_to_non_nullable
as bool,
autoTurnOnTime: null == autoTurnOnTime
? _value.autoTurnOnTime
: autoTurnOnTime // ignore: cast_nullable_to_non_nullable
as Duration,
autoTurnOffTime: null == autoTurnOffTime
? _value.autoTurnOffTime
: autoTurnOffTime // ignore: cast_nullable_to_non_nullable
as Duration,
));
}
}
/// @nodoc
@JsonSerializable()
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,
this.fadeOutAudio = false,
this.shakeDetectThreshold = 0.5,
this.autoRewindWhenStopped = false,
final Map<int, Duration> autoRewindDurations = const {
5: Duration(seconds: 10),
15: Duration(seconds: 30),
45: Duration(seconds: 45),
60: Duration(minutes: 1),
120: Duration(minutes: 2)
},
this.autoTurnOnTimer = false,
this.alwaysAutoTurnOnTimer = true,
this.autoTurnOnTime = const Duration(hours: 22, minutes: 0),
this.autoTurnOffTime = const Duration(hours: 6, minutes: 0)})
: _autoRewindDurations = autoRewindDurations;
factory _$SleepTimerSettingsImpl.fromJson(Map<String, dynamic> json) =>
_$$SleepTimerSettingsImplFromJson(json);
@override
@JsonKey()
final Duration defaultDuration;
@override
@JsonKey()
final 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]
@override
@JsonKey()
final Duration shakeSenseDuration;
@override
@JsonKey()
final bool vibrateWhenReset;
@override
@JsonKey()
final bool beepWhenReset;
@override
@JsonKey()
final bool fadeOutAudio;
@override
@JsonKey()
final double shakeDetectThreshold;
/// if true, the player will automatically rewind the audio when the sleep timer is stopped
@override
@JsonKey()
final bool autoRewindWhenStopped;
/// the key is the duration in minutes
final Map<int, Duration> _autoRewindDurations;
/// the key is the duration in minutes
@override
@JsonKey()
Map<int, Duration> get autoRewindDurations {
if (_autoRewindDurations is EqualUnmodifiableMapView)
return _autoRewindDurations;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(_autoRewindDurations);
}
/// auto turn on timer settings
@override
@JsonKey()
final bool autoTurnOnTimer;
/// always auto turn on timer settings or during specific times
@override
@JsonKey()
final bool alwaysAutoTurnOnTimer;
/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false
///
/// duration is the time from 00:00
@override
@JsonKey()
final Duration autoTurnOnTime;
@override
@JsonKey()
final Duration autoTurnOffTime;
@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)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
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) &&
(identical(other.fadeOutAudio, fadeOutAudio) ||
other.fadeOutAudio == fadeOutAudio) &&
(identical(other.shakeDetectThreshold, shakeDetectThreshold) ||
other.shakeDetectThreshold == shakeDetectThreshold) &&
(identical(other.autoRewindWhenStopped, autoRewindWhenStopped) ||
other.autoRewindWhenStopped == autoRewindWhenStopped) &&
const DeepCollectionEquality()
.equals(other._autoRewindDurations, _autoRewindDurations) &&
(identical(other.autoTurnOnTimer, autoTurnOnTimer) ||
other.autoTurnOnTimer == autoTurnOnTimer) &&
(identical(other.alwaysAutoTurnOnTimer, alwaysAutoTurnOnTimer) ||
other.alwaysAutoTurnOnTimer == alwaysAutoTurnOnTimer) &&
(identical(other.autoTurnOnTime, autoTurnOnTime) ||
other.autoTurnOnTime == autoTurnOnTime) &&
(identical(other.autoTurnOffTime, autoTurnOffTime) ||
other.autoTurnOffTime == autoTurnOffTime));
}
@JsonKey(ignore: true)
@override
int get hashCode => Object.hash(
runtimeType,
defaultDuration,
shakeSenseMode,
shakeSenseDuration,
vibrateWhenReset,
beepWhenReset,
fadeOutAudio,
shakeDetectThreshold,
autoRewindWhenStopped,
const DeepCollectionEquality().hash(_autoRewindDurations),
autoTurnOnTimer,
alwaysAutoTurnOnTimer,
autoTurnOnTime,
autoTurnOffTime);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith =>
__$$SleepTimerSettingsImplCopyWithImpl<_$SleepTimerSettingsImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$SleepTimerSettingsImplToJson(
this,
);
}
}
abstract class _SleepTimerSettings implements SleepTimerSettings {
const factory _SleepTimerSettings(
{final Duration defaultDuration,
final SleepTimerShakeSenseMode shakeSenseMode,
final Duration shakeSenseDuration,
final bool vibrateWhenReset,
final bool beepWhenReset,
final bool fadeOutAudio,
final double shakeDetectThreshold,
final bool autoRewindWhenStopped,
final Map<int, Duration> autoRewindDurations,
final bool autoTurnOnTimer,
final bool alwaysAutoTurnOnTimer,
final Duration autoTurnOnTime,
final Duration autoTurnOffTime}) = _$SleepTimerSettingsImpl;
factory _SleepTimerSettings.fromJson(Map<String, dynamic> json) =
_$SleepTimerSettingsImpl.fromJson;
@override
Duration get defaultDuration;
@override
SleepTimerShakeSenseMode get shakeSenseMode;
@override
/// 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;
@override
bool get vibrateWhenReset;
@override
bool get beepWhenReset;
@override
bool get fadeOutAudio;
@override
double get shakeDetectThreshold;
@override
/// if true, the player will automatically rewind the audio when the sleep timer is stopped
bool get autoRewindWhenStopped;
@override
/// the key is the duration in minutes
Map<int, Duration> get autoRewindDurations;
@override
/// auto turn on timer settings
bool get autoTurnOnTimer;
@override
/// always auto turn on timer settings or during specific times
bool get alwaysAutoTurnOnTimer;
@override
/// auto timer settings, only used if [alwaysAutoTurnOnTimer] is false
///
/// duration is the time from 00:00
Duration get autoTurnOnTime;
@override
Duration get autoTurnOffTime;
@override
@JsonKey(ignore: true)
_$$SleepTimerSettingsImplCopyWith<_$SleepTimerSettingsImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View file

@ -42,9 +42,10 @@ _$PlayerSettingsImpl _$$PlayerSettingsImplFromJson(Map<String, dynamic> json) =>
?.map((e) => (e as num).toDouble())
.toList() ??
const [0.75, 1, 1.25, 1.5, 1.75, 2],
sleepTimer: json['sleepTimer'] == null
? const Duration(minutes: 15)
: Duration(microseconds: (json['sleepTimer'] as num).toInt()),
sleepTimerSettings: json['sleepTimerSettings'] == null
? const SleepTimerSettings()
: SleepTimerSettings.fromJson(
json['sleepTimerSettings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$PlayerSettingsImplToJson(
@ -55,7 +56,7 @@ Map<String, dynamic> _$$PlayerSettingsImplToJson(
'preferredDefaultVolume': instance.preferredDefaultVolume,
'preferredDefaultSpeed': instance.preferredDefaultSpeed,
'speedOptions': instance.speedOptions,
'sleepTimer': instance.sleepTimer.inMicroseconds,
'sleepTimerSettings': instance.sleepTimerSettings,
};
_$ExpandedPlayerSettingsImpl _$$ExpandedPlayerSettingsImplFromJson(
@ -83,3 +84,69 @@ Map<String, dynamic> _$$MinimizedPlayerSettingsImplToJson(
<String, dynamic>{
'useChapterInfo': instance.useChapterInfo,
};
_$SleepTimerSettingsImpl _$$SleepTimerSettingsImplFromJson(
Map<String, dynamic> json) =>
_$SleepTimerSettingsImpl(
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,
fadeOutAudio: json['fadeOutAudio'] as bool? ?? false,
shakeDetectThreshold:
(json['shakeDetectThreshold'] as num?)?.toDouble() ?? 0.5,
autoRewindWhenStopped: json['autoRewindWhenStopped'] as bool? ?? false,
autoRewindDurations:
(json['autoRewindDurations'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(
int.parse(k), Duration(microseconds: (e as num).toInt())),
) ??
const {
5: Duration(seconds: 10),
15: Duration(seconds: 30),
45: Duration(seconds: 45),
60: Duration(minutes: 1),
120: Duration(minutes: 2)
},
autoTurnOnTimer: json['autoTurnOnTimer'] as bool? ?? false,
alwaysAutoTurnOnTimer: json['alwaysAutoTurnOnTimer'] as bool? ?? true,
autoTurnOnTime: json['autoTurnOnTime'] == null
? const Duration(hours: 22, minutes: 0)
: Duration(microseconds: (json['autoTurnOnTime'] as num).toInt()),
autoTurnOffTime: json['autoTurnOffTime'] == null
? const Duration(hours: 6, minutes: 0)
: Duration(microseconds: (json['autoTurnOffTime'] as num).toInt()),
);
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,
'fadeOutAudio': instance.fadeOutAudio,
'shakeDetectThreshold': instance.shakeDetectThreshold,
'autoRewindWhenStopped': instance.autoRewindWhenStopped,
'autoRewindDurations': instance.autoRewindDurations
.map((k, e) => MapEntry(k.toString(), e.inMicroseconds)),
'autoTurnOnTimer': instance.autoTurnOnTimer,
'alwaysAutoTurnOnTimer': instance.alwaysAutoTurnOnTimer,
'autoTurnOnTime': instance.autoTurnOnTime.inMicroseconds,
'autoTurnOffTime': instance.autoTurnOffTime.inMicroseconds,
};
const _$SleepTimerShakeSenseModeEnumMap = {
SleepTimerShakeSenseMode.never: 'never',
SleepTimerShakeSenseMode.always: 'always',
SleepTimerShakeSenseMode.nearEnds: 'nearEnds',
};

View file

@ -70,6 +70,7 @@ dependencies:
riverpod_annotation: ^2.3.5
rxdart: ^0.27.7
scroll_loop_auto_scroll: ^0.0.5
# sensors_plus: ^5.0.1
shelfsdk:
path: ../../_dart/shelfsdk
shimmer: ^3.0.0