From 4a75f3dbd1e7e6f68899de001df70e809533f142 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 19 Jun 2023 21:35:58 +0600 Subject: [PATCH] feat: add sleep timer support --- lib/components/player/player_actions.dart | 75 +++++++++++++++---- .../adaptive/adaptive_pop_sheet_list.dart | 2 +- .../shared/dialogs/track_details_dialog.dart | 4 +- lib/extensions/duration.dart | 2 +- lib/l10n/app_en.arb | 3 +- lib/provider/sleep_timer_provider.dart | 31 ++++++++ 6 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 lib/provider/sleep_timer_provider.dart diff --git a/lib/components/player/player_actions.dart b/lib/components/player/player_actions.dart index 59deeda3e..d7cf6bfb9 100644 --- a/lib/components/player/player_actions.dart +++ b/lib/components/player/player_actions.dart @@ -10,11 +10,13 @@ import 'package:spotube/components/player/sibling_tracks_sheet.dart'; import 'package:spotube/components/shared/adaptive/adaptive_pop_sheet_list.dart'; import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/extensions/context.dart'; +import 'package:spotube/extensions/duration.dart'; import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/logger.dart'; import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/sleep_timer_provider.dart'; import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerActions extends HookConsumerWidget { @@ -39,6 +41,8 @@ class PlayerActions extends HookConsumerWidget { downloader.activeItem!.id == playlist.activeTrack?.id; final localTracks = [] /* ref.watch(localTracksProvider).value */; final auth = ref.watch(AuthenticationNotifier.provider); + final sleepTimer = ref.watch(SleepTimerNotifier.provider); + final sleepTimerNotifier = ref.watch(SleepTimerNotifier.notifier); final isDownloaded = useMemoized(() { return localTracks.any( @@ -53,6 +57,18 @@ class PlayerActions extends HookConsumerWidget { true; }, [localTracks, playlist.activeTrack]); + final sleepTimerEntries = useMemoized( + () => { + context.l10n.mins(15): const Duration(minutes: 15), + context.l10n.mins(30): const Duration(minutes: 30), + context.l10n.hour(1): const Duration(hours: 1), + context.l10n.hour(2): const Duration(hours: 2), + }, + [context.l10n], + ); + + var customHoursEnabled = + sleepTimer == null || sleepTimerEntries.values.contains(sleepTimer); return Row( mainAxisAlignment: mainAxisAlignment, children: [ @@ -129,30 +145,57 @@ class PlayerActions extends HookConsumerWidget { if (playlist.activeTrack != null && !isLocalTrack && auth != null) TrackHeartButton(track: playlist.activeTrack!), AdaptivePopSheetList( - offset: const Offset(0, -50 * 5), + offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)), headings: [ Text(context.l10n.sleep_timer), ], - icon: const Icon(SpotubeIcons.timer), + icon: Icon( + SpotubeIcons.timer, + color: sleepTimer != null ? Colors.red : null, + ), + onSelected: (value) { + if (value == Duration.zero) { + sleepTimerNotifier.cancelSleepTimer(); + } else { + sleepTimerNotifier.setSleepTimer(value); + } + }, children: [ + for (final entry in sleepTimerEntries.entries) + PopSheetEntry( + value: entry.value, + enabled: sleepTimer != entry.value, + title: Text(entry.key), + ), PopSheetEntry( - value: const Duration(minutes: 15), - title: Text(context.l10n.mins(15)), - ), - PopSheetEntry( - value: const Duration(minutes: 30), - title: Text(context.l10n.mins(30)), - ), - PopSheetEntry( - value: const Duration(hours: 1), - title: Text(context.l10n.hour(1)), - ), - PopSheetEntry( - value: const Duration(hours: 2), - title: Text(context.l10n.hours(2)), + title: Text( + customHoursEnabled + ? context.l10n.custom_hours + : sleepTimer.toHumanReadableString(), + ), + // only enabled when there's no preset timers selected + enabled: customHoursEnabled, + onTap: () async { + final currentTime = TimeOfDay.now(); + final time = await showTimePicker( + context: context, + initialTime: currentTime, + ); + + if (time != null) { + sleepTimerNotifier.setSleepTimer( + Duration( + hours: (time.hour - currentTime.hour).abs(), + minutes: (time.minute - currentTime.minute).abs(), + ), + ); + } + }, ), PopSheetEntry( value: Duration.zero, + enabled: sleepTimer != Duration.zero && sleepTimer != null, + textColor: Colors.green, title: Text(context.l10n.cancel), ), ], diff --git a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart index 11fe042b1..41534cb32 100644 --- a/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart +++ b/lib/components/shared/adaptive/adaptive_pop_sheet_list.dart @@ -194,8 +194,8 @@ class _AdaptivePopSheetListItem extends StatelessWidget { ? null : () { item.onTap?.call(); - Navigator.pop(context); if (item.value != null) { + Navigator.pop(context); onSelected?.call(item.value as T); } }, diff --git a/lib/components/shared/dialogs/track_details_dialog.dart b/lib/components/shared/dialogs/track_details_dialog.dart index 09e71e567..a0da2f57c 100644 --- a/lib/components/shared/dialogs/track_details_dialog.dart +++ b/lib/components/shared/dialogs/track_details_dialog.dart @@ -119,9 +119,9 @@ class TrackDetailsDialog extends HookWidget { ), if (entry.value is Widget) entry.value as Widget - else + else if (entry.value is String) Text( - entry.value, + entry.value as String, style: theme.textTheme.bodyMedium, ), ], diff --git a/lib/extensions/duration.dart b/lib/extensions/duration.dart index 250b47af5..a989f5895 100644 --- a/lib/extensions/duration.dart +++ b/lib/extensions/duration.dart @@ -1,6 +1,6 @@ import 'package:spotube/utils/primitive_utils.dart'; extension DurationToHumanReadableString on Duration { - toHumanReadableString() => + String toHumanReadableString() => "${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}"; } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index f92444680..16806c5e1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -243,5 +243,6 @@ "sleep_timer": "Sleep Timer", "mins": "{minutes} Minutes", "hours": "{hours} Hours", - "hour": "{hours} Hour" + "hour": "{hours} Hour", + "custom_hours": "Custom Hours" } \ No newline at end of file diff --git a/lib/provider/sleep_timer_provider.dart b/lib/provider/sleep_timer_provider.dart new file mode 100644 index 000000000..32678ac73 --- /dev/null +++ b/lib/provider/sleep_timer_provider.dart @@ -0,0 +1,31 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SleepTimerNotifier extends StateNotifier { + SleepTimerNotifier() : super(null); + + Timer? _timer; + + static final provider = StateNotifierProvider( + (ref) => SleepTimerNotifier(), + ); + + static AlwaysAliveRefreshable get notifier => + provider.notifier; + + void setSleepTimer(Duration duration) { + state = duration; + + _timer = Timer(duration, () { + //! This can be a reason for app termination in iOS AppStore + exit(0); + }); + } + + void cancelSleepTimer() { + state = null; + _timer?.cancel(); + } +}