From d348311b3d6c9f97cd31c436a8ada8dc30566c45 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 09:30:21 -0300 Subject: [PATCH 1/8] fix: external streams do not throw error --- lib/models/device.dart | 3 +- lib/utils/video_player.dart | 35 +++++++++++-------- .../desktop/desktop_device_grid.dart | 2 +- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/models/device.dart b/lib/models/device.dart index 29cc414e..c29d0d57 100644 --- a/lib/models/device.dart +++ b/lib/models/device.dart @@ -245,7 +245,8 @@ class Device { } Future getHLSUrl([Device? device]) async { - // return hlsURL; + if (url != null) return url!; + device ??= this; var data = { 'id': device.id.toString(), diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index 39541365..05c1292d 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -56,21 +56,26 @@ class UnityPlayers with ChangeNotifier { ..setSpeed(1.0); Future setSource() async { - final (String source, Future fallback) = switch ( - device.preferredStreamingType ?? - device.server.additionalSettings.preferredStreamingType ?? - settings.streamingType) { - StreamingType.rtsp => (device.rtspURL, device.getHLSUrl()), - StreamingType.hls => ( - await device.getHLSUrl(), - Future.value(device.rtspURL) - ), - StreamingType.mjpeg => (device.mjpegURL, Future.value(device.hlsURL)), - }; - debugPrint(source); - controller - ..fallbackUrl = fallback - ..setDataSource(source); + if (device.url != null) { + debugPrint(device.url); + controller.setDataSource(device.url!); + } else { + final (String source, Future fallback) = switch ( + device.preferredStreamingType ?? + device.server.additionalSettings.preferredStreamingType ?? + settings.streamingType) { + StreamingType.rtsp => (device.rtspURL, device.getHLSUrl()), + StreamingType.hls => ( + await device.getHLSUrl(), + Future.value(device.rtspURL) + ), + StreamingType.mjpeg => (device.mjpegURL, Future.value(device.hlsURL)), + }; + debugPrint(source); + controller + ..fallbackUrl = fallback + ..setDataSource(source); + } } setSource(); diff --git a/lib/widgets/device_grid/desktop/desktop_device_grid.dart b/lib/widgets/device_grid/desktop/desktop_device_grid.dart index c0f31a15..06e23a4f 100644 --- a/lib/widgets/device_grid/desktop/desktop_device_grid.dart +++ b/lib/widgets/device_grid/desktop/desktop_device_grid.dart @@ -498,7 +498,7 @@ class _DesktopTileViewportState extends State { ), if (states.isHovering && kDebugMode) TextSpan( - text: '\n' + text: '\ndebug: ' '${video?.player.dataSource}', style: theme.textTheme.labelSmall?.copyWith( color: Colors.white, From 6a42e91477e08a0f9bc99a8ae37d8e5665300f61 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 09:46:33 -0300 Subject: [PATCH 2/8] feat: reload all live views after 5 minutes --- lib/utils/video_player.dart | 38 +++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index 05c1292d..42791fe2 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -17,6 +17,8 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/logging.dart'; @@ -24,7 +26,12 @@ import 'package:flutter/widgets.dart'; import 'package:unity_video_player/unity_video_player.dart'; class UnityPlayers with ChangeNotifier { - UnityPlayers._(); + UnityPlayers._() { + UnityPlayers._reloadTimer = Timer.periodic( + reloadTime, + (_) => reloadDevices(), + ); + } static final instance = UnityPlayers._(); @@ -33,9 +40,28 @@ class UnityPlayers with ChangeNotifier { /// This avoids redundantly creating new video player instance if a [Device] /// is already present in the camera grid on the screen or allows to use /// existing instance when switching tab (if common camera [Device] tile exists). - /// static final players = {}; + /// Devices that should be reloaded at every [reloadTime] interval. + static final _reloadable = []; + static const reloadTime = Duration(minutes: 5); + + static late final Timer _reloadTimer; + + /// Reloads all devices that are marked as reloadable. + static Future reloadDevices() async { + debugPrint('Reloading devices $_reloadable'); + for (final player + in players.entries.where((entry) => _reloadable.contains(entry.key))) { + // reload each device at once + await reloadDevice(Device.fromUUID(player.key)!); + } + } + + /// Whether the given [Device] is reloadable. + static bool isReloadable(String deviceUUID) => + _reloadable.contains(deviceUUID); + /// Helper method to create a video player with required configuration for a [Device]. static UnityVideoPlayer forDevice(Device device) { final settings = SettingsProvider.instance; @@ -75,6 +101,7 @@ class UnityPlayers with ChangeNotifier { controller ..fallbackUrl = fallback ..setDataSource(source); + _reloadable.add(source); } } @@ -92,6 +119,7 @@ class UnityPlayers with ChangeNotifier { /// Release the video player for the given [Device]. static Future releaseDevice(String deviceUUID) async { debugPrint('Releasing device $deviceUUID. ${players[deviceUUID]}'); + _reloadable.remove(deviceUUID); await players[deviceUUID]?.dispose(); players.remove(deviceUUID); } @@ -141,4 +169,10 @@ class UnityPlayers with ChangeNotifier { ); if (isLocalController) await player.dispose(); } + + @override + void dispose() { + _reloadTimer.cancel(); + super.dispose(); + } } From 8906213a7ec61580a533ed6189d3653edba7269d Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 09:58:32 -0300 Subject: [PATCH 3/8] feat: reload camera if it's timed out for some considerable time --- lib/utils/video_player.dart | 39 +++++++++++-------- .../lib/unity_video_player_main.dart | 1 + ...unity_video_player_platform_interface.dart | 35 +++++++++++++++-- 3 files changed, 55 insertions(+), 20 deletions(-) diff --git a/lib/utils/video_player.dart b/lib/utils/video_player.dart index 42791fe2..80be9720 100644 --- a/lib/utils/video_player.dart +++ b/lib/utils/video_player.dart @@ -65,21 +65,7 @@ class UnityPlayers with ChangeNotifier { /// Helper method to create a video player with required configuration for a [Device]. static UnityVideoPlayer forDevice(Device device) { final settings = SettingsProvider.instance; - final controller = UnityVideoPlayer.create( - quality: switch (device.server.additionalSettings.renderingQuality ?? - settings.videoQuality) { - RenderingQuality.p4k => UnityVideoQuality.p4k, - RenderingQuality.p1080 => UnityVideoQuality.p1080, - RenderingQuality.p720 => UnityVideoQuality.p720, - RenderingQuality.p480 => UnityVideoQuality.p480, - RenderingQuality.p360 => UnityVideoQuality.p360, - RenderingQuality.p240 => UnityVideoQuality.p240, - RenderingQuality.automatic => - UnityVideoQuality.qualityForResolutionY(device.resolutionY), - }, - ) - ..setVolume(0.0) - ..setSpeed(1.0); + late UnityVideoPlayer controller; Future setSource() async { if (device.url != null) { @@ -101,10 +87,31 @@ class UnityPlayers with ChangeNotifier { controller ..fallbackUrl = fallback ..setDataSource(source); - _reloadable.add(source); + + // TODO(bdlukaa): reevaluate if this system is still necessary. + // on the player, we now reload the device if the image + // is timed out. See [UnityVideoPlayer.create.onReload] + // _reloadable.add(source); } } + controller = UnityVideoPlayer.create( + quality: switch (device.server.additionalSettings.renderingQuality ?? + settings.videoQuality) { + RenderingQuality.p4k => UnityVideoQuality.p4k, + RenderingQuality.p1080 => UnityVideoQuality.p1080, + RenderingQuality.p720 => UnityVideoQuality.p720, + RenderingQuality.p480 => UnityVideoQuality.p480, + RenderingQuality.p360 => UnityVideoQuality.p360, + RenderingQuality.p240 => UnityVideoQuality.p240, + RenderingQuality.automatic => + UnityVideoQuality.qualityForResolutionY(device.resolutionY), + }, + onReload: setSource, + ) + ..setVolume(0.0) + ..setSpeed(1.0); + setSource(); controller.onError.listen((event) { diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index 00a1e8e3..7a890fc4 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -27,6 +27,7 @@ class UnityVideoPlayerMediaKitInterface extends UnityVideoPlayerInterface { int? height, bool enableCache = false, RTSPProtocol? rtspProtocol, + VoidCallback? onReload, }) { final player = UnityVideoPlayerMediaKit( width: width, diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart index d551cbfe..43c9479f 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart @@ -336,12 +336,31 @@ enum UnityVideoQuality { abstract class UnityVideoPlayer { Future? fallbackUrl; + VoidCallback? onReload; + /// Creates a new [UnityVideoPlayer] instance. + /// + /// The [quality] parameter is used to set the rendering resolution of the + /// video. It defaults to [UnityVideoQuality.p360]. + /// + /// The [enableCache] parameter is used to enable or disable the cache for the + /// video. It defaults to `false`. It is usually true when vieweing recorded + /// videos, not streams. + /// + /// The [rtspProtocol] parameter is used to set the rtsp protocol for the + /// video. It is only applied on rtsp streams. + /// + /// The [fallbackUrl] parameter is used to set the fallback url for the + /// video. It is only used if the initial source fails to load. + /// + /// The [onReload] parameter is called when the video needs to be reloaded. + /// It is usually used when the image has been old for a while. static UnityVideoPlayer create({ UnityVideoQuality quality = UnityVideoQuality.p360, bool enableCache = false, RTSPProtocol? rtspProtocol, Future? fallbackUrl, + VoidCallback? onReload, }) { return UnityVideoPlayerInterface.instance.createPlayer( width: quality.resolution.width.toInt(), @@ -350,7 +369,8 @@ abstract class UnityVideoPlayer { rtspProtocol: rtspProtocol, ) ..quality = quality - ..fallbackUrl = fallbackUrl; + ..fallbackUrl = fallbackUrl + ..onReload = onReload; } static const timerInterval = Duration(seconds: 6); @@ -377,8 +397,15 @@ abstract class UnityVideoPlayer { _oldImageTimer?.cancel(); _oldImageTimer = Timer(timerInterval, () { // If the image is still the same after the interval, then it's old. - if (duration <= duration) { - _isImageOld = true; + _isImageOld = true; + + if (lastImageUpdate != null) { + final difference = lastImageUpdate!.difference(DateTime.now()); + if (difference > timerInterval * 2) { + // If the image is still the same after twice the interval, then + // it's probably stuck and we should reload the video. + onReload?.call(); + } } }); } @@ -392,7 +419,7 @@ abstract class UnityVideoPlayer { /// The duration of the current media. /// - /// May be 0 + /// May be [Duration.zero] Duration get duration; Stream get onDurationUpdate; From 6dc5a54f937837db442cadc8c856227dc519819a Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 10:01:28 -0300 Subject: [PATCH 4/8] fix: correctly apply the rtsp protocol --- .../lib/unity_video_player_main.dart | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index 7a890fc4..ea6b184a 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -192,8 +192,14 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { platform.setProperty('insecure', 'yes'); if (rtspProtocol != null) { - // TODO(bdlukaa): correctly apply rtsp protocol - platform.setProperty('rtsp-transport', 'udp_multicast'); + platform.setProperty( + 'rtsp-transport', + switch (rtspProtocol) { + RTSPProtocol.tcp => 'tcp', + RTSPProtocol.udp => 'udp', + // _ => 'udp_multicast' + }, + ); } platform.setProperty('force-seekable', 'yes'); From 0f168fcfe83babe36a37043ec4f270e09d2a055a Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 10:14:30 -0300 Subject: [PATCH 5/8] fix: errors are now stored in the player instance this was done because, if the video view happens to be updated, the error data was essentially lost - causing the user to see unupdated data. --- .../lib/unity_video_player_main.dart | 14 --- ...unity_video_player_platform_interface.dart | 98 ++++++++++--------- 2 files changed, 50 insertions(+), 62 deletions(-) diff --git a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart index ea6b184a..7838592f 100644 --- a/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart +++ b/packages/unity_video_player/unity_video_player_main/lib/unity_video_player_main.dart @@ -127,7 +127,6 @@ class _MKVideoState extends State<_MKVideo> { class UnityVideoPlayerMediaKit extends UnityVideoPlayer { Player mkPlayer = Player(); late VideoController mkVideoController; - late StreamSubscription errorStream; double _fps = 0; @override @@ -226,18 +225,6 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { // platform.setProperty("untimed", ""); } } - - errorStream = mkPlayer.stream.error.listen((event) async { - debugPrint('==== VIDEO ERROR HAPPENED with $dataSource'); - debugPrint('==== $event'); - - // If the video is not supported, try to play the fallback url - if (event == 'Failed to recognize file format.' && - fallbackUrl != null && - lastImageUpdate != null) { - setDataSource(await fallbackUrl!); - } - }); } Future ensureVideoControllerInitialized( @@ -440,7 +427,6 @@ class UnityVideoPlayerMediaKit extends UnityVideoPlayer { await platform.unobserveProperty('dheight'); } await _fpsStreamController.close(); - await errorStream.cancel(); await mkPlayer.dispose(); UnityVideoPlayerInterface.unregisterPlayer(this); } diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart index 43c9479f..97a7d8ad 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart @@ -199,67 +199,28 @@ class UnityVideoView extends StatefulWidget { } class UnityVideoViewState extends State { - String? error; - late StreamSubscription _onErrorSubscription; - late StreamSubscription _onDurationUpdateSubscription; - late StreamSubscription _onPositionUpdateSubscription; - late StreamSubscription _fpsSubscription; - @override void initState() { super.initState(); - _onErrorSubscription = widget.player.onError.listen(_onError); - _onDurationUpdateSubscription = - widget.player.onDurationUpdate.listen(_onDurationUpdate); - _onPositionUpdateSubscription = - widget.player.onCurrentPosUpdate.listen(_onPositionUpdate); - _fpsSubscription = widget.player.fpsStream.listen(_onFpsUpdate); + widget.player.addListener(_onPlayerUpdate); } @override void didUpdateWidget(covariant UnityVideoView oldWidget) { super.didUpdateWidget(oldWidget); - - if (widget.player != oldWidget.player) { - _onErrorSubscription.cancel(); - _onDurationUpdateSubscription.cancel(); - _fpsSubscription.cancel(); - - _onErrorSubscription = widget.player.onError.listen(_onError); - _onDurationUpdateSubscription = - widget.player.onDurationUpdate.listen(_onDurationUpdate); - _onPositionUpdateSubscription = - widget.player.onCurrentPosUpdate.listen(_onPositionUpdate); - _fpsSubscription = widget.player.fpsStream.listen(_onFpsUpdate); + if (oldWidget.player != widget.player) { + oldWidget.player.removeListener(_onPlayerUpdate); + widget.player.addListener(_onPlayerUpdate); } } - void _onError(String error) { - if (mounted) setState(() => this.error = error); - } - - void _onDurationUpdate(Duration duration) { - if (mounted) { - setState(() { - error = null; - }); - } - } - - void _onPositionUpdate(Duration duration) { - if (mounted) setState(() => error = null); - } - - void _onFpsUpdate(double fps) { - if (mounted) setState(() {}); + void _onPlayerUpdate() { + setState(() {}); } @override void dispose() { - _onErrorSubscription.cancel(); - _onDurationUpdateSubscription.cancel(); - _onPositionUpdateSubscription.cancel(); - _fpsSubscription.cancel(); + widget.player.removeListener(_onPlayerUpdate); super.dispose(); } @@ -267,7 +228,7 @@ class UnityVideoViewState extends State { Widget build(BuildContext context) { final videoView = VideoViewInheritance( player: widget.player, - error: error, + error: widget.player.error, position: widget.player.currentPos, duration: widget.player.duration, isImageOld: widget.player.isImageOld, @@ -334,7 +295,7 @@ enum UnityVideoQuality { } } -abstract class UnityVideoPlayer { +abstract class UnityVideoPlayer with ChangeNotifier { Future? fallbackUrl; VoidCallback? onReload; @@ -379,21 +340,31 @@ abstract class UnityVideoPlayer { bool get isImageOld => _isImageOld; DateTime? _lastImageTime; DateTime? get lastImageUpdate => _lastImageTime; + late StreamSubscription _onDurationUpdateSubscription; + late StreamSubscription _onErrorSubscription; + late StreamSubscription _onPositionUpdateSubscription; + late StreamSubscription _fpsSubscription; int? width; int? height; + String? error; UnityVideoQuality? quality; UnityVideoPlayer({this.width, this.height}) { + _onErrorSubscription = onError.listen(_onError); _onDurationUpdateSubscription = onDurationUpdate.listen(_onDurationUpdate); + _onPositionUpdateSubscription = + onCurrentPosUpdate.listen(_onPositionUpdate); + _fpsSubscription = fpsStream.listen(_onFpsUpdate); } void _onDurationUpdate(Duration duration) { if (duration > Duration.zero) { _lastImageTime = DateTime.now(); _isImageOld = false; + error = null; _oldImageTimer?.cancel(); _oldImageTimer = Timer(timerInterval, () { // If the image is still the same after the interval, then it's old. @@ -408,9 +379,34 @@ abstract class UnityVideoPlayer { } } }); + notifyListeners(); } } + void _onError(String error) async { + this.error = error; + notifyListeners(); + + debugPrint('==== VIDEO ERROR HAPPENED with $dataSource'); + debugPrint('==== $error'); + + // If the video is not supported, try to play the fallback url + if (error == 'Failed to recognize file format.' && + fallbackUrl != null && + lastImageUpdate != null) { + setDataSource(await fallbackUrl!); + } + } + + void _onPositionUpdate(Duration duration) { + error = null; + notifyListeners(); + } + + void _onFpsUpdate(double fps) { + notifyListeners(); + } + /// The current data source url String? get dataSource; @@ -476,10 +472,16 @@ abstract class UnityVideoPlayer { bool get isCropped; @mustCallSuper + @override Future dispose() async { _onDurationUpdateSubscription.cancel(); + _onErrorSubscription.cancel(); + _onPositionUpdateSubscription.cancel(); + _fpsSubscription.cancel(); _oldImageTimer?.cancel(); _lastImageTime = null; _isImageOld = false; + + super.dispose(); } } From c77670a96e7a369ed8cf93b35f3b6d1c8e367624 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 10:51:55 -0300 Subject: [PATCH 6/8] ui: update device grid ui and fix some overflows --- lib/utils/widgets/squared_icon_button.dart | 36 ++++++ .../desktop/desktop_device_grid.dart | 103 ++++++++---------- lib/widgets/device_grid/device_grid.dart | 1 + .../device_grid/video_status_label.dart | 27 ++++- lib/widgets/error_warning.dart | 1 + lib/widgets/misc.dart | 5 + ...unity_video_player_platform_interface.dart | 2 + 7 files changed, 113 insertions(+), 62 deletions(-) create mode 100644 lib/utils/widgets/squared_icon_button.dart diff --git a/lib/utils/widgets/squared_icon_button.dart b/lib/utils/widgets/squared_icon_button.dart new file mode 100644 index 00000000..123aa537 --- /dev/null +++ b/lib/utils/widgets/squared_icon_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class SquaredIconButton extends StatelessWidget { + final VoidCallback onPressed; + final Widget icon; + final String? tooltip; + + const SquaredIconButton({ + super.key, + required this.onPressed, + required this.icon, + this.tooltip, + }); + + @override + Widget build(BuildContext context) { + final widget = Padding( + padding: + const EdgeInsetsDirectional.only(top: 4.0, bottom: 4.0, end: 4.0), + child: InkWell( + borderRadius: BorderRadius.circular(4.0), + onTap: onPressed, + child: Padding( + padding: const EdgeInsetsDirectional.all(2.5), + child: icon, + ), + ), + ); + + if (tooltip != null) { + return Tooltip(message: tooltip!, child: widget); + } + + return widget; + } +} diff --git a/lib/widgets/device_grid/desktop/desktop_device_grid.dart b/lib/widgets/device_grid/desktop/desktop_device_grid.dart index 06e23a4f..d3e2e3cf 100644 --- a/lib/widgets/device_grid/desktop/desktop_device_grid.dart +++ b/lib/widgets/device_grid/desktop/desktop_device_grid.dart @@ -430,11 +430,13 @@ class _DesktopTileViewportState extends State { final theme = Theme.of(context); final view = context.watch(); final settings = context.watch(); - final closeButton = IconButton( - icon: const Icon(Icons.close_outlined), - color: theme.colorScheme.error, + final closeButton = SquaredIconButton( + icon: Icon( + Icons.close_outlined, + color: theme.colorScheme.error, + size: 18.0, + ), tooltip: loc.removeCamera, - iconSize: 18.0, onPressed: () { view.remove(widget.device); }, @@ -444,14 +446,14 @@ class _DesktopTileViewportState extends State { final error = video?.error; final isSubView = AlternativeWindow.maybeOf(context) != null; - final reloadButton = IconButton( + final reloadButton = SquaredIconButton( icon: Icon( Icons.replay_outlined, - shadows: outlinedText(), + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, ), tooltip: loc.reloadCamera, - color: Colors.white, - iconSize: 18.0, onPressed: () async { await UnityPlayers.reloadDevice(widget.device); setState(() {}); @@ -516,23 +518,12 @@ class _DesktopTileViewportState extends State { child: PTZData(commands: commands), ), if (video != null) ...[ - if (!widget.controller!.isSeekable && error == null) - const Center( - child: SizedBox( - height: 20.0, - width: 20.0, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - ), PositionedDirectional( end: 0, start: 0, bottom: 4.0, child: Row(crossAxisAlignment: CrossAxisAlignment.end, children: [ - if (states.isHovering && error == null) ...[ + if (states.isHovering && error == null && !video.isLoading) ...[ const SizedBox(width: 12.0), if (widget.device.hasPTZ) PTZToggleButton( @@ -541,52 +532,52 @@ class _DesktopTileViewportState extends State { setState(() => ptzEnabled = enabled), ), const Spacer(), - () { - final isMuted = volume == 0.0; - - return IconButton( - icon: Icon( - isMuted - ? Icons.volume_mute_rounded - : Icons.volume_up_rounded, - shadows: outlinedText(), - ), - tooltip: isMuted ? loc.enableAudio : loc.disableAudio, - color: Colors.white, - iconSize: 18.0, - onPressed: () async { - if (isMuted) { - await widget.controller!.setVolume(1.0); - } else { - await widget.controller!.setVolume(0.0); - } + if (!video.isLoading) + () { + final isMuted = volume == 0.0; + return SquaredIconButton( + icon: Icon( + isMuted + ? Icons.volume_mute_rounded + : Icons.volume_up_rounded, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: isMuted ? loc.enableAudio : loc.disableAudio, + onPressed: () async { + if (isMuted) { + await widget.controller!.setVolume(1.0); + } else { + await widget.controller!.setVolume(0.0); + } - updateVolume(); - }, - ); - }(), - if (isDesktopPlatform && !isSubView) - IconButton( + updateVolume(); + }, + ); + }(), + if (isDesktopPlatform && !isSubView && !video.isLoading) + SquaredIconButton( icon: Icon( Icons.open_in_new_sharp, - shadows: outlinedText(), + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, ), tooltip: loc.openInANewWindow, - color: Colors.white, - iconSize: 18.0, onPressed: () { widget.device.openInANewWindow(); }, ), - if (!isSubView) - IconButton( + if (!isSubView && !video.isLoading) + SquaredIconButton( icon: Icon( Icons.fullscreen_rounded, - shadows: outlinedText(), + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, ), tooltip: loc.showFullscreenCamera, - color: Colors.white, - iconSize: 18.0, onPressed: () async { UnityPlayers.openFullscreen( context, @@ -604,9 +595,9 @@ class _DesktopTileViewportState extends State { const Spacer(), if (states.isHovering) reloadButton, ], - const SizedBox(width: 12.0), Padding( padding: const EdgeInsetsDirectional.only( + start: 6.0, end: 6.0, bottom: 6.0, ), @@ -628,10 +619,10 @@ class _DesktopTileViewportState extends State { curve: Curves.easeInOut, child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ - IconButton( + SquaredIconButton( icon: Icon( moreIconData, - shadows: outlinedText(), + shadows: outlinedIcon(), color: Colors.white, ), tooltip: loc.more, diff --git a/lib/widgets/device_grid/device_grid.dart b/lib/widgets/device_grid/device_grid.dart index 09a9ac2f..38a1882b 100644 --- a/lib/widgets/device_grid/device_grid.dart +++ b/lib/widgets/device_grid/device_grid.dart @@ -34,6 +34,7 @@ import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/theme.dart'; import 'package:bluecherry_client/utils/video_player.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/external_stream.dart'; diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index b191a5ed..afed2312 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -180,13 +180,28 @@ class _VideoStatusLabelState extends State { color: color, borderRadius: BorderRadius.circular(4.0), ), - child: Text( - text, - style: theme.textTheme.labelSmall?.copyWith( - color: - color.computeLuminance() > 0.5 ? Colors.black : Colors.white, + 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: 2.0, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + ), + Text( + text, + style: theme.textTheme.labelSmall?.copyWith( + color: color.computeLuminance() > 0.5 + ? Colors.black + : Colors.white, + ), ), - ), + ]), ), ), ); diff --git a/lib/widgets/error_warning.dart b/lib/widgets/error_warning.dart index 4c000f16..97a3516a 100644 --- a/lib/widgets/error_warning.dart +++ b/lib/widgets/error_warning.dart @@ -40,6 +40,7 @@ class ErrorWarning extends StatelessWidget { loc.videoError, style: const TextStyle(color: Colors.white), maxLines: 1, + minFontSize: 6.0, ), if (message.isNotEmpty) ...[ const FractionallySizedBox( diff --git a/lib/widgets/misc.dart b/lib/widgets/misc.dart index 0dd3cf66..8819df2e 100644 --- a/lib/widgets/misc.dart +++ b/lib/widgets/misc.dart @@ -347,6 +347,11 @@ List outlinedText({ return result.toList(); } +List outlinedIcon() => outlinedText( + strokeWidth: 0.75, + strokeColor: Colors.black.withOpacity(0.75), + ); + class PopupLabel extends PopupMenuEntry { const PopupLabel({ super.key, diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart index 97a7d8ad..206ac42b 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/unity_video_player_platform_interface.dart @@ -137,6 +137,8 @@ class VideoViewInheritance extends InheritedWidget { /// The player that is currently being used by the video. final UnityVideoPlayer player; + bool get isLoading => !player.isSeekable && error == null; + static VideoViewInheritance? maybeOf(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); } From cedc3df1d575a7b85ec07240369e4d531b67209a Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 11:02:50 -0300 Subject: [PATCH 7/8] ui: use SquaredIconButton instead of IconButton --- lib/models/server.dart | 3 +- lib/utils/widgets/squared_icon_button.dart | 28 ++++++++++++++++--- lib/widgets/collapsable_sidebar.dart | 3 +- lib/widgets/desktop_buttons.dart | 10 +++---- .../desktop/desktop_device_grid.dart | 3 +- .../device_grid/desktop/desktop_sidebar.dart | 6 ++-- .../device_grid/desktop/layout_manager.dart | 12 ++++---- .../device_grid/mobile/device_view.dart | 7 +++-- .../mobile/mobile_device_grid.dart | 1 - lib/widgets/device_selector_screen.dart | 3 +- lib/widgets/downloads_manager.dart | 10 +++---- lib/widgets/events/event_player_desktop.dart | 3 +- lib/widgets/events/events_screen.dart | 1 + lib/widgets/events/events_screen_mobile.dart | 7 ++--- .../events_timeline/desktop/timeline.dart | 3 +- .../desktop/timeline_card.dart | 5 ++-- .../mobile/timeline_device_view.dart | 9 +++--- lib/widgets/player/live_player.dart | 22 ++++++++------- lib/widgets/player/widgets.dart | 12 +++++--- lib/widgets/ptz.dart | 5 ++-- lib/widgets/servers/add_server.dart | 3 +- lib/widgets/settings/mobile/settings.dart | 1 + lib/widgets/settings/shared/server_tile.dart | 12 ++++---- 23 files changed, 101 insertions(+), 68 deletions(-) diff --git a/lib/models/server.dart b/lib/models/server.dart index 391fbb58..b1048a7c 100644 --- a/lib/models/server.dart +++ b/lib/models/server.dart @@ -21,7 +21,6 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; -import 'package:flutter/foundation.dart'; import 'package:unity_video_player/unity_video_player.dart'; class AdditionalServerOptions { @@ -231,7 +230,7 @@ class Server { other.login == login && other.password == password && other.additionalSettings == additionalSettings && - listEquals(other.devices, devices) && + other.devices == devices && other.serverUUID == serverUUID && other.cookie == cookie && other.online == online && diff --git a/lib/utils/widgets/squared_icon_button.dart b/lib/utils/widgets/squared_icon_button.dart index 123aa537..20e3bfa2 100644 --- a/lib/utils/widgets/squared_icon_button.dart +++ b/lib/utils/widgets/squared_icon_button.dart @@ -1,24 +1,44 @@ import 'package:flutter/material.dart'; class SquaredIconButton extends StatelessWidget { - final VoidCallback onPressed; + /// Called when the button is pressed. + /// + /// If null, the button is disabled. + final VoidCallback? onPressed; + + /// The icon final Widget icon; + + /// The tooltip message. + /// + /// See also + /// + /// * [Tooltip.message] final String? tooltip; + /// The padding around this button. + /// + /// Defaults to 4.0 logical pixels on all sides + final EdgeInsetsDirectional padding; + const SquaredIconButton({ super.key, required this.onPressed, required this.icon, this.tooltip, + this.padding = const EdgeInsetsDirectional.only( + top: 4.0, + bottom: 4.0, + end: 4.0, + ), }); @override Widget build(BuildContext context) { final widget = Padding( - padding: - const EdgeInsetsDirectional.only(top: 4.0, bottom: 4.0, end: 4.0), + padding: padding, child: InkWell( - borderRadius: BorderRadius.circular(4.0), + borderRadius: BorderRadius.circular(6.0), onTap: onPressed, child: Padding( padding: const EdgeInsetsDirectional.all(2.5), diff --git a/lib/widgets/collapsable_sidebar.dart b/lib/widgets/collapsable_sidebar.dart index 172f4a06..cad72655 100644 --- a/lib/widgets/collapsable_sidebar.dart +++ b/lib/widgets/collapsable_sidebar.dart @@ -17,6 +17,7 @@ * along with this program. If not, see . */ +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -102,7 +103,7 @@ class _CollapsableSidebarState extends State : widget.left ? const EdgeInsetsDirectional.only(start: 5.0) : const EdgeInsetsDirectional.only(end: 5.0), - child: IconButton( + child: SquaredIconButton( key: collapseButtonKey, tooltip: collapsed ? loc.expand : loc.collapse, icon: RotationTransition( diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index 38b34f09..6eb83e97 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -25,6 +25,7 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/events/events_screen.dart'; import 'package:bluecherry_client/widgets/events_timeline/events_playback.dart'; import 'package:bluecherry_client/widgets/home.dart'; @@ -215,13 +216,12 @@ class _WindowButtonsState extends State with WindowListener { ) else if (home.tab == UnityTab.eventsScreen || home.tab == UnityTab.eventsPlayback && !canPop) - IconButton( + SquaredIconButton( onPressed: () { eventsScreenKey.currentState?.fetch(); eventsPlaybackScreenKey.currentState?.fetch(); }, - icon: const Icon(Icons.refresh), - iconSize: 20.0, + icon: const Icon(Icons.refresh, size: 20.0), tooltip: loc.refresh, ), SizedBox( @@ -244,7 +244,7 @@ class _WindowButtonsState extends State with WindowListener { final icon = isSelected ? data.selectedIcon : data.icon; final text = data.text; - return IconButton( + return SquaredIconButton( icon: AnimatedSwitcher( duration: const Duration(milliseconds: 200), child: Icon( @@ -254,9 +254,9 @@ class _WindowButtonsState extends State with WindowListener { ? theme.colorScheme.primary : theme.hintColor, fill: isSelected ? 1.0 : 0.0, + size: 22.0, ), ), - iconSize: 22.0, tooltip: text, onPressed: () => home.setTab(data.tab, context), ); diff --git a/lib/widgets/device_grid/desktop/desktop_device_grid.dart b/lib/widgets/device_grid/desktop/desktop_device_grid.dart index d3e2e3cf..1440f609 100644 --- a/lib/widgets/device_grid/desktop/desktop_device_grid.dart +++ b/lib/widgets/device_grid/desktop/desktop_device_grid.dart @@ -66,7 +66,7 @@ class _DesktopDeviceGridState extends State { return Column(children: [ collapseButton, const Spacer(), - IconButton( + SquaredIconButton( icon: Icon( Icons.cyclone, size: 18.0, @@ -74,7 +74,6 @@ class _DesktopDeviceGridState extends State { ? theme.colorScheme.primary : IconTheme.of(context).color, ), - padding: EdgeInsetsDirectional.zero, tooltip: loc.cycle, onPressed: settings.toggleCycling, ), diff --git a/lib/widgets/device_grid/desktop/desktop_sidebar.dart b/lib/widgets/device_grid/desktop/desktop_sidebar.dart index 20e9a86e..117cfa20 100644 --- a/lib/widgets/device_grid/desktop/desktop_sidebar.dart +++ b/lib/widgets/device_grid/desktop/desktop_sidebar.dart @@ -80,7 +80,7 @@ class _DesktopSidebarState extends State { trailing: Builder(builder: (context) { if (isLoading) { // wrap in an icon button to ensure ui consistency - return const IconButton( + return const SquaredIconButton( onPressed: null, icon: SizedBox( height: 16.0, @@ -91,7 +91,7 @@ class _DesktopSidebarState extends State { ), ); } else if (!server.online && isSidebarHovering) { - return IconButton( + return SquaredIconButton( icon: const Icon(Icons.refresh), tooltip: loc.refreshServer, onPressed: () => @@ -99,7 +99,7 @@ class _DesktopSidebarState extends State { ); } else if (isSidebarHovering && devices.isNotEmpty) { - return IconButton( + return SquaredIconButton( icon: Icon( isAllInView ? Icons.playlist_remove diff --git a/lib/widgets/device_grid/desktop/layout_manager.dart b/lib/widgets/device_grid/desktop/layout_manager.dart index 7759cf2f..4a465bc2 100644 --- a/lib/widgets/device_grid/desktop/layout_manager.dart +++ b/lib/widgets/device_grid/desktop/layout_manager.dart @@ -24,6 +24,7 @@ import 'package:bluecherry_client/models/layout.dart'; import 'package:bluecherry_client/providers/desktop_view_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; @@ -103,7 +104,7 @@ class _LayoutManagerState extends State { maxLines: 1, ), ), - IconButton( + SquaredIconButton( icon: Icon( Icons.cyclone, size: 18.0, @@ -111,18 +112,16 @@ class _LayoutManagerState extends State { ? theme.colorScheme.primary : IconTheme.of(context).color, ), - padding: EdgeInsetsDirectional.zero, tooltip: loc.cycle, onPressed: settings.toggleCycling, ), - IconButton( + SquaredIconButton( icon: Icon( Icons.add, size: 18.0, color: IconTheme.of(context).color, ), tooltip: loc.newLayout, - padding: EdgeInsetsDirectional.zero, onPressed: () { showDialog( context: context, @@ -576,13 +575,14 @@ class _EditLayoutDialogState extends State { title: Row(children: [ Expanded(child: Text(loc.editSpecificLayout(widget.layout.name))), if (view.layouts.length > 1) - IconButton( + SquaredIconButton( icon: Icon( Icons.delete, color: theme.colorScheme.error, + size: 18.0, ), tooltip: loc.delete, - iconSize: 18.0, + // iconSize: 18.0, onPressed: () { view.removeLayout(widget.layout); Navigator.of(context).pop(); diff --git a/lib/widgets/device_grid/mobile/device_view.dart b/lib/widgets/device_grid/mobile/device_view.dart index cbdcb986..ac9ef59a 100644 --- a/lib/widgets/device_grid/mobile/device_view.dart +++ b/lib/widgets/device_grid/mobile/device_view.dart @@ -22,6 +22,7 @@ import 'package:bluecherry_client/providers/mobile_view_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/video_player.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; import 'package:bluecherry_client/widgets/device_selector_screen.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; @@ -138,7 +139,7 @@ class _MobileDeviceViewState extends State { ], ); }, - child: IconButton( + child: SquaredIconButton( onPressed: null, icon: Icon(moreIconData, color: Colors.white), ), @@ -274,8 +275,8 @@ class DeviceTileState extends State { child: child, ); }, - child: IconButton( - splashRadius: 20.0, + child: SquaredIconButton( + // splashRadius: 20.0, onPressed: () async { await Navigator.of(context).pushNamed( '/fullscreen', diff --git a/lib/widgets/device_grid/mobile/mobile_device_grid.dart b/lib/widgets/device_grid/mobile/mobile_device_grid.dart index da810a66..cecb10a4 100644 --- a/lib/widgets/device_grid/mobile/mobile_device_grid.dart +++ b/lib/widgets/device_grid/mobile/mobile_device_grid.dart @@ -123,7 +123,6 @@ class _MobileDeviceGridState extends State { ? theme.colorScheme.primary : theme.colorScheme.onBackground, ), - padding: EdgeInsetsDirectional.zero, tooltip: loc.cycle, onPressed: settings.toggleCycling, ), diff --git a/lib/widgets/device_selector_screen.dart b/lib/widgets/device_selector_screen.dart index 8c590182..694cc7a8 100644 --- a/lib/widgets/device_selector_screen.dart +++ b/lib/widgets/device_selector_screen.dart @@ -21,6 +21,7 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; @@ -85,7 +86,7 @@ class DeviceSelectorScreen extends StatelessWidget { return Scaffold( appBar: AppBar( - leading: IconButton( + leading: SquaredIconButton( icon: const Icon(Icons.close), tooltip: MaterialLocalizations.of(context).closeButtonTooltip, onPressed: () => Navigator.of(context).maybePop(), diff --git a/lib/widgets/downloads_manager.dart b/lib/widgets/downloads_manager.dart index 0d8a1fb1..fc831a18 100644 --- a/lib/widgets/downloads_manager.dart +++ b/lib/widgets/downloads_manager.dart @@ -26,6 +26,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/theme.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/gestures.dart'; @@ -418,14 +419,14 @@ class DownloadIndicator extends StatelessWidget { final downloads = context.watch(); if (downloads.isEventDownloaded(event.id)) { - return IconButton( + return SquaredIconButton( onPressed: () { context.read().toDownloads(event.id, context); }, tooltip: loc.seeInDownloads, - iconSize: small ? 18.0 : null, icon: Icon( Icons.download_done, + size: small ? 18.0 : null, color: theme.extension()!.successColor, ), ); @@ -440,13 +441,12 @@ class DownloadIndicator extends StatelessWidget { } if (event.mediaURL != null) { - return IconButton( - padding: EdgeInsetsDirectional.zero, + return SquaredIconButton( tooltip: loc.download, onPressed: () => downloads.download(event), - iconSize: small ? 18.0 : 22.0, icon: Icon( Icons.download, + size: small ? 18.0 : 22.0, color: highlight ? Colors.white : null, shadows: highlight ? outlinedText() : null, ), diff --git a/lib/widgets/events/event_player_desktop.dart b/lib/widgets/events/event_player_desktop.dart index 3255b071..fc1f9c74 100644 --- a/lib/widgets/events/event_player_desktop.dart +++ b/lib/widgets/events/event_player_desktop.dart @@ -26,6 +26,7 @@ import 'package:bluecherry_client/models/event.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/collapsable_sidebar.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; @@ -279,7 +280,7 @@ class _EventPlayerDesktopState extends State { ]), Row(children: [ padd, - IconButton( + SquaredIconButton( onPressed: _playPause, tooltip: videoController.isPlaying ? loc.pause : loc.play, diff --git a/lib/widgets/events/events_screen.dart b/lib/widgets/events/events_screen.dart index b0b910ed..5f5a4a45 100644 --- a/lib/widgets/events/events_screen.dart +++ b/lib/widgets/events/events_screen.dart @@ -30,6 +30,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/widgets/tree_view.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:bluecherry_client/widgets/downloads_manager.dart'; diff --git a/lib/widgets/events/events_screen_mobile.dart b/lib/widgets/events/events_screen_mobile.dart index 74ac929e..ab2e0fa0 100644 --- a/lib/widgets/events/events_screen_mobile.dart +++ b/lib/widgets/events/events_screen_mobile.dart @@ -65,15 +65,14 @@ class _EventsScreenMobileState extends State { title: Text(loc.eventBrowser), actions: [ if (!isLoading) - IconButton( + SquaredIconButton( onPressed: () => eventsScreenKey.currentState?.fetch(), - icon: const Icon(Icons.refresh), - iconSize: 20.0, + icon: const Icon(Icons.refresh, size: 20.0), tooltip: loc.refresh, ), Padding( padding: const EdgeInsetsDirectional.only(end: 15.0), - child: IconButton( + child: SquaredIconButton( icon: const Icon(Icons.filter_list), tooltip: loc.filter, onPressed: widget.showFilter, diff --git a/lib/widgets/events_timeline/desktop/timeline.dart b/lib/widgets/events_timeline/desktop/timeline.dart index 88c02ff4..34f7dc98 100644 --- a/lib/widgets/events_timeline/desktop/timeline.dart +++ b/lib/widgets/events_timeline/desktop/timeline.dart @@ -27,6 +27,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/device_grid/device_grid.dart' show calculateCrossAxisCount; import 'package:bluecherry_client/widgets/events/events_screen.dart'; @@ -561,7 +562,7 @@ class _TimelineEventsViewState extends State { ]), ), const SizedBox(width: 20.0), - IconButton( + SquaredIconButton( tooltip: timeline.isPlaying ? loc.pause : loc.play, icon: PlayPauseIcon(isPlaying: timeline.isPlaying), onPressed: () { diff --git a/lib/widgets/events_timeline/desktop/timeline_card.dart b/lib/widgets/events_timeline/desktop/timeline_card.dart index 682ff86c..8c192d0e 100644 --- a/lib/widgets/events_timeline/desktop/timeline_card.dart +++ b/lib/widgets/events_timeline/desktop/timeline_card.dart @@ -21,6 +21,7 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/device_grid/video_status_label.dart'; import 'package:bluecherry_client/widgets/downloads_manager.dart'; import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline.dart'; @@ -216,7 +217,7 @@ class _TimelineCardState extends State { settings.cameraViewFit, onChanged: (fit) => setState(() => _fit = fit), ), - IconButton( + SquaredIconButton( tooltip: loc.showFullscreenCamera, onPressed: () { Navigator.of(context).pushNamed( @@ -224,11 +225,11 @@ class _TimelineCardState extends State { arguments: {'event': currentEvent.event}, ); }, - iconSize: 18.0, icon: Icon( Icons.fullscreen, color: Colors.white, shadows: outlinedText(), + size: 18.0, ), ) ], diff --git a/lib/widgets/events_timeline/mobile/timeline_device_view.dart b/lib/widgets/events_timeline/mobile/timeline_device_view.dart index 2f33319a..34af4b85 100644 --- a/lib/widgets/events_timeline/mobile/timeline_device_view.dart +++ b/lib/widgets/events_timeline/mobile/timeline_device_view.dart @@ -27,6 +27,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; import 'package:bluecherry_client/utils/theme.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/device_selector_screen.dart'; import 'package:bluecherry_client/widgets/downloads_manager.dart'; import 'package:bluecherry_client/widgets/events_timeline/desktop/timeline.dart'; @@ -478,7 +479,7 @@ class _TimelineDeviceViewState extends State { ), const Spacer(), ], - IconButton( + SquaredIconButton( icon: const Icon(Icons.fullscreen), tooltip: currentEvent == null ? null : loc.showFullscreenCamera, onPressed: @@ -486,7 +487,7 @@ class _TimelineDeviceViewState extends State { ), ]), ), - IconButton( + SquaredIconButton( icon: const Icon(Icons.skip_previous), tooltip: isFirstEvent ? null : loc.previous, onPressed: isFirstEvent @@ -519,7 +520,7 @@ class _TimelineDeviceViewState extends State { }, ), const SizedBox(width: 6.0), - IconButton( + SquaredIconButton( icon: const Icon(Icons.skip_next), tooltip: isLastEvent ? null : loc.next, onPressed: isLastEvent @@ -532,7 +533,7 @@ class _TimelineDeviceViewState extends State { child: Padding( padding: const EdgeInsetsDirectional.only(end: 12.0), child: Row(children: [ - IconButton( + SquaredIconButton( icon: const Icon(Icons.event), tooltip: loc.timeFilter, onPressed: () async { diff --git a/lib/widgets/player/live_player.dart b/lib/widgets/player/live_player.dart index 3f4e48d7..2c44423a 100644 --- a/lib/widgets/player/live_player.dart +++ b/lib/widgets/player/live_player.dart @@ -23,6 +23,7 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/home_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/multicast_view.dart'; @@ -212,12 +213,13 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { widget.device.server.name, style: const TextStyle(color: Colors.white70), ), - leading: IconButton( + leading: SquaredIconButton( onPressed: () => Navigator.of(context).maybePop(), icon: const BackButtonIcon(), tooltip: MaterialLocalizations.of(context) .backButtonTooltip, - color: Colors.white, + // todo: + // color: Colors.white, ), trailing: Row(mainAxisSize: MainAxisSize.min, children: [ @@ -244,7 +246,7 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { key: const ValueKey('restorer'), start: 14.0, top: 14.0, - child: IconButton( + child: SquaredIconButton( icon: const Icon( Icons.keyboard_arrow_down_rounded, color: Colors.white, @@ -349,7 +351,7 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { start: 8.0, child: Row(children: [ if (widget.device.hasPTZ) - IconButton( + SquaredIconButton( icon: Icon( Icons.videogame_asset, color: ptzEnabled ? Colors.white : null, @@ -364,16 +366,16 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { () { final isMuted = widget.player.volume == 0.0; - return IconButton( + return SquaredIconButton( icon: Icon( isMuted ? Icons.volume_mute_rounded : Icons.volume_up_rounded, shadows: outlinedText(), + color: Colors.white, + size: 18.0, ), tooltip: isMuted ? loc.enableAudio : loc.disableAudio, - color: Colors.white, - iconSize: 18.0, onPressed: () async { if (isMuted) { await widget.player.setVolume(1.0); @@ -384,14 +386,14 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { ); }(), if (isDesktopPlatform && !isSubView) - IconButton( + SquaredIconButton( icon: Icon( Icons.open_in_new, shadows: outlinedText(), + color: Colors.white, + size: 18.0, ), tooltip: loc.openInANewWindow, - color: Colors.white, - iconSize: 18.0, onPressed: () { widget.device.openInANewWindow(); }, diff --git a/lib/widgets/player/widgets.dart b/lib/widgets/player/widgets.dart index 40cbec81..4548f54a 100644 --- a/lib/widgets/player/widgets.dart +++ b/lib/widgets/player/widgets.dart @@ -18,6 +18,7 @@ */ import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:flutter/material.dart'; import 'package:unity_video_player/unity_video_player.dart'; @@ -34,12 +35,15 @@ class CameraViewFitButton extends StatelessWidget { @override Widget build(BuildContext context) { - return IconButton( + return SquaredIconButton( tooltip: fit.locale(context), onPressed: () => onChanged(fit.next), - iconSize: 18.0, - icon: Icon(fit.icon, shadows: outlinedText()), - color: Colors.white, + icon: Icon( + fit.icon, + size: 18.0, + shadows: outlinedText(), + color: Colors.white, + ), ); } } diff --git a/lib/widgets/ptz.dart b/lib/widgets/ptz.dart index 5ea42834..d95d8ad7 100644 --- a/lib/widgets/ptz.dart +++ b/lib/widgets/ptz.dart @@ -19,6 +19,7 @@ import 'package:bluecherry_client/api/api.dart'; import 'package:bluecherry_client/models/device.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; @@ -230,7 +231,7 @@ class PTZToggleButton extends StatelessWidget { final loc = AppLocalizations.of(context); final theme = Theme.of(context); return Row(children: [ - IconButton( + SquaredIconButton( icon: Icon( Icons.videogame_asset, color: ptzEnabled @@ -242,7 +243,7 @@ class PTZToggleButton extends StatelessWidget { onPressed: () => onChanged(!ptzEnabled), ), // TODO(bdlukaa): enable presets when the API is ready - // IconButton( + // SquaredIconButton( // icon: Icon( // Icons.dataset, // color: ptzEnabled ? Colors.white : theme.disabledColor, diff --git a/lib/widgets/servers/add_server.dart b/lib/widgets/servers/add_server.dart index 5fc8a4ad..d5bb8ed2 100644 --- a/lib/widgets/servers/add_server.dart +++ b/lib/widgets/servers/add_server.dart @@ -24,6 +24,7 @@ import 'package:bluecherry_client/providers/server_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/methods.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/device_grid/desktop/stream_data.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/servers/error.dart'; @@ -48,7 +49,7 @@ Widget _buildCardAppBar({ if (onBack != null) Padding( padding: const EdgeInsetsDirectional.only(end: 8.0), - child: IconButton( + child: SquaredIconButton( icon: const BackButtonIcon(), tooltip: MaterialLocalizations.of(context).backButtonTooltip, onPressed: () { diff --git a/lib/widgets/settings/mobile/settings.dart b/lib/widgets/settings/mobile/settings.dart index 234c2c76..eaab5c7e 100644 --- a/lib/widgets/settings/mobile/settings.dart +++ b/lib/widgets/settings/mobile/settings.dart @@ -26,6 +26,7 @@ import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/providers/update_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; import 'package:bluecherry_client/utils/extensions.dart'; +import 'package:bluecherry_client/utils/widgets/squared_icon_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; import 'package:bluecherry_client/widgets/servers/edit_server.dart'; import 'package:bluecherry_client/widgets/servers/edit_server_settings.dart'; diff --git a/lib/widgets/settings/shared/server_tile.dart b/lib/widgets/settings/shared/server_tile.dart index d72cfe04..c43b02ec 100644 --- a/lib/widgets/settings/shared/server_tile.dart +++ b/lib/widgets/settings/shared/server_tile.dart @@ -211,13 +211,13 @@ class ServerTile extends StatelessWidget { : loc.gettingDevices, overflow: TextOverflow.ellipsis, ), - trailing: IconButton( + trailing: SquaredIconButton( icon: Icon( Icons.delete, color: theme.colorScheme.error, ), tooltip: loc.disconnectServer, - splashRadius: 24.0, + // splashRadius: 24.0, onPressed: () => onRemoveServer(context, server), ), onTap: () { @@ -320,11 +320,11 @@ class ServerCard extends StatelessWidget { PositionedDirectional( top: 4, end: 2, - child: IconButton( - iconSize: 20.0, - splashRadius: 16.0, + child: SquaredIconButton( + // iconSize: 20.0, + // splashRadius: 16.0, tooltip: loc.serverOptions, - icon: Icon(moreIconData), + icon: Icon(moreIconData, size: 20.0), onPressed: showMenu, ), ), From 83c48d1e98c62900b09b70ad43be0c99e12e2005 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 1 Dec 2023 11:34:56 -0300 Subject: [PATCH 8/8] ui: highlight to-zoom area --- lib/providers/update_provider.dart | 87 ++++++++++--------- .../device_grid/desktop/multicast_view.dart | 20 +++-- .../device_grid/video_status_label.dart | 2 +- lib/widgets/hover_button.dart | 7 +- lib/widgets/player/live_player.dart | 1 - 5 files changed, 68 insertions(+), 49 deletions(-) diff --git a/lib/providers/update_provider.dart b/lib/providers/update_provider.dart index 5f14c06b..93d49fcd 100644 --- a/lib/providers/update_provider.dart +++ b/lib/providers/update_provider.dart @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/utils/logging.dart'; import 'package:bluecherry_client/utils/methods.dart'; import 'package:bluecherry_client/utils/storage.dart'; import 'package:dio/dio.dart'; @@ -377,51 +378,57 @@ class UpdateManager extends ChangeNotifier { loading = true; notifyListeners(); - final response = await http.get(Uri.parse(appCastUrl)); + try { + final response = await http.get(Uri.parse(appCastUrl)); - if (response.statusCode != 200) { - debugPrint( - 'Failed to check for updates (${response.statusCode}): ${response.body}', - ); - loading = false; - notifyListeners(); - return; - } + if (response.statusCode != 200) { + debugPrint( + 'Failed to check for updates (${response.statusCode}): ${response.body}', + ); + loading = false; + notifyListeners(); + return; + } - var versions = []; - final doc = XmlDocument.parse(response.body); - for (final item in doc.findAllElements('item')) { - late String version; - late String description; - late String publishedAt; - for (var child in item.children.whereType()) { - switch (child.name.toString()) { - case UpdateVersion.titleField: - version = child.innerText.replaceAll('Version', '').trim(); - break; - case UpdateVersion.descriptionField: - description = child.innerText.trim(); - break; - case UpdateVersion.publishedAtField: - publishedAt = child.innerText.trim(); - break; - default: + var versions = []; + final doc = XmlDocument.parse(response.body); + for (final item in doc.findAllElements('item')) { + late String version; + late String description; + late String publishedAt; + for (var child in item.children.whereType()) { + switch (child.name.toString()) { + case UpdateVersion.titleField: + version = child.innerText.replaceAll('Version', '').trim(); + break; + case UpdateVersion.descriptionField: + description = child.innerText.trim(); + break; + case UpdateVersion.publishedAtField: + publishedAt = child.innerText.trim(); + break; + default: + } } + versions.add(UpdateVersion( + version: version, + description: description, + publishedAt: publishedAt, + )); } - versions.add(UpdateVersion( - version: version, - description: description, - publishedAt: publishedAt, - )); - } - // versions.sort( - // (a, b) => a.publishedAt.compareTo(b.publishedAt), - // ); - versions = versions.reversed.toList(); + // versions.sort( + // (a, b) => a.publishedAt.compareTo(b.publishedAt), + // ); + versions = versions.reversed.toList(); - if (versions != this.versions) this.versions = versions; + if (versions != this.versions) this.versions = versions; - loading = false; - lastCheck = DateTime.now(); // this updates the screen already + loading = false; + lastCheck = DateTime.now(); // this updates the screen already + } catch (error, stackTrace) { + debugPrint(error.toString()); + debugPrint(stackTrace.toString()); + writeErrorToFile(error, stackTrace); + } } } diff --git a/lib/widgets/device_grid/desktop/multicast_view.dart b/lib/widgets/device_grid/desktop/multicast_view.dart index 8bc18e99..ffd7c46c 100644 --- a/lib/widgets/device_grid/desktop/multicast_view.dart +++ b/lib/widgets/device_grid/desktop/multicast_view.dart @@ -24,8 +24,8 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/desktop_view_provider.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; import 'package:bluecherry_client/utils/constants.dart'; +import 'package:bluecherry_client/widgets/hover_button.dart'; import 'package:bluecherry_client/widgets/misc.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -151,8 +151,7 @@ class _MulticastViewportState extends State { final row = index ~/ size; final col = index % size; - return GestureDetector( - behavior: HitTestBehavior.opaque, + return HoverButton( onDoubleTap: () { views.updateDevice( widget.device, @@ -161,13 +160,22 @@ class _MulticastViewportState extends State { ), ); }, - onTap: () { + onPressed: () { view.player.crop(row, col, size); currentZoom = (row, col); }, - child: const SizedBox.expand( + builder: (context, states) => SizedBox.expand( child: IgnorePointer( - child: kDebugMode ? Placeholder() : null, + child: states.isHovering + ? Container( + decoration: BoxDecoration( + border: Border.all( + color: theme.colorScheme.secondary, + width: 2.25, + ), + ), + ) + : null, ), ), ); diff --git a/lib/widgets/device_grid/video_status_label.dart b/lib/widgets/device_grid/video_status_label.dart index afed2312..0b26a0d8 100644 --- a/lib/widgets/device_grid/video_status_label.dart +++ b/lib/widgets/device_grid/video_status_label.dart @@ -188,7 +188,7 @@ class _VideoStatusLabelState extends State { height: 12.0, width: 12.0, child: CircularProgressIndicator.adaptive( - strokeWidth: 2.0, + strokeWidth: 1.5, valueColor: AlwaysStoppedAnimation(Colors.white), ), ), diff --git a/lib/widgets/hover_button.dart b/lib/widgets/hover_button.dart index 71acf8e4..bc4ac2ae 100644 --- a/lib/widgets/hover_button.dart +++ b/lib/widgets/hover_button.dart @@ -29,6 +29,7 @@ class HoverButton extends StatefulWidget { this.onLongPressStart, this.onLongPressCancel, this.onLongPressUp, + this.onDoubleTap, this.onHorizontalDragStart, this.onHorizontalDragUpdate, this.onHorizontalDragEnd, @@ -71,6 +72,8 @@ class HoverButton extends StatefulWidget { final GestureLongPressCancelCallback? onLongPressCancel; final VoidCallback? onLongPressUp; + final VoidCallback? onDoubleTap; + final VoidCallback? onPressed; final VoidCallback? onTapUp; final VoidCallback? onTapDown; @@ -250,7 +253,8 @@ class HoverButtonState extends State { widget.onVerticalDragStart != null || widget.onVerticalDragEnd != null || widget.onVerticalDragUpdate != null || - widget.onSecondaryTap != null; + widget.onSecondaryTap != null || + widget.onDoubleTap != null; Set get states { if (!enabled) return {ButtonStates.disabled}; @@ -324,6 +328,7 @@ class HoverButtonState extends State { // onScaleStart: widget.onScaleStart, // onScaleUpdate: widget.onScaleUpdate, // onScaleEnd: widget.onScaleEnd, + onDoubleTap: widget.onDoubleTap, child: Builder(builder: (context) => widget.builder(context, states)), ); if (widget.focusEnabled) { diff --git a/lib/widgets/player/live_player.dart b/lib/widgets/player/live_player.dart index 2c44423a..b7e94689 100644 --- a/lib/widgets/player/live_player.dart +++ b/lib/widgets/player/live_player.dart @@ -218,7 +218,6 @@ class __MobileLivePlayerState extends State<_MobileLivePlayer> { icon: const BackButtonIcon(), tooltip: MaterialLocalizations.of(context) .backButtonTooltip, - // todo: // color: Colors.white, ), trailing: