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:
2026-05-01 09:40:50 +02:00
parent 3a00b0e677
commit 4e9f757ce1
7 changed files with 730 additions and 342 deletions

View File

@@ -2,10 +2,9 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:PSLauncher.App.ViewModels"
xmlns:views="clr-namespace:PSLauncher.App.Views"
Title="PROSERVE Launcher"
Width="1280" Height="800"
MinWidth="900" MinHeight="600"
Width="960" Height="720"
MinWidth="780" MinHeight="500"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -16,7 +15,7 @@
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Top bar -->
<RowDefinition Height="*" /> <!-- Body -->
<RowDefinition Height="Auto" /> <!-- Footer (download) -->
<RowDefinition Height="Auto" /> <!-- Footer -->
</Grid.RowDefinitions>
<!-- Top bar -->
@@ -24,7 +23,8 @@
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,0,1"
Padding="20,12">
<Grid>
<TextBlock Text="PROSERVE" FontSize="18" FontWeight="Bold"
<TextBlock Text="PROSERVE Launcher"
FontSize="18" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center" />
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center">
@@ -35,133 +35,181 @@
</Grid>
</Border>
<!-- Body : sidebar + content -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Body -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="32,24">
<!-- Sidebar -->
<Border Grid.Column="0" Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,1,0">
<StackPanel>
<RadioButton Style="{StaticResource SidebarItem}" GroupName="Nav"
Content="📚 Bibliothèque" IsChecked="True" />
<RadioButton Style="{StaticResource SidebarItem}" GroupName="Nav"
Content="📰 Nouveautés" IsEnabled="False" />
<RadioButton Style="{StaticResource SidebarItem}" GroupName="Nav"
Content="⚙️ Paramètres" IsEnabled="False" />
</StackPanel>
</Border>
<!-- Library content -->
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="40,30">
<!-- Hero -->
<Border Background="#2A2A30" CornerRadius="8" Height="280"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1">
<Grid>
<TextBlock Text="PROSERVE"
FontSize="64" FontWeight="Bold"
Foreground="#3D3D45"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<Border VerticalAlignment="Bottom" HorizontalAlignment="Left"
Margin="24" Background="#000000B0"
CornerRadius="4" Padding="12,6">
<TextBlock Text="{Binding HeroTitle}"
FontSize="22" FontWeight="SemiBold"
Foreground="White" />
</Border>
</Grid>
</Border>
<!-- Action row -->
<Grid Margin="0,24,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Version selector -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Version :" Foreground="{StaticResource Brush.Text.Secondary}"
VerticalAlignment="Center" Margin="0,0,10,0" />
<ComboBox Width="160"
ItemsSource="{Binding InstalledVersions}"
DisplayMemberPath="Display"
SelectedItem="{Binding SelectedVersion, Mode=TwoWay}" />
<Border Style="{StaticResource VersionPill}" Margin="12,0,0,0">
<TextBlock Text="{Binding StatusBadge}" FontSize="12"
Foreground="{StaticResource Brush.Text.Primary}" />
</Border>
</StackPanel>
<!-- Primary action -->
<Button Grid.Column="2"
Style="{StaticResource PrimaryButton}"
Content="{Binding PrimaryActionLabel}"
Command="{Binding PrimaryActionCommand}"
IsEnabled="{Binding PrimaryActionEnabled}" />
</Grid>
<!-- Secondary actions -->
<StackPanel Orientation="Horizontal" Margin="0,16,0,0">
<Button Style="{StaticResource SecondaryButton}"
Content="🔄 Vérifier les mises à jour"
Command="{Binding CheckForUpdatesCommand}"
Margin="0,0,10,0" />
<Button Style="{StaticResource SecondaryButton}"
Content="📁 Ouvrir le dossier"
Command="{Binding OpenFolderCommand}"
Margin="0,0,10,0" />
<Button Style="{StaticResource SecondaryButton}"
Content="🔍 Rescanner local"
Command="{Binding RefreshCommand}" />
<!-- Header / actions -->
<Grid Margin="0,0,0,16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Versions"
FontSize="22" FontWeight="SemiBold"
Foreground="{StaticResource Brush.Text.Primary}" />
<TextBlock Text="Lance, installe ou supprime une version. Les versions installées et celles disponibles sur le serveur cohabitent."
FontSize="12"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,4,0,0"
TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<Button Style="{StaticResource SecondaryButton}"
Content="🔄 Vérifier les MAJ"
Command="{Binding CheckForUpdatesCommand}"
Margin="0,0,8,0" />
<Button Style="{StaticResource SecondaryButton}"
Content="📁 Dossier"
Command="{Binding OpenInstallRootCommand}" />
</StackPanel>
</Grid>
<!-- Installed versions list -->
<TextBlock Text="Versions installées" FontSize="16" FontWeight="SemiBold"
Margin="0,30,0,12"
Foreground="{StaticResource Brush.Text.Primary}" />
<ItemsControl ItemsSource="{Binding InstalledVersions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource Brush.Bg.Card}"
CornerRadius="6" Margin="0,0,0,8" Padding="16,12"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Display}"
FontSize="15" FontWeight="SemiBold" />
<TextBlock Text="{Binding Subtitle}"
FontSize="12"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,2,0,0" />
<!-- Version list -->
<ItemsControl ItemsSource="{Binding Versions}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:VersionRowViewModel}">
<Border Margin="0,0,0,8"
CornerRadius="6"
BorderThickness="1">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Card}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Border}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsRemoteOnly}" Value="True">
<Setter Property="Background" Value="#1F2A3A" />
<Setter Property="BorderBrush" Value="#2C4D7A" />
</DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="Background" Value="#2A2F26" />
<Setter Property="BorderBrush" Value="#5C8C3F" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid Margin="16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Left : version, state, meta -->
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
<TextBlock Text="{Binding Version, StringFormat='v{0}'}"
FontSize="16" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource Brush.Text.Primary}" />
<Border Background="#0F0F12" CornerRadius="10"
Padding="8,3" Margin="12,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding StateLabel}"
FontSize="11"
Foreground="{StaticResource Brush.Text.Secondary}" />
</Border>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0"
FontSize="11"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,4,0,0">
<Run Text="{Binding SizeDisplay, Mode=OneWay}" />
<Run Text=" • " />
<Run Text="{Binding PrimaryDate, Mode=OneWay}" />
</TextBlock>
<!-- Center : action button (Lancer / Installer / Annuler) -->
<StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<!-- Bouton LANCER (installée) -->
<Button Style="{StaticResource PrimaryButton}"
Content="▶ Lancer"
Padding="22,8"
FontSize="14"
Command="{Binding LaunchCommand}"
Visibility="{Binding IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<!-- Bouton INSTALLER (remote uniquement) -->
<Button Style="{StaticResource AccentButton}"
Content="⬇ Installer"
Padding="22,8"
FontSize="14"
Command="{Binding InstallCommand}"
Visibility="{Binding IsRemoteOnly, Converter={StaticResource BoolToVisibility}}" />
<!-- Pendant DL/install : barre de progression compacte -->
<StackPanel Orientation="Horizontal"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}">
<ProgressBar Width="160" Height="6"
Value="{Binding ProgressPercent}"
Maximum="100"
Foreground="{StaticResource Brush.Play}"
Background="#2A2A30"
BorderThickness="0"
VerticalAlignment="Center" />
<TextBlock Text="{Binding ProgressPercent, StringFormat={}{0:0}%}"
FontSize="11"
Margin="8,0,0,0"
VerticalAlignment="Center"
Foreground="{StaticResource Brush.Text.Secondary}" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Empty state -->
<!-- Right : ... menu -->
<Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="2"
Style="{StaticResource IconButton}"
Content="⋯"
FontSize="18" FontWeight="Bold"
Margin="8,0,0,0"
VerticalAlignment="Center"
ToolTip="Plus d'actions"
Tag="{Binding}"
Click="OnMoreMenuClick">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="📜 Voir les release notes"
Command="{Binding PlacementTarget.Tag.ShowReleaseNotesCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="📁 Ouvrir le dossier"
Command="{Binding PlacementTarget.Tag.OpenFolderCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<Separator Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="🗑 Supprimer cette version…"
Command="{Binding PlacementTarget.Tag.UninstallCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
</ContextMenu>
</Button.ContextMenu>
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Empty state -->
<Border Background="{StaticResource Brush.Bg.Card}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6"
Padding="32"
Visibility="{Binding EmptyHintVisibility}">
<TextBlock Text="{Binding EmptyHint}"
Foreground="{StaticResource Brush.Text.Secondary}"
TextWrapping="Wrap" Margin="0,12,0,0"
Visibility="{Binding EmptyHintVisibility}" />
TextWrapping="Wrap" />
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</StackPanel>
</ScrollViewer>
<!-- Footer (download/install status) -->
<!-- Footer (busy / status) -->
<Border Grid.Row="2" Background="{StaticResource Brush.Bg.Footer}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,1,0,0"
Padding="20,8"

View File

@@ -1,4 +1,6 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
namespace PSLauncher.App.Views;
@@ -8,4 +10,17 @@ public partial class MainWindow : Window
{
InitializeComponent();
}
/// <summary>
/// Le bouton "..." ouvre son ContextMenu sur clic gauche (par défaut, ContextMenu ne s'ouvre qu'au clic droit).
/// </summary>
private void OnMoreMenuClick(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.ContextMenu is { } menu)
{
menu.PlacementTarget = btn;
menu.Placement = PlacementMode.Bottom;
menu.IsOpen = true;
}
}
}

View File

@@ -0,0 +1,42 @@
<Window x:Class="PSLauncher.App.Views.ReleaseNotesViewerDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Notes de version"
Width="640" Height="540"
MinWidth="480" MinHeight="400"
WindowStartupLocation="CenterOwner"
Background="{StaticResource Brush.Bg.Window}">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{Binding Title}"
FontSize="22" FontWeight="SemiBold"
Margin="0,0,0,18"
Foreground="{StaticResource Brush.Text.Primary}" />
<Border Grid.Row="1"
Background="{StaticResource Brush.Bg.Card}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6"
Padding="16">
<FlowDocumentScrollViewer
Document="{Binding ReleaseNotesDocument}"
Foreground="{StaticResource Brush.Text.Primary}"
Background="Transparent" />
</Border>
<Button Grid.Row="2"
Style="{StaticResource SecondaryButton}"
Content="Fermer"
IsCancel="True"
IsDefault="True"
HorizontalAlignment="Right"
Margin="0,18,0,0"
Click="OnClose" />
</Grid>
</Window>

View File

@@ -0,0 +1,37 @@
using System.Windows;
using System.Windows.Documents;
using Markdig;
using Markdig.Wpf;
namespace PSLauncher.App.Views;
public partial class ReleaseNotesViewerDialog : Window
{
public ReleaseNotesViewerDialog(string version, string releaseNotesMarkdown)
{
InitializeComponent();
var pipeline = new MarkdownPipelineBuilder().UseSupportedExtensions().Build();
FlowDocument doc;
try
{
doc = Markdig.Wpf.Markdown.ToFlowDocument(releaseNotesMarkdown, pipeline);
}
catch
{
doc = new FlowDocument(new Paragraph(new Run(releaseNotesMarkdown)));
}
DataContext = new
{
Title = $"Notes de version — v{version}",
ReleaseNotesDocument = doc
};
}
private void OnClose(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}