diff --git a/src/OpenIPC.Viewer.App/Services/OverlayDialogPresenter.cs b/src/OpenIPC.Viewer.App/Services/OverlayDialogPresenter.cs
index ff32a13..bfc0a62 100644
--- a/src/OpenIPC.Viewer.App/Services/OverlayDialogPresenter.cs
+++ b/src/OpenIPC.Viewer.App/Services/OverlayDialogPresenter.cs
@@ -29,6 +29,18 @@ public static class OverlayDialogPresenter
{
private static readonly TimeSpan FadeIn = TimeSpan.FromMilliseconds(160);
+ // Number of overlay dialogs currently on screen. Mobile dialogs live in the
+ // TopLevel.OverlayLayer; the dim Border does not reliably intercept taps on
+ // the bottom nav, so the shell gates navigation on this instead. Desktop
+ // uses real modal Windows (ShowDialog) and never goes through here.
+ private static int _activeCount;
+
+ /// True while at least one overlay (mobile modal) dialog is open.
+ public static bool IsAnyOpen => _activeCount > 0;
+
+ /// Raised on the UI thread whenever may have changed.
+ public static event Action? ActiveChanged;
+
public static async Task ShowAsync(Control content, Task completion)
{
var overlay = GetOverlayLayer();
@@ -82,6 +94,8 @@ public static async Task ShowAsync(Control content, Task ShowAsync(Control content, Task(this);
WeakReferenceMessenger.Default.Register(this);
+
+ // While a mobile overlay dialog is open the bottom nav must not switch
+ // pages under it — the dim layer doesn't reliably swallow those taps.
+ // Re-evaluate CanNavigate whenever an overlay opens/closes; this greys
+ // out the nav buttons (Avalonia disables a control whose command can't
+ // execute). This VM is an app-lifetime singleton, so the static
+ // subscription lives as long as the process — no unsubscribe needed.
+ OverlayDialogPresenter.ActiveChanged += () => NavigateCommand.NotifyCanExecuteChanged();
}
- [RelayCommand]
+ private static bool CanNavigate() => !OverlayDialogPresenter.IsAnyOpen;
+
+ [RelayCommand(CanExecute = nameof(CanNavigate))]
private void Navigate(string target)
{
if (_activeSingleCamera is not null && target != "library")