using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using PSLauncher.Core.Localization; using PSLauncher.Models; namespace PSLauncher.App.ViewModels; public enum VersionRowState { InstalledIdle, // Installée localement, pas d'action en cours AvailableIdle, // Disponible côté serveur, pas installée Downloading, Verifying, // SHA-256 du ZIP téléchargé en cours Installing, Uninstalling, } public sealed partial class VersionRowViewModel : ObservableObject { public string Version { get; } public DateTime ReleasedAt { get; } public string ExecutablePath { get; } public string FolderPath { get; } public long SizeBytes { get; } public InstalledVersion? Installed { get; } public VersionManifest? Remote { get; } public bool HasReleaseNotes => Remote?.ReleaseNotesUrl is not null; [ObservableProperty] [NotifyPropertyChangedFor(nameof(StateLabel))] [NotifyPropertyChangedFor(nameof(IsBusy))] [NotifyPropertyChangedFor(nameof(IsInstalled))] [NotifyPropertyChangedFor(nameof(IsRemoteOnly))] [NotifyPropertyChangedFor(nameof(ShowRestartFromZero))] [NotifyCanExecuteChangedFor(nameof(LaunchCommand))] [NotifyCanExecuteChangedFor(nameof(InstallCommand))] [NotifyCanExecuteChangedFor(nameof(UninstallCommand))] private VersionRowState _state; [ObservableProperty] private double _progressPercent; [ObservableProperty] private string? _progressDetail; [ObservableProperty] [NotifyPropertyChangedFor(nameof(InstallButtonLabel))] [NotifyPropertyChangedFor(nameof(HasResumableDownload))] [NotifyPropertyChangedFor(nameof(ShowRestartFromZero))] [NotifyCanExecuteChangedFor(nameof(RestartFromZeroCommand))] private long _resumableBytes; [ObservableProperty] [NotifyPropertyChangedFor(nameof(InstallButtonLabel))] [NotifyPropertyChangedFor(nameof(LicenseAllowsInstall))] [NotifyCanExecuteChangedFor(nameof(InstallCommand))] private bool _licenseAllowsDownload = true; public bool HasResumableDownload => ResumableBytes > 0; public bool LicenseAllowsInstall => LicenseAllowsDownload; /// /// Visibilité du bouton rouge « Annuler » à côté de « ↻ Reprendre ». /// Doit être vrai uniquement quand un partial existe ET qu'aucun DL n'est en /// cours sur cette row (sinon le bouton « Annuler » se mélange visuellement /// avec la barre de progression du DL actif, qui a son propre cancel). /// public bool ShowRestartFromZero => HasResumableDownload && State == VersionRowState.AvailableIdle; public string InstallButtonLabel { get { if (!LicenseAllowsDownload) return Strings.ActionLicenseInsufficient; if (!HasResumableDownload) return Strings.ActionInstall; if (Remote is null || Remote.Download.SizeBytes <= 0) return Strings.ResumeButtonNoPercent; var pct = (int)Math.Round((double)ResumableBytes / Remote.Download.SizeBytes * 100.0); return Strings.ResumeButton(pct); } } public bool IsInstalled => State is VersionRowState.InstalledIdle or VersionRowState.Uninstalling; public bool IsRemoteOnly => State is VersionRowState.AvailableIdle; public bool IsBusy => State is VersionRowState.Downloading or VersionRowState.Verifying or VersionRowState.Installing or VersionRowState.Uninstalling; public string StateLabel => State switch { VersionRowState.InstalledIdle => Strings.StatusInstalled, VersionRowState.AvailableIdle => Strings.StatusAvailable, VersionRowState.Downloading => Strings.StatusDownloading, VersionRowState.Verifying => Strings.StatusVerifying, VersionRowState.Installing => Strings.StatusInstalling, VersionRowState.Uninstalling => Strings.StatusUninstalling, _ => string.Empty }; public string PrimaryDate => ReleasedAt > DateTime.MinValue ? Strings.FormatDate(ReleasedAt) : "—"; public string SizeDisplay => FormatSize(SizeBytes); /// Constructeur ligne « installée localement » (peut aussi être présente en remote). public static VersionRowViewModel ForInstalled(InstalledVersion installed, VersionManifest? remote) { return new VersionRowViewModel( version: installed.Version, releasedAt: remote?.ReleasedAt ?? installed.InstalledAt, executablePath: installed.ExecutablePath, folderPath: installed.FolderPath, sizeBytes: installed.SizeBytes, installed: installed, remote: remote, initialState: VersionRowState.InstalledIdle); } /// Constructeur ligne « disponible côté serveur uniquement ». public static VersionRowViewModel ForRemote(VersionManifest remote) { return new VersionRowViewModel( version: remote.Version, releasedAt: remote.ReleasedAt, executablePath: string.Empty, folderPath: string.Empty, sizeBytes: remote.Download.SizeBytes, installed: null, remote: remote, initialState: VersionRowState.AvailableIdle); } private VersionRowViewModel( string version, DateTime releasedAt, string executablePath, string folderPath, long sizeBytes, InstalledVersion? installed, VersionManifest? remote, VersionRowState initialState) { Version = version; ReleasedAt = releasedAt; ExecutablePath = executablePath; FolderPath = folderPath; SizeBytes = sizeBytes; Installed = installed; Remote = remote; _state = initialState; } // Les commandes sont câblées par le MainViewModel après instanciation // (les actions ont besoin de services). public Action? LaunchHandler { get; set; } public Action? InstallHandler { get; set; } public Action? UninstallHandler { get; set; } public Action? ShowReleaseNotesHandler { get; set; } public Action? OpenFolderHandler { get; set; } public Action? RestartFromZeroHandler { get; set; } [RelayCommand(CanExecute = nameof(CanLaunch))] private void Launch() => LaunchHandler?.Invoke(this); private bool CanLaunch() => State == VersionRowState.InstalledIdle; [RelayCommand(CanExecute = nameof(CanInstall))] private void Install() => InstallHandler?.Invoke(this); private bool CanInstall() => State == VersionRowState.AvailableIdle && LicenseAllowsDownload; [RelayCommand(CanExecute = nameof(CanUninstall))] private void Uninstall() => UninstallHandler?.Invoke(this); private bool CanUninstall() => State == VersionRowState.InstalledIdle; [RelayCommand] private void ShowReleaseNotes() => ShowReleaseNotesHandler?.Invoke(this); [RelayCommand] private void OpenFolder() => OpenFolderHandler?.Invoke(this); [RelayCommand(CanExecute = nameof(CanRestartFromZero))] private void RestartFromZero() => RestartFromZeroHandler?.Invoke(this); private bool CanRestartFromZero() => HasResumableDownload; private static string FormatSize(long bytes) => bytes <= 0 ? "—" : Strings.FormatSize(bytes); }