From c77b1a9d5004921b76591f5d388f6b2f964aaf90 Mon Sep 17 00:00:00 2001 From: office-pc nix root Date: Sat, 28 Sep 2024 13:15:39 +0800 Subject: [PATCH 1/3] add sleep timer for videos fix #603 --- lib/l10n/app_en.arb | 7 +- lib/player/models/sleep_timer.dart | 11 ++ lib/player/models/sleep_timer.freezed.dart | 165 ++++++++++++++++++ lib/player/states/player.dart | 22 +++ lib/player/states/player.freezed.dart | 25 ++- lib/player/states/sleep_timer.dart | 18 ++ lib/player/states/tv_player_controls.dart | 8 + lib/player/states/tv_player_settings.dart | 15 +- .../states/tv_player_settings.freezed.dart | 51 +++++- .../views/components/player_controls.dart | 28 ++- lib/player/views/components/sleep_timer.dart | 80 +++++++++ .../views/tv/components/player_settings.dart | 61 +++++-- .../views/tv/components/sleep_timer.dart | 71 ++++++++ pubspec.yaml | 2 +- 14 files changed, 534 insertions(+), 30 deletions(-) create mode 100644 lib/player/models/sleep_timer.dart create mode 100644 lib/player/models/sleep_timer.freezed.dart create mode 100644 lib/player/states/sleep_timer.dart create mode 100644 lib/player/views/components/sleep_timer.dart create mode 100644 lib/player/views/tv/components/sleep_timer.dart 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: From a3b9ee5c97e7ea5fbee76f3018ad723487e9a1de Mon Sep 17 00:00:00 2001 From: office-pc nix root Date: Sat, 28 Sep 2024 15:23:29 +0800 Subject: [PATCH 2/3] add small player notification when the sleep timer is set or removed --- lib/player/states/player.dart | 2 ++ .../views/components/player_controls.dart | 29 +++++++++++++++++++ pubspec.yaml | 2 +- 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/player/states/player.dart b/lib/player/states/player.dart index 2d39fe4b..d09e2bab 100644 --- a/lib/player/states/player.dart +++ b/lib/player/states/player.dart @@ -241,6 +241,7 @@ class PlayerCubit extends Cubit with WidgetsBindingObserver { type: MediaEventType.miniDisplayChanged, value: state.isMini); emit(state.copyWith(isClosing: true)); + cancelSleep(); Future.delayed( animationDuration * 1.5, () { @@ -969,6 +970,7 @@ class PlayerCubit extends Cubit with WidgetsBindingObserver { void sleep(SleepTimer sleepTimer) { emit(state.copyWith(hasTimer: true)); + EasyDebounce.debounce( 'video-sleep-timer', sleepTimer.duration, diff --git a/lib/player/views/components/player_controls.dart b/lib/player/views/components/player_controls.dart index 7dd8d7a6..2e3bd8a5 100644 --- a/lib/player/views/components/player_controls.dart +++ b/lib/player/views/components/player_controls.dart @@ -249,6 +249,9 @@ class PlayerControls extends StatelessWidget { cubit.state.offlineCurrentlyPlaying?.title ?? ''); + bool hasTimer = + context.select((PlayerCubit cubit) => cubit.state.hasTimer); + bool isPausedAndDone = playerState.position.inMilliseconds > player.duration.inMilliseconds * 0.99 && context.select((SettingsCubit value) => @@ -621,6 +624,32 @@ class PlayerControls extends StatelessWidget { )), ), ), + // show icon when sleep is enabled or not + Positioned( + right: 20, + bottom: 20, + child: Icon(hasTimer + ? Icons.bedtime_outlined + : Icons.bedtime_off_outlined)) + .animate(target: hasTimer ? 1 : 0) + .fadeIn( + duration: animationDuration, + curve: animationCurve) + .slideY( + duration: animationDuration, + curve: animationCurve, + begin: 2, + end: 0) + .fadeOut( + delay: const Duration(seconds: 3), + duration: animationDuration, + curve: animationCurve) + .slideY( + delay: const Duration(seconds: 3), + duration: animationDuration, + curve: animationCurve, + begin: 0, + end: 2), ], ), ), diff --git a/pubspec.yaml b/pubspec.yaml index 276257e9..6770fdf6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: clipious -version: 1.22.0+4062 +version: 1.22.1+4062 publish_to: none description: Client for invidious. environment: From fb2add289352aab08114f4be977ce3eb61b850da Mon Sep 17 00:00:00 2001 From: office-pc nix root Date: Sat, 28 Sep 2024 15:28:14 +0800 Subject: [PATCH 3/3] fix translations --- lib/l10n/app_fr.arb | 6 +++--- lib/l10n/app_sv.arb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 82572b44..951a70b8 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -825,7 +825,7 @@ "@videoAddedToQueue": { "description": "Pop up message when a video was added at the end of the video queue" }, - "shareLinkWithTimestamp": "Ajouter l'horodatage", + "shareLinkWithTimestamp": "Ajouter l''horodatage", "@shareLinkWithTimestamp": { "description": "asking user to share link along with timestamp" }, @@ -1361,7 +1361,7 @@ "@refresh": {}, "wrongThumbnailConfiguration": "Le serveur est joignable mais mal configurée, les vidéos ainsi que les photo de profile des chaînes risquent de ne pas apparaitre proprement. Désactivez le test de la configuration si cela vous convient, sinon réglez le serveur", "@wrongThumbnailConfiguration": {}, - "addBasicAuth": "ajouter une méthode d'autentification basique", + "addBasicAuth": "ajouter une méthode d''autentification basique", "@addBasicAuth": {}, "value": "Valeur", "@value": {}, @@ -1381,7 +1381,7 @@ "@alsoTestServerConfig": {}, "serverAlreadyExists": "Ce serveur existe déjà dans les paramètres", "@serverAlreadyExists": {}, - "openWikiLink": "Ouvrir le Wiki pour avoir de l'aide", + "openWikiLink": "Ouvrir le Wiki pour avoir de l''aide", "@openWikiLink": {}, "serverUnreachable": "Le serveur n'est pas joignable, ou ceci n'est pas un serveur invidious valide", "@serverUnreachable": {}, diff --git a/lib/l10n/app_sv.arb b/lib/l10n/app_sv.arb index 290794b7..eb8e2aab 100644 --- a/lib/l10n/app_sv.arb +++ b/lib/l10n/app_sv.arb @@ -481,7 +481,7 @@ "@sponsorBlockCategoryIntroDescription": { "description": "Sponsorblock 'Intro' Category description" }, - "sponsorBlockCategoryOutroDescription": "Eftertexter eller när YouTube's slutbild visas. Ej för sammanfattning med information.", + "sponsorBlockCategoryOutroDescription": "Eftertexter eller när YouTube''s slutbild visas. Ej för sammanfattning med information.", "@sponsorBlockCategoryOutroDescription": { "description": "Outro block 'Outro' Category description" },