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.Play" Color="#0CA84B" />
<SolidColorBrush x:Key="Brush.PlayHover" Color="#10C257" /> <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"> <Style TargetType="Window">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Window}" /> <Setter Property="Background" Value="{StaticResource Brush.Bg.Window}" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" /> <Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />

View File

@@ -32,7 +32,16 @@ public sealed partial class MainViewModel : ObservableObject
private CancellationTokenSource? _activeDownloadCts; private CancellationTokenSource? _activeDownloadCts;
private VersionRowViewModel? _activeRow; 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] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(FooterText))] [NotifyPropertyChangedFor(nameof(FooterText))]
@@ -57,7 +66,8 @@ public sealed partial class MainViewModel : ObservableObject
_config.InstallRoot; _config.InstallRoot;
public Visibility EmptyHintVisibility => public Visibility EmptyHintVisibility =>
Versions.Count == 0 ? Visibility.Visible : Visibility.Collapsed; FeaturedVersion is null && OtherVersions.Count == 0
? Visibility.Visible : Visibility.Collapsed;
public Visibility FooterVisibility => public Visibility FooterVisibility =>
IsBusy || !string.IsNullOrEmpty(StatusMessage) ? Visibility.Visible : Visibility.Collapsed; IsBusy || !string.IsNullOrEmpty(StatusMessage) ? Visibility.Visible : Visibility.Collapsed;
@@ -89,7 +99,11 @@ public sealed partial class MainViewModel : ObservableObject
RebuildList(); 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() private void RebuildList()
{ {
var installed = _registry.Scan().ToDictionary(v => v.Version); var installed = _registry.Scan().ToDictionary(v => v.Version);
@@ -100,7 +114,7 @@ public sealed partial class MainViewModel : ObservableObject
.OrderByDescending(v => SemVer.Parse(v)) .OrderByDescending(v => SemVer.Parse(v))
.ToList(); .ToList();
Versions.Clear(); var rows = new List<VersionRowViewModel>();
foreach (var ver in allVersions) foreach (var ver in allVersions)
{ {
VersionRowViewModel row; VersionRowViewModel row;
@@ -113,11 +127,24 @@ public sealed partial class MainViewModel : ObservableObject
row = VersionRowViewModel.ForRemote(remoteByVer[ver]); row = VersionRowViewModel.ForRemote(remoteByVer[ver]);
} }
WireRowHandlers(row); 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(EmptyHint));
OnPropertyChanged(nameof(EmptyHintVisibility)); OnPropertyChanged(nameof(EmptyHintVisibility));
OnPropertyChanged(nameof(HasFeaturedVersion));
OnPropertyChanged(nameof(HasOtherVersions));
} }
private void WireRowHandlers(VersionRowViewModel row) private void WireRowHandlers(VersionRowViewModel row)

View File

@@ -3,7 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:PSLauncher.App.ViewModels" xmlns:vm="clr-namespace:PSLauncher.App.ViewModels"
Title="PROSERVE Launcher" Title="PROSERVE Launcher"
Width="960" Height="720" Width="980" Height="780"
MinWidth="780" MinHeight="500" MinWidth="780" MinHeight="500"
WindowStartupLocation="CenterScreen" WindowStartupLocation="CenterScreen"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}" d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
@@ -11,167 +11,118 @@
mc:Ignorable="d" mc:Ignorable="d"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"> xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<Grid> <Window.Resources>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Top bar -->
<RowDefinition Height="*" /> <!-- Body -->
<RowDefinition Height="Auto" /> <!-- Footer -->
</Grid.RowDefinitions>
<!-- Top bar --> <!-- =================== TEMPLATE : LIGNE COMPACTE (autres versions) =================== -->
<Border Grid.Row="0" Background="{StaticResource Brush.Bg.Sidebar}" <DataTemplate x:Key="CompactRowTemplate" DataType="{x:Type vm:VersionRowViewModel}">
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,0,1" <Border Margin="0,0,0,8" CornerRadius="6" BorderThickness="1">
Padding="20,12">
<Grid>
<TextBlock Text="PROSERVE Launcher"
FontSize="18" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center" />
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding LicenseSummary}"
Foreground="{StaticResource Brush.Text.Secondary}"
VerticalAlignment="Center" Margin="0,0,12,0" />
</StackPanel>
</Grid>
</Border>
<!-- Body -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="32,24">
<!-- 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>
<!-- 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> <Border.Style>
<Style TargetType="Border"> <Style TargetType="Border">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Card}" /> <Setter Property="Background" Value="{StaticResource Brush.Bg.Card}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Border}" /> <Setter Property="BorderBrush" Value="{StaticResource Brush.Border}" />
<Style.Triggers> <Style.Triggers>
<DataTrigger Binding="{Binding IsRemoteOnly}" Value="True"> <DataTrigger Binding="{Binding IsRemoteOnly}" Value="True">
<Setter Property="Background" Value="#1F2A3A" /> <Setter Property="Background" Value="{StaticResource Brush.Status.AvailableBg}" />
<Setter Property="BorderBrush" Value="#2C4D7A" /> <Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Available}" />
</DataTrigger> </DataTrigger>
<DataTrigger Binding="{Binding IsBusy}" Value="True"> <DataTrigger Binding="{Binding IsBusy}" Value="True">
<Setter Property="Background" Value="#2A2F26" /> <Setter Property="Background" Value="{StaticResource Brush.Status.BusyBg}" />
<Setter Property="BorderBrush" Value="#5C8C3F" /> <Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger> </DataTrigger>
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</Border.Style> </Border.Style>
<Grid>
<Grid Margin="16,12">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="4" /> <!-- accent strip -->
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </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> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal">
<!-- Left : version, state, meta -->
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
<TextBlock Text="{Binding Version, StringFormat='v{0}'}" <TextBlock Text="{Binding Version, StringFormat='v{0}'}"
FontSize="16" FontWeight="SemiBold" FontSize="15" FontWeight="SemiBold"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="{StaticResource Brush.Text.Primary}" /> Foreground="{StaticResource Brush.Text.Primary}" />
<Border Background="#0F0F12" CornerRadius="10" <Border CornerRadius="10" Padding="8,3" Margin="12,0,0,0"
Padding="8,3" Margin="12,0,0,0"
VerticalAlignment="Center"> VerticalAlignment="Center">
<TextBlock Text="{Binding StateLabel}" <Border.Style>
FontSize="11" <Style TargetType="Border">
Foreground="{StaticResource Brush.Text.Secondary}" /> <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> </Border>
</StackPanel> </StackPanel>
<TextBlock Grid.Row="1" FontSize="11"
<TextBlock Grid.Row="1" Grid.Column="0"
FontSize="11"
Foreground="{StaticResource Brush.Text.Secondary}" Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,4,0,0"> Margin="0,4,0,0">
<Run Text="{Binding SizeDisplay, Mode=OneWay}" /> <Run Text="{Binding SizeDisplay, Mode=OneWay}" />
<Run Text=" • " /> <Run Text=" • " />
<Run Text="{Binding PrimaryDate, Mode=OneWay}" /> <Run Text="{Binding PrimaryDate, Mode=OneWay}" />
</TextBlock> </TextBlock>
</Grid>
<!-- Center : action button (Lancer / Installer / Annuler) --> <!-- Action button -->
<StackPanel Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" <StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="12,0,0,0">
Orientation="Horizontal"
VerticalAlignment="Center"
Margin="12,0,0,0">
<!-- Bouton LANCER (installée) -->
<Button Style="{StaticResource PrimaryButton}" <Button Style="{StaticResource PrimaryButton}"
Content="▶ Lancer" Content="▶ Lancer" Padding="22,8" FontSize="13"
Padding="22,8"
FontSize="14"
Command="{Binding LaunchCommand}" Command="{Binding LaunchCommand}"
Visibility="{Binding IsInstalled, Converter={StaticResource BoolToVisibility}}" /> Visibility="{Binding IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<!-- Bouton INSTALLER (remote uniquement) -->
<Button Style="{StaticResource AccentButton}" <Button Style="{StaticResource AccentButton}"
Content="⬇ Installer" Content="⬇ Installer" Padding="22,8" FontSize="13"
Padding="22,8"
FontSize="14"
Command="{Binding InstallCommand}" Command="{Binding InstallCommand}"
Visibility="{Binding IsRemoteOnly, Converter={StaticResource BoolToVisibility}}" /> Visibility="{Binding IsRemoteOnly, Converter={StaticResource BoolToVisibility}}" />
<!-- Pendant DL/install : barre de progression compacte -->
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}"> Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}">
<ProgressBar Width="160" Height="6" <ProgressBar Width="140" Height="6"
Value="{Binding ProgressPercent}" Value="{Binding ProgressPercent}" Maximum="100"
Maximum="100" Foreground="{StaticResource Brush.Status.Busy}"
Foreground="{StaticResource Brush.Play}" Background="#2A2A30" BorderThickness="0"
Background="#2A2A30"
BorderThickness="0"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock Text="{Binding ProgressPercent, StringFormat={}{0:0}%}" <TextBlock Text="{Binding ProgressPercent, StringFormat={}{0:0}%}"
FontSize="11" FontSize="11" Margin="8,0,0,0" VerticalAlignment="Center"
Margin="8,0,0,0"
VerticalAlignment="Center"
Foreground="{StaticResource Brush.Text.Secondary}" /> Foreground="{StaticResource Brush.Text.Secondary}" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<!-- Right : ... menu --> <!-- ... menu -->
<Button Grid.Row="0" Grid.RowSpan="2" Grid.Column="2" <Button Grid.Column="3"
Style="{StaticResource IconButton}" Style="{StaticResource IconButton}"
Content="⋯" Content="⋯" FontSize="18" FontWeight="Bold"
FontSize="18" FontWeight="Bold" Margin="8,0,8,0" VerticalAlignment="Center"
Margin="8,0,0,0"
VerticalAlignment="Center"
ToolTip="Plus d'actions" ToolTip="Plus d'actions"
Tag="{Binding}" Tag="{Binding}"
Click="OnMoreMenuClick"> Click="OnMoreMenuClick">
@@ -192,8 +143,221 @@
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </Window.Resources>
</ItemsControl>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Top bar -->
<Border Grid.Row="0" Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,0,1"
Padding="20,12">
<Grid>
<TextBlock Text="PROSERVE Launcher"
FontSize="18" FontWeight="Bold"
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" />
</StackPanel>
</Grid>
</Border>
<!-- Body -->
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="32,28">
<!-- ========= 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}"
VerticalAlignment="Center" />
<Border Grid.Column="1" Background="{StaticResource Brush.Border}"
Height="1" Margin="12,0,0,0" VerticalAlignment="Center" />
</Grid>
<ItemsControl ItemsSource="{Binding OtherVersions}"
ItemTemplate="{StaticResource CompactRowTemplate}" />
</StackPanel>
<!-- Empty state --> <!-- Empty state -->
<Border Background="{StaticResource Brush.Bg.Card}" <Border Background="{StaticResource Brush.Bg.Card}"
@@ -223,17 +387,14 @@
<TextBlock Text="{Binding FooterText}" <TextBlock Text="{Binding FooterText}"
Foreground="{StaticResource Brush.Text.Secondary}" /> Foreground="{StaticResource Brush.Text.Secondary}" />
<ProgressBar Height="4" Margin="0,4,0,0" <ProgressBar Height="4" Margin="0,4,0,0"
Value="{Binding ProgressPercent}" Value="{Binding ProgressPercent}" Maximum="100"
Maximum="100"
Foreground="{StaticResource Brush.Accent}" Foreground="{StaticResource Brush.Accent}"
Background="#2A2A30" Background="#2A2A30" BorderThickness="0" />
BorderThickness="0" />
</StackPanel> </StackPanel>
<Button Grid.Column="1" <Button Grid.Column="1"
Style="{StaticResource SecondaryButton}" Style="{StaticResource SecondaryButton}"
Content="Annuler" Content="Annuler"
VerticalAlignment="Center" VerticalAlignment="Center" Margin="12,0,0,0"
Margin="12,0,0,0"
Command="{Binding CancelDownloadCommand}" Command="{Binding CancelDownloadCommand}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" /> Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
</Grid> </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;
using System.Windows.Documents;
using Markdig;
using Markdig.Wpf;
namespace PSLauncher.App.Views; namespace PSLauncher.App.Views;
@@ -11,21 +8,10 @@ public partial class ReleaseNotesViewerDialog : Window
{ {
InitializeComponent(); 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 DataContext = new
{ {
Title = $"Notes de version — v{version}", Title = $"Notes de version — v{version}",
ReleaseNotesDocument = doc ReleaseNotesDocument = MarkdownTheming.BuildThemedDocument(releaseNotesMarkdown)
}; };
} }

View File

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