Merge pull request #85 from tommyxchow/79-sleep-timer

Implement sleep timer
This commit is contained in:
Tommy Chow 2022-03-22 19:06:34 -04:00 • committed by GitHub
commit aa3859adc6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 229 additions and 37 deletions

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:frosty/api/twitch_api.dart';
@ -19,6 +21,17 @@ abstract class _VideoStoreBase with Store {
late Timer _overlayTimer;
Timer? sleepTimer;
@observable
var sleepHours = 0;
@observable
var sleepMinutes = 0;
@observable
var timeRemaining = const Duration();
@readonly
var _paused = true;
@ -149,4 +162,42 @@ abstract class _VideoStoreBase with Store {
debugPrint(e.toString());
}
}
/// Updates the sleep timer with [sleepHours] and [sleepMinutes].
void updateSleepTimer() {
// If hours and minutes are 0, do nothing.
if (sleepHours == 0 && sleepMinutes == 0) return;
// If there is an ongoing timer, cancel it since it'll be replaced.
if (sleepTimer != null) cancelSleepTimer();
// Update the new time remaining
timeRemaining = Duration(hours: sleepHours, minutes: sleepMinutes);
// Reset the hours and minutes in the dropdown buttons.
sleepHours = 0;
sleepMinutes = 0;
// Set a periodic timer that will update the time remaining every second.
sleepTimer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
// If the timer is up, cancel the timer and exit the app.
if (timeRemaining.inSeconds == 0) {
timer.cancel();
Platform.isIOS ? exit(0) : SystemNavigator.pop();
return;
}
// Decrement the time remaining.
timeRemaining = Duration(seconds: timeRemaining.inSeconds - 1);
},
);
}
/// Cancels the sleep timer and resets the time remaining.
void cancelSleepTimer() {
sleepTimer?.cancel();
timeRemaining = const Duration();
}
}

View file

@ -17,6 +17,51 @@ mixin _$VideoStore on _VideoStoreBase, Store {
name: '_VideoStoreBase.videoUrl'))
.value;
final _$sleepHoursAtom = Atom(name: '_VideoStoreBase.sleepHours');
@override
int get sleepHours {
_$sleepHoursAtom.reportRead();
return super.sleepHours;
}
@override
set sleepHours(int value) {
_$sleepHoursAtom.reportWrite(value, super.sleepHours, () {
super.sleepHours = value;
});
}
final _$sleepMinutesAtom = Atom(name: '_VideoStoreBase.sleepMinutes');
@override
int get sleepMinutes {
_$sleepMinutesAtom.reportRead();
return super.sleepMinutes;
}
@override
set sleepMinutes(int value) {
_$sleepMinutesAtom.reportWrite(value, super.sleepMinutes, () {
super.sleepMinutes = value;
});
}
final _$timeRemainingAtom = Atom(name: '_VideoStoreBase.timeRemaining');
@override
Duration get timeRemaining {
_$timeRemainingAtom.reportRead();
return super.timeRemaining;
}
@override
set timeRemaining(Duration value) {
_$timeRemainingAtom.reportWrite(value, super.timeRemaining, () {
super.timeRemaining = value;
});
}
final _$_pausedAtom = Atom(name: '_VideoStoreBase._paused');
bool get paused {
@ -124,6 +169,9 @@ mixin _$VideoStore on _VideoStoreBase, Store {
@override
String toString() {
return '''
sleepHours: ${sleepHours},
sleepMinutes: ${sleepMinutes},
timeRemaining: ${timeRemaining},
videoUrl: ${videoUrl}
''';
}

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@ -7,11 +8,81 @@ import 'package:frosty/screens/channel/stores/video_store.dart';
import 'package:frosty/screens/settings/settings.dart';
import 'package:intl/intl.dart';
class VideoOverlay extends StatelessWidget {
class VideoOverlay extends StatefulWidget {
final VideoStore videoStore;
const VideoOverlay({Key? key, required this.videoStore}) : super(key: key);
@override
State<VideoOverlay> createState() => _VideoOverlayState();
}
class _VideoOverlayState extends State<VideoOverlay> {
Future<void> _showSleepTimerDialog(BuildContext context) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Sleep Timer'),
content: Observer(
builder: (context) => Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
DropdownButton(
value: widget.videoStore.sleepHours,
items: List.generate(24, (index) => index).map((e) => DropdownMenuItem(value: e, child: Text(e.toString()))).toList(),
onChanged: (int? hours) => widget.videoStore.sleepHours = hours!,
menuMaxHeight: 200,
),
const SizedBox(width: 10.0),
const Text('Hours'),
],
),
Row(
children: [
DropdownButton(
value: widget.videoStore.sleepMinutes,
items: List.generate(60, (index) => index).map((e) => DropdownMenuItem(value: e, child: Text(e.toString()))).toList(),
onChanged: (int? minutes) => widget.videoStore.sleepMinutes = minutes!,
menuMaxHeight: 200,
),
const SizedBox(width: 10.0),
const Text('Minutes'),
],
),
if (widget.videoStore.sleepTimer != null && widget.videoStore.sleepTimer!.isActive)
Row(
children: [
Text('Timer: ${widget.videoStore.timeRemaining.toString().split('.')[0]}'),
const Spacer(),
IconButton(
tooltip: 'Cancel Timer',
onPressed: widget.videoStore.cancelSleepTimer,
icon: const Icon(Icons.cancel),
),
],
),
],
),
),
actions: [
TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('Close'),
),
Observer(
builder: (context) => TextButton(
onPressed: widget.videoStore.sleepHours == 0 && widget.videoStore.sleepMinutes == 0 ? null : widget.videoStore.updateSleepTimer,
child: const Text('Set Timer'),
style: TextButton.styleFrom(primary: Colors.green),
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final orientation = MediaQuery.of(context).orientation;
@ -28,21 +99,18 @@ class VideoOverlay extends StatelessWidget {
),
);
final settingsButton = Align(
alignment: Alignment.topRight,
child: IconButton(
tooltip: 'Settings',
icon: const Icon(
Icons.settings,
color: Colors.white,
),
onPressed: () => showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => SizedBox(
height: MediaQuery.of(context).size.height * 0.8,
child: Settings(settingsStore: videoStore.settingsStore),
),
final settingsButton = IconButton(
tooltip: 'Settings',
icon: const Icon(
Icons.settings,
color: Colors.white,
),
onPressed: () => showModalBottomSheet(
isScrollControlled: true,
context: context,
builder: (context) => SizedBox(
height: MediaQuery.of(context).size.height * 0.8,
child: Settings(settingsStore: widget.videoStore.settingsStore),
),
),
);
@ -53,12 +121,12 @@ class VideoOverlay extends StatelessWidget {
Icons.refresh,
color: Colors.white,
),
onPressed: videoStore.handleRefresh,
onPressed: widget.videoStore.handleRefresh,
);
final fullScreenButton = IconButton(
tooltip: videoStore.settingsStore.fullScreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
icon: videoStore.settingsStore.fullScreen
tooltip: widget.videoStore.settingsStore.fullScreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
icon: widget.videoStore.settingsStore.fullScreen
? const Icon(
Icons.fullscreen_exit,
color: Colors.white,
@ -67,16 +135,28 @@ class VideoOverlay extends StatelessWidget {
Icons.fullscreen,
color: Colors.white,
),
onPressed: () => videoStore.settingsStore.fullScreen = !videoStore.settingsStore.fullScreen,
onPressed: () => widget.videoStore.settingsStore.fullScreen = !widget.videoStore.settingsStore.fullScreen,
);
final streamInfo = videoStore.streamInfo;
final sleepTimerButton = IconButton(
tooltip: 'Sleep Timer',
icon: const Icon(Icons.timer),
onPressed: () => _showSleepTimerDialog(context),
);
final streamInfo = widget.videoStore.streamInfo;
if (streamInfo == null) {
return Stack(
children: [
backButton,
settingsButton,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
backButton,
const Spacer(),
settingsButton,
],
),
Align(
alignment: Alignment.bottomRight,
child: Row(
@ -103,16 +183,23 @@ class VideoOverlay extends StatelessWidget {
return Observer(
builder: (context) => Stack(
children: [
backButton,
settingsButton,
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
backButton,
const Spacer(),
sleepTimerButton,
settingsButton,
],
),
// Add a play button when paused for Android
// When an ad is paused on Android there is no way to unpause, so a play button is necessary.
if (Platform.isAndroid)
Center(
child: IconButton(
tooltip: videoStore.paused ? 'Play' : 'Pause',
tooltip: widget.videoStore.paused ? 'Play' : 'Pause',
iconSize: 50.0,
icon: videoStore.paused
icon: widget.videoStore.paused
? const Icon(
Icons.play_arrow,
color: Colors.white,
@ -121,10 +208,10 @@ class VideoOverlay extends StatelessWidget {
Icons.pause,
color: Colors.white,
),
onPressed: videoStore.handlePausePlay,
onPressed: widget.videoStore.handlePausePlay,
),
)
else if (!videoStore.paused)
else if (!widget.videoStore.paused)
Center(
child: IconButton(
tooltip: 'Pause',
@ -133,7 +220,7 @@ class VideoOverlay extends StatelessWidget {
Icons.pause,
color: Colors.white,
),
onPressed: videoStore.handlePausePlay,
onPressed: widget.videoStore.handlePausePlay,
),
),
Align(
@ -143,10 +230,10 @@ class VideoOverlay extends StatelessWidget {
children: [
Expanded(
child: GestureDetector(
onTap: videoStore.handleExpand,
onTap: widget.videoStore.handleExpand,
child: Padding(
padding: const EdgeInsets.all(10.0),
child: videoStore.settingsStore.expandInfo
child: widget.videoStore.settingsStore.expandInfo
? Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.start,
@ -154,11 +241,11 @@ class VideoOverlay extends StatelessWidget {
streamer,
const SizedBox(height: 5.0),
Tooltip(
message: videoStore.streamInfo!.title.trim(),
message: widget.videoStore.streamInfo!.title.trim(),
preferBelow: false,
padding: const EdgeInsets.all(10.0),
child: Text(
videoStore.streamInfo!.title.trim(),
widget.videoStore.streamInfo!.title.trim(),
maxLines: orientation == Orientation.portrait ? 1 : 5,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
@ -168,7 +255,7 @@ class VideoOverlay extends StatelessWidget {
),
const SizedBox(height: 5.0),
Text(
'${videoStore.streamInfo?.gameName} for ${NumberFormat().format(videoStore.streamInfo?.viewerCount)} viewers',
'${widget.videoStore.streamInfo?.gameName} for ${NumberFormat().format(widget.videoStore.streamInfo?.viewerCount)} viewers',
style: const TextStyle(
color: Colors.white,
),
@ -180,14 +267,14 @@ class VideoOverlay extends StatelessWidget {
),
),
refreshButton,
if (Platform.isIOS && videoStore.settingsStore.pictureInPicture)
if (Platform.isIOS && widget.videoStore.settingsStore.pictureInPicture)
IconButton(
tooltip: 'Picture-in-Picture',
icon: const Icon(
Icons.picture_in_picture_alt_rounded,
color: Colors.white,
),
onPressed: videoStore.requestPictureInPicture,
onPressed: widget.videoStore.requestPictureInPicture,
),
if (orientation == Orientation.landscape) fullScreenButton
],
@ -197,4 +284,10 @@ class VideoOverlay extends StatelessWidget {
),
);
}
@override
void dispose() {
widget.videoStore.cancelSleepTimer();
super.dispose();
}
}