Right sidebar nav: Library / Reports / Documentation tabs (WebView2)

The body of the launcher is now a 2-column layout:
- Left : swappable content area
- Right (200px) : sidebar with 3 nav buttons + active-state highlighting

Three pages, all under the same window chrome / footer:

1. **Library** (default) — the existing main view (featured version,
   other versions list, floating "Vérifier les MAJ" button, copyright).
2. **Reports** — embedded WebView2 navigated to a configurable URL.
   Default http://localhost/ProserveReport/ which matches the local
   stats tool installed on each customer machine alongside XAMPP.
3. **Documentation** — WebView2 with a configurable URL, falls back
   to a placeholder explaining where to set it if empty.

Implementation
- LocalConfig gains ReportUrl + DocumentationUrl (string).
- MainViewModel gains LauncherPage enum + CurrentPage state with
  Navigate{Library,Report,Documentation}Command. ReportUri /
  DocumentationUri parse the strings to Uri for WebView2.Source.
- Theme.xaml: NavButton style (transparent, left-aligned, hover
  darken, active state highlighted via Tag bool + accent left-border).
- InverseBoolToVisibilityConverter added (true → Collapsed) for the
  documentation placeholder fallback.
- Microsoft.Web.WebView2 NuGet (1.0.2792.45). Runtime is pre-installed
  on Win11 and auto-pushed via Windows Update on Win10. If absent,
  WebView2 surfaces an error which the user sees inline.
- Settings → Avancés → Serveur extended with the two URL fields.
- Strings.cs: NavLibrary / NavReport / NavDocumentation,
  SettingsReportUrl / SettingsDocsUrl, DocsPlaceholder, WebViewLoadError.
  Sidebar localized in 5 languages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-03 08:52:51 +02:00
parent f62b212fc1
commit 288cad585a
9 changed files with 237 additions and 1 deletions

View File

@@ -40,6 +40,11 @@
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="3.0.0" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<!-- WebView2 (Edge Chromium) pour les onglets Report et Documentation.
Le runtime est pré-installé sur Win11 et auto-mis-à-jour via Windows
Update sur Win10 récent. Si absent, le contrôle affiche une erreur
et on aiguille l'user vers https://go.microsoft.com/fwlink/p/?LinkId=2124703 -->
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2792.45" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,18 @@
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace PSLauncher.App.Resources;
/// <summary>
/// Inverse de <see cref="System.Windows.Controls.BooleanToVisibilityConverter"/> : true → Collapsed, false → Visible.
/// Utile pour afficher un placeholder quand un binding bool est false (ex. "DocumentationUrl est vide").
/// </summary>
public sealed class InverseBoolToVisibilityConverter : IValueConverter
{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is bool b && b ? Visibility.Collapsed : Visibility.Visible;
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is Visibility v && v != Visibility.Visible;
}

View File

@@ -3,6 +3,7 @@
xmlns:res="clr-namespace:PSLauncher.App.Resources">
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
<res:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibility" />
<!-- Custom font Pirulen pour les titres de marque -->
<FontFamily x:Key="Font.Brand">pack://application:,,,/Resources/#Pirulen</FontFamily>
@@ -72,6 +73,47 @@
</Setter>
</Style>
<!--
NavButton : item de la sidebar de droite (Library / Report / Documentation).
Plein-largeur, alignement à gauche, pas de bordure, surligné quand actif
via Tag (bool) qui est lié à IsLibrary/IsReport/IsDocumentation.
-->
<Style x:Key="NavButton" TargetType="Button">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Secondary}" />
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="20,12" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<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}"
BorderThickness="3,0,0,0"
BorderBrush="Transparent"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Left" VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1A1F2A" />
</Trigger>
<!-- "Tag" est utilisé comme flag actif (IsLibrary/IsReport/IsDocumentation) -->
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=Tag}" Value="True">
<Setter TargetName="Bd" Property="Background" Value="#1F2937" />
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource Brush.Accent}" />
<Setter Property="Foreground" Value="{StaticResource Brush.Text.Primary}" />
</DataTrigger>
</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" />

View File

@@ -21,6 +21,12 @@ using PSLauncher.Models;
namespace PSLauncher.App.ViewModels;
/// <summary>
/// Onglets affichés dans la sidebar droite du launcher. Chacun correspond à un
/// bloc de contenu visible/caché dans la fenêtre principale.
/// </summary>
public enum LauncherPage { Library, Report, Documentation }
public sealed partial class MainViewModel : ObservableObject
{
private readonly IInstallationRegistry _registry;
@@ -68,6 +74,34 @@ public sealed partial class MainViewModel : ObservableObject
[ObservableProperty] private double _progressPercent;
/// <summary>
/// Onglet actuellement actif dans la sidebar droite. La UI montre le bon bloc
/// via les converters IsLibrary / IsReport / IsDocumentation. Default = Library
/// pour que le launcher s'ouvre directement sur l'écran principal.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsLibrary))]
[NotifyPropertyChangedFor(nameof(IsReport))]
[NotifyPropertyChangedFor(nameof(IsDocumentation))]
private LauncherPage _currentPage = LauncherPage.Library;
public bool IsLibrary => CurrentPage == LauncherPage.Library;
public bool IsReport => CurrentPage == LauncherPage.Report;
public bool IsDocumentation => CurrentPage == LauncherPage.Documentation;
/// <summary>URL résolue pour le panneau Reports. WebView2.Source attend un Uri.</summary>
public Uri? ReportUri => TryParseUri(_config.ReportUrl);
/// <summary>URL résolue pour le panneau Documentation. Null si non configurée.</summary>
public Uri? DocumentationUri => TryParseUri(_config.DocumentationUrl);
public bool HasDocumentationUrl => DocumentationUri is not null;
private static Uri? TryParseUri(string? s)
=> !string.IsNullOrWhiteSpace(s) && Uri.TryCreate(s, UriKind.Absolute, out var u) ? u : null;
[RelayCommand] private void NavigateLibrary() => CurrentPage = LauncherPage.Library;
[RelayCommand] private void NavigateReport() => CurrentPage = LauncherPage.Report;
[RelayCommand] private void NavigateDocumentation() => CurrentPage = LauncherPage.Documentation;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FooterText))]
[NotifyPropertyChangedFor(nameof(FooterVisibility))]

View File

@@ -42,6 +42,8 @@ public sealed partial class SettingsViewModel : ObservableObject
[ObservableProperty] private string _serverBaseUrl;
[ObservableProperty] private string _installRoot;
[ObservableProperty] private string _language;
[ObservableProperty] private string _reportUrl;
[ObservableProperty] private string _documentationUrl;
// ----- DB Migration settings -----
[ObservableProperty] private string _dbHost;
@@ -138,6 +140,8 @@ public sealed partial class SettingsViewModel : ObservableObject
_serverBaseUrl = config.ServerBaseUrl;
_installRoot = config.InstallRoot;
_language = string.IsNullOrWhiteSpace(config.Language) ? "auto" : config.Language;
_reportUrl = config.ReportUrl;
_documentationUrl = config.DocumentationUrl;
_dbHost = config.Database.Host;
_dbPort = config.Database.Port;
@@ -252,6 +256,8 @@ public sealed partial class SettingsViewModel : ObservableObject
_config.ServerBaseUrl = ServerBaseUrl.TrimEnd('/');
_config.InstallRoot = InstallRoot;
_config.Language = Language ?? "auto";
_config.ReportUrl = ReportUrl?.Trim() ?? "";
_config.DocumentationUrl = DocumentationUrl?.Trim() ?? "";
// DB settings
_config.Database.Host = DbHost.Trim();

View File

@@ -4,6 +4,7 @@
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"
@@ -315,8 +316,23 @@
</Grid>
</Border>
<!-- Body : Grid pour pouvoir superposer le bouton flottant "Vérifier les MAJ" -->
<!--
Body : grid 2 colonnes — content area swap-pable à gauche, sidebar de
navigation fixe à droite. Le content area lui-même est un Grid qui
superpose plusieurs blocs (Library / Report / Documentation) gérés
par leur Visibility liée à CurrentPage.
-->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="200" />
</Grid.ColumnDefinitions>
<!-- ============== Content area (col 0, swap par CurrentPage) ============== -->
<Grid Grid.Column="0">
<!-- ====================== LIBRARY PAGE ====================== -->
<Grid Visibility="{Binding IsLibrary, Converter={StaticResource BoolToVisibility}}">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Margin="32,28">
@@ -547,6 +563,67 @@
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 ============== -->
<!-- ============== Right sidebar : nav buttons ============== -->
<Border Grid.Column="1"
Background="#0A0A0E"
BorderBrush="{StaticResource Brush.Border}"
BorderThickness="1,0,0,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>
</Grid>
<!-- Footer : visible uniquement quand busy / status à afficher.

View File

@@ -213,6 +213,23 @@
<TextBlock Text="{Binding ConnectionStatus}"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="12" Margin="0,8,0,0" TextWrapping="Wrap" />
<!-- URLs des onglets latéraux Report / Documentation -->
<TextBlock Text="{x:Static loc:Strings.SettingsReportUrl}"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="12" Margin="0,16,0,4" />
<TextBox Text="{Binding ReportUrl, UpdateSourceTrigger=PropertyChanged}"
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
FontFamily="Consolas" />
<TextBlock Text="{x:Static loc:Strings.SettingsDocsUrl}"
Foreground="{StaticResource Brush.Text.Secondary}"
FontSize="12" Margin="0,12,0,4" />
<TextBox Text="{Binding DocumentationUrl, UpdateSourceTrigger=PropertyChanged}"
Background="#1A1A20" Foreground="{StaticResource Brush.Text.Primary}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1" Padding="8"
FontFamily="Consolas" />
</StackPanel>
</Border>