Files
PS_Launcher/src/PSLauncher.App/Resources/Theme.xaml
j.foucher 9cea07d6be Downloads: parallel multi-segment, sparse pre-alloc, faster SHA-256, URL auto-refresh
DownloadManager
- Parallel multi-segment download (default 8 connections, configurable up to 16)
  with per-segment Range requests. Bypasses OVH/Apache per-connection bandwidth
  throttling — typical speed-up ×4 to ×8 vs single connection.
- Reporter task on a dedicated Task with Interlocked aggregate counter (no lock
  contention with workers). Reports speed/ETA every 250ms even mid-download,
  fixes the "speed only shows at install transition" bug.
- Sparse file pre-allocation via FSCTL_SET_SPARSE before SetLength: no zero-fill,
  no SeManageVolumePrivilege, instant on any disk type. Removes 5-30s of
  preparation lag on HDD.
- HEAD probe skipped (trust manifest size, signed Ed25519). Falls back to
  single-segment if first segment returns 200 instead of 206.
- Resume URL comparison fixed: ignores HMAC querystring (?exp=&sig=) which
  changes per request, compares only host+path. Previously every resume started
  fresh because the old URL never matched the freshly signed one.
- Auto-refresh signed URL on 403/410 mid-DL: SemaphoreSlim with 5s debounce so
  8 simultaneous segment expirations trigger a single /api/download-url/ call.
  Slow-connection users (1 Mbps, 30+ hours for 14 GB) keep downloading
  transparently across multiple TTL cycles.
- Per-version hashAlgorithm:none in manifest skips client SHA-256 verification
  (still relying on Ed25519 manifest signature + HMAC URL).
- DangerButton style (red) added to Theme.xaml for the new cancel-resume action.

IntegrityService
- 16 MiB buffer (was 1 MiB), FileOptions.SequentialScan + Asynchronous,
  IncrementalHash (uses SHA-NI hardware extensions on .NET 8), double-buffering
  to overlap CPU and I/O. Typical 14 GB hash verification: 60-180s → 15-40s.

HttpClient
- MaxConnectionsPerServer=16, EnableMultipleHttp2Connections, HTTP/2 preferred,
  AutomaticDecompression=None (ZIPs are already compressed), 5min pooled
  connection lifetime.

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

443 lines
23 KiB
XML

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:res="clr-namespace:PSLauncher.App.Resources">
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
<!-- Custom font Pirulen pour les titres de marque -->
<FontFamily x:Key="Font.Brand">pack://application:,,,/Resources/#Pirulen</FontFamily>
<!-- Inverse bool : utilisé pour griser un bouton pendant qu'une opération est en cours -->
<res:InverseBoolConverter x:Key="InverseBoolToBool" />
<!-- Fond fenêtre : noir pur pour ne pas délaver le bitmap overlay -->
<SolidColorBrush x:Key="Brush.Bg.Window" Color="#000000" />
<!-- Cards & sidebar : très légère teinte bleutée pour le contraste -->
<SolidColorBrush x:Key="Brush.Bg.Sidebar" Color="#0E1218" />
<SolidColorBrush x:Key="Brush.Bg.Card" Color="#161B23" />
<SolidColorBrush x:Key="Brush.Bg.Footer" Color="#050709" />
<SolidColorBrush x:Key="Brush.Border" Color="#2A2F3A" />
<SolidColorBrush x:Key="Brush.Text.Primary" Color="#F2F2F2" />
<SolidColorBrush x:Key="Brush.Text.Secondary" Color="#A0A0A8" />
<SolidColorBrush x:Key="Brush.Accent" Color="#0078D4" />
<SolidColorBrush x:Key="Brush.AccentHover" Color="#1A8CE0" />
<SolidColorBrush x:Key="Brush.Play" Color="#0CA84B" />
<SolidColorBrush x:Key="Brush.PlayHover" Color="#10C257" />
<!-- Status colors -->
<SolidColorBrush x:Key="Brush.Status.Installed" Color="#16A34A" /> <!-- vert vif -->
<SolidColorBrush x:Key="Brush.Status.InstalledBg" Color="#0E2A1B" /> <!-- vert très sombre -->
<SolidColorBrush x:Key="Brush.Status.Available" Color="#3B82F6" /> <!-- bleu vif -->
<SolidColorBrush x:Key="Brush.Status.AvailableBg" Color="#13243F" /> <!-- bleu très sombre -->
<SolidColorBrush x:Key="Brush.Status.Busy" Color="#F59E0B" /> <!-- ambre -->
<SolidColorBrush x:Key="Brush.Status.BusyBg" Color="#3A2A0E" />
<Style TargetType="Window">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Window}" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
</Style>
<Style x:Key="SidebarItem" TargetType="RadioButton">
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Secondary}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="FontSize" Value="14" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Padding" Value="20,10" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2C2C32" />
</Trigger>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2C2C32" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="PrimaryButton" TargetType="Button">
<Setter Property="Background" Value="{StaticResource Brush.Play}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="18" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="48,14" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Brush.PlayHover}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Background" Value="#444" />
<Setter Property="Foreground" Value="#888" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SecondaryButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="#3A3A40" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="Normal" />
<Setter Property="Padding" Value="14,8" />
</Style>
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="{StaticResource Brush.Accent}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Brush.AccentHover}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Background" Value="#444" />
<Setter Property="Foreground" Value="#888" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!--
DangerButton : action destructive (rouge). Utilisé pour le bouton « Annuler »
à côté de « ↻ Reprendre » sur les rows ayant un téléchargement partiel — pour
que l'utilisateur puisse abandonner le partial et repartir de zéro.
-->
<Style x:Key="DangerButton" TargetType="Button" BasedOn="{StaticResource PrimaryButton}">
<Setter Property="Background" Value="#DC2626" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="14,8" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#EF4444" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="Bd" Property="Background" Value="#444" />
<Setter Property="Foreground" Value="#888" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Boutons de chrome window (min / max / close), 46x40 façon Windows -->
<Style x:Key="WindowControlButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Width" Value="46" />
<Setter Property="Height" Value="32" />
<Setter Property="Padding" Value="0" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FontSize" Value="14" />
<Setter Property="FontFamily" Value="Segoe UI" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Brush.Accent}" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="WindowCloseButton" TargetType="Button" BasedOn="{StaticResource WindowControlButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#E81123" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="IconButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Secondary}" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="4"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2C2C32" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- ComboBox sombre.
Re-template minimal pour avoir le contenu, l'arrow et le popup tous en
palette dark. WPF native ComboBox style serait grisé sur blanc. -->
<Style x:Key="ComboBoxToggleButton" TargetType="ToggleButton">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border Background="Transparent">
<Path x:Name="Arrow"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 L 8,0 L 4,5 Z"
Fill="{StaticResource Brush.Text.Secondary}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Arrow" Property="Fill" Value="{StaticResource Brush.Text.Primary}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBox">
<Setter Property="Background" Value="#1A1A20" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid>
<!-- ToggleButton couvre TOUT le combo : clic au centre ouvre le dropdown,
pas seulement clic sur la flèche. Le ContentPresenter par-dessus
est en IsHitTestVisible=False pour ne pas voler le clic. -->
<ToggleButton x:Name="ToggleBtn"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Cursor="Hand"
Focusable="False"
IsChecked="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="4">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="28" />
</Grid.ColumnDefinitions>
<Path Grid.Column="1"
HorizontalAlignment="Center" VerticalAlignment="Center"
Data="M 0,0 L 8,0 L 4,5 Z"
Fill="{StaticResource Brush.Text.Secondary}" />
</Grid>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Brush.Accent}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
<ContentPresenter IsHitTestVisible="False"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
ContentStringFormat="{TemplateBinding SelectionBoxItemStringFormat}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="Left" VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}" />
<Popup x:Name="PART_Popup"
Placement="Bottom"
IsOpen="{TemplateBinding IsDropDownOpen}"
AllowsTransparency="True"
Focusable="False"
PopupAnimation="Slide">
<Border Background="{StaticResource Brush.Bg.Card}"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="1"
CornerRadius="4"
MinWidth="{TemplateBinding ActualWidth}"
MaxHeight="300">
<ScrollViewer>
<ItemsPresenter />
</ScrollViewer>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ComboBoxItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
<Setter Property="Padding" Value="10,6" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}">
<ContentPresenter />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Brush.Accent}" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#2C2C32" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Style ContextMenu sombre, cohérent avec le thème.
On retemplatise complètement pour virer la "icon column" claire que WPF
dessine par défaut sur le bord gauche du popup. -->
<Style TargetType="ContextMenu">
<Setter Property="Background" Value="{StaticResource Brush.Bg.Card}" />
<Setter Property="BorderBrush" Value="{StaticResource Brush.Border}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
<Setter Property="Padding" Value="4" />
<Setter Property="HasDropShadow" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ContextMenu">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}"
CornerRadius="4">
<StackPanel IsItemsHost="True"
KeyboardNavigation.DirectionalNavigation="Cycle" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- MenuItem dark : on retemplatise pour virer la "icon column" claire à gauche
que WPF dessine par défaut. -->
<Style TargetType="MenuItem">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
<Setter Property="Padding" Value="12,8" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="MenuItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
CornerRadius="3"
Padding="{TemplateBinding Padding}">
<ContentPresenter ContentSource="Header"
VerticalAlignment="Center"
RecognizesAccessKey="True" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background" Value="{StaticResource Brush.Accent}" />
<Setter Property="Foreground" Value="White" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Secondary}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="Separator">
<Setter Property="Height" Value="1" />
<Setter Property="Background" Value="{StaticResource Brush.Border}" />
<Setter Property="Margin" Value="4,4" />
</Style>
<Style x:Key="VersionPill" TargetType="Border">
<Setter Property="Background" Value="#2C2C32" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Padding" Value="10,4" />
</Style>
</ResourceDictionary>