Files
PS_Launcher/src/PSLauncher.App/Views/MainWindow.xaml
j.foucher 7de0c01b97 v0.24.9 — Deploy _api/, fix WebView2 Program Files, DL robustness
Deploy auto de l'API PHP de stats (parallèle Report/Doc) :
- ApiToolDeployer : copy _api/ → C:\xampp\htdocs\proserve\ avec atomic
  rename + backups horodatés + revert + pruning. Strict clone du pattern
  DocToolDeployer.
- ApiToolConfig dans LocalConfig (default folder "proserve" en minuscule
  pour matcher l'install standard PROSERVE côté client).
- Settings → Avancés : section dédiée (HtdocsRoot, FolderName, AutoDeploy,
  MaxBackups, Re-déployer, liste backups + revert) + ApiBackupViewModel.
- DI registration dans App.xaml.cs, injection MainViewModel, appel
  post-install après le bloc Doc.
- 8 nouvelles strings i18n (5 langues).

Fix WebView2 vide en Program Files :
- Set WEBVIEW2_USER_DATA_FOLDER vers %LocalAppData%\PSLauncher\WebView2\
  dans App.OnStartup. Sans ça, WebView2 essaie d'écrire son cache à côté
  du .exe (read-only en Program Files) → init silencieusement KO →
  onglets Report/Doc vides.

Refresh auto WebView2 sur navigation d'onglet :
- Hook IsVisibleChanged sur ReportWebView et DocsWebView. À chaque
  ré-affichage, CoreWebView2.Reload() pour voir les données serveur à
  jour. Le binding Source garde la nav initiale au cold start.

Fix UI : Cancel décalant le footer pendant Check updates :
- Nouveau flag IsDownloadActive distinct de IsBusy. Visibility du bouton
  Cancel passe de IsBusy → IsDownloadActive : couvre uniquement les DL,
  plus aucune apparition pendant Check updates / Install / Uninstall /
  Self-update qui décalait tout le layout 1 s.

DL robustness (multi-PC) :
- SafeInstallAsync : error boundary sur l'install handler. Plus aucune
  exception silencieuse — log + popup d'erreur visible (résout les cas
  "le 2e PC ne démarre pas, sans message").
- DownloadManager : vérif explicite que CHAQUE segment a Completed=true
  && DownloadedBytes >= Length AVANT le SHA-256. Le check actualSize
  était inutile (fichier sparse-préalloué).
- MaxRetryAttempts Polly 6 → 10 pour absorber les outages PHP-FPM
  request_terminate_timeout sous charge multi-PC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:53:13 +02:00

775 lines
50 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"
WindowState="Maximized"
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}">
<!-- La fenêtre est correctement clampée à la work area du moniteur en mode
Maximized via WM_GETMINMAXINFO (cf. MainWindow.xaml.cs). Plus besoin
de hack via Margin sur trigger WindowState. -->
<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, Col 1 uniquement) ==============
Pills colorées indiquant l'état des dépendances système (SteamVR,
Vive Streaming, ping casque…). Le banner ne couvre QUE la colonne
content — la sidebar à gauche s'étend du sous-topbar jusqu'en bas
en RowSpan=3 (banner + body + footer) pour rester visuellement le
chapeau de tout. Visible uniquement sur la page Library. -->
<Border Grid.Row="1" Grid.Column="1"
Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="0,0,0,1"
Padding="16,8"
Visibility="{Binding ShowHealthBanner, 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.).
Bottom margin volontairement faible pour rester collé au footer
(barre de DL) quand il s'affiche. -->
<Border HorizontalAlignment="Center" VerticalAlignment="Bottom"
Margin="0,0,0,8"
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 1 + RowSpan=3 → la sidebar s'étend du bas du top bar jusqu'au
bas de la fenêtre, couvrant les 3 rows (health banner, content,
footer). Visuellement la sidebar reste le « chapeau de tout »
côté gauche, peu importe ce qui se passe à droite. -->
<Border Grid.Row="1" Grid.RowSpan="3" Grid.Column="0"
Background="#0A0A0E"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="0,0,1,0">
<DockPanel LastChildFill="True">
<!-- Bloc d'info en bas de sidebar : version du launcher + IP locale.
Utilité diagnostic terrain : le support peut lire les deux infos
sans avoir à naviguer dans Windows ou About. DockPanel.Dock=Bottom
pour rester collé au bas peu importe la hauteur de la fenêtre. -->
<Border DockPanel.Dock="Bottom"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="0,1,0,0"
Padding="16,10">
<!-- Grid 2 colonnes : la colonne label (Auto) prend la largeur du
plus long ("PS_Launcher:"), la colonne valeur démarre au même X
sur les deux lignes → version et IP visuellement alignées. -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0"
Text="PS_Launcher"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="11" Margin="0,0,8,0"
ToolTip="Version du launcher" />
<TextBlock Grid.Row="0" Grid.Column="1"
Text="{Binding AppVersion}"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="11" />
<TextBlock Grid.Row="1" Grid.Column="0"
Text="IP"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="11" Margin="0,2,8,0"
ToolTip="IPv4 locale du PC (interface avec gateway par défaut)" />
<TextBlock Grid.Row="1" Grid.Column="1"
Text="{Binding LocalIpAddress}"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="11" Margin="0,2,0,0" />
</Grid>
</Border>
<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>
</DockPanel>
</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 IsDownloadActive, Converter={StaticResource BoolToVisibility}}" />
</Grid>
</Border>
</Grid>
</Window>