Files
PS_Launcher/src/PSLauncher.App/Views/MainWindow.xaml
j.foucher 60358a6cf5 v0.21.0 — System health banner with colored indicators + tooltips
New row between top bar and content showing pill-shaped status indicators
for the system dependencies that PROSERVE needs (SteamVR, Vive Business
Streaming process, VR headset reachable on the network, etc.). Each pill
is colored : 🟢 OK, 🟠 limitation détectée (e.g. ping élevé), 🔴 KO,
gris = non configuré. Hover → tooltip with full check kind, target,
last RTT/process count and timestamp.

Architecture
- LocalConfig.HealthChecksConfig : list of HealthCheckEntry (Kind=Ping
  or Process, Target=IP/host or process name, ping thresholds in ms,
  refresh interval in s).
- ISystemHealthService + SystemHealthService : ping via
  System.Net.NetworkInformation.Ping (no admin required), process
  lookup via System.Diagnostics.Process.GetProcessesByName. Returns
  HealthResult { Severity, Detail }.
- HealthIndicatorViewModel : observable wrapper per entry with
  Severity-driven Brushes (background pill, border, icon colour) and
  composed Tooltip text.
- MainViewModel.InitHealthIndicators + StartHealthLoop : populates
  ObservableCollection<HealthIndicatorViewModel> from config and runs
  parallel checks every RefreshIntervalSeconds (default 10s).
- MainWindow.xaml : new Row 1 (between top bar and body) hosting an
  ItemsControl bound to HealthIndicators. Row collapses cleanly if the
  list is empty (HasHealthIndicators=false).

Defaults shipped (all editable in %LocalAppData%\PSLauncher\config.json)
- 🎮 SteamVR (Process : vrserver)
- 📡 Vive Business Streaming (Process : HtcConnectionUtility)
- 🥽 Casque VR (Ping : empty, user fills in the headset IP)

The Settings UI editor for managing the list is intentionally deferred
to a future iteration — config.json edit is enough to start using it.

Versions bumped to 0.21.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:44:15 +02:00

722 lines
46 KiB
XML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<Window x:Class="PSLauncher.App.Views.MainWindow"
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:loc="clr-namespace:PSLauncher.Core.Localization;assembly=PSLauncher.Core"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Title="PROSERVE Launcher"
Icon="pack://application:,,,/Resources/favicon.ico"
Background="Black"
Width="1280" Height="720"
MinWidth="980" MinHeight="600"
WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="CanResize"
AllowsTransparency="False"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<shell:WindowChrome.WindowChrome>
<shell:WindowChrome CaptionHeight="56"
GlassFrameThickness="0"
CornerRadius="0"
ResizeBorderThickness="6"
UseAeroCaptionButtons="False" />
</shell:WindowChrome.WindowChrome>
<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 : Border avec coins arrondis à gauche
pour suivre la rondeur (CornerRadius=6) de la card parent. -->
<Border Grid.Column="0" CornerRadius="5,0,0,5">
<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>
</Border>
<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="{x:Static loc:Strings.ActionLaunch}" Padding="22,8" FontSize="13"
Command="{Binding LaunchCommand}"
Visibility="{Binding IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<Button Style="{StaticResource AccentButton}"
Content="{Binding InstallButtonLabel}" Padding="22,8" FontSize="13"
Command="{Binding InstallCommand}"
Visibility="{Binding IsRemoteOnly, Converter={StaticResource BoolToVisibility}}" />
<!-- Bouton rouge « Annuler » : visible uniquement quand on a un partial.
Permet d'abandonner le partial et repartir de zéro (avec confirmation). -->
<Button Style="{StaticResource DangerButton}"
Content="{x:Static loc:Strings.ActionCancel}"
Padding="14,8" FontSize="13" Margin="6,0,0,0"
Command="{Binding RestartFromZeroCommand}"
ToolTip="{x:Static loc:Strings.MenuRestartFromZero}"
Visibility="{Binding ShowRestartFromZero, 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="{x:Static loc:Strings.MenuReleaseNotes}"
Command="{Binding PlacementTarget.Tag.ShowReleaseNotesCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{x:Static loc:Strings.MenuOpenFolder}"
Command="{Binding PlacementTarget.Tag.OpenFolderCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="{x:Static loc:Strings.MenuRestartFromZero}"
Command="{Binding PlacementTarget.Tag.RestartFromZeroCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.HasResumableDownload, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<Separator Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="{x:Static loc:Strings.MenuUninstall}"
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>
<!--
Layout principal — grille 2 colonnes × 3 rangées :
┌─────────────────────────────────────────────────────────────┐
│ Top bar (ColSpan 2) │ Row 0
├──────────┬──────────────────────────────────────────────────┤
│ │ │
│ Sidebar │ Content area │ Row 1
│ (Library │ (Library / Report / Doc) │
│ Report │ │
│ Doc) ├──────────────────────────────────────────────────┤
│ RowSpan2 │ Footer (DL/install) │ Row 2
│ │ visible only on Library │
└──────────┴──────────────────────────────────────────────────┘
200px *
La sidebar a RowSpan=2 → s'étend du sous-topbar jusqu'au bas
de fenêtre. Le footer est confiné à la colonne content.
-->
<Grid Background="{StaticResource Brush.Bg.Window}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Top bar -->
<RowDefinition Height="Auto" /> <!-- Health banner -->
<RowDefinition Height="*" /> <!-- Body (sidebar + content) -->
<RowDefinition Height="Auto" /> <!-- Footer -->
</Grid.RowDefinitions>
<!-- Background image : limité à la zone content (Row 2, Col 1) pour ne pas
passer derrière la sidebar, le banner ou le footer. -->
<Rectangle Grid.Row="2" Grid.Column="1" IsHitTestVisible="False">
<Rectangle.Fill>
<ImageBrush ImageSource="pack://application:,,,/Resources/Background.png"
Stretch="UniformToFill"
AlignmentX="Right"
AlignmentY="Bottom"
Opacity="0.25" />
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Row="2" Grid.Column="1"
Fill="{StaticResource Brush.Bg.BlueTint}"
IsHitTestVisible="False" />
<!-- Top bar : 3 colonnes (brand / license center / chrome).
Grid.ColumnSpan=2 → s'étend sur sidebar + content. -->
<Border Grid.Row="0" Grid.ColumnSpan="2" Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,0,1"
Padding="20,12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- brand -->
<ColumnDefinition Width="*" /> <!-- license (center) -->
<ColumnDefinition Width="*" /> <!-- chrome (right) -->
</Grid.ColumnDefinitions>
<!-- Col 1 : brand -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<Image Source="pack://application:,,,/Resources/logo-asterion-white.png"
Height="36"
VerticalAlignment="Center"
Margin="0,0,14,0"
SnapsToDevicePixels="True"
RenderOptions.BitmapScalingMode="HighQuality" />
<TextBlock Text="PROSERVE"
FontFamily="{StaticResource Font.Brand}"
FontSize="22"
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center" />
<TextBlock Text="Launcher"
FontSize="20"
FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center"
Margin="14,0,0,0" />
</StackPanel>
<!-- Col 2 : badge license cliquable, centré -->
<Border Grid.Column="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Cursor="Hand"
CornerRadius="16" Padding="14,6"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
ToolTip="{x:Static loc:Strings.LicenseTooltip}">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#2A1414" />
<Setter Property="BorderBrush" Value="#EF4444" />
<Setter Property="BorderThickness" Value="1" />
<Style.Triggers>
<DataTrigger Binding="{Binding LicenseSeverity}" Value="Ok">
<Setter Property="Background" Value="{StaticResource Brush.Status.InstalledBg}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Installed}" />
</DataTrigger>
<DataTrigger Binding="{Binding LicenseSeverity}" Value="Warning">
<Setter Property="Background" Value="{StaticResource Brush.Status.BusyBg}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
<Border.InputBindings>
<MouseBinding Gesture="LeftClick" Command="{Binding OpenLicenseCommand}" />
</Border.InputBindings>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding LicenseIcon}"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center"
Margin="0,0,8,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#EF4444" />
<Style.Triggers>
<DataTrigger Binding="{Binding LicenseSeverity}" Value="Ok">
<Setter Property="Foreground" Value="{StaticResource Brush.Status.Installed}" />
</DataTrigger>
<DataTrigger Binding="{Binding LicenseSeverity}" Value="Warning">
<Setter Property="Foreground" Value="{StaticResource Brush.Status.Busy}" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<TextBlock Text="{Binding LicenseSummary}"
FontSize="13" FontWeight="SemiBold"
VerticalAlignment="Center">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="#FCA5A5" />
<Style.Triggers>
<DataTrigger Binding="{Binding LicenseSeverity}" Value="Ok">
<Setter Property="Foreground" Value="#86EFAC" />
</DataTrigger>
<DataTrigger Binding="{Binding LicenseSeverity}" Value="Warning">
<Setter Property="Foreground" Value="#FCD34D" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
</Border>
<!-- Col 3 : Paramètres + window chrome -->
<StackPanel Grid.Column="2" Orientation="Horizontal"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Style="{StaticResource SecondaryButton}"
Content="{x:Static loc:Strings.TopBarSettings}"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
Command="{Binding OpenSettingsCommand}"
Margin="0,0,16,0" />
<Button Style="{StaticResource WindowControlButton}"
Content="—" ToolTip="{x:Static loc:Strings.Minimize}"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
Click="OnMinimizeClick" />
<Button Style="{StaticResource WindowControlButton}"
Content="☐" ToolTip="{x:Static loc:Strings.Maximize}"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
Click="OnMaxRestoreClick" />
<Button Style="{StaticResource WindowCloseButton}"
Content="✕" ToolTip="{x:Static loc:Strings.CloseTooltip}"
shell:WindowChrome.IsHitTestVisibleInChrome="True"
Click="OnCloseClick" />
</StackPanel>
</Grid>
</Border>
<!-- ============== Health banner (Row 1, ColSpan 2) ==============
Pills colorées indiquant l'état des dépendances système (SteamVR,
Vive Streaming, ping casque…). Cachée si la config n'a aucun check.
Tooltip détaillé au survol de chaque pill. -->
<Border Grid.Row="1" Grid.ColumnSpan="2"
Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="0,0,0,1"
Padding="16,8"
Visibility="{Binding HasHealthIndicators, Converter={StaticResource BoolToVisibility}}">
<ItemsControl ItemsSource="{Binding HealthIndicators}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{Binding StatusBrush}"
BorderBrush="{Binding BorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="10,4"
Margin="0,0,8,0"
ToolTip="{Binding Tooltip}"
ToolTipService.InitialShowDelay="200"
ToolTipService.ShowDuration="20000">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Icon}"
FontSize="14"
Foreground="{Binding IconForeground}"
VerticalAlignment="Center" />
<TextBlock Text="{Binding Name}"
Margin="6,0,0,0"
FontSize="12"
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<!-- ============== Content area (Row 2, Col 1, swap par CurrentPage) ==============
Superpose plusieurs blocs (Library / Report / Documentation) gérés par
leur Visibility liée à CurrentPage. -->
<Grid Grid.Row="2" Grid.Column="1">
<!-- ====================== LIBRARY PAGE ====================== -->
<Grid Visibility="{Binding IsLibrary, Converter={StaticResource BoolToVisibility}}">
<ScrollViewer 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 : Border avec coins arrondis seulement à
gauche pour suivre la rondeur de la card parent (CornerRadius=10).
Rectangle ne se clippe pas sur le parent ⇒ on utilisait un visuel
qui dépassait les coins. -->
<Border Grid.Column="0" CornerRadius="9,0,0,9">
<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>
</Border>
<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="{x:Static loc:Strings.FeaturedCurrent}"
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="{x:Static loc:Strings.ReleasedOn}" />
<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="{x:Static loc:Strings.ActionLaunchBig}"
FontSize="20" Padding="56,16"
VerticalAlignment="Center"
Command="{Binding FeaturedVersion.LaunchCommand}"
Visibility="{Binding FeaturedVersion.IsInstalled, Converter={StaticResource BoolToVisibility}}" />
<StackPanel Grid.Row="0" Grid.RowSpan="3" Grid.Column="1"
Orientation="Vertical" VerticalAlignment="Center"
Visibility="{Binding FeaturedVersion.IsRemoteOnly, Converter={StaticResource BoolToVisibility}}">
<Button Style="{StaticResource AccentButton}"
Content="{Binding FeaturedVersion.InstallButtonLabel}"
FontSize="20" Padding="48,16"
Command="{Binding FeaturedVersion.InstallCommand}" />
<!-- Bouton rouge « Annuler » : visible uniquement quand un partial existe.
Permet d'abandonner et repartir de zéro (confirmation requise). -->
<Button Style="{StaticResource DangerButton}"
Content="{x:Static loc:Strings.ActionCancel}"
FontSize="13" Padding="20,8" Margin="0,8,0,0"
HorizontalAlignment="Center"
Command="{Binding FeaturedVersion.RestartFromZeroCommand}"
ToolTip="{x:Static loc:Strings.MenuRestartFromZero}"
Visibility="{Binding FeaturedVersion.ShowRestartFromZero, Converter={StaticResource BoolToVisibility}}" />
</StackPanel>
<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="{x:Static loc:Strings.MenuReleaseNotes}"
Command="{Binding PlacementTarget.Tag.ShowReleaseNotesCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
<MenuItem Header="{x:Static loc:Strings.MenuOpenFolder}"
Command="{Binding PlacementTarget.Tag.OpenFolderCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="{x:Static loc:Strings.MenuRestartFromZero}"
Command="{Binding PlacementTarget.Tag.RestartFromZeroCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"
Visibility="{Binding PlacementTarget.Tag.HasResumableDownload, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<Separator Visibility="{Binding PlacementTarget.Tag.IsInstalled, RelativeSource={RelativeSource AncestorType=ContextMenu}, Converter={StaticResource BoolToVisibility}}" />
<MenuItem Header="{x:Static loc:Strings.MenuUninstall}"
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="{x:Static loc:Strings.SectionOther}"
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 -->
<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" />
</Border>
</StackPanel>
</ScrollViewer>
<!-- Bouton flottant "Vérifier les MAJ" : ancré bas-gauche du body, au-dessus
du background bitmap, à l'extérieur du footer. -->
<Button HorizontalAlignment="Left" VerticalAlignment="Bottom"
Margin="32,0,0,24"
Style="{StaticResource SecondaryButton}"
Content="{x:Static loc:Strings.TopBarCheckUpdates}"
Command="{Binding CheckForUpdatesCommand}" />
<!-- Copyright flottant en bas centré, sur une pill sombre pour rester
lisible quel que soit l'arrière-plan (image, voile noir, etc.). -->
<Border HorizontalAlignment="Center" VerticalAlignment="Bottom"
Margin="0,0,0,28"
Background="#A0000000"
CornerRadius="10"
Padding="10,4">
<TextBlock Text="{x:Static loc:Strings.Copyright}"
FontSize="11"
Foreground="White" />
</Border>
</Grid>
<!-- ====================== END LIBRARY PAGE ====================== -->
<!-- ====================== REPORT PAGE (WebView2) ====================== -->
<Grid Background="{StaticResource Brush.Bg.Window}"
Visibility="{Binding IsReport, Converter={StaticResource BoolToVisibility}}">
<wv2:WebView2 x:Name="ReportWebView"
Source="{Binding ReportUri, Mode=OneWay}" />
</Grid>
<!-- ====================== DOCUMENTATION PAGE ====================== -->
<Grid Background="{StaticResource Brush.Bg.Window}"
Visibility="{Binding IsDocumentation, Converter={StaticResource BoolToVisibility}}">
<!-- WebView2 si une URL est configurée, sinon un placeholder explicatif -->
<wv2:WebView2 x:Name="DocsWebView"
Source="{Binding DocumentationUri, Mode=OneWay}"
Visibility="{Binding HasDocumentationUrl, Converter={StaticResource BoolToVisibility}}" />
<TextBlock Text="{x:Static loc:Strings.DocsPlaceholder}"
HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="14" TextAlignment="Center" TextWrapping="Wrap"
MaxWidth="500"
Visibility="{Binding HasDocumentationUrl, Converter={StaticResource InverseBoolToVisibility}}" />
</Grid>
</Grid>
<!-- ============== END Content area ============== -->
<!-- ============== Left sidebar : nav buttons ==============
Row 2 + RowSpan=2 → la sidebar s'étend depuis le bas du health
banner jusqu'en bas de la fenêtre, couvrant content + footer.
(Row 0 = top bar, Row 1 = health banner full-width.) -->
<Border Grid.Row="2" Grid.RowSpan="2" Grid.Column="0"
Background="#0A0A0E"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="0,0,1,0">
<StackPanel Orientation="Vertical" Margin="0,16,0,0">
<!-- Style des boutons définis dans Theme.xaml (NavButton) -->
<Button Style="{StaticResource NavButton}"
Command="{Binding NavigateLibraryCommand}"
Tag="{Binding IsLibrary}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="📚" FontSize="16" Margin="0,0,12,0" VerticalAlignment="Center" />
<TextBlock Text="{x:Static loc:Strings.NavLibrary}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Style="{StaticResource NavButton}"
Command="{Binding NavigateReportCommand}"
Tag="{Binding IsReport}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="📊" FontSize="16" Margin="0,0,12,0" VerticalAlignment="Center" />
<TextBlock Text="{x:Static loc:Strings.NavReport}" VerticalAlignment="Center" />
</StackPanel>
</Button>
<Button Style="{StaticResource NavButton}"
Command="{Binding NavigateDocumentationCommand}"
Tag="{Binding IsDocumentation}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="📖" FontSize="16" Margin="0,0,12,0" VerticalAlignment="Center" />
<TextBlock Text="{x:Static loc:Strings.NavDocumentation}" VerticalAlignment="Center" />
</StackPanel>
</Button>
</StackPanel>
</Border>
<!-- Footer : Row 3, Col 1 → cantonné à la colonne content, sous la zone
Library/Report/Doc. La sidebar (RowSpan=2 sur rows 2-3) le contourne
par sa colonne. -->
<Border Grid.Row="3" Grid.Column="1"
Background="{StaticResource Brush.Bg.Footer}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,1,0,0"
Padding="20,10"
Visibility="{Binding FooterVisibility}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding FooterText}"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="12" />
<ProgressBar Height="4" Margin="0,4,0,0"
Value="{Binding ProgressPercent}" Maximum="100"
Foreground="{StaticResource Brush.Accent}"
Background="#2A2A30" BorderThickness="0" />
</StackPanel>
<Button Grid.Column="1"
Style="{StaticResource SecondaryButton}"
Content="{x:Static loc:Strings.ActionCancel}"
VerticalAlignment="Center" Margin="12,0,0,0"
Command="{Binding CancelDownloadCommand}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
</Grid>
</Border>
</Grid>
</Window>