From 1eb92f469e215a88fd887d459f8bf24bdc70dc93 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Tue, 6 Feb 2024 20:44:52 -0300 Subject: [PATCH] feat: Press late label to dismiss the late video automatically --- lib/l10n/app_en.arb | 15 +- lib/l10n/app_fr.arb | 15 +- lib/l10n/app_pl.arb | 15 +- lib/l10n/app_pt.arb | 15 +- lib/providers/settings_provider.dart | 4 +- .../device_grid/video_status_label.dart | 131 ++++++++++-------- lib/widgets/settings/desktop/server.dart | 52 ++++++- 7 files changed, 175 insertions(+), 72 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index b00bbee9..864de8c8 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -488,10 +488,21 @@ "contain": "Contain", "fill": "Fill", "cover": "Cover", + "@@Late stream behavior": {}, "lateStreamBehavior": "Late stream behavior", "lateStreamBehaviorDescription": "What to do when a stream is late", - "automatic": "Automatic", - "manual": "Manual", + "automaticBehavior": "Automatic", + "automaticBehaviorDescription": "The app will try to reposition the stream automatically", + "manualBehavior": "Manual", + "manualBehaviorDescription": "Press {label} to reposition the stream", + "@manualBehaviorDescription": { + "placeholders": { + "label": { + "type": "String" + } + } + }, + "neverBehaviorDescription": "The app will not try to reposition the stream", "@@LOCALIZATION": {}, "dateLanguage": "Date and Language", "language": "Language", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5b9a6100..0b5ee540 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -462,10 +462,21 @@ "contain": "Contenir", "fill": "Remplir", "cover": "Couvrir", + "@@Late stream behavior": {}, "lateStreamBehavior": "Late stream behavior", "lateStreamBehaviorDescription": "What to do when a stream is late", - "automatic": "Automatic", - "manual": "Manual", + "automaticBehavior": "Automatic", + "automaticBehaviorDescription": "The app will try to reposition the stream automatically", + "manualBehavior": "Manual", + "manualBehaviorDescription": "Press {label} to reposition the stream", + "@manualBehaviorDescription": { + "placeholders": { + "label": { + "type": "String" + } + } + }, + "neverBehaviorDescription": "The app will not try to reposition the stream", "@@LOCALIZATION": {}, "dateLanguage": "Date et Langue", "language": "Langue", diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index e95c27b2..951a2604 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -488,10 +488,21 @@ "contain": "Zawartość", "fill": "Wypełnienie", "cover": "Pokrycie", + "@@Late stream behavior": {}, "lateStreamBehavior": "Late stream behavior", "lateStreamBehaviorDescription": "What to do when a stream is late", - "automatic": "Automatic", - "manual": "Manual", + "automaticBehavior": "Automatic", + "automaticBehaviorDescription": "The app will try to reposition the stream automatically", + "manualBehavior": "Manual", + "manualBehaviorDescription": "Press {label} to reposition the stream", + "@manualBehaviorDescription": { + "placeholders": { + "label": { + "type": "String" + } + } + }, + "neverBehaviorDescription": "The app will not try to reposition the stream", "@@LOCALIZATION": {}, "dateLanguage": "Date and Language", "language": "Language", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 96a554f7..77358ce4 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -488,10 +488,21 @@ "contain": "Limitar", "fill": "Preencher", "cover": "Cobrir", + "@@Late stream behavior": {}, "lateStreamBehavior": "Transmissão atrasada", "lateStreamBehaviorDescription": "O que fazer quando a transmissão está atrasada.", - "automatic": "Automático", - "manual": "Manual", + "automaticBehavior": "Automático", + "automaticBehaviorDescription": "A transmissão será reajustada automaticamente", + "manualBehavior": "Manual", + "manualBehaviorDescription": "Pressione {label} para reposicionar a transmissão", + "@manualBehaviorDescription": { + "placeholders": { + "label": { + "type": "String" + } + } + }, + "neverBehaviorDescription": "A transmissão não será reajustada", "@@LOCALIZATION": {}, "dateLanguage": "Data e Idioma", "language": "Idioma", diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index 37a013ef..8c9a21a6 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -405,8 +405,8 @@ extension LateVideoBehaviorExtension on LateVideoBehavior { String locale(BuildContext context) { final loc = AppLocalizations.of(context); return switch (this) { - LateVideoBehavior.automatic => loc.automatic, - LateVideoBehavior.manual => loc.manual, + LateVideoBehavior.automatic => loc.automaticBehavior, + LateVideoBehavior.manual => loc.manualBehavior, LateVideoBehavior.never => loc.never, }; } diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index ea28a278..ee8f0833 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -43,7 +43,7 @@ class VideoStatusLabel extends StatefulWidget { State createState() => _VideoStatusLabelState(); } -enum _VideoLabel { +enum VideoLabel { /// When the video hasn't loaded any frame yet. loading, @@ -77,17 +77,17 @@ class _VideoStatusLabelState extends State { _source.contains('media/mjpeg') || _source.contains('.m3u8') /* hls */); - _VideoLabel get status => widget.video.error != null - ? _VideoLabel.error + VideoLabel get status => widget.video.error != null + ? VideoLabel.error : isLoading - ? _VideoLabel.loading + ? VideoLabel.loading : !isLive - ? _VideoLabel.recorded + ? VideoLabel.recorded : widget.video.player.isImageOld - ? _VideoLabel.timedOut + ? VideoLabel.timedOut : widget.video.player.isLate - ? _VideoLabel.late - : _VideoLabel.live; + ? VideoLabel.late + : VideoLabel.live; bool _openWithTap = false; OverlayEntry? entry; @@ -147,28 +147,8 @@ class _VideoStatusLabelState extends State { @override Widget build(BuildContext context) { - final loc = AppLocalizations.of(context); - final theme = Theme.of(context); final settings = context.watch(); - final text = switch (status) { - _VideoLabel.live => loc.live, - _VideoLabel.loading => loc.loading, - _VideoLabel.recorded => loc.recorded, - _VideoLabel.timedOut => loc.timedOut, - _VideoLabel.error => loc.error, - _VideoLabel.late => loc.late, - }; - - final color = switch (status) { - _VideoLabel.live => Colors.red.shade600, - _VideoLabel.loading => Colors.blue, - _VideoLabel.recorded => Colors.green, - _VideoLabel.timedOut => Colors.amber.shade600, - _VideoLabel.error => Colors.grey, - _VideoLabel.late => Colors.purple, - }; - // This opens the overlay when a property is updated. This is a frame late if (isOverlayOpen) { WidgetsBinding.instance.addPostFrameCallback((_) { @@ -177,7 +157,7 @@ class _VideoStatusLabelState extends State { }); } - final isLateDismissal = status == _VideoLabel.late && + final isLateDismissal = status == VideoLabel.late && settings.lateVideoBehavior == LateVideoBehavior.manual; return MouseRegion( @@ -197,40 +177,69 @@ class _VideoStatusLabelState extends State { showOverlay(); } }, - child: AnimatedContainer( - duration: const Duration(milliseconds: 500), - padding: const EdgeInsetsDirectional.symmetric( - horizontal: 4.0, - vertical: 2.0, - ), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(4.0), - ), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - if (status == _VideoLabel.loading) - const Padding( - padding: EdgeInsetsDirectional.only(end: 8.0), - child: SizedBox( - height: 12.0, - width: 12.0, - child: CircularProgressIndicator.adaptive( - strokeWidth: 1.5, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - ), - Text( - text, - style: theme.textTheme.labelSmall?.copyWith( - color: color.computeLuminance() > 0.5 - ? Colors.black - : Colors.white, + child: VideoStatusLabelIndicator(status: status), + ), + ); + } +} + +class VideoStatusLabelIndicator extends StatelessWidget { + final VideoLabel status; + + const VideoStatusLabelIndicator({super.key, required this.status}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final loc = AppLocalizations.of(context); + final text = switch (status) { + VideoLabel.live => loc.live, + VideoLabel.loading => loc.loading, + VideoLabel.recorded => loc.recorded, + VideoLabel.timedOut => loc.timedOut, + VideoLabel.error => loc.error, + VideoLabel.late => loc.late, + }; + + final color = switch (status) { + VideoLabel.live => Colors.red.shade600, + VideoLabel.loading => Colors.blue, + VideoLabel.recorded => Colors.green, + VideoLabel.timedOut => Colors.amber.shade600, + VideoLabel.error => Colors.grey, + VideoLabel.late => Colors.purple, + }; + + return AnimatedContainer( + duration: const Duration(milliseconds: 500), + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 4.0, + vertical: 2.0, + ), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4.0), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + if (status == VideoLabel.loading) + const Padding( + padding: EdgeInsetsDirectional.only(end: 8.0), + child: SizedBox( + height: 12.0, + width: 12.0, + child: CircularProgressIndicator.adaptive( + strokeWidth: 1.5, + valueColor: AlwaysStoppedAnimation(Colors.white), ), ), - ]), + ), + Text( + text, + style: theme.textTheme.labelSmall?.copyWith( + color: color.computeLuminance() > 0.5 ? Colors.black : Colors.white, + ), ), - ), + ]), ); } } @@ -238,7 +247,7 @@ class _VideoStatusLabelState extends State { class _DeviceVideoInfo extends StatelessWidget { final Device device; final VideoViewInheritance video; - final _VideoLabel label; + final VideoLabel label; final bool isLive; final Event? event; diff --git a/lib/widgets/settings/desktop/server.dart b/lib/widgets/settings/desktop/server.dart index e346f5be..6c78fd00 100644 --- a/lib/widgets/settings/desktop/server.dart +++ b/lib/widgets/settings/desktop/server.dart @@ -19,6 +19,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; import 'package:bluecherry_client/widgets/settings/desktop/settings.dart'; import 'package:bluecherry_client/widgets/settings/mobile/settings.dart'; import 'package:flutter/material.dart'; @@ -125,6 +126,7 @@ class CamerasSettings extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); final settings = context.watch(); final loc = AppLocalizations.of(context); return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -180,7 +182,55 @@ class CamerasSettings extends StatelessWidget { borderRadius: BorderRadius.circular(6.0), child: ListTile( title: Text(loc.lateStreamBehavior), - subtitle: Text(loc.lateStreamBehaviorDescription), + subtitle: RichText( + text: TextSpan( + text: loc.lateStreamBehaviorDescription, + style: theme.textTheme.bodyMedium, + children: [ + const TextSpan(text: '\n'), + switch (settings.lateVideoBehavior) { + LateVideoBehavior.automatic => TextSpan( + text: loc.automaticBehaviorDescription, + style: const TextStyle(fontWeight: FontWeight.w600), + ), + LateVideoBehavior.manual => TextSpan( + children: [ + ...() { + final list = loc + .manualBehaviorDescription( + 'manualBehaviorDescription', + ) + .split(' '); + + return list.map((part) { + if (part == 'manualBehaviorDescription') { + return const WidgetSpan( + child: Padding( + padding: EdgeInsetsDirectional.only( + start: 2.0, + end: 6.0, + ), + child: VideoStatusLabelIndicator( + status: VideoLabel.late, + ), + ), + ); + } else { + return TextSpan(text: '$part '); + } + }); + }() + ], + style: const TextStyle(fontWeight: FontWeight.w600), + ), + LateVideoBehavior.never => TextSpan( + text: loc.neverBehaviorDescription, + style: const TextStyle(fontWeight: FontWeight.w600), + ) + }, + ], + ), + ), trailing: DropdownButton( value: settings.lateVideoBehavior, onChanged: (v) {