UI rework: per-row actions, drop sidebar
Replace the sidebar + hero + single big-button layout with a flat list of per-version cards. Each card carries its own action button: - Installed: ▶ Lancer (green, primary) - Available on server only: ⬇ Installer (blue, accent) + lighter blue card - Busy: inline mini progress bar with %, card tinted green Each card also exposes a "..." menu (left-click opens it) with: - Voir les release notes (works for installed and remote-only versions) - Ouvrir le dossier (installed only) - Supprimer cette version (installed only, with confirmation dialog) VersionRowViewModel owns its state (InstalledIdle / AvailableIdle / Downloading / Installing / Uninstalling) and its commands; MainViewModel wires per-row handlers after instantiation so the row VM stays UI-only and the services live one layer up. ReleaseNotesViewerDialog: separate dialog reused by the menu — same Markdown rendering as UpdateAvailableDialog but no download CTA. Theme: AccentButton + IconButton + dark ContextMenu/MenuItem styles. Behavior changes: - The single global SelectedVersion + AvailableUpdate are gone; each row is independently actionable. - The list now merges installed + remote: a version present on the server but not locally appears as a "remote-only" row, and vice-versa. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
139
src/PSLauncher.App/ViewModels/VersionRowViewModel.cs
Normal file
139
src/PSLauncher.App/ViewModels/VersionRowViewModel.cs
Normal file
@@ -0,0 +1,139 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
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,
|
||||
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))]
|
||||
[NotifyCanExecuteChangedFor(nameof(LaunchCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(InstallCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(UninstallCommand))]
|
||||
private VersionRowState _state;
|
||||
|
||||
[ObservableProperty] private double _progressPercent;
|
||||
[ObservableProperty] private string? _progressDetail;
|
||||
|
||||
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.Installing or VersionRowState.Uninstalling;
|
||||
|
||||
public string StateLabel => State switch
|
||||
{
|
||||
VersionRowState.InstalledIdle => "● Installée",
|
||||
VersionRowState.AvailableIdle => "○ Disponible",
|
||||
VersionRowState.Downloading => "⬇ Téléchargement…",
|
||||
VersionRowState.Installing => "📦 Installation…",
|
||||
VersionRowState.Uninstalling => "🗑 Suppression…",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
public string PrimaryDate => ReleasedAt > DateTime.MinValue
|
||||
? ReleasedAt.ToLocalTime().ToString("dd/MM/yyyy")
|
||||
: "—";
|
||||
|
||||
public string SizeDisplay => FormatSize(SizeBytes);
|
||||
|
||||
/// <summary>Constructeur ligne « installée localement » (peut aussi être présente en remote).</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Constructeur ligne « disponible côté serveur uniquement ».</summary>
|
||||
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<VersionRowViewModel>? LaunchHandler { get; set; }
|
||||
public Action<VersionRowViewModel>? InstallHandler { get; set; }
|
||||
public Action<VersionRowViewModel>? UninstallHandler { get; set; }
|
||||
public Action<VersionRowViewModel>? ShowReleaseNotesHandler { get; set; }
|
||||
public Action<VersionRowViewModel>? OpenFolderHandler { 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;
|
||||
|
||||
[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);
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
if (bytes <= 0) return "—";
|
||||
string[] units = { "o", "Ko", "Mo", "Go", "To" };
|
||||
double v = bytes;
|
||||
int u = 0;
|
||||
while (v >= 1024 && u < units.Length - 1) { v /= 1024; u++; }
|
||||
return $"{v:0.#} {units[u]}";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user