UI: featured hero card, vivid status colors, themed Markdown render

Featured version
----------------
The highest installed version (or highest remote if none installed) now
gets a large hero card at the top of the window: 32px Proserve title,
oversized colored status pill, and a much bigger primary action button
(LANCER / INSTALLER 56px padding). All other versions move below into a
"AUTRES VERSIONS" section with a horizontal divider, displayed as the
existing compact rows.

MainViewModel exposes FeaturedVersion + OtherVersions instead of one
flat Versions collection.

Vivid status colors
-------------------
Card backgrounds were too close (dark gray vs slightly bluer dark gray).
New scheme:
- Installed: card stays dark gray, but a 4px green strip on the left and
  a vivid green "● Installée" pill make it unmistakable.
- Available remote-only: distinct dark blue card + vivid blue strip + blue
  "○ Disponible" pill.
- Busy: amber strip + amber pill, amber progress bar.

Brushes added to Theme.xaml: Brush.Status.Installed (#16A34A),
Brush.Status.Available (#3B82F6), Brush.Status.Busy (#F59E0B), each with
a matching very-dark-tint *Bg variant for card backgrounds.

Markdown theming
----------------
Release notes dialogs were unreadable (white background, near-white text).
Markdig.Wpf produces a FlowDocument with default white bg / black fg
regardless of the host control. Added MarkdownTheming.BuildThemedDocument
which renders the Markdown then walks all blocks/inlines to apply the
launcher's dark palette (Brush.Bg.Card transparent, Brush.Text.Primary
foreground, Brush.Accent links). Both UpdateAvailableDialog and
ReleaseNotesViewerDialog use this helper.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 09:50:51 +02:00
parent 4e9f757ce1
commit 6128f7d220
6 changed files with 479 additions and 202 deletions

View File

@@ -16,6 +16,14 @@
<SolidColorBrush x:Key="Brush.Play" Color="#0CA84B" />
<SolidColorBrush x:Key="Brush.PlayHover" Color="#10C257" />
<!-- Status colors -->
<SolidColorBrush x:Key="Brush.Status.Installed" Color="#16A34A" /> <!-- vert vif -->
<SolidColorBrush x:Key="Brush.Status.InstalledBg" Color="#0E2A1B" /> <!-- vert très sombre -->
<SolidColorBrush x:Key="Brush.Status.Available" Color="#3B82F6" /> <!-- bleu vif -->
<SolidColorBrush x:Key="Brush.Status.AvailableBg" Color="#13243F" /> <!-- bleu très sombre -->
<SolidColorBrush x:Key="Brush.Status.Busy" Color="#F59E0B" /> <!-- ambre -->
<SolidColorBrush x:Key="Brush.Status.BusyBg" Color="#3A2A0E" />
<Style TargetType="Window">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Window}" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />

View File

@@ -32,7 +32,16 @@ public sealed partial class MainViewModel : ObservableObject
private CancellationTokenSource? _activeDownloadCts;
private VersionRowViewModel? _activeRow;
public ObservableCollection<VersionRowViewModel> Versions { get; } = new();
public ObservableCollection<VersionRowViewModel> OtherVersions { get; } = new();
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasFeaturedVersion))]
[NotifyPropertyChangedFor(nameof(HasOtherVersions))]
[NotifyPropertyChangedFor(nameof(EmptyHintVisibility))]
private VersionRowViewModel? _featuredVersion;
public bool HasFeaturedVersion => FeaturedVersion is not null;
public bool HasOtherVersions => OtherVersions.Count > 0;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FooterText))]
@@ -57,7 +66,8 @@ public sealed partial class MainViewModel : ObservableObject
_config.InstallRoot;
public Visibility EmptyHintVisibility =>
Versions.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
FeaturedVersion is null && OtherVersions.Count == 0
? Visibility.Visible : Visibility.Collapsed;
public Visibility FooterVisibility =>
IsBusy || !string.IsNullOrEmpty(StatusMessage) ? Visibility.Visible : Visibility.Collapsed;
@@ -89,7 +99,11 @@ public sealed partial class MainViewModel : ObservableObject
RebuildList();
}
/// <summary>Recompose la liste affichée à partir du scan local + du dernier manifest distant.</summary>
/// <summary>
/// Recompose la liste affichée à partir du scan local + du dernier manifest distant.
/// La version « courante » (FeaturedVersion) est la plus haute installée ; à défaut,
/// la plus haute disponible côté serveur. Toutes les autres vont dans OtherVersions.
/// </summary>
private void RebuildList()
{
var installed = _registry.Scan().ToDictionary(v => v.Version);
@@ -100,7 +114,7 @@ public sealed partial class MainViewModel : ObservableObject
.OrderByDescending(v => SemVer.Parse(v))
.ToList();
Versions.Clear();
var rows = new List<VersionRowViewModel>();
foreach (var ver in allVersions)
{
VersionRowViewModel row;
@@ -113,11 +127,24 @@ public sealed partial class MainViewModel : ObservableObject
row = VersionRowViewModel.ForRemote(remoteByVer[ver]);
}
WireRowHandlers(row);
Versions.Add(row);
rows.Add(row);
}
// Featured : plus haute installée, sinon plus haute distante
var featured = rows.FirstOrDefault(r => r.IsInstalled) ?? rows.FirstOrDefault();
FeaturedVersion = featured;
OtherVersions.Clear();
foreach (var r in rows)
{
if (!ReferenceEquals(r, featured))
OtherVersions.Add(r);
}
OnPropertyChanged(nameof(EmptyHint));
OnPropertyChanged(nameof(EmptyHintVisibility));
OnPropertyChanged(nameof(HasFeaturedVersion));
OnPropertyChanged(nameof(HasOtherVersions));
}
private void WireRowHandlers(VersionRowViewModel row)

View File

@@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:PSLauncher.App.ViewModels"
Title="PROSERVE Launcher"
Width="960" Height="720"
Width="980" Height="780"
MinWidth="780" MinHeight="500"
WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
@@ -11,11 +11,145 @@
mc:Ignorable="d"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Window.Resources>
<!-- =================== TEMPLATE : LIGNE COMPACTE (autres versions) =================== -->
<DataTemplate x:Key="CompactRowTemplate" 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="{StaticResource Brush.Status.AvailableBg}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="Background" Value="{StaticResource Brush.Status.BusyBg}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="4" /> <!-- accent strip -->
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Strip latérale colorée -->
<Rectangle Grid.Column="0" RadiusX="2" RadiusY="2" Margin="0,8">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="{StaticResource Brush.Status.Installed}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsRemoteOnly}" Value="True">
<Setter Property="Fill" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="Fill" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<Grid Grid.Column="1" Margin="16,12">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="{Binding Version, StringFormat='v{0}'}"
FontSize="15" FontWeight="SemiBold"
VerticalAlignment="Center"
Foreground="{StaticResource Brush.Text.Primary}" />
<Border CornerRadius="10" Padding="8,3" Margin="12,0,0,0"
VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource Brush.Status.Installed}" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsRemoteOnly}" Value="True">
<Setter Property="Background" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="Background" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding StateLabel}" FontSize="11" FontWeight="SemiBold"
Foreground="White" />
</Border>
</StackPanel>
<TextBlock Grid.Row="1" 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>
</Grid>
<!-- Action button -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="12,0,0,0">
<Button Style="{StaticResource PrimaryButton}"
Content="▶ Lancer" Padding="22,8" FontSize="13"
Command="{Binding LaunchCommand}"
Visibility="{Binding IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<Button Style="{StaticResource AccentButton}"
Content="⬇ Installer" Padding="22,8" FontSize="13"
Command="{Binding InstallCommand}"
Visibility="{Binding IsRemoteOnly, Converter={StaticResource BoolToVisibility}}" />
<StackPanel Orientation="Horizontal"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}">
<ProgressBar Width="140" Height="6"
Value="{Binding ProgressPercent}" Maximum="100"
Foreground="{StaticResource Brush.Status.Busy}"
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>
</StackPanel>
<!-- ... menu -->
<Button Grid.Column="3"
Style="{StaticResource IconButton}"
Content="⋯" FontSize="18" FontWeight="Bold"
Margin="8,0,8,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>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Top bar -->
<RowDefinition Height="*" /> <!-- Body -->
<RowDefinition Height="Auto" /> <!-- Footer -->
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Top bar -->
@@ -28,172 +162,202 @@
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center" />
<StackPanel HorizontalAlignment="Right" 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}"
Margin="0,0,12,0" />
<TextBlock Text="{Binding LicenseSummary}"
Foreground="{StaticResource Brush.Text.Secondary}"
VerticalAlignment="Center" Margin="0,0,12,0" />
VerticalAlignment="Center" />
</StackPanel>
</Grid>
</Border>
<!-- Body -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="32,24">
<StackPanel Margin="32,28">
<!-- 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"
<!-- ========= FEATURED (version courante) ========= -->
<Border CornerRadius="10" BorderThickness="1"
Padding="0"
Margin="0,0,0,32"
Visibility="{Binding HasFeaturedVersion, Converter={StaticResource BoolToVisibility}}">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Card}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Installed}" />
<Style.Triggers>
<DataTrigger Binding="{Binding FeaturedVersion.IsRemoteOnly}" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger>
<DataTrigger Binding="{Binding FeaturedVersion.IsBusy}" Value="True">
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="6" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Bande latérale large -->
<Rectangle Grid.Column="0">
<Rectangle.Style>
<Style TargetType="Rectangle">
<Setter Property="Fill" Value="{StaticResource Brush.Status.Installed}" />
<Style.Triggers>
<DataTrigger Binding="{Binding FeaturedVersion.IsRemoteOnly}" Value="True">
<Setter Property="Fill" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger>
<DataTrigger Binding="{Binding FeaturedVersion.IsBusy}" Value="True">
<Setter Property="Fill" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Rectangle.Style>
</Rectangle>
<Grid Grid.Column="1" Margin="32,28">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Label "Version courante" -->
<TextBlock Grid.Row="0" Grid.Column="0"
Text="VERSION COURANTE"
FontSize="11" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,0,0,4" />
<!-- Numéro version + badge -->
<StackPanel Grid.Row="1" Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding FeaturedVersion.Version, StringFormat='Proserve v{0}'}"
FontSize="32" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Primary}" />
<Border CornerRadius="12" Padding="10,4" Margin="16,8,0,0"
VerticalAlignment="Center">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource Brush.Status.Installed}" />
<Style.Triggers>
<DataTrigger Binding="{Binding FeaturedVersion.IsRemoteOnly}" Value="True">
<Setter Property="Background" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger>
<DataTrigger Binding="{Binding FeaturedVersion.IsBusy}" Value="True">
<Setter Property="Background" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock Text="{Binding FeaturedVersion.StateLabel}" FontSize="12" FontWeight="SemiBold"
Foreground="White" />
</Border>
</StackPanel>
<!-- Meta -->
<TextBlock Grid.Row="2" Grid.Column="0"
FontSize="13"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,8,0,0">
<Run Text="Sortie le " />
<Run Text="{Binding FeaturedVersion.PrimaryDate, Mode=OneWay}" />
<Run Text=" • " />
<Run Text="{Binding FeaturedVersion.SizeDisplay, Mode=OneWay}" />
</TextBlock>
<!-- Action principale (gros bouton) -->
<Button Grid.Row="0" Grid.RowSpan="3" Grid.Column="1"
Style="{StaticResource PrimaryButton}"
Content="▶ LANCER"
FontSize="20" Padding="56,16"
VerticalAlignment="Center"
Command="{Binding FeaturedVersion.LaunchCommand}"
Visibility="{Binding FeaturedVersion.IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<Button Grid.Row="0" Grid.RowSpan="3" Grid.Column="1"
Style="{StaticResource AccentButton}"
Content="⬇ INSTALLER"
FontSize="20" Padding="48,16"
VerticalAlignment="Center"
Command="{Binding FeaturedVersion.InstallCommand}"
Visibility="{Binding FeaturedVersion.IsRemoteOnly, Converter={StaticResource BoolToVisibility}}" />
<StackPanel Grid.Row="0" Grid.RowSpan="3" Grid.Column="1"
Orientation="Vertical" VerticalAlignment="Center" Width="220"
Visibility="{Binding FeaturedVersion.IsBusy, Converter={StaticResource BoolToVisibility}}">
<TextBlock Text="{Binding FeaturedVersion.StateLabel}"
HorizontalAlignment="Center"
Foreground="{StaticResource Brush.Status.Busy}"
FontWeight="SemiBold" Margin="0,0,0,8" />
<ProgressBar Height="8"
Value="{Binding FeaturedVersion.ProgressPercent}" Maximum="100"
Foreground="{StaticResource Brush.Status.Busy}"
Background="#2A2A30" BorderThickness="0" />
<TextBlock Text="{Binding FeaturedVersion.ProgressPercent, StringFormat={}{0:0}%}"
HorizontalAlignment="Center"
FontSize="12" Margin="0,4,0,0"
Foreground="{StaticResource Brush.Text.Secondary}" />
</StackPanel>
<!-- ... menu -->
<Button Grid.Row="0" Grid.RowSpan="3" Grid.Column="2"
Style="{StaticResource IconButton}"
Content="⋯" FontSize="22" FontWeight="Bold"
Margin="12,0,0,0"
VerticalAlignment="Center"
ToolTip="Plus d'actions"
Tag="{Binding FeaturedVersion}"
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>
</Grid>
</Border>
<!-- ========= AUTRES VERSIONS ========= -->
<StackPanel Visibility="{Binding HasOtherVersions, Converter={StaticResource BoolToVisibility}}">
<Grid Margin="0,0,0,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="AUTRES VERSIONS"
FontSize="12" FontWeight="Bold"
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>
VerticalAlignment="Center" />
<Border Grid.Column="1" Background="{StaticResource Brush.Border}"
Height="1" Margin="12,0,0,0" VerticalAlignment="Center" />
</Grid>
<!-- 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>
</StackPanel>
<!-- 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>
<ItemsControl ItemsSource="{Binding OtherVersions}"
ItemTemplate="{StaticResource CompactRowTemplate}" />
</StackPanel>
<!-- Empty state -->
<Border Background="{StaticResource Brush.Bg.Card}"
@@ -223,17 +387,14 @@
<TextBlock Text="{Binding FooterText}"
Foreground="{StaticResource Brush.Text.Secondary}" />
<ProgressBar Height="4" Margin="0,4,0,0"
Value="{Binding ProgressPercent}"
Maximum="100"
Value="{Binding ProgressPercent}" Maximum="100"
Foreground="{StaticResource Brush.Accent}"
Background="#2A2A30"
BorderThickness="0" />
Background="#2A2A30" BorderThickness="0" />
</StackPanel>
<Button Grid.Column="1"
Style="{StaticResource SecondaryButton}"
Content="Annuler"
VerticalAlignment="Center"
Margin="12,0,0,0"
VerticalAlignment="Center" Margin="12,0,0,0"
Command="{Binding CancelDownloadCommand}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
</Grid>

View File

@@ -0,0 +1,111 @@
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using Markdig;
using Markdig.Wpf;
namespace PSLauncher.App.Views;
/// <summary>
/// Helper pour rendre un FlowDocument à partir de Markdown avec le thème sombre du launcher.
/// Markdig.Wpf produit un FlowDocument dont le Background est blanc et le Foreground noir
/// par défaut — on force ici les couleurs sur le document et tous ses éléments visuels.
/// </summary>
internal static class MarkdownTheming
{
public static FlowDocument BuildThemedDocument(string markdown)
{
var pipeline = new MarkdownPipelineBuilder().UseSupportedExtensions().Build();
FlowDocument doc;
try
{
doc = Markdig.Wpf.Markdown.ToFlowDocument(markdown, pipeline);
}
catch
{
doc = new FlowDocument(new Paragraph(new Run(markdown)));
}
ApplyDarkTheme(doc);
return doc;
}
public static void ApplyDarkTheme(FlowDocument doc)
{
var bg = (Brush)Application.Current.FindResource("Brush.Bg.Card");
var fg = (Brush)Application.Current.FindResource("Brush.Text.Primary");
var fgSecondary = (Brush)Application.Current.FindResource("Brush.Text.Secondary");
var border = (Brush)Application.Current.FindResource("Brush.Border");
var accent = (Brush)Application.Current.FindResource("Brush.Accent");
doc.Background = Brushes.Transparent;
doc.Foreground = fg;
doc.FontFamily = new FontFamily("Segoe UI");
doc.FontSize = 13;
doc.LineHeight = 20;
doc.PagePadding = new Thickness(0);
foreach (var block in doc.Blocks.ToList())
ApplyToBlock(block, fg, fgSecondary, border, accent, bg);
}
private static void ApplyToBlock(Block block, Brush fg, Brush fgSecondary, Brush border, Brush accent, Brush bg)
{
block.Foreground = fg;
block.Background = Brushes.Transparent;
switch (block)
{
case Paragraph p:
foreach (var inline in p.Inlines.ToList())
ApplyToInline(inline, fg, fgSecondary, accent);
break;
case Section s:
foreach (var b in s.Blocks.ToList())
ApplyToBlock(b, fg, fgSecondary, border, accent, bg);
break;
case List list:
foreach (var item in list.ListItems.ToList())
{
item.Foreground = fg;
foreach (var b in item.Blocks.ToList())
ApplyToBlock(b, fg, fgSecondary, border, accent, bg);
}
break;
case Table tbl:
foreach (var rg in tbl.RowGroups.ToList())
foreach (var row in rg.Rows.ToList())
foreach (var cell in row.Cells.ToList())
{
cell.Foreground = fg;
cell.BorderBrush = border;
foreach (var b in cell.Blocks.ToList())
ApplyToBlock(b, fg, fgSecondary, border, accent, bg);
}
break;
case BlockUIContainer:
// Contenu UI custom — laissé tel quel.
break;
}
}
private static void ApplyToInline(Inline inline, Brush fg, Brush fgSecondary, Brush accent)
{
inline.Foreground = fg;
switch (inline)
{
case Hyperlink h:
h.Foreground = accent;
foreach (var i in h.Inlines.ToList())
ApplyToInline(i, accent, fgSecondary, accent);
break;
case Span s:
foreach (var i in s.Inlines.ToList())
ApplyToInline(i, fg, fgSecondary, accent);
break;
}
}
}

View File

@@ -1,7 +1,4 @@
using System.Windows;
using System.Windows.Documents;
using Markdig;
using Markdig.Wpf;
namespace PSLauncher.App.Views;
@@ -11,21 +8,10 @@ public partial class ReleaseNotesViewerDialog : Window
{
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
ReleaseNotesDocument = MarkdownTheming.BuildThemedDocument(releaseNotesMarkdown)
};
}

View File

@@ -1,7 +1,4 @@
using System.Windows;
using System.Windows.Documents;
using Markdig;
using Markdig.Wpf;
using PSLauncher.Models;
namespace PSLauncher.App.Views;
@@ -12,24 +9,11 @@ public partial class UpdateAvailableDialog : Window
{
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 = $"Proserve v{version.Version} disponible",
Subtitle = $"Date de release : {version.ReleasedAt.ToLocalTime():dd MMMM yyyy} • {FormatSize(version.Download.SizeBytes)}",
ReleaseNotesDocument = doc
ReleaseNotesDocument = MarkdownTheming.BuildThemedDocument(releaseNotesMarkdown)
};
}