From e352082db88d44b5863cb491105dafa40dbc83ab Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 4 Apr 2025 11:45:12 -0300 Subject: [PATCH 1/9] feat: Redesign Live Player --- lib/screens/players/live_player.dart | 249 +++++++++++++++------------ lib/widgets/desktop_buttons.dart | 75 +++++--- lib/widgets/ptz.dart | 2 +- 3 files changed, 193 insertions(+), 133 deletions(-) diff --git a/lib/screens/players/live_player.dart b/lib/screens/players/live_player.dart index 047dfd56..dfc3e85e 100644 --- a/lib/screens/players/live_player.dart +++ b/lib/screens/players/live_player.dart @@ -40,6 +40,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:unity_video_player/unity_video_player.dart'; +import 'package:window_manager/window_manager.dart'; /// The player that plays the streams in full-screen. class LivePlayer extends StatefulWidget { @@ -350,124 +351,158 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { super.dispose(); } + bool _isHovering = false; + Timer? _hoverTimer; + @override Widget build(BuildContext context) { final theme = Theme.of(context); final loc = AppLocalizations.of(context); final settings = context.watch(); - final isSubView = AlternativeWindow.maybeOf(context) != null; - - return Column( - children: [ - WindowButtons( - title: widget.device.fullName, - showNavigator: false, - flexible: Row( - children: [ - if (widget.device.hasPTZ) - SquaredIconButton( - icon: Icon( - Icons.videogame_asset, - color: ptzEnabled ? Colors.white : null, - shadows: outlinedText(), - ), - tooltip: ptzEnabled ? loc.enabledPTZ : loc.disabledPTZ, - onPressed: () => setState(() => ptzEnabled = !ptzEnabled), - ), - () { - final isMuted = widget.player.volume == 0.0; + final isAlternativeWindow = AlternativeWindow.maybeOf(context) != null; - return SquaredIconButton( - icon: Icon( - isMuted - ? Icons.volume_mute_rounded - : Icons.volume_up_rounded, - shadows: outlinedText(), - color: Colors.white, - ), - tooltip: isMuted ? loc.enableAudio : loc.disableAudio, - onPressed: () async { - if (isMuted) { - await widget.player.setVolume(1.0); - } else { - await widget.player.setVolume(0.0); - } - }, - ); - }(), - if (isDesktopPlatform && !isSubView) - SquaredIconButton( - icon: Icon( - Icons.open_in_new, - shadows: outlinedText(), - color: Colors.white, - ), - tooltip: loc.openInANewWindow, - onPressed: widget.device.openInANewWindow, - ), - CameraViewFitButton( - fit: fit, - onChanged: (newFit) => setState(() => fit = newFit), - ), - const SizedBox(width: 8.0), - if (_videoViewKey.currentContext != null) - VideoStatusLabel( - device: widget.device, - video: UnityVideoView.of(_videoViewKey.currentContext!), - position: VideoStatusLabelPosition.top, - ), - const SizedBox(width: 8.0), - ], - ), - ), - Expanded( - child: PTZController( - device: widget.device, - enabled: ptzEnabled, - builder: (context, commands, constraints) { - final states = HoverButton.of(context).states; - return UnityVideoView( - heroTag: widget.device.streamURL, - player: widget.player, - fit: fit, - paneBuilder: (context, player) { - return Stack( - key: _videoViewKey, - children: [ - if (commands.isNotEmpty) PTZData(commands: commands), - Positioned.fill( - child: Center( - child: AspectRatio( - aspectRatio: - player.aspectRatio == 0 || - player.aspectRatio == double.infinity - ? 16 / 9 - : player.aspectRatio, - child: MulticastViewport(device: widget.device), + return MouseRegion( + hitTestBehavior: HitTestBehavior.opaque, + onEnter: (_) { + if (mounted) setState(() => _isHovering = true); + }, + onExit: (_) { + if (mounted) setState(() => _isHovering = false); + }, + onHover: (event) { + if (mounted) setState(() => _isHovering = true); + _hoverTimer?.cancel(); + _hoverTimer = Timer(const Duration(milliseconds: 2000), () { + if (mounted) setState(() => _isHovering = false); + }); + }, + child: Stack( + children: [ + Positioned.fill( + child: PTZController( + device: widget.device, + enabled: ptzEnabled, + builder: (context, commands, constraints) { + final states = HoverButton.of(context).states; + final view = UnityVideoView( + heroTag: widget.device.streamURL, + player: widget.player, + fit: fit, + paneBuilder: (context, player) { + return Stack( + key: _videoViewKey, + children: [ + if (commands.isNotEmpty) PTZData(commands: commands), + Positioned.fill( + child: Center( + child: AspectRatio( + aspectRatio: + player.aspectRatio == 0 || + player.aspectRatio == double.infinity + ? 16 / 9 + : player.aspectRatio, + child: MulticastViewport(device: widget.device), + ), ), ), - ), - if (states.isHovering && settings.kShowDebugInfo.value) - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'source: ${player.dataSource ?? loc.unknown}' - '\nposition: ${player.currentPos}' - '\nduration ${player.duration}', - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.white, - shadows: outlinedText(), + if (states.isHovering && settings.kShowDebugInfo.value) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'source: ${player.dataSource ?? loc.unknown}' + '\nposition: ${player.currentPos}' + '\nduration ${player.duration}', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.white, + shadows: outlinedText(), + ), ), ), - ), - ], - ); - }, - ); - }, + ], + ); + }, + ); + if (ptzEnabled || !isAlternativeWindow) return view; + return DragToMoveArea(child: view); + }, + ), ), - ), - ], + AnimatedPositioned( + duration: const Duration(milliseconds: 320), + curve: Curves.easeInOut, + top: _isHovering ? 0.0 : -64.0, + left: 0.0, + right: 0.0, + child: WindowButtons( + title: widget.device.fullName, + showNavigator: false, + forceShow: _isHovering, + backgroundColor: theme.colorScheme.surfaceContainer.withValues( + alpha: 0.6, + ), + flexible: Row( + spacing: 8.0, + children: [ + if (widget.device.hasPTZ) + SquaredIconButton( + icon: Icon( + Icons.videogame_asset, + color: ptzEnabled ? Colors.white : null, + shadows: outlinedText(), + ), + tooltip: ptzEnabled ? loc.enabledPTZ : loc.disabledPTZ, + onPressed: () => setState(() => ptzEnabled = !ptzEnabled), + ), + () { + final isMuted = widget.player.volume == 0.0; + + return SquaredIconButton( + icon: Icon( + isMuted + ? Icons.volume_mute_rounded + : Icons.volume_up_rounded, + shadows: outlinedText(), + color: Colors.white, + ), + tooltip: isMuted ? loc.enableAudio : loc.disableAudio, + onPressed: () async { + if (isMuted) { + await widget.player.setVolume(1.0); + } else { + await widget.player.setVolume(0.0); + } + setState(() {}); + }, + ); + }(), + if (isDesktopPlatform && !isAlternativeWindow) + SquaredIconButton( + icon: Icon( + Icons.open_in_new, + shadows: outlinedText(), + color: Colors.white, + ), + tooltip: loc.openInANewWindow, + onPressed: widget.device.openInANewWindow, + ), + CameraViewFitButton( + fit: fit, + onChanged: (newFit) => setState(() => fit = newFit), + ), + const SizedBox(width: 8.0), + if (_videoViewKey.currentContext != null) + VideoStatusLabel( + device: widget.device, + video: UnityVideoView.of(_videoViewKey.currentContext!), + position: VideoStatusLabelPosition.top, + ), + const SizedBox(width: 8.0), + ], + ), + ), + ), + ], + ), ); } } diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index b16875c7..1c9d6c50 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -91,6 +91,9 @@ class WindowButtons extends StatefulWidget { this.showNavigator = true, this.onBack, this.flexible, + this.forceImmersive = false, + this.forceShow = false, + this.backgroundColor, }); /// The current window title. @@ -112,6 +115,17 @@ class WindowButtons extends StatefulWidget { /// The widget displayed in the remaining space. final Widget? flexible; + /// Whether to force the immersive mode. + final bool forceImmersive; + + /// Whether to force show the window buttons. + final bool forceShow; + + /// The background color of the bar. + /// + /// If not provided, the default color is used. + final Color? backgroundColor; + @override State createState() => _WindowButtonsState(); } @@ -208,6 +222,7 @@ class _WindowButtonsState extends State ); return Material( + color: widget.backgroundColor, child: SizedBox( height: 40.0, child: Stack( @@ -219,30 +234,7 @@ class _WindowButtonsState extends State if (isMacOSPlatform) const SizedBox(width: 70.0, height: 40.0), if (canPop) - Padding( - padding: const EdgeInsetsDirectional.only(start: 8.0), - child: SquaredIconButton( - onPressed: () async { - await widget.onBack?.call(); - await navigatorKey.currentState?.maybePop(); - }, - tooltip: - MaterialLocalizations.of( - context, - ).backButtonTooltip, - icon: Container( - padding: const EdgeInsetsDirectional.all(4.0), - // height: 40.0, - // width: 40.0, - alignment: AlignmentDirectional.center, - child: Icon( - Icons.adaptive.arrow_back, - size: 20.0, - color: theme.hintColor, - ), - ), - ), - ) + UnityBackButton(onBack: widget.onBack) else if (isWindowsPlatform) Padding( padding: const EdgeInsetsDirectional.only(start: 8.0), @@ -423,7 +415,7 @@ class _WindowButtonsState extends State }, ); - if (settings.isImmersiveMode) { + if (widget.forceImmersive || settings.isImmersiveMode) { return MouseRegion( onEnter: (_) { showOverlayEntry(context, bar); @@ -501,6 +493,39 @@ class _WindowButtonsState extends State } } +class UnityBackButton extends StatelessWidget { + const UnityBackButton({super.key, this.onBack}); + + final Future Function()? onBack; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsetsDirectional.only(start: 8.0), + child: SquaredIconButton( + onPressed: () async { + await onBack?.call(); + await navigatorKey.currentState?.maybePop(); + }, + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + icon: Container( + padding: const EdgeInsetsDirectional.all(4.0), + // height: 40.0, + // width: 40.0, + alignment: AlignmentDirectional.center, + child: Icon( + Icons.adaptive.arrow_back, + size: 20.0, + color: theme.hintColor, + ), + ), + ), + ); + } +} + /// A widget that shows whether something in the app is loading class UnityLoadingIndicator extends StatefulWidget { const UnityLoadingIndicator({super.key}); diff --git a/lib/widgets/ptz.dart b/lib/widgets/ptz.dart index 6dc60a83..a38dfea8 100644 --- a/lib/widgets/ptz.dart +++ b/lib/widgets/ptz.dart @@ -69,7 +69,7 @@ class _PTZControllerState extends State { if (!widget.enabled) { return HoverButton( forceEnabled: true, - hitTestBehavior: HitTestBehavior.translucent, + hitTestBehavior: HitTestBehavior.deferToChild, listenTo: const {ButtonStates.hovering}, builder: (context, _) => LayoutBuilder( From c4a4466989a5e1207b6f809ac8e50177e83fa692 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 4 Apr 2025 12:21:58 -0300 Subject: [PATCH 2/9] feat: Extract Device Buttons --- lib/screens/layouts/desktop/viewport.dart | 286 +++++++++--------- lib/screens/players/live_player.dart | 181 ++++------- lib/widgets/desktop_buttons.dart | 3 +- lib/widgets/ptz.dart | 4 + .../lib/unity_video_player_flutter.dart | 10 +- .../lib/unity_video_player_main.dart | 8 +- ...unity_video_player_platform_interface.dart | 8 + .../lib/video_view.dart | 18 +- 8 files changed, 245 insertions(+), 273 deletions(-) diff --git a/lib/screens/layouts/desktop/viewport.dart b/lib/screens/layouts/desktop/viewport.dart index 84fe1ddb..3c3f1349 100644 --- a/lib/screens/layouts/desktop/viewport.dart +++ b/lib/screens/layouts/desktop/viewport.dart @@ -17,8 +17,6 @@ * along with this program. If not, see . */ -import 'dart:async'; - import 'package:bluecherry_client/l10n/generated/app_localizations.dart'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/layouts_provider.dart'; @@ -108,24 +106,6 @@ class DesktopTileViewport extends StatefulWidget { class _DesktopTileViewportState extends State { bool ptzEnabled = false; - double get volume => widget.controller?.volume ?? 0.0; - StreamSubscription? volumeSubscription; - - @override - void initState() { - super.initState(); - if (widget.controller != null) { - volumeSubscription = widget.controller!.volumeStream.listen((_) { - if (mounted) setState(() {}); - }); - } - } - - @override - void dispose() { - volumeSubscription?.cancel(); - super.dispose(); - } @override Widget build(BuildContext context) { @@ -135,7 +115,6 @@ class _DesktopTileViewportState extends State { final settings = context.watch(); var video = UnityVideoView.maybeOf(context); final isSubView = AlternativeWindow.maybeOf(context) != null; - final isMuted = volume == 0.0; final showDebugInfo = widget.showDebugInfo ?? settings.kShowDebugInfo.value; if (showDebugInfo && widget.controller != null) { @@ -146,6 +125,7 @@ class _DesktopTileViewportState extends State { lastImageUpdate: DateTime.now(), fps: 0, player: widget.controller!, + fit: UnityVideoFit.fill, child: const SizedBox.shrink(), ); } @@ -161,20 +141,6 @@ class _DesktopTileViewportState extends State { widget.device.server.additionalSettings.videoFit ?? settings.kVideoFit.value; - final reloadButton = SquaredIconButton( - icon: Icon( - Icons.replay_outlined, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: loc.reloadCamera, - onPressed: () async { - await UnityPlayers.reloadDevice(widget.device); - if (mounted) setState(() {}); - }, - ); - return Stack( children: [ Positioned.fill(child: MulticastViewport(device: widget.device)), @@ -251,107 +217,12 @@ class _DesktopTileViewportState extends State { end: 0, start: 0, bottom: 4.0, - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (states.isHovering && - video.error == null && - !video.isLoading) ...[ - const SizedBox(width: 12.0), - if (widget.device.hasPTZ) - PTZToggleButton( - ptzEnabled: ptzEnabled, - onChanged: - (enabled) => setState(() => ptzEnabled = enabled), - ), - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - reverse: true, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (!video.isLoading) - 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(0.0); - } else { - await widget.controller!.setVolume(1.0); - } - }, - ), - if (isDesktopPlatform && - !isSubView && - !video.isLoading) - SquaredIconButton( - icon: Icon( - Icons.open_in_new_sharp, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: loc.openInANewWindow, - onPressed: widget.device.openInANewWindow, - ), - if (!isSubView && !video.isLoading) - SquaredIconButton( - icon: Icon( - Icons.fullscreen_rounded, - shadows: outlinedIcon(), - color: Colors.white, - size: 16.0, - ), - tooltip: loc.showFullscreenCamera, - onPressed: () async { - UnityPlayers.openFullscreen( - context, - widget.device, - ptzEnabled: ptzEnabled, - ); - }, - ), - reloadButton, - CameraViewFitButton( - fit: fit, - onChanged: widget.onFitChanged, - ), - ], - ), - ), - ), - ] else ...[ - const Spacer(), - if (states.isHovering) reloadButton, - ], - settings.kShowVideoStatusLabelOn.value.build( - Padding( - padding: const EdgeInsetsDirectional.only( - start: 6.0, - end: 6.0, - bottom: 6.0, - ), - child: VideoStatusLabel( - video: video, - device: widget.device, - ), - ), - const SizedBox.shrink(), - states, - ), - ], + child: DeviceOptions( + device: widget.device, + onPTZEnabledChanged: (enabled) { + setState(() => ptzEnabled = enabled); + }, + onFitChanged: widget.onFitChanged, ), ), if (!isSubView && @@ -434,3 +305,146 @@ class _DesktopTileViewportState extends State { ); } } + +class DeviceOptions extends StatefulWidget { + final Device device; + final ValueChanged onPTZEnabledChanged; + final ValueChanged onFitChanged; + + const DeviceOptions({ + super.key, + required this.device, + required this.onPTZEnabledChanged, + required this.onFitChanged, + }); + + @override + State createState() => _DeviceOptionsState(); +} + +class _DeviceOptionsState extends State { + @override + Widget build(BuildContext context) { + final states = HoverButton.of(context).states; + final loc = AppLocalizations.of(context); + final settings = context.watch(); + final video = UnityVideoView.of(context); + final controller = video.player; + final isAlternativeWindow = AlternativeWindow.maybeOf(context) != null; + final ptzEnabled = PTZController.of(context).enabled; + + final reloadButton = SquaredIconButton( + icon: Icon( + Icons.replay_outlined, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: loc.reloadCamera, + onPressed: () async { + await UnityPlayers.reloadDevice(widget.device); + if (mounted) setState(() {}); + }, + ); + + return Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (states.isHovering && video.error == null && !video.isLoading) ...[ + const SizedBox(width: 12.0), + if (widget.device.hasPTZ) + PTZToggleButton( + ptzEnabled: ptzEnabled, + onChanged: (enabled) => widget.onPTZEnabledChanged(enabled), + ), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + reverse: true, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (!video.isLoading) + StreamBuilder( + stream: controller.volumeStream, + builder: (context, snapshot) { + final isMuted = snapshot.data == 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 controller.setVolume(0.0); + } else { + await controller.setVolume(1.0); + } + }, + ); + }, + ), + if (isDesktopPlatform && + !isAlternativeWindow && + !video.isLoading) + SquaredIconButton( + icon: Icon( + Icons.open_in_new_sharp, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: loc.openInANewWindow, + onPressed: widget.device.openInANewWindow, + ), + if (!isAlternativeWindow && !video.isLoading) + SquaredIconButton( + icon: Icon( + Icons.fullscreen_rounded, + shadows: outlinedIcon(), + color: Colors.white, + size: 16.0, + ), + tooltip: loc.showFullscreenCamera, + onPressed: () async { + UnityPlayers.openFullscreen( + context, + widget.device, + ptzEnabled: ptzEnabled, + ); + }, + ), + reloadButton, + CameraViewFitButton( + fit: video.fit, + onChanged: widget.onFitChanged, + ), + ], + ), + ), + ), + ] else ...[ + const Spacer(), + if (states.isHovering) reloadButton, + ], + settings.kShowVideoStatusLabelOn.value.build( + Padding( + padding: const EdgeInsetsDirectional.only( + start: 6.0, + end: 6.0, + bottom: 6.0, + ), + child: VideoStatusLabel(video: video, device: widget.device), + ), + const SizedBox.shrink(), + states, + ), + ], + ); + } +} diff --git a/lib/screens/players/live_player.dart b/lib/screens/players/live_player.dart index dfc3e85e..72582735 100644 --- a/lib/screens/players/live_player.dart +++ b/lib/screens/players/live_player.dart @@ -24,10 +24,10 @@ 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/screens/layouts/desktop/multicast_view.dart'; +import 'package:bluecherry_client/screens/layouts/desktop/viewport.dart'; import 'package:bluecherry_client/screens/layouts/video_status_label.dart'; import 'package:bluecherry_client/screens/multi_window/window.dart'; import 'package:bluecherry_client/utils/methods.dart'; -import 'package:bluecherry_client/utils/window.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:bluecherry_client/widgets/error_warning.dart'; import 'package:bluecherry_client/widgets/hover_button.dart'; @@ -376,132 +376,75 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { if (mounted) setState(() => _isHovering = false); }); }, - child: Stack( - children: [ - Positioned.fill( - child: PTZController( - device: widget.device, - enabled: ptzEnabled, - builder: (context, commands, constraints) { - final states = HoverButton.of(context).states; - final view = UnityVideoView( - heroTag: widget.device.streamURL, - player: widget.player, - fit: fit, - paneBuilder: (context, player) { - return Stack( - key: _videoViewKey, - children: [ - if (commands.isNotEmpty) PTZData(commands: commands), - Positioned.fill( - child: Center( - child: AspectRatio( - aspectRatio: - player.aspectRatio == 0 || - player.aspectRatio == double.infinity - ? 16 / 9 - : player.aspectRatio, - child: MulticastViewport(device: widget.device), - ), - ), - ), - if (states.isHovering && settings.kShowDebugInfo.value) - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'source: ${player.dataSource ?? loc.unknown}' - '\nposition: ${player.currentPos}' - '\nduration ${player.duration}', - style: theme.textTheme.labelSmall?.copyWith( - color: Colors.white, - shadows: outlinedText(), - ), - ), - ), - ], - ); - }, - ); - if (ptzEnabled || !isAlternativeWindow) return view; - return DragToMoveArea(child: view); - }, - ), - ), - AnimatedPositioned( - duration: const Duration(milliseconds: 320), - curve: Curves.easeInOut, - top: _isHovering ? 0.0 : -64.0, - left: 0.0, - right: 0.0, - child: WindowButtons( - title: widget.device.fullName, - showNavigator: false, - forceShow: _isHovering, - backgroundColor: theme.colorScheme.surfaceContainer.withValues( - alpha: 0.6, - ), - flexible: Row( - spacing: 8.0, + child: PTZController( + device: widget.device, + enabled: ptzEnabled, + builder: (context, commands, constraints) { + final states = HoverButton.of(context).states; + final view = UnityVideoView( + heroTag: widget.device.streamURL, + player: widget.player, + fit: fit, + paneBuilder: (context, player) { + return Stack( + key: _videoViewKey, children: [ - if (widget.device.hasPTZ) - SquaredIconButton( - icon: Icon( - Icons.videogame_asset, - color: ptzEnabled ? Colors.white : null, - shadows: outlinedText(), + if (commands.isNotEmpty) PTZData(commands: commands), + Positioned.fill( + child: Center( + child: AspectRatio( + aspectRatio: + player.aspectRatio == 0 || + player.aspectRatio == double.infinity + ? 16 / 9 + : player.aspectRatio, + child: MulticastViewport(device: widget.device), ), - tooltip: ptzEnabled ? loc.enabledPTZ : loc.disabledPTZ, - onPressed: () => setState(() => ptzEnabled = !ptzEnabled), ), - () { - final isMuted = widget.player.volume == 0.0; - - return SquaredIconButton( - icon: Icon( - isMuted - ? Icons.volume_mute_rounded - : Icons.volume_up_rounded, - shadows: outlinedText(), - color: Colors.white, + ), + if (states.isHovering && settings.kShowDebugInfo.value) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + 'source: ${player.dataSource ?? loc.unknown}' + '\nposition: ${player.currentPos}' + '\nduration ${player.duration}', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.white, + shadows: outlinedText(), + ), ), - tooltip: isMuted ? loc.enableAudio : loc.disableAudio, - onPressed: () async { - if (isMuted) { - await widget.player.setVolume(1.0); - } else { - await widget.player.setVolume(0.0); - } - setState(() {}); - }, - ); - }(), - if (isDesktopPlatform && !isAlternativeWindow) - SquaredIconButton( - icon: Icon( - Icons.open_in_new, - shadows: outlinedText(), - color: Colors.white, + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 320), + curve: Curves.easeInOut, + top: _isHovering ? 0.0 : -64.0, + left: 0.0, + right: 0.0, + height: 40.0, + child: WindowButtons( + title: widget.device.fullName, + showNavigator: false, + forceShow: _isHovering, + backgroundColor: theme.colorScheme.surfaceContainer + .withValues(alpha: 0.6), + flexible: DeviceOptions( + device: widget.device, + onPTZEnabledChanged: + (enabled) => setState(() => ptzEnabled = enabled), + onFitChanged: (newFit) { + setState(() => fit = newFit); + }, ), - tooltip: loc.openInANewWindow, - onPressed: widget.device.openInANewWindow, ), - CameraViewFitButton( - fit: fit, - onChanged: (newFit) => setState(() => fit = newFit), ), - const SizedBox(width: 8.0), - if (_videoViewKey.currentContext != null) - VideoStatusLabel( - device: widget.device, - video: UnityVideoView.of(_videoViewKey.currentContext!), - position: VideoStatusLabelPosition.top, - ), - const SizedBox(width: 8.0), ], - ), - ), - ), - ], + ); + }, + ); + if (ptzEnabled || !isAlternativeWindow) return view; + return DragToMoveArea(child: view); + }, ), ); } diff --git a/lib/widgets/desktop_buttons.dart b/lib/widgets/desktop_buttons.dart index 1c9d6c50..35eb284d 100644 --- a/lib/widgets/desktop_buttons.dart +++ b/lib/widgets/desktop_buttons.dart @@ -275,7 +275,8 @@ class _WindowButtonsState extends State icon: const Icon(Icons.refresh, size: 20.0), tooltip: loc.refresh, ), - if (widget.flexible != null) widget.flexible!, + if (widget.flexible != null) + Flexible(child: widget.flexible!), // Do not render the Window Buttons on web nor macOS nor when // in fullscreen. macOS render the buttons natively. diff --git a/lib/widgets/ptz.dart b/lib/widgets/ptz.dart index a38dfea8..4a8ecdfb 100644 --- a/lib/widgets/ptz.dart +++ b/lib/widgets/ptz.dart @@ -56,6 +56,10 @@ class PTZController extends StatefulWidget { @override State createState() => _PTZControllerState(); + + static PTZController of(BuildContext context) { + return context.findAncestorWidgetOfExactType()!; + } } class _PTZControllerState extends State { diff --git a/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart b/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart index 878265cb..5be2adbd 100644 --- a/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart +++ b/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart @@ -72,7 +72,7 @@ class UnityVideoPlayerFlutterInterface extends UnityVideoPlayerInterface { return Builder(builder: (context) { return Stack(children: [ - const SizedBox.expand(), + const Positioned.fill(child: SizedBox.expand()), Positioned.fill( child: videoBuilder!( context, @@ -80,7 +80,13 @@ class UnityVideoPlayerFlutterInterface extends UnityVideoPlayerInterface { color: color, child: player.player == null ? const SizedBox.expand() - : VideoPlayer(player.player!), + : FittedBox( + fit: fit.boxFit, + child: SizedBox.fromSize( + size: player.player!.value.size, + child: VideoPlayer(player.player!), + ), + ), ), ), ), 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 875249d9..98afec79 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 @@ -67,13 +67,7 @@ class UnityVideoPlayerMediaKitInterface extends UnityVideoPlayerInterface { videoController: player.mkVideoController, mkPlayer: mkPlayer, color: color, - fit: () { - return switch (fit) { - UnityVideoFit.contain => BoxFit.contain, - UnityVideoFit.cover => BoxFit.cover, - UnityVideoFit.fill => BoxFit.fill, - }; - }(), + fit: fit.boxFit, ), ), ), 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 3b0e90b4..1ac4cd33 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 @@ -30,6 +30,14 @@ enum UnityVideoFit { UnityVideoFit.cover => UnityVideoFit.contain }; } + + BoxFit get boxFit { + return switch (this) { + UnityVideoFit.contain => BoxFit.contain, + UnityVideoFit.cover => BoxFit.cover, + UnityVideoFit.fill => BoxFit.fill, + }; + } } enum RTSPProtocol { diff --git a/packages/unity_video_player/unity_video_player_platform_interface/lib/video_view.dart b/packages/unity_video_player/unity_video_player_platform_interface/lib/video_view.dart index 06483c73..05f4407f 100644 --- a/packages/unity_video_player/unity_video_player_platform_interface/lib/video_view.dart +++ b/packages/unity_video_player/unity_video_player_platform_interface/lib/video_view.dart @@ -12,6 +12,7 @@ class VideoViewInheritance extends InheritedWidget { required this.lastImageUpdate, required this.fps, required this.player, + required this.fit, }); /// When the video is in an error state, this will be set with a description @@ -33,6 +34,8 @@ class VideoViewInheritance extends InheritedWidget { /// The player that is currently being used by the video. final UnityVideoPlayer player; + final UnityVideoFit fit; + bool get isLoading => !player.isSeekable && error == null; static VideoViewInheritance? maybeOf(BuildContext context) { @@ -143,6 +146,7 @@ class UnityVideoViewState extends State { duration: widget.player.duration, fps: widget.player.fps.toInt(), lastImageUpdate: widget.player.lastImageUpdate, + fit: widget.fit, child: UnityVideoPlayerInterface.instance.createVideoView( player: widget.player, color: widget.color, @@ -184,14 +188,12 @@ class UnityVideoViewState extends State { paneBuilder: widget.paneBuilder == null ? null : (context, player) { - return Center( - child: AspectRatio( - aspectRatio: widget.player.aspectRatio == 0 || - widget.player.aspectRatio == double.infinity - ? 16 / 9 - : widget.player.aspectRatio, - child: widget.paneBuilder?.call(context, player), - ), + return AspectRatio( + aspectRatio: widget.player.aspectRatio == 0 || + widget.player.aspectRatio == double.infinity + ? 16 / 9 + : widget.player.aspectRatio, + child: widget.paneBuilder?.call(context, player), ); }, ), From f3af7131a5e017946f10fdb2c015b48123cfb2c6 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Fri, 4 Apr 2025 12:35:41 -0300 Subject: [PATCH 3/9] feat: Correctly align device buttons --- lib/screens/layouts/desktop/viewport.dart | 37 +++++++++++++++-------- lib/screens/players/live_player.dart | 19 +++++++----- lib/widgets/ptz.dart | 4 +-- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/screens/layouts/desktop/viewport.dart b/lib/screens/layouts/desktop/viewport.dart index 3c3f1349..e6e1e7c8 100644 --- a/lib/screens/layouts/desktop/viewport.dart +++ b/lib/screens/layouts/desktop/viewport.dart @@ -310,12 +310,14 @@ class DeviceOptions extends StatefulWidget { final Device device; final ValueChanged onPTZEnabledChanged; final ValueChanged onFitChanged; + final bool isFullScreen; const DeviceOptions({ super.key, required this.device, required this.onPTZEnabledChanged, required this.onFitChanged, + this.isFullScreen = false, }); @override @@ -349,21 +351,23 @@ class _DeviceOptionsState extends State { return Row( crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, children: [ if (states.isHovering && video.error == null && !video.isLoading) ...[ - const SizedBox(width: 12.0), - if (widget.device.hasPTZ) - PTZToggleButton( - ptzEnabled: ptzEnabled, - onChanged: (enabled) => widget.onPTZEnabledChanged(enabled), - ), Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, reverse: true, child: Row( mainAxisAlignment: MainAxisAlignment.end, + spacing: 6.0, children: [ + if (widget.device.hasPTZ) + PTZToggleButton( + ptzEnabled: ptzEnabled, + onChanged: + (enabled) => widget.onPTZEnabledChanged(enabled), + ), if (!video.isLoading) StreamBuilder( stream: controller.volumeStream, @@ -402,7 +406,9 @@ class _DeviceOptionsState extends State { tooltip: loc.openInANewWindow, onPressed: widget.device.openInANewWindow, ), - if (!isAlternativeWindow && !video.isLoading) + if (!isAlternativeWindow && + !video.isLoading && + !widget.isFullScreen) SquaredIconButton( icon: Icon( Icons.fullscreen_rounded, @@ -420,10 +426,10 @@ class _DeviceOptionsState extends State { }, ), reloadButton, - CameraViewFitButton( - fit: video.fit, - onChanged: widget.onFitChanged, - ), + // CameraViewFitButton( + // fit: video.fit, + // onChanged: widget.onFitChanged, + // ), ], ), ), @@ -439,7 +445,14 @@ class _DeviceOptionsState extends State { end: 6.0, bottom: 6.0, ), - child: VideoStatusLabel(video: video, device: widget.device), + child: VideoStatusLabel( + video: video, + device: widget.device, + position: + widget.isFullScreen || isAlternativeWindow + ? VideoStatusLabelPosition.top + : VideoStatusLabelPosition.bottom, + ), ), const SizedBox.shrink(), states, diff --git a/lib/screens/players/live_player.dart b/lib/screens/players/live_player.dart index 72582735..0d01ff19 100644 --- a/lib/screens/players/live_player.dart +++ b/lib/screens/players/live_player.dart @@ -387,6 +387,7 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { fit: fit, paneBuilder: (context, player) { return Stack( + fit: StackFit.expand, key: _videoViewKey, children: [ if (commands.isNotEmpty) PTZData(commands: commands), @@ -428,13 +429,17 @@ class __DesktopLivePlayerState extends State<_DesktopLivePlayer> { forceShow: _isHovering, backgroundColor: theme.colorScheme.surfaceContainer .withValues(alpha: 0.6), - flexible: DeviceOptions( - device: widget.device, - onPTZEnabledChanged: - (enabled) => setState(() => ptzEnabled = enabled), - onFitChanged: (newFit) { - setState(() => fit = newFit); - }, + flexible: Align( + alignment: AlignmentDirectional.centerEnd, + child: DeviceOptions( + device: widget.device, + isFullScreen: true, + onPTZEnabledChanged: + (enabled) => setState(() => ptzEnabled = enabled), + onFitChanged: (newFit) { + setState(() => fit = newFit); + }, + ), ), ), ), diff --git a/lib/widgets/ptz.dart b/lib/widgets/ptz.dart index 4a8ecdfb..028dfe26 100644 --- a/lib/widgets/ptz.dart +++ b/lib/widgets/ptz.dart @@ -258,9 +258,7 @@ class PTZToggleButton extends StatelessWidget { ptzEnabled ? enabledColor ?? Colors.white : disabledColor ?? - theme.colorScheme.onInverseSurface.withValues( - alpha: 0.86, - ), + theme.colorScheme.onSurface.withValues(alpha: 0.86), ), tooltip: ptzEnabled ? loc.enabledPTZ : loc.disabledPTZ, onPressed: () => onChanged(!ptzEnabled), From dcc9fe370d41c1e068af5388404b48135639090e Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 5 Apr 2025 11:37:58 -0300 Subject: [PATCH 4/9] feat: Open layout in fullscreen --- lib/main.dart | 16 ++++++++++++++++ lib/screens/layouts/desktop/layout_view.dart | 12 ++++++++++++ 2 files changed, 28 insertions(+) diff --git a/lib/main.dart b/lib/main.dart index 7cb818c7..c0bf0f39 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:bluecherry_client/firebase_messaging_background_handler.dart'; import 'package:bluecherry_client/l10n/generated/app_localizations.dart'; import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/models/event.dart'; +import 'package:bluecherry_client/models/layout.dart'; import 'package:bluecherry_client/models/server.dart'; import 'package:bluecherry_client/providers/downloads_provider.dart'; import 'package:bluecherry_client/providers/events_provider.dart'; @@ -396,6 +397,21 @@ class _UnityAppState extends State ); } + if (settings.name == '/fullscreen-layout') { + final data = settings.arguments! as Map; + final Layout layout = data['layout']; + + return MaterialPageRoute( + settings: RouteSettings( + name: settings.name, + arguments: layout, + ), + builder: (context) { + return AlternativeLayoutView(layout: layout); + }, + ); + } + if (settings.name == '/rtsp') { final url = settings.arguments as String; return MaterialPageRoute( diff --git a/lib/screens/layouts/desktop/layout_view.dart b/lib/screens/layouts/desktop/layout_view.dart index ec4dedcd..2e192ef0 100644 --- a/lib/screens/layouts/desktop/layout_view.dart +++ b/lib/screens/layouts/desktop/layout_view.dart @@ -432,6 +432,7 @@ class _LayoutOptionsState extends State { final isAlternativeWindow = AlternativeWindow.maybeOf(context) != null; return Row( + spacing: 4.0, children: [ if (widget.layout.devices.isNotEmpty) ...() { @@ -493,6 +494,17 @@ class _LayoutOptionsState extends State { tooltip: loc.openInANewWindow, onPressed: widget.layout.openInANewWindow, ), + if (!isAlternativeWindow) + SquaredIconButton( + icon: Icon(Icons.fullscreen_rounded, color: Colors.white), + tooltip: loc.showFullscreenCamera, + onPressed: () async { + Navigator.of(context).pushNamed( + '/fullscreen-layout', + arguments: {'layout': widget.layout}, + ); + }, + ), // TODO(bdlukaa): "Add" button. Displays a popup with the current // available cameras SquaredIconButton( From 67310dfced3c4d23f0a5074a1885b35bb0445264 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 5 Apr 2025 11:41:19 -0300 Subject: [PATCH 5/9] feat: Use the same view for fullscreen and secondary window --- .../multi_window/single_camera_window.dart | 25 ++----------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/lib/screens/multi_window/single_camera_window.dart b/lib/screens/multi_window/single_camera_window.dart index 109ccd1e..120a78bd 100644 --- a/lib/screens/multi_window/single_camera_window.dart +++ b/lib/screens/multi_window/single_camera_window.dart @@ -19,9 +19,8 @@ import 'package:bluecherry_client/models/device.dart'; import 'package:bluecherry_client/providers/settings_provider.dart'; -import 'package:bluecherry_client/screens/layouts/desktop/viewport.dart'; +import 'package:bluecherry_client/screens/players/live_player.dart'; import 'package:bluecherry_client/utils/video_player.dart'; -import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:flutter/material.dart'; import 'package:unity_video_player/unity_video_player.dart'; @@ -54,26 +53,6 @@ class _CameraViewState extends State { @override Widget build(BuildContext context) { - return Material( - color: Colors.black, - child: Column( - children: [ - WindowButtons(title: widget.device.name, showNavigator: false), - Expanded( - child: UnityVideoView( - player: _controller, - fit: fit, - paneBuilder: (context, controller) { - return DesktopTileViewport( - controller: controller, - device: widget.device, - onFitChanged: (fit) => setState(() => this.fit = fit), - ); - }, - ), - ), - ], - ), - ); + return LivePlayer(player: _controller, device: widget.device); } } From 1d7c9b42189bb3be0d9fef8008d5f2847bf49b6e Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sat, 5 Apr 2025 12:23:28 -0300 Subject: [PATCH 6/9] feat: Restructure secondary layout view --- lib/screens/layouts/desktop/layout_view.dart | 35 ++++++---- .../multi_window/single_layout_window.dart | 66 +++++++++++++++---- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/lib/screens/layouts/desktop/layout_view.dart b/lib/screens/layouts/desktop/layout_view.dart index 2e192ef0..cd8c221c 100644 --- a/lib/screens/layouts/desktop/layout_view.dart +++ b/lib/screens/layouts/desktop/layout_view.dart @@ -206,6 +206,7 @@ class LayoutView extends StatelessWidget { this.onAccept, this.onWillAccept, this.onReorder, + this.showOptions, }); final Layout layout; @@ -213,6 +214,7 @@ class LayoutView extends StatelessWidget { final ValueChanged? onAccept; final DragTargetWillAccept? onWillAccept; final ReorderCallback? onReorder; + final bool? showOptions; @override Widget build(BuildContext context) { @@ -378,7 +380,7 @@ class LayoutView extends StatelessWidget { child: SafeArea( child: Column( children: [ - if (!settings.isImmersiveMode && !isAlternativeWindow) + if (showOptions ?? !settings.isImmersiveMode) Padding( padding: const EdgeInsets.all(16.0), child: IntrinsicHeight( @@ -414,8 +416,13 @@ class LayoutView extends StatelessWidget { class LayoutOptions extends StatefulWidget { final Layout layout; + final bool isFullscreen; - const LayoutOptions({super.key, required this.layout}); + const LayoutOptions({ + super.key, + required this.layout, + this.isFullscreen = false, + }); @override State createState() => _LayoutOptionsState(); @@ -433,6 +440,7 @@ class _LayoutOptionsState extends State { return Row( spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.end, children: [ if (widget.layout.devices.isNotEmpty) ...() { @@ -494,7 +502,7 @@ class _LayoutOptionsState extends State { tooltip: loc.openInANewWindow, onPressed: widget.layout.openInANewWindow, ), - if (!isAlternativeWindow) + if (!isAlternativeWindow && !widget.isFullscreen) SquaredIconButton( icon: Icon(Icons.fullscreen_rounded, color: Colors.white), tooltip: loc.showFullscreenCamera, @@ -507,16 +515,17 @@ class _LayoutOptionsState extends State { ), // TODO(bdlukaa): "Add" button. Displays a popup with the current // available cameras - SquaredIconButton( - icon: const Icon(Icons.edit, color: Colors.white), - tooltip: loc.editLayout, - onPressed: () { - showDialog( - context: context, - builder: (context) => EditLayoutDialog(layout: widget.layout), - ); - }, - ), + if (!isAlternativeWindow && !widget.isFullscreen) + SquaredIconButton( + icon: const Icon(Icons.edit, color: Colors.white), + tooltip: loc.editLayout, + onPressed: () { + showDialog( + context: context, + builder: (context) => EditLayoutDialog(layout: widget.layout), + ); + }, + ), SquaredIconButton( icon: const Icon(Icons.import_export, color: Colors.white), tooltip: loc.exportLayout, diff --git a/lib/screens/multi_window/single_layout_window.dart b/lib/screens/multi_window/single_layout_window.dart index cf7154be..1cfed9c2 100644 --- a/lib/screens/multi_window/single_layout_window.dart +++ b/lib/screens/multi_window/single_layout_window.dart @@ -17,30 +17,70 @@ * along with this program. If not, see . */ +import 'dart:async'; + import 'package:bluecherry_client/models/layout.dart'; import 'package:bluecherry_client/screens/layouts/device_grid.dart'; import 'package:bluecherry_client/widgets/desktop_buttons.dart'; import 'package:flutter/material.dart'; -class AlternativeLayoutView extends StatelessWidget { +class AlternativeLayoutView extends StatefulWidget { const AlternativeLayoutView({super.key, required this.layout}); final Layout layout; + @override + State createState() => _AlternativeLayoutViewState(); +} + +class _AlternativeLayoutViewState extends State { + bool _isHovering = false; + Timer? _hoverTimer; + @override Widget build(BuildContext context) { - return Material( - color: Colors.black, - child: SafeArea( - child: Column( - children: [ - WindowButtons( - title: layout.name, - showNavigator: false, - flexible: LayoutOptions(layout: layout), - ), - Expanded(child: LayoutView(layout: layout)), - ], + return MouseRegion( + hitTestBehavior: HitTestBehavior.opaque, + onEnter: (_) { + if (mounted) setState(() => _isHovering = true); + }, + onExit: (d) { + _hoverTimer?.cancel(); + if (mounted) setState(() => _isHovering = false); + }, + onHover: (event) { + if (mounted) setState(() => _isHovering = true); + _hoverTimer?.cancel(); + _hoverTimer = Timer(const Duration(milliseconds: 2000), () { + if (mounted) setState(() => _isHovering = false); + }); + }, + child: Material( + color: Colors.black, + child: SafeArea( + child: Stack( + children: [ + Positioned.fill( + child: LayoutView(layout: widget.layout, showOptions: false), + ), + AnimatedPositioned( + duration: const Duration(milliseconds: 320), + curve: Curves.easeInOut, + top: _isHovering ? 0.0 : -64.0, + left: 0.0, + right: 0.0, + height: 40.0, + child: WindowButtons( + title: widget.layout.name, + showNavigator: false, + flexible: LayoutOptions( + layout: widget.layout, + isFullscreen: true, + ), + ), + ), + ], + ), ), ), ); From 0204f46fa1b31c872246ebef2e9af73e387763e2 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 20 Apr 2025 15:18:56 -0300 Subject: [PATCH 7/9] chore: Run build info on shell --- .github/workflows/build.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 471d52af..e6a88c43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} submodules: recursive - name: Extract Build Info + shell: bash run: | echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then @@ -96,6 +97,7 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} submodules: recursive - name: Extract Build Info + shell: bash run: | echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then @@ -196,6 +198,7 @@ jobs: submodules: recursive - name: Extract Build Info + shell: bash run: | echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then @@ -267,6 +270,7 @@ jobs: submodules: recursive - name: Extract Build Info + shell: bash run: | echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then @@ -411,6 +415,7 @@ jobs: submodules: recursive - name: Extract Build Info + shell: bash run: | echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then @@ -445,6 +450,7 @@ jobs: submodules: recursive - name: Extract Build Info + shell: bash run: | echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then From 19f12ed9f8e436acc2114785aad346277e1bca69 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 20 Apr 2025 15:36:40 -0300 Subject: [PATCH 8/9] chore: Maybe show desktop sidebar on layout window --- lib/screens/layouts/desktop/sidebar.dart | 26 ++++++++++++------- .../multi_window/single_layout_window.dart | 13 ++++++++++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/screens/layouts/desktop/sidebar.dart b/lib/screens/layouts/desktop/sidebar.dart index 11a7119c..23261aff 100644 --- a/lib/screens/layouts/desktop/sidebar.dart +++ b/lib/screens/layouts/desktop/sidebar.dart @@ -25,8 +25,13 @@ const kCompactSidebarConstraints = BoxConstraints(maxWidth: 80.0); class DesktopSidebar extends StatefulWidget { final Widget collapseButton; + final bool showLayoutManager; - const DesktopSidebar({super.key, required this.collapseButton}); + const DesktopSidebar({ + super.key, + required this.collapseButton, + this.showLayoutManager = true, + }); @override State createState() => _DesktopSidebarState(); @@ -70,15 +75,16 @@ class _DesktopSidebarState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - LayoutManager( - collapseButton: widget.collapseButton, - onSearchChanged: (text) { - setState(() { - searchQuery = text; - _updateServers(); - }); - }, - ), + if (widget.showLayoutManager) + LayoutManager( + collapseButton: widget.collapseButton, + onSearchChanged: (text) { + setState(() { + searchQuery = text; + _updateServers(); + }); + }, + ), if (servers.servers.isEmpty) const Expanded(child: NoServers()) else diff --git a/lib/screens/multi_window/single_layout_window.dart b/lib/screens/multi_window/single_layout_window.dart index 1cfed9c2..b208d0cc 100644 --- a/lib/screens/multi_window/single_layout_window.dart +++ b/lib/screens/multi_window/single_layout_window.dart @@ -63,6 +63,18 @@ class _AlternativeLayoutViewState extends State { Positioned.fill( child: LayoutView(layout: widget.layout, showOptions: false), ), + /* AnimatedPositioned( + duration: const Duration(milliseconds: 320), + curve: Curves.easeInOut, + top: 40.0, + bottom: 0.0, + right: _isHovering ? 0.0 : -kSidebarConstraints.maxWidth, + width: kSidebarConstraints.maxWidth, + child: DesktopSidebar( + collapseButton: SizedBox.shrink(), + showLayoutManager: false, + ), + ), */ AnimatedPositioned( duration: const Duration(milliseconds: 320), curve: Curves.easeInOut, @@ -73,6 +85,7 @@ class _AlternativeLayoutViewState extends State { child: WindowButtons( title: widget.layout.name, showNavigator: false, + backgroundColor: Colors.black.withValues(alpha: 0.5), flexible: LayoutOptions( layout: widget.layout, isFullscreen: true, From 6d7629d875a76704d96effe6dbc0ba6fea177c93 Mon Sep 17 00:00:00 2001 From: Bruno D'Luka Date: Sun, 20 Apr 2025 15:52:29 -0300 Subject: [PATCH 9/9] chore: Compile on the web --- .github/workflows/build.yml | 16 ++++++++-------- .../lib/unity_video_player_flutter.dart | 5 ++--- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6a88c43..356dfcc0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -414,13 +414,13 @@ jobs: token: ${{ secrets.GITHUB_TOKEN }} submodules: recursive - - name: Extract Build Info - shell: bash - run: | - echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV - if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then - echo "BUILD_NAME=3.0.0-bleeding_edge+${{ github.run_number }}" >> $GITHUB_ENV - fi + # - name: Extract Build Info + # shell: bash + # run: | + # echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV + # if [[ "${{ github.ref }}" == "refs/tags/bleeding_edge" ]]; then + # echo "BUILD_NAME=3.0.0-bleeding_edge+${{ github.run_number }}" >> $GITHUB_ENV + # fi - name: Install Flutter uses: subosito/flutter-action@v2.8.0 @@ -437,7 +437,7 @@ jobs: - name: Build run: | - flutterpi_tool build --release --cpu=pi4 --build-number=${{ env.BUILD_NUMBER }} ${{ env.BUILD_NAME && format('--build-name={0}', env.BUILD_NAME) }} + flutterpi_tool build --release --cpu=pi4 build_web: name: Bluecherry Web diff --git a/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart b/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart index 5be2adbd..d89be56d 100644 --- a/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart +++ b/packages/unity_video_player/unity_video_player_flutter/lib/unity_video_player_flutter.dart @@ -256,10 +256,9 @@ class UnityVideoPlayerFlutter extends UnityVideoPlayer { @override double get fps { - if (!isPi || player == null) return 0.0; + if (!isPi || player == null || kIsWeb) return 0.0; - return player - ?.getMediaInfo() + return (player?.getMediaInfo() as dynamic) // Make it web safe ?.video ?.firstOrNull ?.codec