feat: add sleep timer support

This commit is contained in:
Kingkor Roy Tirtho 2023-06-19 21:35:58 +06:00
parent 0620b62023
commit 4a75f3dbd1
6 changed files with 96 additions and 21 deletions

View File

@ -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/adaptive/adaptive_pop_sheet_list.dart';
import 'package:spotube/components/shared/heart_button.dart'; import 'package:spotube/components/shared/heart_button.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
import 'package:spotube/models/local_track.dart'; import 'package:spotube/models/local_track.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/download_manager_provider.dart'; import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication_provider.dart'; import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_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'; import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerActions extends HookConsumerWidget { class PlayerActions extends HookConsumerWidget {
@ -39,6 +41,8 @@ class PlayerActions extends HookConsumerWidget {
downloader.activeItem!.id == playlist.activeTrack?.id; downloader.activeItem!.id == playlist.activeTrack?.id;
final localTracks = [] /* ref.watch(localTracksProvider).value */; final localTracks = [] /* ref.watch(localTracksProvider).value */;
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
final sleepTimer = ref.watch(SleepTimerNotifier.provider);
final sleepTimerNotifier = ref.watch(SleepTimerNotifier.notifier);
final isDownloaded = useMemoized(() { final isDownloaded = useMemoized(() {
return localTracks.any( return localTracks.any(
@ -53,6 +57,18 @@ class PlayerActions extends HookConsumerWidget {
true; true;
}, [localTracks, playlist.activeTrack]); }, [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( return Row(
mainAxisAlignment: mainAxisAlignment, mainAxisAlignment: mainAxisAlignment,
children: [ children: [
@ -129,30 +145,57 @@ class PlayerActions extends HookConsumerWidget {
if (playlist.activeTrack != null && !isLocalTrack && auth != null) if (playlist.activeTrack != null && !isLocalTrack && auth != null)
TrackHeartButton(track: playlist.activeTrack!), TrackHeartButton(track: playlist.activeTrack!),
AdaptivePopSheetList( AdaptivePopSheetList(
offset: const Offset(0, -50 * 5), offset: Offset(0, -50 * (sleepTimerEntries.values.length + 2)),
headings: [ headings: [
Text(context.l10n.sleep_timer), 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: [ children: [
for (final entry in sleepTimerEntries.entries)
PopSheetEntry( PopSheetEntry(
value: const Duration(minutes: 15), value: entry.value,
title: Text(context.l10n.mins(15)), enabled: sleepTimer != entry.value,
title: Text(entry.key),
), ),
PopSheetEntry( PopSheetEntry(
value: const Duration(minutes: 30), title: Text(
title: Text(context.l10n.mins(30)), customHoursEnabled
? context.l10n.custom_hours
: sleepTimer.toHumanReadableString(),
), ),
PopSheetEntry( // only enabled when there's no preset timers selected
value: const Duration(hours: 1), enabled: customHoursEnabled,
title: Text(context.l10n.hour(1)), 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: const Duration(hours: 2), }
title: Text(context.l10n.hours(2)), },
), ),
PopSheetEntry( PopSheetEntry(
value: Duration.zero, value: Duration.zero,
enabled: sleepTimer != Duration.zero && sleepTimer != null,
textColor: Colors.green,
title: Text(context.l10n.cancel), title: Text(context.l10n.cancel),
), ),
], ],

View File

@ -194,8 +194,8 @@ class _AdaptivePopSheetListItem<T> extends StatelessWidget {
? null ? null
: () { : () {
item.onTap?.call(); item.onTap?.call();
Navigator.pop(context);
if (item.value != null) { if (item.value != null) {
Navigator.pop(context);
onSelected?.call(item.value as T); onSelected?.call(item.value as T);
} }
}, },

View File

@ -119,9 +119,9 @@ class TrackDetailsDialog extends HookWidget {
), ),
if (entry.value is Widget) if (entry.value is Widget)
entry.value as Widget entry.value as Widget
else else if (entry.value is String)
Text( Text(
entry.value, entry.value as String,
style: theme.textTheme.bodyMedium, style: theme.textTheme.bodyMedium,
), ),
], ],

View File

@ -1,6 +1,6 @@
import 'package:spotube/utils/primitive_utils.dart'; import 'package:spotube/utils/primitive_utils.dart';
extension DurationToHumanReadableString on Duration { extension DurationToHumanReadableString on Duration {
toHumanReadableString() => String toHumanReadableString() =>
"${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}"; "${inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(inSeconds.remainder(60))}";
} }

View File

@ -243,5 +243,6 @@
"sleep_timer": "Sleep Timer", "sleep_timer": "Sleep Timer",
"mins": "{minutes} Minutes", "mins": "{minutes} Minutes",
"hours": "{hours} Hours", "hours": "{hours} Hours",
"hour": "{hours} Hour" "hour": "{hours} Hour",
"custom_hours": "Custom Hours"
} }

View File

@ -0,0 +1,31 @@
import 'dart:async';
import 'dart:io';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class SleepTimerNotifier extends StateNotifier<Duration?> {
SleepTimerNotifier() : super(null);
Timer? _timer;
static final provider = StateNotifierProvider<SleepTimerNotifier, Duration?>(
(ref) => SleepTimerNotifier(),
);
static AlwaysAliveRefreshable<SleepTimerNotifier> 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();
}
}