From 0ddfa046b976274e5d03f45b4800caef95c3620c Mon Sep 17 00:00:00 2001 From: gianlucaparadise Date: Mon, 3 Jan 2022 11:44:48 +0100 Subject: [PATCH] ExpandedControl: stub ad skip handling --- .../expanded_controls/ExpandedControls.dart | 37 ++++-- .../ExpandedControlsAdSkipBox.dart | 119 ++++++++++++++++++ 2 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 lib/src/cast/widgets/expanded_controls/ExpandedControlsAdSkipBox.dart diff --git a/lib/src/cast/widgets/expanded_controls/ExpandedControls.dart b/lib/src/cast/widgets/expanded_controls/ExpandedControls.dart index f78777b..170adfc 100644 --- a/lib/src/cast/widgets/expanded_controls/ExpandedControls.dart +++ b/lib/src/cast/widgets/expanded_controls/ExpandedControls.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_cast_framework/src/cast/widgets/expanded_controls/ExpandedControlsAdSkipBox.dart'; import '../../../../cast.dart'; -import 'ExpandedControlsBasicButton.dart'; import 'ExpandedControlsConnectedDeviceLabel.dart'; import 'ExpandedControlsHighlightedText.dart'; import 'ExpandedControlsInfoTextBox.dart'; @@ -34,16 +34,28 @@ const _bottomUpBlackGradient = BoxDecoration( class ExpandedControls extends StatefulWidget { final FlutterCastFramework castFramework; - /// Label to introduce cast device. Default is "Casting to {{device_name}}", where {{device_name}} is replaced with the device name + /// Label to introduce cast device. Default is "Casting to {{cast_device_name}}", where {{cast_device_name}} is replaced with the device name + /// {{cast_device_name}} can be found in the constant CAST_DEVICE_NAME_PLACEHOLDER. final String? castingToText; + /// Label to indicate remaining time for ad. Default is "You can skip this ad in {{skip_remaining_time}}...", + /// where {{skip_remaining_time}} is replaced with the remaining time. + /// {{skip_remaining_time}} can be found in the constant SKIP_AD_TIMER_PLACEHOLDER. + final String? skipAdTimerText; + + /// Label for the Skip Ad button. Default is "Skip Ad". + final String? skipAdButtonText; + /// This is called when the back button is tapped or when the session is closed final VoidCallback? onCloseRequested; - final controller = ExpandedControlsProgressController(); + final progressController = ExpandedControlsProgressController(); + final adSkipBoxController = ExpandedControlsAdSkipBoxController(); ExpandedControls({ required this.castFramework, this.castingToText, + this.skipAdTimerText, + this.skipAdButtonText, this.onCloseRequested, }); @@ -74,7 +86,7 @@ class _ExpandedControlsState extends State { sessionManager.remoteMediaClient.onProgressUpdated = null; sessionManager.remoteMediaClient.onAdBreakClipProgressUpdated = null; - widget.controller.dispose(); + widget.progressController.dispose(); super.dispose(); } @@ -107,7 +119,7 @@ class _ExpandedControlsState extends State { } void _onProgressUpdated(int progress, int duration) { - widget.controller.updateProgress(progress, duration); + widget.progressController.updateProgress(progress, duration); } void _onAdBreakClipProgressUpdated( @@ -117,9 +129,8 @@ class _ExpandedControlsState extends State { int durationMs, int whenSkippableMs, ) { - debugPrint( - "adBreakId: $adBreakId adBreakClipId: $adBreakClipId progress: $progressMs duration: $durationMs whenSkip: $whenSkippableMs"); - widget.controller.updateProgress(progressMs, durationMs); + widget.adSkipBoxController + .updateProgress(progressMs, durationMs, whenSkippableMs); } Widget _getDecoratedToolbar(MediaInfo? mediaInfo) { @@ -155,7 +166,7 @@ class _ExpandedControlsState extends State { Padding( padding: const EdgeInsets.all(8.0), child: ExpandedControlsProgress( - controller: widget.controller, + controller: widget.progressController, ), ), Padding( @@ -202,9 +213,11 @@ class _ExpandedControlsState extends State { ), ), const Spacer(flex: 1), - ExpandedControlsBasicButton( - text: "Skip Ad", // TODO: localize label - onPressed: () {/* TODO: skip ad */}, + ExpandedControlsAdSkipBox( + controller: widget.adSkipBoxController, + skipAdButtonText: widget.skipAdButtonText, + skipAdTimerText: widget.skipAdTimerText, + onSkipPressed: () {/* TODO: skip ad */}, ), ]; } diff --git a/lib/src/cast/widgets/expanded_controls/ExpandedControlsAdSkipBox.dart b/lib/src/cast/widgets/expanded_controls/ExpandedControlsAdSkipBox.dart new file mode 100644 index 0000000..b59175b --- /dev/null +++ b/lib/src/cast/widgets/expanded_controls/ExpandedControlsAdSkipBox.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; + +import 'ExpandedControlsBasicButton.dart'; + +class ExpandedControlsAdSkipBoxController extends ChangeNotifier { + int progress = 0; + int duration = 0; + int whenSkippable = 0; + + void updateProgress(int progress, int duration, int whenSkippable) { + this.progress = progress; + this.duration = duration; + this.whenSkippable = whenSkippable; + notifyListeners(); + } +} + +/// Placeholder to be used for the castingToText of ExpandedControlsConnectedDeviceLabel +const SKIP_AD_TIMER_PLACEHOLDER = "{{skip_remaining_time}}"; + +class ExpandedControlsAdSkipBox extends StatefulWidget { + final ExpandedControlsAdSkipBoxController controller; + + final _defaultSkipAdTimerText = + "You can skip this ad in $SKIP_AD_TIMER_PLACEHOLDER..."; + final _defaultSkipAdButtonText = "Skip Ad"; + + /// Label to indicate remaining time for ad. Default is "You can skip this ad in {{skip_remaining_time}}...", + /// where {{skip_remaining_time}} is replaced with the remaining time. + /// {{skip_remaining_time}} can be found in the constant SKIP_AD_TIMER_PLACEHOLDER. + final String? skipAdTimerText; + + /// Label for the Skip Ad button. Default is "Skip Ad". + final String? skipAdButtonText; + final VoidCallback? onSkipPressed; + + const ExpandedControlsAdSkipBox({ + Key? key, + required this.controller, + this.skipAdButtonText, + this.skipAdTimerText, + this.onSkipPressed, + }) : super(key: key); + + @override + State createState() => + _ExpandedControlsAdSkipBoxState(); +} + +class _ExpandedControlsAdSkipBoxState extends State { + int progress = 0; + int duration = 0; + int whenSkippable = 5000; + + @override + void initState() { + widget.controller.addListener(_onProgressUpdated); + super.initState(); + } + + @override + void dispose() { + widget.controller.removeListener(_onProgressUpdated); + super.dispose(); + } + + void _onProgressUpdated() { + setState(() { + if (!mounted) return; + this.progress = widget.controller.progress; + this.duration = widget.controller.duration; + this.whenSkippable = widget.controller.whenSkippable; + }); + } + + String _replaceRemainingTime( + String textWithPlaceholder, String remainingTime) { + return textWithPlaceholder.replaceAll( + SKIP_AD_TIMER_PLACEHOLDER, remainingTime); + } + + @override + Widget build(BuildContext context) { + final canSkip = progress > whenSkippable; + if (canSkip) { + return ExpandedControlsBasicButton( + text: widget.skipAdButtonText ?? widget._defaultSkipAdButtonText, + onPressed: widget.onSkipPressed, + ); + } + + final remainingTimeMs = this.whenSkippable - this.progress; + final remainingTimeD = Duration(milliseconds: remainingTimeMs); + final durationD = Duration(milliseconds: this.duration); + + final remainingTime = _positionToString(remainingTimeD, durationD); + final baseLabel = widget.skipAdTimerText ?? widget._defaultSkipAdTimerText; + final label = _replaceRemainingTime(baseLabel, remainingTime); + return Text( + label, + style: TextStyle(color: Colors.white), + ); + } +} + +String _positionToString(Duration d, Duration total) { + String twoDigits(int n) => n.toString().padLeft(2, "0"); + if (total.inSeconds <= 60) { + // This is less than a minute, I display only seconds + return "${d.inSeconds}"; + } + + if (total.inMinutes <= 60) { + // This is less than a hour, I display only minutes and seconds + return "${twoDigits(d.inMinutes)}:${twoDigits(d.inSeconds.remainder(60))}"; + } + + return "${twoDigits(d.inHours)}:${twoDigits(d.inMinutes.remainder(60))}:${twoDigits(d.inSeconds.remainder(60))}"; +}