using System; using System.Collections.Generic; using System.Threading.Tasks; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using LANCommander.Launcher.ViewModels; namespace LANCommander.Launcher.Services; public record NavigationEntry(ViewModelBase ViewModel); public partial class NavigationService : ObservableObject, INavigationService { private const int MaxHistoryDepth = 20; private readonly Stack _history = new(); [ObservableProperty] [NotifyPropertyChangedFor(nameof(CanGoBack))] private ViewModelBase? _currentView; [ObservableProperty] private bool _isNavigating; public bool CanGoBack => _history.Count > 0; public async void NavigateTo(ViewModelBase viewModel, bool clearHistory = false, Func? initializeAsync = null) { if (clearHistory) { _history.Clear(); } else if (CurrentView != null) { _history.Push(new NavigationEntry(CurrentView)); // Trim oldest entries if we exceed the cap if (_history.Count > MaxHistoryDepth) { var trimmed = new Stack(MaxHistoryDepth); var items = _history.ToArray(); for (int i = MaxHistoryDepth - 1; i >= 0; i--) trimmed.Push(items[i]); _history.Clear(); foreach (var entry in trimmed) _history.Push(entry); } } // Show the loading overlay and yield so the UI thread can paint it // over the CURRENT view before we do any heavy work. IsNavigating = true; await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Render).GetTask(); try { if (initializeAsync != null) await initializeAsync(); CurrentView = viewModel; // Yield again so the new view finishes layout/render // before we hide the overlay. await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background).GetTask(); } catch { // Initialization errors are handled by the caller's callback. // Still swap to the target view so navigation isn't left stuck. CurrentView = viewModel; } finally { IsNavigating = false; } } public void GoBack() { if (_history.Count == 0) return; IsNavigating = true; // Post the actual view swap so the overlay paints first, // then yield after layout to hide it. Dispatcher.UIThread.Post(async () => { try { var entry = _history.Pop(); CurrentView = entry.ViewModel; await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background).GetTask(); } finally { IsNavigating = false; } }, DispatcherPriority.Background); } public void ReplaceCurrent(ViewModelBase viewModel) { CurrentView = viewModel; } }