Initial scaffolding: PS_Launcher v0.2

Client (C# / .NET 8 / WPF, MVVM via CommunityToolkit.Mvvm):
- PSLauncher.App: WPF UI dark theme (Epic-style sidebar + hero + big play button)
  with UpdateAvailableDialog rendering Markdown release notes via Markdig.Wpf.
- PSLauncher.Core: services for installation registry (scans Proserve v{X.Y.Z}/),
  process launcher, manifest fetch, SHA-256 integrity, HTTP download with
  progress, ZIP install via .tmp + atomic rename, update orchestrator.
- PSLauncher.Models: RemoteManifest, InstalledVersion, LocalConfig DTOs.

Server (PHP 8 for OVH mutualisé, deployed under www/PS_Launcher/):
- Front controller + routes /manifest and /releasenotes/{version}.
- Static signed-manifest workflow with tools/sign-manifest.php CLI to
  recompute SHA-256 and sizeBytes after each ZIP upload.
- .htaccess: HTTPS redirect, rewrite, security headers.
- config.example.php template; real config.php is gitignored.

Cohabiting versions: each release lives in its own Proserve v{version}/ folder
under installRoot. Old versions are never deleted automatically.

Roadmap: v0.3 = HTTP Range resume + Polly retry + state.json,
v0.4 = MySQL license + Ed25519 signatures + DPAPI, v0.5 = settings/UX polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 08:54:45 +02:00
commit 1c8c6803e8
48 changed files with 2269 additions and 0 deletions

View File

@@ -0,0 +1,194 @@
<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:views="clr-namespace:PSLauncher.App.Views"
Title="PROSERVE Launcher"
Width="1280" Height="800"
MinWidth="900" MinHeight="600"
WindowStartupLocation="CenterScreen"
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">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- Top bar -->
<RowDefinition Height="*" /> <!-- Body -->
<RowDefinition Height="Auto" /> <!-- Footer (download) -->
</Grid.RowDefinitions>
<!-- Top bar -->
<Border Grid.Row="0" Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,0,1"
Padding="20,12">
<Grid>
<TextBlock Text="PROSERVE" FontSize="18" FontWeight="Bold"
Foreground="{StaticResource Brush.Text.Primary}"
VerticalAlignment="Center" />
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{Binding LicenseSummary}"
Foreground="{StaticResource Brush.Text.Secondary}"
VerticalAlignment="Center" Margin="0,0,12,0" />
</StackPanel>
</Grid>
</Border>
<!-- Body : sidebar + content -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Sidebar -->
<Border Grid.Column="0" Background="{StaticResource Brush.Bg.Sidebar}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,0,1,0">
<StackPanel>
<RadioButton Style="{StaticResource SidebarItem}" GroupName="Nav"
Content="📚 Bibliothèque" IsChecked="True" />
<RadioButton Style="{StaticResource SidebarItem}" GroupName="Nav"
Content="📰 Nouveautés" IsEnabled="False" />
<RadioButton Style="{StaticResource SidebarItem}" GroupName="Nav"
Content="⚙️ Paramètres" IsEnabled="False" />
</StackPanel>
</Border>
<!-- Library content -->
<ScrollViewer Grid.Column="1" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="40,30">
<!-- Hero -->
<Border Background="#2A2A30" CornerRadius="8" Height="280"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1">
<Grid>
<TextBlock Text="PROSERVE"
FontSize="64" FontWeight="Bold"
Foreground="#3D3D45"
HorizontalAlignment="Center" VerticalAlignment="Center" />
<Border VerticalAlignment="Bottom" HorizontalAlignment="Left"
Margin="24" Background="#000000B0"
CornerRadius="4" Padding="12,6">
<TextBlock Text="{Binding HeroTitle}"
FontSize="22" FontWeight="SemiBold"
Foreground="White" />
</Border>
</Grid>
</Border>
<!-- Action row -->
<Grid Margin="0,24,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Version selector -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="Version :" Foreground="{StaticResource Brush.Text.Secondary}"
VerticalAlignment="Center" Margin="0,0,10,0" />
<ComboBox Width="160"
ItemsSource="{Binding InstalledVersions}"
DisplayMemberPath="Display"
SelectedItem="{Binding SelectedVersion, Mode=TwoWay}" />
<Border Style="{StaticResource VersionPill}" Margin="12,0,0,0">
<TextBlock Text="{Binding StatusBadge}" FontSize="12"
Foreground="{StaticResource Brush.Text.Primary}" />
</Border>
</StackPanel>
<!-- Primary action -->
<Button Grid.Column="2"
Style="{StaticResource PrimaryButton}"
Content="{Binding PrimaryActionLabel}"
Command="{Binding PrimaryActionCommand}"
IsEnabled="{Binding PrimaryActionEnabled}" />
</Grid>
<!-- Secondary actions -->
<StackPanel Orientation="Horizontal" Margin="0,16,0,0">
<Button Style="{StaticResource SecondaryButton}"
Content="🔄 Vérifier les mises à jour"
Command="{Binding CheckForUpdatesCommand}"
Margin="0,0,10,0" />
<Button Style="{StaticResource SecondaryButton}"
Content="📁 Ouvrir le dossier"
Command="{Binding OpenFolderCommand}"
Margin="0,0,10,0" />
<Button Style="{StaticResource SecondaryButton}"
Content="🔍 Rescanner local"
Command="{Binding RefreshCommand}" />
</StackPanel>
<!-- Installed versions list -->
<TextBlock Text="Versions installées" FontSize="16" FontWeight="SemiBold"
Margin="0,30,0,12"
Foreground="{StaticResource Brush.Text.Primary}" />
<ItemsControl ItemsSource="{Binding InstalledVersions}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{StaticResource Brush.Bg.Card}"
CornerRadius="6" Margin="0,0,0,8" Padding="16,12"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="{Binding Display}"
FontSize="15" FontWeight="SemiBold" />
<TextBlock Text="{Binding Subtitle}"
FontSize="12"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,2,0,0" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<!-- Empty state -->
<TextBlock Text="{Binding EmptyHint}"
Foreground="{StaticResource Brush.Text.Secondary}"
TextWrapping="Wrap" Margin="0,12,0,0"
Visibility="{Binding EmptyHintVisibility}" />
</StackPanel>
</ScrollViewer>
</Grid>
<!-- Footer (download/install status) -->
<Border Grid.Row="2" Background="{StaticResource Brush.Bg.Footer}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="0,1,0,0"
Padding="20,8"
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}" />
<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="Annuler"
VerticalAlignment="Center"
Margin="12,0,0,0"
Command="{Binding CancelDownloadCommand}"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVisibility}}" />
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,11 @@
using System.Windows;
namespace PSLauncher.App.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,50 @@
<Window x:Class="PSLauncher.App.Views.UpdateAvailableDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Mise à jour disponible"
Width="640" Height="540"
MinWidth="480" MinHeight="400"
WindowStartupLocation="CenterOwner"
Background="{StaticResource Brush.Bg.Window}">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{Binding Title}"
FontSize="22" FontWeight="SemiBold"
Foreground="{StaticResource Brush.Text.Primary}" />
<TextBlock Grid.Row="1"
Text="{Binding Subtitle}"
Foreground="{StaticResource Brush.Text.Secondary}"
Margin="0,4,0,18" />
<Border Grid.Row="2"
Background="{StaticResource Brush.Bg.Card}"
BorderBrush="{StaticResource Brush.Border}" BorderThickness="1"
CornerRadius="6"
Padding="16">
<FlowDocumentScrollViewer
Document="{Binding ReleaseNotesDocument}"
Foreground="{StaticResource Brush.Text.Primary}"
Background="Transparent" />
</Border>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
<Button Style="{StaticResource SecondaryButton}"
Content="Plus tard"
IsCancel="True"
Margin="0,0,12,0"
Click="OnLater" />
<Button Style="{StaticResource PrimaryButton}"
Content="⬇ Télécharger"
Padding="32,10"
Click="OnDownload" />
</StackPanel>
</Grid>
</Window>

View File

@@ -0,0 +1,61 @@
using System.Windows;
using System.Windows.Documents;
using Markdig;
using Markdig.Wpf;
using PSLauncher.Models;
namespace PSLauncher.App.Views;
public partial class UpdateAvailableDialog : Window
{
public UpdateAvailableDialog(VersionManifest version, string releaseNotesMarkdown)
{
InitializeComponent();
var pipeline = new MarkdownPipelineBuilder()
.UseSupportedExtensions()
.Build();
FlowDocument doc;
try
{
doc = Markdig.Wpf.Markdown.ToFlowDocument(releaseNotesMarkdown, pipeline);
}
catch
{
doc = new FlowDocument(new Paragraph(new Run(releaseNotesMarkdown)));
}
DataContext = new
{
Title = $"Proserve v{version.Version} disponible",
Subtitle = $"Date de release : {version.ReleasedAt.ToLocalTime():dd MMMM yyyy} • {FormatSize(version.Download.SizeBytes)}",
ReleaseNotesDocument = doc
};
}
public bool DownloadRequested { get; private set; }
private void OnDownload(object sender, RoutedEventArgs e)
{
DownloadRequested = true;
DialogResult = true;
Close();
}
private void OnLater(object sender, RoutedEventArgs e)
{
DownloadRequested = false;
DialogResult = false;
Close();
}
private static string FormatSize(long bytes)
{
if (bytes <= 0) return "taille inconnue";
string[] units = { "o", "Ko", "Mo", "Go", "To" };
double v = bytes;
int u = 0;
while (v >= 1024 && u < units.Length - 1) { v /= 1024; u++; }
return $"{v:0.#} {units[u]}";
}
}