diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ac6f492e..ee2ca03c 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1374,5 +1374,10 @@ "serverUnreachable": "Server is unreachable, or is not a valid invidious server", "copyToDownloadFolder": "Copy to download folder", "fileCopiedToDownloadFolder": "File copied to download folder", - "videoDeleted": "Video deleted" + "videoDeleted": "Video deleted", + "sleepTimer": "Sleep timer", + "stopTheVideo": "Stop the video", + "stopTheVideoExplanation": "If enabled, the video will be closed, if disabled the video will be simply paused", + "setTimer": "Set timer", + "cancelSleepTimer": "Cancel sleep timer" } diff --git a/lib/player/models/sleep_timer.dart b/lib/player/models/sleep_timer.dart new file mode 100644 index 00000000..4adfeda1 --- /dev/null +++ b/lib/player/models/sleep_timer.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'sleep_timer.freezed.dart'; + +@freezed +class SleepTimer with _$SleepTimer { + const factory SleepTimer({ + @Default(Duration(minutes: 5)) Duration duration, + @Default(true) bool stopVideo, + }) = _SleepTimer; +} diff --git a/lib/player/models/sleep_timer.freezed.dart b/lib/player/models/sleep_timer.freezed.dart new file mode 100644 index 00000000..45e6e320 --- /dev/null +++ b/lib/player/models/sleep_timer.freezed.dart @@ -0,0 +1,165 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'sleep_timer.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$SleepTimer { + Duration get duration => throw _privateConstructorUsedError; + bool get stopVideo => throw _privateConstructorUsedError; + + /// Create a copy of SleepTimer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SleepTimerCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SleepTimerCopyWith<$Res> { + factory $SleepTimerCopyWith( + SleepTimer value, $Res Function(SleepTimer) then) = + _$SleepTimerCopyWithImpl<$Res, SleepTimer>; + @useResult + $Res call({Duration duration, bool stopVideo}); +} + +/// @nodoc +class _$SleepTimerCopyWithImpl<$Res, $Val extends SleepTimer> + implements $SleepTimerCopyWith<$Res> { + _$SleepTimerCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SleepTimer + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? duration = null, + Object? stopVideo = null, + }) { + return _then(_value.copyWith( + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + stopVideo: null == stopVideo + ? _value.stopVideo + : stopVideo // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SleepTimerImplCopyWith<$Res> + implements $SleepTimerCopyWith<$Res> { + factory _$$SleepTimerImplCopyWith( + _$SleepTimerImpl value, $Res Function(_$SleepTimerImpl) then) = + __$$SleepTimerImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Duration duration, bool stopVideo}); +} + +/// @nodoc +class __$$SleepTimerImplCopyWithImpl<$Res> + extends _$SleepTimerCopyWithImpl<$Res, _$SleepTimerImpl> + implements _$$SleepTimerImplCopyWith<$Res> { + __$$SleepTimerImplCopyWithImpl( + _$SleepTimerImpl _value, $Res Function(_$SleepTimerImpl) _then) + : super(_value, _then); + + /// Create a copy of SleepTimer + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? duration = null, + Object? stopVideo = null, + }) { + return _then(_$SleepTimerImpl( + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + stopVideo: null == stopVideo + ? _value.stopVideo + : stopVideo // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$SleepTimerImpl implements _SleepTimer { + const _$SleepTimerImpl( + {this.duration = const Duration(minutes: 5), this.stopVideo = true}); + + @override + @JsonKey() + final Duration duration; + @override + @JsonKey() + final bool stopVideo; + + @override + String toString() { + return 'SleepTimer(duration: $duration, stopVideo: $stopVideo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SleepTimerImpl && + (identical(other.duration, duration) || + other.duration == duration) && + (identical(other.stopVideo, stopVideo) || + other.stopVideo == stopVideo)); + } + + @override + int get hashCode => Object.hash(runtimeType, duration, stopVideo); + + /// Create a copy of SleepTimer + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SleepTimerImplCopyWith<_$SleepTimerImpl> get copyWith => + __$$SleepTimerImplCopyWithImpl<_$SleepTimerImpl>(this, _$identity); +} + +abstract class _SleepTimer implements SleepTimer { + const factory _SleepTimer({final Duration duration, final bool stopVideo}) = + _$SleepTimerImpl; + + @override + Duration get duration; + @override + bool get stopVideo; + + /// Create a copy of SleepTimer + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SleepTimerImplCopyWith<_$SleepTimerImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/player/states/player.dart b/lib/player/states/player.dart index 00ed1410..2d39fe4b 100644 --- a/lib/player/states/player.dart +++ b/lib/player/states/player.dart @@ -5,6 +5,7 @@ import 'package:audio_service/audio_service.dart'; import 'package:audio_session/audio_session.dart'; import 'package:back_button_interceptor/back_button_interceptor.dart'; import 'package:bloc/bloc.dart'; +import 'package:clipious/player/models/sleep_timer.dart'; import 'package:easy_debounce/easy_debounce.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; @@ -965,6 +966,26 @@ class PlayerCubit extends Cubit with WidgetsBindingObserver { setFullScreen(FullScreenState.fullScreen); } } + + void sleep(SleepTimer sleepTimer) { + emit(state.copyWith(hasTimer: true)); + EasyDebounce.debounce( + 'video-sleep-timer', + sleepTimer.duration, + () { + emit(state.copyWith(hasTimer: false)); + pause(); + if (sleepTimer.stopVideo && !isClosed) { + hide(); + } + }, + ); + } + + void cancelSleep() { + emit(state.copyWith(hasTimer: false)); + EasyDebounce.cancel('video-sleep-timer'); + } } @freezed @@ -973,6 +994,7 @@ class PlayerState with _$PlayerState { { // player display properties @Default(true) bool isMini, + @Default(false) bool hasTimer, double? top, @Default(false) bool isDragging, @Default(0) int selectedFullScreenIndex, diff --git a/lib/player/states/player.freezed.dart b/lib/player/states/player.freezed.dart index 8842456e..8e5d2a41 100644 --- a/lib/player/states/player.freezed.dart +++ b/lib/player/states/player.freezed.dart @@ -18,6 +18,7 @@ final _privateConstructorUsedError = UnsupportedError( mixin _$PlayerState { // player display properties bool get isMini => throw _privateConstructorUsedError; + bool get hasTimer => throw _privateConstructorUsedError; double? get top => throw _privateConstructorUsedError; bool get isDragging => throw _privateConstructorUsedError; int get selectedFullScreenIndex => throw _privateConstructorUsedError; @@ -76,6 +77,7 @@ abstract class $PlayerStateCopyWith<$Res> { @useResult $Res call( {bool isMini, + bool hasTimer, double? top, bool isDragging, int selectedFullScreenIndex, @@ -129,6 +131,7 @@ class _$PlayerStateCopyWithImpl<$Res, $Val extends PlayerState> @override $Res call({ Object? isMini = null, + Object? hasTimer = null, Object? top = freezed, Object? isDragging = null, Object? selectedFullScreenIndex = null, @@ -170,6 +173,10 @@ class _$PlayerStateCopyWithImpl<$Res, $Val extends PlayerState> ? _value.isMini : isMini // ignore: cast_nullable_to_non_nullable as bool, + hasTimer: null == hasTimer + ? _value.hasTimer + : hasTimer // ignore: cast_nullable_to_non_nullable + as bool, top: freezed == top ? _value.top : top // ignore: cast_nullable_to_non_nullable @@ -324,6 +331,7 @@ abstract class _$$PlayerStateImplCopyWith<$Res> @useResult $Res call( {bool isMini, + bool hasTimer, double? top, bool isDragging, int selectedFullScreenIndex, @@ -375,6 +383,7 @@ class __$$PlayerStateImplCopyWithImpl<$Res> @override $Res call({ Object? isMini = null, + Object? hasTimer = null, Object? top = freezed, Object? isDragging = null, Object? selectedFullScreenIndex = null, @@ -416,6 +425,10 @@ class __$$PlayerStateImplCopyWithImpl<$Res> ? _value.isMini : isMini // ignore: cast_nullable_to_non_nullable as bool, + hasTimer: null == hasTimer + ? _value.hasTimer + : hasTimer // ignore: cast_nullable_to_non_nullable + as bool, top: freezed == top ? _value.top : top // ignore: cast_nullable_to_non_nullable @@ -559,6 +572,7 @@ class __$$PlayerStateImplCopyWithImpl<$Res> class _$PlayerStateImpl extends _PlayerState { const _$PlayerStateImpl( {this.isMini = true, + this.hasTimer = false, this.top, this.isDragging = false, this.selectedFullScreenIndex = 0, @@ -605,6 +619,9 @@ class _$PlayerStateImpl extends _PlayerState { @JsonKey() final bool isMini; @override + @JsonKey() + final bool hasTimer; + @override final double? top; @override @JsonKey() @@ -740,7 +757,7 @@ class _$PlayerStateImpl extends _PlayerState { @override String toString() { - return 'PlayerState(isMini: $isMini, top: $top, isDragging: $isDragging, selectedFullScreenIndex: $selectedFullScreenIndex, isHidden: $isHidden, isClosing: $isClosing, dragDistance: $dragDistance, showMiniPlaceholder: $showMiniPlaceholder, dragStartMini: $dragStartMini, height: $height, fullScreenState: $fullScreenState, muted: $muted, aspectRatio: $aspectRatio, currentlyPlaying: $currentlyPlaying, offlineCurrentlyPlaying: $offlineCurrentlyPlaying, videos: $videos, offlineVideos: $offlineVideos, playedVideos: $playedVideos, playQueue: $playQueue, isAudio: $isAudio, isPip: $isPip, offset: $offset, startAt: $startAt, position: $position, bufferedPosition: $bufferedPosition, isPlaying: $isPlaying, speed: $speed, mediaCommand: $mediaCommand, mediaEvent: $mediaEvent, sponsorSegments: $sponsorSegments, nextSegment: $nextSegment, forwardStep: $forwardStep, rewindStep: $rewindStep, totalFastForward: $totalFastForward, totalRewind: $totalRewind, orientation: $orientation)'; + return 'PlayerState(isMini: $isMini, hasTimer: $hasTimer, top: $top, isDragging: $isDragging, selectedFullScreenIndex: $selectedFullScreenIndex, isHidden: $isHidden, isClosing: $isClosing, dragDistance: $dragDistance, showMiniPlaceholder: $showMiniPlaceholder, dragStartMini: $dragStartMini, height: $height, fullScreenState: $fullScreenState, muted: $muted, aspectRatio: $aspectRatio, currentlyPlaying: $currentlyPlaying, offlineCurrentlyPlaying: $offlineCurrentlyPlaying, videos: $videos, offlineVideos: $offlineVideos, playedVideos: $playedVideos, playQueue: $playQueue, isAudio: $isAudio, isPip: $isPip, offset: $offset, startAt: $startAt, position: $position, bufferedPosition: $bufferedPosition, isPlaying: $isPlaying, speed: $speed, mediaCommand: $mediaCommand, mediaEvent: $mediaEvent, sponsorSegments: $sponsorSegments, nextSegment: $nextSegment, forwardStep: $forwardStep, rewindStep: $rewindStep, totalFastForward: $totalFastForward, totalRewind: $totalRewind, orientation: $orientation)'; } @override @@ -749,6 +766,8 @@ class _$PlayerStateImpl extends _PlayerState { (other.runtimeType == runtimeType && other is _$PlayerStateImpl && (identical(other.isMini, isMini) || other.isMini == isMini) && + (identical(other.hasTimer, hasTimer) || + other.hasTimer == hasTimer) && (identical(other.top, top) || other.top == top) && (identical(other.isDragging, isDragging) || other.isDragging == isDragging) && @@ -817,6 +836,7 @@ class _$PlayerStateImpl extends _PlayerState { int get hashCode => Object.hashAll([ runtimeType, isMini, + hasTimer, top, isDragging, selectedFullScreenIndex, @@ -866,6 +886,7 @@ class _$PlayerStateImpl extends _PlayerState { abstract class _PlayerState extends PlayerState { const factory _PlayerState( {final bool isMini, + final bool hasTimer, final double? top, final bool isDragging, final int selectedFullScreenIndex, @@ -907,6 +928,8 @@ abstract class _PlayerState extends PlayerState { @override bool get isMini; @override + bool get hasTimer; + @override double? get top; @override bool get isDragging; diff --git a/lib/player/states/sleep_timer.dart b/lib/player/states/sleep_timer.dart new file mode 100644 index 00000000..5bc2ae39 --- /dev/null +++ b/lib/player/states/sleep_timer.dart @@ -0,0 +1,18 @@ +import 'package:clipious/player/models/sleep_timer.dart'; +import 'package:clipious/player/views/components/sleep_timer.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SleepTimerCubit extends Cubit { + SleepTimerCubit(super.initialState); + + setDuration(Duration duration) { + if (duration >= sleepTimerMinDuration && + duration <= sleepTimerMaxDuration) { + emit(state.copyWith(duration: duration)); + } + } + + setStopVideo(bool stop) { + emit(state.copyWith(stopVideo: stop)); + } +} diff --git a/lib/player/states/tv_player_controls.dart b/lib/player/states/tv_player_controls.dart index 893f288a..b7ed46a2 100644 --- a/lib/player/states/tv_player_controls.dart +++ b/lib/player/states/tv_player_controls.dart @@ -111,6 +111,14 @@ class TvPlayerControlsCubit extends Cubit { return KeyEventResult.ignored; } + hideSettings() { + emit(state.copyWith( + controlsOpacity: 0, + showSettings: false, + showQueue: false, + displayControls: false)); + } + hideControls() { EasyDebounce.debounce('tv-controls', controlFadeOut, () { if (!isClosed) { diff --git a/lib/player/states/tv_player_settings.dart b/lib/player/states/tv_player_settings.dart index c30840f6..fcb75540 100644 --- a/lib/player/states/tv_player_settings.dart +++ b/lib/player/states/tv_player_settings.dart @@ -1,3 +1,4 @@ +import 'package:clipious/player/models/sleep_timer.dart'; import 'package:river_player/river_player.dart'; import 'package:bloc/bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -27,7 +28,8 @@ enum Tabs { video, audio, captions, - playbackSpeed; + playbackSpeed, + sleepTimer; } class TvPlayerSettingsCubit extends Cubit { @@ -80,6 +82,12 @@ class TvPlayerSettingsCubit extends Cubit { } } + sleepTimerButtonFocusChange(bool focus) { + if (focus) { + emit(state.copyWith(selected: Tabs.sleepTimer)); + } + } + playbackSpeedButtonFocusChange(bool focus) { if (focus) { emit(state.copyWith(selected: Tabs.playbackSpeed)); @@ -140,6 +148,7 @@ class TvPlayerSettingsCubit extends Cubit { @freezed class TvPlayerSettingsState with _$TvPlayerSettingsState { - const factory TvPlayerSettingsState({@Default(Tabs.video) Tabs selected}) = - _TvPlayerSettingsState; + const factory TvPlayerSettingsState( + {@Default(Tabs.video) Tabs selected, + @Default(SleepTimer()) SleepTimer sleepTimer}) = _TvPlayerSettingsState; } diff --git a/lib/player/states/tv_player_settings.freezed.dart b/lib/player/states/tv_player_settings.freezed.dart index c5829b03..41d08676 100644 --- a/lib/player/states/tv_player_settings.freezed.dart +++ b/lib/player/states/tv_player_settings.freezed.dart @@ -17,6 +17,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$TvPlayerSettingsState { Tabs get selected => throw _privateConstructorUsedError; + SleepTimer get sleepTimer => throw _privateConstructorUsedError; /// Create a copy of TvPlayerSettingsState /// with the given fields replaced by the non-null parameter values. @@ -31,7 +32,9 @@ abstract class $TvPlayerSettingsStateCopyWith<$Res> { $Res Function(TvPlayerSettingsState) then) = _$TvPlayerSettingsStateCopyWithImpl<$Res, TvPlayerSettingsState>; @useResult - $Res call({Tabs selected}); + $Res call({Tabs selected, SleepTimer sleepTimer}); + + $SleepTimerCopyWith<$Res> get sleepTimer; } /// @nodoc @@ -51,14 +54,29 @@ class _$TvPlayerSettingsStateCopyWithImpl<$Res, @override $Res call({ Object? selected = null, + Object? sleepTimer = null, }) { return _then(_value.copyWith( selected: null == selected ? _value.selected : selected // ignore: cast_nullable_to_non_nullable as Tabs, + sleepTimer: null == sleepTimer + ? _value.sleepTimer + : sleepTimer // ignore: cast_nullable_to_non_nullable + as SleepTimer, ) as $Val); } + + /// Create a copy of TvPlayerSettingsState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $SleepTimerCopyWith<$Res> get sleepTimer { + return $SleepTimerCopyWith<$Res>(_value.sleepTimer, (value) { + return _then(_value.copyWith(sleepTimer: value) as $Val); + }); + } } /// @nodoc @@ -70,7 +88,10 @@ abstract class _$$TvPlayerSettingsStateImplCopyWith<$Res> __$$TvPlayerSettingsStateImplCopyWithImpl<$Res>; @override @useResult - $Res call({Tabs selected}); + $Res call({Tabs selected, SleepTimer sleepTimer}); + + @override + $SleepTimerCopyWith<$Res> get sleepTimer; } /// @nodoc @@ -88,12 +109,17 @@ class __$$TvPlayerSettingsStateImplCopyWithImpl<$Res> @override $Res call({ Object? selected = null, + Object? sleepTimer = null, }) { return _then(_$TvPlayerSettingsStateImpl( selected: null == selected ? _value.selected : selected // ignore: cast_nullable_to_non_nullable as Tabs, + sleepTimer: null == sleepTimer + ? _value.sleepTimer + : sleepTimer // ignore: cast_nullable_to_non_nullable + as SleepTimer, )); } } @@ -101,15 +127,19 @@ class __$$TvPlayerSettingsStateImplCopyWithImpl<$Res> /// @nodoc class _$TvPlayerSettingsStateImpl implements _TvPlayerSettingsState { - const _$TvPlayerSettingsStateImpl({this.selected = Tabs.video}); + const _$TvPlayerSettingsStateImpl( + {this.selected = Tabs.video, this.sleepTimer = const SleepTimer()}); @override @JsonKey() final Tabs selected; + @override + @JsonKey() + final SleepTimer sleepTimer; @override String toString() { - return 'TvPlayerSettingsState(selected: $selected)'; + return 'TvPlayerSettingsState(selected: $selected, sleepTimer: $sleepTimer)'; } @override @@ -118,11 +148,13 @@ class _$TvPlayerSettingsStateImpl implements _TvPlayerSettingsState { (other.runtimeType == runtimeType && other is _$TvPlayerSettingsStateImpl && (identical(other.selected, selected) || - other.selected == selected)); + other.selected == selected) && + (identical(other.sleepTimer, sleepTimer) || + other.sleepTimer == sleepTimer)); } @override - int get hashCode => Object.hash(runtimeType, selected); + int get hashCode => Object.hash(runtimeType, selected, sleepTimer); /// Create a copy of TvPlayerSettingsState /// with the given fields replaced by the non-null parameter values. @@ -135,11 +167,14 @@ class _$TvPlayerSettingsStateImpl implements _TvPlayerSettingsState { } abstract class _TvPlayerSettingsState implements TvPlayerSettingsState { - const factory _TvPlayerSettingsState({final Tabs selected}) = - _$TvPlayerSettingsStateImpl; + const factory _TvPlayerSettingsState( + {final Tabs selected, + final SleepTimer sleepTimer}) = _$TvPlayerSettingsStateImpl; @override Tabs get selected; + @override + SleepTimer get sleepTimer; /// Create a copy of TvPlayerSettingsState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/player/views/components/player_controls.dart b/lib/player/views/components/player_controls.dart index b24cd311..7dd8d7a6 100644 --- a/lib/player/views/components/player_controls.dart +++ b/lib/player/views/components/player_controls.dart @@ -1,14 +1,15 @@ import 'dart:math'; -import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:clipious/globals.dart'; import 'package:clipious/main.dart'; import 'package:clipious/player/states/interfaces/media_player.dart'; import 'package:clipious/player/states/player.dart'; +import 'package:clipious/player/views/components/sleep_timer.dart'; import 'package:clipious/settings/states/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../../utils.dart'; import '../../../videos/models/video.dart'; @@ -186,6 +187,25 @@ class PlayerControls extends StatelessWidget { ), title: Text(locals.useDash), ), + player.state.hasTimer + ? ListTile( + leading: const Icon(Icons.bedtime_off), + title: Text(locals.cancelSleepTimer), + onTap: () { + Navigator.of(context).pop(); + player.cancelSleep(); + }, + ) + : ListTile( + leading: const Icon(Icons.bedtime), + title: Text(locals.sleepTimer), + onTap: () async { + Navigator.of(context).pop(); + final sleepTimer = await SleepTimerSheet.show(context); + if (sleepTimer != null) { + player.sleep(sleepTimer); + } + }) ], ), ); diff --git a/lib/player/views/components/sleep_timer.dart b/lib/player/views/components/sleep_timer.dart new file mode 100644 index 00000000..6cddd4f9 --- /dev/null +++ b/lib/player/views/components/sleep_timer.dart @@ -0,0 +1,80 @@ +import 'package:clipious/player/models/sleep_timer.dart'; +import 'package:clipious/player/states/sleep_timer.dart'; +import 'package:clipious/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:gap/gap.dart'; + +const sleepTimerMinDuration = Duration(minutes: 1); +const sleepTimerMaxDuration = Duration(hours: 6); + +class SleepTimerSheet extends StatelessWidget { + const SleepTimerSheet({super.key}); + + static Future show(BuildContext context) { + return showModalBottomSheet( + showDragHandle: true, + context: context, + builder: (context) => const SleepTimerSheet()); + } + + @override + Widget build(BuildContext context) { + var locals = AppLocalizations.of(context)!; + + return BlocProvider( + create: (context) => SleepTimerCubit(const SleepTimer()), + child: + BlocBuilder(builder: (context, state) { + final cubit = context.read(); + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Slider( + min: sleepTimerMinDuration.inMilliseconds.toDouble(), + max: sleepTimerMaxDuration.inMilliseconds.toDouble(), + value: state.duration.inMilliseconds.toDouble(), + divisions: + (sleepTimerMaxDuration - sleepTimerMinDuration).inMinutes, + label: prettyDuration(state.duration), + onChanged: (value) => + cubit.setDuration(Duration(milliseconds: value.floor())), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + onPressed: () => cubit.setDuration( + state.duration - const Duration(minutes: 1)), + icon: const Icon(Icons.remove)), + SizedBox( + width: 50, + child: Text( + prettyDuration(state.duration), + textAlign: TextAlign.center, + )), + IconButton( + onPressed: () => cubit.setDuration( + state.duration + const Duration(minutes: 1)), + icon: const Icon(Icons.add)), + ], + ), + SwitchListTile( + title: Text(locals.stopTheVideo), + subtitle: Text(locals.stopTheVideoExplanation), + value: state.stopVideo, + onChanged: (value) => cubit.setStopVideo(value), + ), + const Gap(10), + FilledButton.tonal( + onPressed: () => Navigator.of(context).pop(state), + child: Text(locals.setTimer)), + const Gap(10), + ], + ); + }), + ); + } +} diff --git a/lib/player/views/tv/components/player_settings.dart b/lib/player/views/tv/components/player_settings.dart index ec419b33..d21811c6 100644 --- a/lib/player/views/tv/components/player_settings.dart +++ b/lib/player/views/tv/components/player_settings.dart @@ -1,45 +1,68 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:clipious/player/states/player.dart'; +import 'package:clipious/player/states/tv_player_controls.dart'; import 'package:clipious/player/states/tv_player_settings.dart'; import 'package:clipious/player/states/video_player.dart'; +import 'package:clipious/player/views/tv/components/sleep_timer.dart'; import 'package:clipious/settings/states/settings.dart'; import 'package:clipious/utils/views/tv/components/tv_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; class TvPlayerSettings extends StatelessWidget { const TvPlayerSettings({super.key}); List getContent(BuildContext context) { - var state = context.read(); - switch (state.state.selected) { + var cubit = context.read(); + var player = context.read(); + + var locals = AppLocalizations.of(context)!; + + switch (cubit.state.selected) { case Tabs.video: - return state.videoTrackNames + return cubit.videoTrackNames .map((e) => TvSettingButton( label: e, - onPressed: state.changeVideoTrack, + onPressed: cubit.changeVideoTrack, )) .toList(); case Tabs.audio: - return state.audioTrackNames + return cubit.audioTrackNames .map((e) => TvSettingButton( label: e, - onPressed: state.changeChangeAudioTrack, + onPressed: cubit.changeChangeAudioTrack, )) .toList(); case Tabs.captions: - return state.availableCaptions + return cubit.availableCaptions .map((e) => TvSettingButton( label: e, - onPressed: state.changeSubtitles, + onPressed: cubit.changeSubtitles, )) .toList(); case Tabs.playbackSpeed: return tvAvailablePlaybackSpeeds .map((e) => TvSettingButton( label: e, - onPressed: state.changePlaybackSpeed, + onPressed: cubit.changePlaybackSpeed, )) .toList(); + case Tabs.sleepTimer: + return [ + player.state.hasTimer + ? TvSettingButton( + label: locals.cancelSleepTimer, + onPressed: (selected) { + player.cancelSleep(); + context.read().hideSettings(); + }) + : TvSleepTimer( + onSet: (sleepTimer) { + player.sleep(sleepTimer!); + context.read().hideSettings(); + }, + ) + ]; default: return const [SizedBox.shrink()]; } @@ -130,6 +153,20 @@ class TvPlayerSettings extends StatelessWidget { ), ), ), + Padding( + padding: const EdgeInsets.all(8.0), + child: TvButton( + onFocusChanged: cubit.sleepTimerButtonFocusChange, + unfocusedColor: playerState.selected == Tabs.sleepTimer + ? colors.secondaryContainer + : Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, horizontal: 16), + child: Text(locals.sleepTimer, style: settingStyle), + ), + ), + ), ], ), SizedBox( diff --git a/lib/player/views/tv/components/sleep_timer.dart b/lib/player/views/tv/components/sleep_timer.dart new file mode 100644 index 00000000..cff44a84 --- /dev/null +++ b/lib/player/views/tv/components/sleep_timer.dart @@ -0,0 +1,71 @@ +import 'package:clipious/player/models/sleep_timer.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import 'package:clipious/player/states/sleep_timer.dart'; +import 'package:clipious/player/views/tv/components/player_settings.dart'; +import 'package:clipious/utils.dart'; +import 'package:clipious/utils/views/tv/components/tv_button.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; + +class TvSleepTimer extends StatelessWidget { + final Function(SleepTimer? sleepTimer) onSet; + + const TvSleepTimer({super.key, required this.onSet}); + + @override + Widget build(BuildContext context) { + var locals = AppLocalizations.of(context)!; + + return BlocProvider( + create: (context) => + SleepTimerCubit(const SleepTimer(stopVideo: false)), + child: + BlocBuilder(builder: (context, state) { + final cubit = context.read(); + + var textTheme = Theme.of(context).textTheme; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + children: [ + TvButton( + unfocusedColor: Colors.transparent, + onPressed: (context) => cubit.setDuration( + state.duration - const Duration(minutes: 1)), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.remove), + ), + ), + const Gap(16), + Text( + prettyDuration(state.duration), + style: textTheme.bodyLarge, + ), + const Gap(16), + TvButton( + unfocusedColor: Colors.transparent, + onPressed: (context) => cubit.setDuration( + state.duration + const Duration(minutes: 1)), + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Icon(Icons.add), + ), + ) + ], + ), + TvSettingButton( + label: locals.setTimer, + onPressed: (selected) { + onSet(state); + }, + ) + ], + ); + })); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 9ae5f142..276257e9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: clipious -version: 1.21.3+4062 +version: 1.22.0+4062 publish_to: none description: Client for invidious. environment: