Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/OpenIPC.Viewer.App/ViewModels/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ public sealed partial class MainWindowViewModel : ViewModelBase, IRecipient<Open
[NotifyPropertyChangedFor(nameof(IsSettingsSelected))]
private ViewModelBase _currentPage;

// Orientation-driven fullscreen (mobile only). MainView reports whether
// the viewport is in mobile landscape; fullscreen engages only while the
// single-camera page is open, so list/settings pages keep their chrome.
private bool _isMobileLandscape;

[ObservableProperty]
private bool _isFullscreen;

public bool IsLiveSelected => CurrentPage is GridPageViewModel;
public bool IsLibrarySelected => CurrentPage is CameraLibraryPageViewModel or SingleCameraPageViewModel;
public bool IsRecordingsSelected => CurrentPage is RecordingsPageViewModel;
Expand Down Expand Up @@ -71,6 +79,26 @@ public MainWindowViewModel(
OverlayDialogPresenter.ActiveChanged += () => NavigateCommand.NotifyCanExecuteChanged();
}

public void SetViewportOrientation(bool isMobileLandscape)
{
if (_isMobileLandscape == isMobileLandscape)
return;
_isMobileLandscape = isMobileLandscape;
UpdateFullscreen();
}

partial void OnCurrentPageChanged(ViewModelBase value) => UpdateFullscreen();

private void UpdateFullscreen()
{
IsFullscreen = _isMobileLandscape && CurrentPage is SingleCameraPageViewModel;
// Camera-to-camera swipe replaces the page VM while IsFullscreen stays
// true, so the flag is pushed to the current page explicitly instead
// of relying on the property-changed callback.
if (CurrentPage is SingleCameraPageViewModel camera)
camera.IsFullscreen = IsFullscreen;
}

private static bool CanNavigate() => !OverlayDialogPresenter.IsAnyOpen;

[RelayCommand(CanExecute = nameof(CanNavigate))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ public sealed partial class SingleCameraPageViewModel : ViewModelBase, IAsyncDis
public bool IsConnecting =>
Session is not null && State is SessionState.Connecting or SessionState.Reconnecting;
public bool IsFailed => State == SessionState.Failed;
// Set by MainWindowViewModel while the device is in mobile landscape —
// hides the header / Majestic panel / bottom bar so the video gets the
// whole screen. Overlays (LIVE badge, telemetry, PTZ) stay on the video.
[ObservableProperty] private bool _isFullscreen;

[ObservableProperty] private string? _snapshotPath;
[ObservableProperty] private PtzController? _ptz;
[ObservableProperty] private string _newPresetName = "";
Expand Down
8 changes: 5 additions & 3 deletions src/OpenIPC.Viewer.App/Views/MainView.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
Responsive layout. The split happens at 700px:
wide → desktop sidebar on the left, content fills the rest
narrow → bottom-nav under the content (mobile / pinned-narrow window)
IsWideLayout is updated by code-behind on every SizeChanged.
ShowSidebar / ShowBottomNav are updated by code-behind on every SizeChanged
and both collapse while the mobile landscape-fullscreen camera view is
active (MainWindowViewModel.IsFullscreen).

CurrentPage is hosted in a SINGLE ContentControl shared by both layouts.
Hosting it twice (one ContentControl per layout) made the same page VM bind
Expand All @@ -29,7 +31,7 @@
<!-- Desktop sidebar — fixed 200px wide; collapses to nothing when narrow. -->
<Border Grid.Column="0"
Width="200"
IsVisible="{Binding #Root.IsWideLayout}"
IsVisible="{Binding #Root.ShowSidebar}"
Background="{StaticResource Bg2Brush}"
BorderBrush="{StaticResource BorderWeakBrush}"
BorderThickness="0,0,1,0">
Expand Down Expand Up @@ -96,7 +98,7 @@
<!-- Content host + bottom nav (narrow only). One ContentControl, period. -->
<DockPanel Grid.Column="1" LastChildFill="True">
<views:BottomNavBar DockPanel.Dock="Bottom"
IsVisible="{Binding #Root.IsNarrowLayout}" />
IsVisible="{Binding #Root.ShowBottomNav}" />
<ContentControl Content="{Binding CurrentPage}"
Padding="{Binding #Root.ContentPadding}" />
</DockPanel>
Expand Down
102 changes: 76 additions & 26 deletions src/OpenIPC.Viewer.App/Views/MainView.axaml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using System;
using System.ComponentModel;
using Avalonia;
using Avalonia.Controls;
using OpenIPC.Viewer.App.ViewModels;

namespace OpenIPC.Viewer.App.Views;

Expand All @@ -10,13 +13,18 @@ public partial class MainView : UserControl
// windows) gets the bottom strip.
private const double WideBreakpoint = 700;

public static readonly DirectProperty<MainView, bool> IsWideLayoutProperty =
// Orientation-driven fullscreen is mobile-only: on desktop a window that
// is wider than tall is just a window, not a rotated device.
private static readonly bool IsMobilePlatform =
OperatingSystem.IsAndroid() || OperatingSystem.IsIOS();

public static readonly DirectProperty<MainView, bool> ShowSidebarProperty =
AvaloniaProperty.RegisterDirect<MainView, bool>(
nameof(IsWideLayout), o => o.IsWideLayout);
nameof(ShowSidebar), o => o.ShowSidebar);

public static readonly DirectProperty<MainView, bool> IsNarrowLayoutProperty =
public static readonly DirectProperty<MainView, bool> ShowBottomNavProperty =
AvaloniaProperty.RegisterDirect<MainView, bool>(
nameof(IsNarrowLayout), o => o.IsNarrowLayout);
nameof(ShowBottomNav), o => o.ShowBottomNav);

// Content inset differs by layout (desktop has more breathing room). Exposed
// as a property because the single shared ContentControl can no longer pick
Expand All @@ -29,41 +37,83 @@ public partial class MainView : UserControl
private static readonly Thickness NarrowPadding = new(12);

private bool _isWideLayout = true;
private bool _isFullscreen;
private bool _showSidebar = true;
private bool _showBottomNav;
private Thickness _contentPadding = WidePadding;
private MainWindowViewModel? _vm;

public bool IsWideLayout
{
get => _isWideLayout;
private set
{
if (SetAndRaise(IsWideLayoutProperty, ref _isWideLayout, value))
{
RaisePropertyChanged(IsNarrowLayoutProperty, !value, value == false);
ContentPadding = value ? WidePadding : NarrowPadding;
}
}
}

public bool IsNarrowLayout => !_isWideLayout;

public Thickness ContentPadding
{
get => _contentPadding;
private set => SetAndRaise(ContentPaddingProperty, ref _contentPadding, value);
}
public bool ShowSidebar => _showSidebar;
public bool ShowBottomNav => _showBottomNav;
public Thickness ContentPadding => _contentPadding;

public MainView()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
// Seed off the initial bounds — XAML evaluates IsVisible before
// the first SizeChanged fires, so without this both layouts could
// flash for a frame on narrow viewports.
IsWideLayout = Bounds.Width >= WideBreakpoint;
_isWideLayout = Bounds.Width >= WideBreakpoint;
UpdateChrome();
}

protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
IsWideLayout = e.NewSize.Width >= WideBreakpoint;
_isWideLayout = e.NewSize.Width >= WideBreakpoint;
// Landscape on a phone = the user rotated the device. The VM decides
// whether that means fullscreen (only while a camera page is open).
if (IsMobilePlatform)
_vm?.SetViewportOrientation(e.NewSize.Width > e.NewSize.Height);
UpdateChrome();
}

private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_vm is not null)
_vm.PropertyChanged -= OnVmPropertyChanged;
_vm = DataContext as MainWindowViewModel;
if (_vm is not null)
{
_vm.PropertyChanged += OnVmPropertyChanged;
// Push current orientation in case the device started in landscape
// and SizeChanged fired before the DataContext was assigned.
if (IsMobilePlatform && Bounds.Width > 0)
_vm.SetViewportOrientation(Bounds.Width > Bounds.Height);
}
SetFullscreen(_vm?.IsFullscreen ?? false);
}

private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MainWindowViewModel.IsFullscreen) && _vm is not null)
SetFullscreen(_vm.IsFullscreen);
}

private void SetFullscreen(bool on)
{
if (_isFullscreen == on)
return;
_isFullscreen = on;
UpdateChrome();

// System status/navigation bars follow the app chrome on mobile;
// InsetsManager is null on desktop so this is a no-op there.
var insets = TopLevel.GetTopLevel(this)?.InsetsManager;
if (insets is not null)
insets.IsSystemBarVisible = !on;
}

private void UpdateChrome()
{
var showSidebar = _isWideLayout && !_isFullscreen;
var showBottomNav = !_isWideLayout && !_isFullscreen;
var padding = _isFullscreen ? new Thickness(0)
: _isWideLayout ? WidePadding : NarrowPadding;

SetAndRaise(ShowSidebarProperty, ref _showSidebar, showSidebar);
SetAndRaise(ShowBottomNavProperty, ref _showBottomNav, showBottomNav);
SetAndRaise(ContentPaddingProperty, ref _contentPadding, padding);
}
}
19 changes: 13 additions & 6 deletions src/OpenIPC.Viewer.App/Views/Pages/SingleCameraPage.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

<Grid RowDefinitions="Auto,*,Auto,Auto">

<!-- Header -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,12">
<!-- Header. Hidden in landscape fullscreen — only the video row stays. -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,12"
IsVisible="{Binding !IsFullscreen}">
<Button Grid.Column="0"
Content="{Binding [Common.Back], Source={x:Static svc:Localizer.Instance}}"
Command="{Binding BackCommand}"
Expand Down Expand Up @@ -214,8 +215,13 @@
Background="{StaticResource Bg2Brush}"
BorderBrush="{StaticResource BorderWeakBrush}"
BorderThickness="1"
CornerRadius="8"
IsVisible="{Binding IsMajestic}">
CornerRadius="8">
<Border.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsMajestic" />
<Binding Path="!IsFullscreen" />
</MultiBinding>
</Border.IsVisible>
<StackPanel Spacing="8">

<!-- Title row -->
Expand Down Expand Up @@ -330,8 +336,9 @@
</StackPanel>
</Border>

<!-- Bottom bar -->
<Grid Grid.Row="3" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,12,0,0">
<!-- Bottom bar. Hidden in landscape fullscreen. -->
<Grid Grid.Row="3" ColumnDefinitions="Auto,*,Auto,Auto" Margin="0,12,0,0"
IsVisible="{Binding !IsFullscreen}">

<!-- REC indicator (visible while recording) -->
<Border Grid.Column="0"
Expand Down
Loading