Merge pull request #85 from tommyxchow/79-sleep-timer
Implement sleep timer
This commit is contained in:
commit
aa3859adc6
3 changed files with 229 additions and 37 deletions
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue