using System; using System.Threading; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Ui.Services; using ClaudeDo.Ui.ViewModels.Islands; namespace ClaudeDo.Ui.ViewModels; public sealed partial class IslandsShellViewModel : ViewModelBase { public ListsIslandViewModel Lists { get; } public TasksIslandViewModel Tasks { get; } public DetailsIslandViewModel Details { get; } public WorkerClient Worker { get; } public UpdateCheckService UpdateCheck => _updateCheck; public string ConnectionText => Worker.IsConnected ? "Online" : Worker.IsReconnecting ? "Connecting…" : "Offline"; public bool IsOffline => !Worker.IsConnected && !Worker.IsReconnecting; private readonly UpdateCheckService _updateCheck; private readonly InstallerLocator _installerLocator; [ObservableProperty] private bool _isUpdateBannerVisible; [ObservableProperty] private string? _updateBannerLatestVersion; [ObservableProperty] private string? _inlineUpdateStatus; private bool _bannerDismissedThisSession; [ObservableProperty] private double _windowWidth = 1280; public bool ShowDetails => WindowWidth >= 1100; public bool ShowLists => WindowWidth >= 780; [RelayCommand] private void FocusSearch() => Lists.RequestFocusSearch(); [RelayCommand] private void FocusAddTask() => Tasks.RequestFocusAddTask(); public async Task ToggleSelectedDoneAsync() { if (Tasks.SelectedTask is { } row) await Tasks.ToggleDoneCommand.ExecuteAsync(row); } partial void OnWindowWidthChanged(double value) { OnPropertyChanged(nameof(ShowDetails)); OnPropertyChanged(nameof(ShowLists)); } public IslandsShellViewModel( ListsIslandViewModel lists, TasksIslandViewModel tasks, DetailsIslandViewModel details, WorkerClient worker, UpdateCheckService updateCheck, InstallerLocator installerLocator) { Lists = lists; Tasks = tasks; Details = details; Worker = worker; _updateCheck = updateCheck; _installerLocator = installerLocator; Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList); Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask); Tasks.TasksChanged += (_, _) => _ = Lists.RefreshCountsAsync(); Details.CloseDetail = () => Tasks.SelectedTask = null; Details.DeleteFromList = row => { Tasks.LoadForList(Lists.SelectedList); _ = Lists.RefreshCountsAsync(); return System.Threading.Tasks.Task.CompletedTask; }; Worker.PropertyChanged += (_, e) => { if (e.PropertyName is nameof(WorkerClient.IsConnected) or nameof(WorkerClient.IsReconnecting)) { OnPropertyChanged(nameof(ConnectionText)); OnPropertyChanged(nameof(IsOffline)); } }; _ = Lists.LoadAsync(); _updateCheck.PropertyChanged += (_, e) => { if (e.PropertyName == nameof(UpdateCheckService.LastCheckStatus)) { RefreshBannerFromStatus(); } }; // Fire-and-forget startup check — never block UI. _ = Task.Run(async () => { try { await _updateCheck.CheckNowAsync(CancellationToken.None); } catch { } }); } private void RefreshBannerFromStatus() { switch (_updateCheck.LastCheckStatus) { case UpdateCheckStatus.UpdateAvailable: if (_bannerDismissedThisSession) { IsUpdateBannerVisible = false; break; } UpdateBannerLatestVersion = _updateCheck.LatestVersion; IsUpdateBannerVisible = true; InlineUpdateStatus = null; break; case UpdateCheckStatus.UpToDate: IsUpdateBannerVisible = false; ShowInlineStatus($"You're up to date (v{_updateCheck.CurrentVersion})"); break; case UpdateCheckStatus.CheckFailed: ShowInlineStatus("Could not check for updates"); break; } } private async void ShowInlineStatus(string text) { InlineUpdateStatus = text; await Task.Delay(3000); if (InlineUpdateStatus == text) InlineUpdateStatus = null; } [RelayCommand] private async Task CheckForUpdatesAsync() { await _updateCheck.CheckNowAsync(CancellationToken.None); } [RelayCommand] private void DismissBanner() { _bannerDismissedThisSession = true; IsUpdateBannerVisible = false; } [RelayCommand] private void UpdateNow() { var path = _installerLocator.Find(); if (path is null) return; try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(path) { UseShellExecute = true }); Environment.Exit(0); } catch { // Intentionally silent — if this fails there's nothing useful to show. } } }