Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9a491622b | ||
|
|
c210754a18 | ||
|
|
389593a3b4 | ||
|
|
22ee3a6dee | ||
|
|
a72761ccd0 | ||
|
|
62011342ec | ||
|
|
0f93b784f5 | ||
|
|
3ad27965fd | ||
|
|
69e710d3b7 |
No files matched your search
@@ -288,6 +288,16 @@ Deux fenêtres distinctes :
|
||||
Au premier démarrage d'une version qui sépare les paramètres, les réglages *Lancement du jeu*
|
||||
sont repris de ceux du jeu affiché par défaut.
|
||||
|
||||
### Plein écran
|
||||
|
||||
La fenêtre principale et le menu d'administration ont deux modes : **fenêtré** (taille d'origine) ou
|
||||
**plein écran** (fenêtre agrandie sur tout l'écran, la barre des tâches restant visible). Bascule : bouton entre « Réduire » et
|
||||
« Fermer », ou **F11**. Le mode de chaque fenêtre est retenu pour la fois suivante. Les fenêtres ne se
|
||||
redimensionnent jamais par les bords ou les coins ; la mise en page s'adapte à la place disponible
|
||||
(plein écran : colonnes plus larges ; petit écran ou mise à l'échelle 125-150 % : contenu réduit d'un
|
||||
bloc, sans rien rogner). Les autres fenêtres (paramètres, notes de version agrandies, boîtes de
|
||||
dialogue) gardent leur taille fixe.
|
||||
|
||||
### Zone de notification (icônes cachées, près de l'horloge)
|
||||
|
||||
Deux options des *Paramètres du launcher* (désactivées par défaut) :
|
||||
@@ -312,6 +322,7 @@ referme aussitôt. Il se ferme dès qu'il perd le premier plan (clic ailleurs) o
|
||||
├─ launcher-settings.json paramètres du launcher (communs à tous les jeux)
|
||||
├─ self-update.json mise à jour du launcher (§7)
|
||||
├─ launcher-state.json dernier jeu affiché (plusieurs jeux)
|
||||
├─ window-modes.json plein écran ou fenêtré, par fenêtre (« main », « admin »)
|
||||
├─ logs\launcher.log journal du launcher (roue crantée › Ouvrir le dossier des journaux)
|
||||
└─ Games\<GameId>\settings.json paramètres de chaque jeu (menu « … » › Paramètres du jeu)
|
||||
```
|
||||
@@ -595,13 +606,15 @@ servi aux joueurs (page **Signature**, voir plus bas). Ajoutée ensuite : la pag
|
||||
* **Page « Journal d'audit »** (permission `audit.read` : propriétaires et administrateurs) : connexions,
|
||||
invitations, approbations, publications, modifications et refus, les plus récents d'abord (par pages
|
||||
de 200). Filtres par **période** (aujourd'hui, 7 ou 30 derniers jours, ce mois-ci, mois précédent, ou dates
|
||||
« du / au » au format jj/mm/aaaa), catégorie, résultat et texte (nom, IP, clé…). La période est demandée au
|
||||
« du / au » au format jj/mm/aaaa), catégorie, résultat et texte (nom, appareil, IP, clé…). La période est demandée au
|
||||
serveur (paramètre `before`) et les pages sont chargées jusqu'à son début (5000 entrées au plus) ; l'**export
|
||||
CSV** reprend toutes les entrées filtrées, période comprise. Le journal se surveille lui-même : chaque
|
||||
**consultation** (ouverture, « Actualiser », période changée) est inscrite par le serveur (« Journal d'audit
|
||||
consulté » : auteur, appareil, IP, nombre d'entrées lues), et chaque **export** l'est avant l'écriture du
|
||||
fichier (« Journal d'audit exporté (CSV) » : nombre d'entrées, période, filtres) ; si le serveur ne peut pas
|
||||
l'inscrire, le fichier n'est pas écrit. Catégorie de filtre « Consultations et exports du journal ».
|
||||
Chaque entrée indique l'**appareil** sous le nom choisi à l'enregistrement de sa clé (« J'ai une
|
||||
invitation »), jamais le nom Windows du PC (colonne « Appareil » de l'export CSV).
|
||||
* **Page « Fichiers du serveur »** (permission `files.manage` : propriétaires et administrateurs) : contenu du
|
||||
site public (dossiers, tailles, dates, espace libre du disque) et état de chaque fichier : **Utilisé** (archive,
|
||||
fichier d'une version, exécutable du launcher, image du catalogue), **Protégé** (`.json`), **Inutilisé**
|
||||
|
||||
@@ -139,6 +139,48 @@ public sealed record AdminPanelDescriptor(
|
||||
module, et son `DataTemplate` dans `Themes/Admin.xaml` (ou un dictionnaire dédié fusionné dans
|
||||
`App.xaml`). La fenêtre d'administration elle-même ne change pas.
|
||||
|
||||
### 5.3 Affichage : mode fenêtré ou plein écran, mise en page responsive (✅)
|
||||
|
||||
Deux modes seulement, pour la fenêtre principale et le menu d'administration (les autres fenêtres —
|
||||
paramètres, notes agrandies, boîtes de dialogue — gardent leur taille fixe) : **fenêtré** (taille
|
||||
d'origine, jamais redimensionnable par les bords ou les coins) et **plein écran** au sens d'une fenêtre
|
||||
Windows agrandie (toute la zone utile de l'écran où se trouve la fenêtre, la barre des tâches restant visible,
|
||||
même en masquage automatique ; pas de plein écran exclusif comme un jeu). Bascule : bouton entre « Réduire » et « Fermer », ou **F11**.
|
||||
Le mode est mémorisé par fenêtre (`%AppData%\…\GameLauncher\window-modes.json`, ex.
|
||||
`{ "main": true, "admin": false }` ; simple préférence, sans donnée personnelle).
|
||||
|
||||
* `Controls/WindowModeAssist` (propriétés attachées, aucun code dans la fenêtre) :
|
||||
`ui:WindowModeAssist.AllowFullScreen="True"` + `ui:WindowModeAssist.PersistenceKey="clé"` sur la
|
||||
fenêtre ; bouton `<Button Style="{StaticResource WindowModeButtonStyle}" />` (icône et infobulle
|
||||
selon le mode, masqué si la fenêtre ne le permet pas). En plein écran, les ressources de la fenêtre
|
||||
nommées `WindowChrome.*` (CornerRadius, Thickness : arrondis, marge de l'ombre) passent à 0 ; elles
|
||||
sont utilisées en `DynamicResource`. En mode fenêtré, la taille est limitée à la zone utile de
|
||||
l'écran de la fenêtre. Déplacement par la barre de titre : `WindowModeAssist.DragMove(window)` (sans
|
||||
effet en plein écran) ; réaffichage d'une fenêtre réduite : `WindowModeAssist.RestoreFromMinimized`
|
||||
(jamais `WindowState = Normal`, qui ferait quitter le plein écran).
|
||||
* `Controls/ScaleToFitDecorator` (responsive) : `DesignWidth` × `DesignHeight` = taille du contenu
|
||||
en mode fenêtré. Plus de place (plein écran) : le contenu s'étale (colonnes fluides, listes et
|
||||
ascenseurs) ; moins de place (petit écran, mise à l'échelle Windows 125-150 %) : il est mis en page
|
||||
à cette taille puis réduit d'un bloc. Rien n'est jamais rogné : tous les boutons restent
|
||||
accessibles, tous les textes visibles. Le contenu n'est jamais agrandi.
|
||||
* **Nouvelle page d'administration** (module futur) : elle s'affiche dans la fenêtre d'administration
|
||||
et profite donc du plein écran sans rien déclarer. Règles de mise en page pour qu'elle reste
|
||||
lisible dans les deux modes :
|
||||
* ne pas fixer la largeur d'un bloc de texte (`MaxWidth` sur un bandeau…) : texte en
|
||||
`TextWrapping="Wrap"` dans un `DockPanel` ou une colonne `*` d'un `Grid`, jamais dans un
|
||||
`StackPanel` horizontal (il ne revient alors jamais à la ligne) ;
|
||||
* listes de tuiles : `WrapPanel` ; libellés sur une ligne : `TextTrimming="CharacterEllipsis"` +
|
||||
infobulle ;
|
||||
* colonnes de largeur fixe : seulement pour une liste de navigation (≤ 260) ; la partie
|
||||
principale en `*` ;
|
||||
* page en deux colonnes qui défilent séparément : `HasSplitLayout => true` ; la page reçoit la
|
||||
hauteur visible (au moins 360 px, sinon la page entière défile en plus) ;
|
||||
* bandeaux d'erreur et de résultat : reprendre ceux des pages existantes (`AdminErrorBannerStyle`,
|
||||
`AdminInfoBannerStyle`, icône à gauche dans un `DockPanel`).
|
||||
* **Futur module du launcher** affiché dans sa propre fenêtre : `AllowFullScreen` seulement si la fenêtre
|
||||
contient assez d'informations pour le mériter, avec une `PersistenceKey` propre et un
|
||||
`ScaleToFitDecorator` autour du contenu.
|
||||
|
||||
## 6. Authentification par clé publique/privée
|
||||
|
||||
### 6.1 Clés
|
||||
@@ -562,6 +604,15 @@ que le serveur accepte ; ses refus sont affichés tels quels (le serveur reste s
|
||||
sauter l'annonce, mais pas la lecture, elle-même tracée) ; export CSV des entrées filtrées (période
|
||||
comprise), toutes les cellules entre guillemets et celles qui commenceraient par `= + - @` préfixées d'une apostrophe (certaines valeurs
|
||||
viennent de requêtes anonymes ou de saisies : pas de formule exécutée par le tableur).
|
||||
* **Appareil** : chaque entrée liée à une clé affiche le **nom de l'appareil choisi à l'enregistrement de
|
||||
la clé** (formulaire « J'ai une invitation »), jamais le nom Windows du PC. Le serveur l'ajoute à la
|
||||
lecture (`AuditEntry.KeyLabel`, d'après `admins.json`) : il n'est jamais écrit dans les fichiers du
|
||||
journal (ajout seul), et les anciennes entrées en profitent aussi ; colonne « Appareil » dans l'export
|
||||
CSV, recherche comprise. À la connexion (`auth.login`), le détail est « appareil « Nom » · GameLauncher
|
||||
x.y.z » : le launcher n'envoie plus le nom Windows du PC (`LoginRequest.ClientName` = logiciel et
|
||||
version) et le serveur retire celui qu'ajoutaient les launchers jusqu'à la version 2.4.1 (il n'est ni
|
||||
journalisé ni conservé dans la session). Les lignes déjà écrites par un ancien serveur gardent leur texte
|
||||
d'origine (journal en ajout seul).
|
||||
* Après une modification, la page se recharge séparément : un échec du rechargement n'efface ni le
|
||||
résultat annoncé ni un code d'invitation (affiché avant le rechargement). Pas de nouvelle invitation
|
||||
tant qu'un code non transmis est affiché.
|
||||
|
||||
@@ -29,7 +29,8 @@ public sealed class AuditModule : IApiModule
|
||||
api.MapPost(ApiRoutes.AuditExport, RecordExport).RequirePermission(AdminPermissions.AuditRead).RequireSignedRequest();
|
||||
}
|
||||
|
||||
private static IResult Read(int? limit, string? cursor, string? before, HttpContext context, AuditLog audit, AuditConsultations consultations)
|
||||
private static IResult Read(int? limit, string? cursor, string? before, HttpContext context, AuditLog audit, AuditConsultations consultations,
|
||||
AdminStore store)
|
||||
{
|
||||
if (cursor is { Length: > 32 }) return ApiResults.BadRequest("Curseur invalide.");
|
||||
DateTimeOffset? until = null;
|
||||
@@ -43,7 +44,7 @@ public sealed class AuditModule : IApiModule
|
||||
until = parsed;
|
||||
}
|
||||
|
||||
var page = audit.Read(limit ?? 50, cursor, until);
|
||||
var page = WithDeviceNames(audit.Read(limit ?? 50, cursor, until), store);
|
||||
|
||||
// Consultation inscrite APRÈS la lecture (la page renvoyée montre le journal tel qu'il était). Première page
|
||||
// (ouverture, « Actualiser », période changée) : toujours ; pages suivantes (« Charger plus ») : une fois par
|
||||
@@ -61,6 +62,25 @@ public sealed class AuditModule : IApiModule
|
||||
return Results.Json(page, ApiJson.Options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nom de l'appareil de chaque clé (défini à l'enregistrement de la clé, avec l'invitation), ajouté à la lecture :
|
||||
/// les anciennes entrées en profitent aussi, et le journal sur le disque reste inchangé (ajout seul).
|
||||
/// </summary>
|
||||
internal static AuditPage WithDeviceNames(AuditPage page, AdminStore store)
|
||||
{
|
||||
if (page.Entries.All(e => e.KeyId is null && e.KeyLabel is null)) return page;
|
||||
var database = store.Read();
|
||||
var labels = new Dictionary<string, string?>(StringComparer.Ordinal);
|
||||
var entries = page.Entries.Select(entry =>
|
||||
{
|
||||
if (entry.KeyId is not { } keyId) return entry with { KeyLabel = null };
|
||||
if (!labels.TryGetValue(keyId, out var label))
|
||||
labels[keyId] = label = database.FindKey(keyId) is { } found ? Auth.AuthModule.DeviceName(found.Key.Label) : null;
|
||||
return entry with { KeyLabel = label }; // seul le serveur renseigne ce champ (jamais lu depuis le fichier)
|
||||
}).ToList();
|
||||
return page with { Entries = entries };
|
||||
}
|
||||
|
||||
private static IResult RecordExport(HttpContext context, AuditLog audit)
|
||||
{
|
||||
AuditExportRequest? request;
|
||||
|
||||
@@ -97,7 +97,7 @@ public sealed class AuthModule : IApiModule
|
||||
return ApiResults.Unauthorized(LoginRefused);
|
||||
}
|
||||
|
||||
var clientName = ApiText.CleanLine(request.ClientName, ApiText.MaxClientNameLength);
|
||||
var clientName = ClientSoftware(ApiText.CleanLine(request.ClientName, ApiText.MaxClientNameLength));
|
||||
var (token, session) = sessions.Create(admin.Id, key.KeyId, ip, clientName);
|
||||
// Date de dernière utilisation : enregistrée au plus une fois par heure (évite de réécrire
|
||||
// admins.json à chaque connexion).
|
||||
@@ -117,7 +117,10 @@ public sealed class AuthModule : IApiModule
|
||||
}
|
||||
}
|
||||
|
||||
audit.Write("auth.login", AuditResults.Ok, admin.Id, admin.DisplayName, key.KeyId, ip, details: clientName);
|
||||
// Appareil : le nom choisi à l'enregistrement de la clé (invitation), jamais le nom Windows du PC.
|
||||
var device = $"appareil « {DeviceName(key.Label)} »";
|
||||
audit.Write("auth.login", AuditResults.Ok, admin.Id, admin.DisplayName, key.KeyId, ip,
|
||||
details: clientName is null ? device : $"{device} · {clientName}");
|
||||
var database = store.Read();
|
||||
var authenticated = new AuthenticatedAdmin(session, admin, key, database.PermissionsOf(admin.Role), database.GroupName(admin.Role));
|
||||
return Results.Json(new LoginResponse
|
||||
@@ -130,6 +133,23 @@ public sealed class AuthModule : IApiModule
|
||||
}, ApiJson.Options);
|
||||
}
|
||||
|
||||
/// <summary>Nom de l'appareil tel que défini à l'enregistrement de la clé (invitation).</summary>
|
||||
internal static string DeviceName(string? keyLabel) => string.IsNullOrWhiteSpace(keyLabel) ? "sans nom" : keyLabel;
|
||||
|
||||
/// <summary>
|
||||
/// Logiciel client (« GameLauncher 2.5.0 »), pour le journal. Les launchers jusqu'à la version 2.4.1 y ajoutaient le
|
||||
/// nom Windows du PC entre parenthèses (« GameLauncher 2.4.1 (DESKTOP-ABC123) ») : il est retiré, seul le nom de
|
||||
/// l'appareil choisi à l'enregistrement de la clé identifie le poste (et le nom Windows n'est pas conservé).
|
||||
/// </summary>
|
||||
internal static string? ClientSoftware(string? clientName)
|
||||
{
|
||||
if (clientName is null) return null;
|
||||
if (clientName.StartsWith("GameLauncher ", StringComparison.Ordinal) && clientName.EndsWith(')') &&
|
||||
clientName.IndexOf(" (", StringComparison.Ordinal) is var open and > 0)
|
||||
return clientName[..open];
|
||||
return clientName;
|
||||
}
|
||||
|
||||
private static IResult Logout(HttpContext context, SessionStore sessions, AuditLog audit)
|
||||
{
|
||||
var admin = context.Admin();
|
||||
|
||||
@@ -13,6 +13,14 @@ public sealed record AuditEntry
|
||||
public Guid? ActorId { get; init; }
|
||||
public string? ActorName { get; init; }
|
||||
public string? KeyId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Nom de l'appareil de la clé <see cref="KeyId"/>, tel que défini à l'enregistrement de la clé (invitation).
|
||||
/// Jamais écrit dans les fichiers du journal : ajouté par le serveur à la lecture (null : clé inconnue, ou serveur
|
||||
/// plus ancien).
|
||||
/// </summary>
|
||||
public string? KeyLabel { get; init; }
|
||||
|
||||
public string? Ip { get; init; }
|
||||
public string? Target { get; init; }
|
||||
public string? Details { get; init; }
|
||||
|
||||
@@ -428,7 +428,10 @@ public sealed class AuditItem
|
||||
if (!string.IsNullOrEmpty(entry.Target)) parts.Add(entry.Target);
|
||||
if (!string.IsNullOrEmpty(entry.Details)) parts.Add(entry.Details);
|
||||
DetailText = string.Join(" — ", parts);
|
||||
// Appareil : nom choisi à l'enregistrement de la clé (invitation), fourni par le serveur — jamais le nom Windows.
|
||||
DeviceText = string.IsNullOrEmpty(entry.KeyLabel) ? "" : $"Appareil « {entry.KeyLabel} »";
|
||||
var origin = new List<string>();
|
||||
if (DeviceText.Length > 0) origin.Add(DeviceText);
|
||||
if (!string.IsNullOrEmpty(entry.Ip)) origin.Add($"IP {entry.Ip}");
|
||||
if (!string.IsNullOrEmpty(entry.KeyId)) origin.Add($"clé {entry.KeyId}");
|
||||
OriginText = string.Join(" · ", origin);
|
||||
@@ -447,6 +450,10 @@ public sealed class AuditItem
|
||||
public bool IsDenied => Entry.Result == AuditResults.Denied;
|
||||
public bool IsFailed => Entry.Result == AuditResults.Failed;
|
||||
public string ActorText { get; }
|
||||
|
||||
/// <summary>« Appareil « PC bureau » » (vide : entrée sans clé, ou clé inconnue du serveur).</summary>
|
||||
public string DeviceText { get; }
|
||||
|
||||
public string DetailText { get; }
|
||||
public bool HasDetail => DetailText.Length > 0;
|
||||
public string OriginText { get; }
|
||||
@@ -524,14 +531,14 @@ public static class AuditTexts
|
||||
public static string ToCsv(IEnumerable<AuditItem> items)
|
||||
{
|
||||
var csv = new StringBuilder();
|
||||
csv.AppendLine("Date;Action;Code;Résultat;Administrateur;IP;Clé;Cible;Détails");
|
||||
csv.AppendLine("Date;Action;Code;Résultat;Administrateur;Appareil;IP;Clé;Cible;Détails");
|
||||
foreach (var item in items)
|
||||
{
|
||||
var e = item.Entry;
|
||||
csv.AppendLine(string.Join(';', new[]
|
||||
{
|
||||
e.Timestamp.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture),
|
||||
item.ActionText, e.Action, item.ResultText, e.ActorName, e.Ip, e.KeyId, e.Target, e.Details
|
||||
item.ActionText, e.Action, item.ResultText, e.ActorName, e.KeyLabel, e.Ip, e.KeyId, e.Target, e.Details
|
||||
}.Select(Cell)));
|
||||
}
|
||||
return csv.ToString();
|
||||
|
||||
@@ -14,7 +14,7 @@ public sealed class AdminLoginService
|
||||
private readonly IAdminTpm? _tpm;
|
||||
private readonly IReadOnlyList<(string KeyId, byte[] PublicKey)> _rootKeys;
|
||||
|
||||
/// <param name="clientName">Description du poste pour le journal d'audit, ex. « GameLauncher 1.4.0 (PC-BUREAU) ».</param>
|
||||
/// <param name="clientName">Logiciel pour le journal d'audit, ex. « GameLauncher 2.5.0 » (l'appareil est identifié par sa clé).</param>
|
||||
/// <param name="tpm">Puce de sécurité de ce PC (null : clés TPM indisponibles).</param>
|
||||
/// <param name="rootKeys">
|
||||
/// Clés racine du launcher (update-signing.pub) : une clé d'administrateur identique est refusée, quel que soit
|
||||
|
||||
@@ -59,6 +59,10 @@ public sealed class AdminEnvironment
|
||||
/// <summary>Faux dans les tests : la surveillance de session est déclenchée à la main.</summary>
|
||||
public bool StartTimers { get; init; } = true;
|
||||
|
||||
/// <summary>Poste, pour le journal d'audit du serveur.</summary>
|
||||
public string ClientName => $"GameLauncher {LauncherVersion} ({System.Environment.MachineName})";
|
||||
/// <summary>
|
||||
/// Logiciel, pour le journal d'audit du serveur (« GameLauncher 2.5.0 »). Le poste y est identifié par le nom de
|
||||
/// l'appareil choisi à l'enregistrement de sa clé (invitation), que le serveur connaît : jamais par le nom Windows du
|
||||
/// PC, qui n'est pas envoyé.
|
||||
/// </summary>
|
||||
public string ClientName => $"GameLauncher {LauncherVersion}";
|
||||
}
|
||||
@@ -255,6 +255,9 @@ public partial class App : Application, ILauncherRestartHost
|
||||
var version = VersionUtil.Display(_version);
|
||||
Log.Info($"Démarrage du launcher {version} ({catalog.Games.Count} jeu(x))");
|
||||
|
||||
// Mode d'affichage (plein écran ou fenêtré) mémorisé pour chaque fenêtre qui le permet. Pas en mode --capture.
|
||||
Controls.WindowModeAssist.Store = readOnly ? null : new WindowModeStore(Path.Combine(catalog.DataDirectory, WindowModeStore.FileName));
|
||||
|
||||
var window = new Views.MainWindow();
|
||||
MainWindow = window;
|
||||
_mainWindow = window;
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace GameLauncher.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Mise en page responsive d'une fenêtre à deux modes (fenêtré / plein écran, voir <see cref="WindowModeAssist"/>) :
|
||||
/// <list type="bullet">
|
||||
/// <item>place disponible plus GRANDE que la taille minimale de mise en page (<see cref="DesignWidth"/> ×
|
||||
/// <see cref="DesignHeight"/>, un peu en dessous de la taille fenêtrée : le contenu défile déjà verticalement) : le contenu
|
||||
/// occupe toute la place (colonnes, listes et ascenseurs s'adaptent) ;</item>
|
||||
/// <item>place plus PETITE (petit écran, mise à l'échelle Windows 125-150 %) : le contenu est mis en page à la taille de
|
||||
/// conception puis réduit d'un bloc, sans rien rogner ni masquer : tous les boutons restent accessibles, tous les textes
|
||||
/// visibles.</item>
|
||||
/// </list>
|
||||
/// Le contenu n'est jamais agrandi : en plein écran sur un grand écran, il gagne de la place, pas des pixels flous.
|
||||
/// </summary>
|
||||
public sealed class ScaleToFitDecorator : Decorator
|
||||
{
|
||||
public static readonly DependencyProperty DesignWidthProperty = DependencyProperty.Register(
|
||||
nameof(DesignWidth), typeof(double), typeof(ScaleToFitDecorator),
|
||||
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure), IsValidSize);
|
||||
|
||||
public static readonly DependencyProperty DesignHeightProperty = DependencyProperty.Register(
|
||||
nameof(DesignHeight), typeof(double), typeof(ScaleToFitDecorator),
|
||||
new FrameworkPropertyMetadata(0.0, FrameworkPropertyMetadataOptions.AffectsMeasure), IsValidSize);
|
||||
|
||||
/// <summary>Largeur minimale de mise en page du contenu (0 : jamais réduit en largeur).</summary>
|
||||
public double DesignWidth
|
||||
{
|
||||
get => (double)GetValue(DesignWidthProperty);
|
||||
set => SetValue(DesignWidthProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Hauteur minimale de mise en page du contenu (0 : jamais réduit en hauteur).</summary>
|
||||
public double DesignHeight
|
||||
{
|
||||
get => (double)GetValue(DesignHeightProperty);
|
||||
set => SetValue(DesignHeightProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>Échelle appliquée (1 : taille réelle ; en dessous, contenu réduit).</summary>
|
||||
public double Scale { get; private set; } = 1;
|
||||
|
||||
/// <summary>Plus petite échelle acceptée : en dessous, les ascenseurs du contenu prennent le relais.</summary>
|
||||
private const double MinScale = 0.5;
|
||||
|
||||
/// <summary>Écart négligeable (arrondis) : pas de réduction, qui rendrait le texte un peu flou pour rien.</summary>
|
||||
private const double Tolerance = 0.995;
|
||||
|
||||
private static bool IsValidSize(object value) => value is double d && !double.IsNaN(d) && !double.IsInfinity(d) && d >= 0;
|
||||
|
||||
private double ScaleFor(Size available)
|
||||
{
|
||||
var scale = 1.0;
|
||||
if (DesignWidth > 0 && !double.IsInfinity(available.Width) && available.Width < DesignWidth)
|
||||
scale = Math.Min(scale, available.Width / DesignWidth);
|
||||
if (DesignHeight > 0 && !double.IsInfinity(available.Height) && available.Height < DesignHeight)
|
||||
scale = Math.Min(scale, available.Height / DesignHeight);
|
||||
return scale >= Tolerance ? 1 : Math.Max(MinScale, scale);
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size constraint)
|
||||
{
|
||||
if (Child is not { } child) return default;
|
||||
var scale = ScaleFor(constraint);
|
||||
child.Measure(new Size(constraint.Width / scale, constraint.Height / scale));
|
||||
var desired = child.DesiredSize;
|
||||
return new Size(Math.Min(desired.Width * scale, constraint.Width), Math.Min(desired.Height * scale, constraint.Height));
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size arrangeSize)
|
||||
{
|
||||
if (Child is not { } child) return arrangeSize;
|
||||
var scale = ScaleFor(arrangeSize);
|
||||
Scale = scale;
|
||||
// Mis en page à la taille « non réduite », puis réduit à l'affichage (le clic et les infobulles suivent la réduction).
|
||||
child.Arrange(new Rect(0, 0, arrangeSize.Width / scale, arrangeSize.Height / scale));
|
||||
if (scale >= 1)
|
||||
{
|
||||
if (child.RenderTransform is not MatrixTransform { Matrix.IsIdentity: true }) child.RenderTransform = Transform.Identity;
|
||||
}
|
||||
else if (child.RenderTransform is not ScaleTransform current || Math.Abs(current.ScaleX - scale) > 0.0001)
|
||||
{
|
||||
child.RenderTransform = new ScaleTransform(scale, scale);
|
||||
}
|
||||
return arrangeSize;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using GameLauncher.Platform;
|
||||
|
||||
namespace GameLauncher.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Deux modes d'affichage seulement pour une fenêtre sans bordure : FENÊTRÉ (taille d'origine, jamais redimensionnable
|
||||
/// par les bords ou les coins) ou PLEIN ÉCRAN, au sens d'une fenêtre Windows agrandie : toute la zone utile de l'écran où
|
||||
/// se trouve la fenêtre, la barre des tâches restant visible (même en masquage automatique). Ce n'est pas le plein écran
|
||||
/// exclusif d'un jeu.
|
||||
/// <para>
|
||||
/// Une fenêtre s'y inscrit en XAML, sans code : <c>ui:WindowModeAssist.AllowFullScreen="True"</c> (et, pour retrouver
|
||||
/// le mode au prochain affichage, <c>ui:WindowModeAssist.PersistenceKey="clé"</c>). Elle reçoit alors :
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>la touche F11 et la commande <see cref="ToggleFullScreenCommand"/> (bouton prêt à l'emploi : style
|
||||
/// « WindowModeButtonStyle » de Themes/Controls.xaml, icône et infobulle selon le mode) ;</item>
|
||||
/// <item>les ressources « WindowChrome.* » de la fenêtre (CornerRadius, Thickness : arrondis, marge de l'ombre) mises à 0
|
||||
/// en plein écran, puis restaurées : les éléments qui les utilisent en DynamicResource suivent le mode ;</item>
|
||||
/// <item>en mode fenêtré, une taille limitée à la zone utile de SON écran (petits écrans, mise à l'échelle 125-150 %).</item>
|
||||
/// </list>
|
||||
/// Les fenêtres qui ne s'y inscrivent pas (boîtes de dialogue, paramètres…) gardent leur taille fixe.
|
||||
/// </summary>
|
||||
public static class WindowModeAssist
|
||||
{
|
||||
/// <summary>Bascule fenêtré / plein écran de la fenêtre qui contient l'élément (F11).</summary>
|
||||
public static readonly RoutedUICommand ToggleFullScreenCommand =
|
||||
new("Plein écran", nameof(ToggleFullScreenCommand), typeof(WindowModeAssist));
|
||||
|
||||
/// <summary>Préfixe des ressources de la fenêtre (CornerRadius, Thickness) remises à zéro en plein écran.</summary>
|
||||
public const string ChromeResourcePrefix = "WindowChrome.";
|
||||
|
||||
/// <summary>
|
||||
/// Modes mémorisés d'une fenêtre à l'autre (<see cref="PersistenceKeyProperty"/>). Null : rien n'est mémorisé
|
||||
/// (mode --capture, tests). Fixé par l'application avant l'affichage des fenêtres.
|
||||
/// </summary>
|
||||
public static WindowModeStore? Store { get; set; }
|
||||
|
||||
// ------------------------------------------------------------------ Propriétés attachées
|
||||
|
||||
public static readonly DependencyProperty AllowFullScreenProperty = DependencyProperty.RegisterAttached(
|
||||
"AllowFullScreen", typeof(bool), typeof(WindowModeAssist), new PropertyMetadata(false, OnAllowFullScreenChanged));
|
||||
|
||||
public static bool GetAllowFullScreen(DependencyObject element) => (bool)element.GetValue(AllowFullScreenProperty);
|
||||
public static void SetAllowFullScreen(DependencyObject element, bool value) => element.SetValue(AllowFullScreenProperty, value);
|
||||
|
||||
/// <summary>Mode courant de la fenêtre (liable ; les déclencheurs des styles s'en servent).</summary>
|
||||
public static readonly DependencyProperty IsFullScreenProperty = DependencyProperty.RegisterAttached(
|
||||
"IsFullScreen", typeof(bool), typeof(WindowModeAssist),
|
||||
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnIsFullScreenChanged));
|
||||
|
||||
public static bool GetIsFullScreen(DependencyObject element) => (bool)element.GetValue(IsFullScreenProperty);
|
||||
public static void SetIsFullScreen(DependencyObject element, bool value) => element.SetValue(IsFullScreenProperty, value);
|
||||
|
||||
/// <summary>Clé sous laquelle le mode de la fenêtre est mémorisé (« main », « admin »…) ; vide : non mémorisé.</summary>
|
||||
public static readonly DependencyProperty PersistenceKeyProperty = DependencyProperty.RegisterAttached(
|
||||
"PersistenceKey", typeof(string), typeof(WindowModeAssist), new PropertyMetadata(null));
|
||||
|
||||
public static string? GetPersistenceKey(DependencyObject element) => (string?)element.GetValue(PersistenceKeyProperty);
|
||||
public static void SetPersistenceKey(DependencyObject element, string? value) => element.SetValue(PersistenceKeyProperty, value);
|
||||
|
||||
// État interne d'une fenêtre inscrite.
|
||||
private static readonly DependencyProperty StateProperty = DependencyProperty.RegisterAttached(
|
||||
"State", typeof(WindowModeState), typeof(WindowModeAssist), new PropertyMetadata(null));
|
||||
|
||||
private sealed class WindowModeState
|
||||
{
|
||||
public readonly Dictionary<object, object> WindowedChrome = new();
|
||||
public bool Syncing;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ API pour le code
|
||||
|
||||
/// <summary>Bascule le mode d'une fenêtre inscrite (sans effet sinon).</summary>
|
||||
public static void Toggle(Window window)
|
||||
{
|
||||
if (GetAllowFullScreen(window)) SetIsFullScreen(window, !GetIsFullScreen(window));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Réaffiche une fenêtre réduite dans SON mode (plein écran ou fenêtré) : à utiliser à la place de
|
||||
/// <c>WindowState = Normal</c>, qui ferait quitter le plein écran.
|
||||
/// </summary>
|
||||
public static void RestoreFromMinimized(Window window)
|
||||
{
|
||||
if (window.WindowState != WindowState.Minimized) return;
|
||||
window.WindowState = GetAllowFullScreen(window) && GetIsFullScreen(window) ? WindowState.Maximized : WindowState.Normal;
|
||||
}
|
||||
|
||||
/// <summary>Déplacement par la barre de titre : seulement en mode fenêtré (en plein écran, la fenêtre reste en place).</summary>
|
||||
public static void DragMove(Window window)
|
||||
{
|
||||
if (window.WindowState != WindowState.Normal) return;
|
||||
try
|
||||
{
|
||||
window.DragMove();
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// bouton de la souris déjà relâché
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Zone utile (hors barre des tâches) de l'écran où se trouve l'élément, en pixels indépendants (DIP).</summary>
|
||||
public static Rect WorkArea(Visual? reference)
|
||||
{
|
||||
if (reference is not null && PresentationSource.FromVisual(reference) is HwndSource { Handle: var handle } source &&
|
||||
handle != IntPtr.Zero && source.CompositionTarget is { } target)
|
||||
{
|
||||
var monitor = MonitorFromWindow(handle, MonitorDefaultToNearest);
|
||||
var info = new MonitorInfo { Size = Marshal.SizeOf<MonitorInfo>() };
|
||||
if (monitor != IntPtr.Zero && GetMonitorInfo(monitor, ref info))
|
||||
{
|
||||
var fromDevice = target.TransformFromDevice;
|
||||
var topLeft = fromDevice.Transform(new Point(info.Work.Left, info.Work.Top));
|
||||
var bottomRight = fromDevice.Transform(new Point(info.Work.Right, info.Work.Bottom));
|
||||
return new Rect(topLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
return SystemParameters.WorkArea;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Mise en place
|
||||
|
||||
private static void OnAllowFullScreenChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (element is not Window window) return;
|
||||
if (e.NewValue is true)
|
||||
{
|
||||
if (window.GetValue(StateProperty) is not null) return;
|
||||
window.SetValue(StateProperty, new WindowModeState());
|
||||
window.CommandBindings.Add(new CommandBinding(ToggleFullScreenCommand, (_, args) =>
|
||||
{
|
||||
Toggle(window);
|
||||
args.Handled = true;
|
||||
}, (_, args) =>
|
||||
{
|
||||
args.CanExecute = true;
|
||||
args.Handled = true;
|
||||
}));
|
||||
window.InputBindings.Add(new KeyBinding(ToggleFullScreenCommand, Key.F11, ModifierKeys.None));
|
||||
window.SourceInitialized += OnSourceInitialized;
|
||||
window.StateChanged += OnStateChanged;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetIsFullScreen(window, false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Juste avant le premier affichage : taille adaptée à l'écran, puis mode mémorisé.</summary>
|
||||
private static void OnSourceInitialized(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is not Window window) return;
|
||||
// Une fenêtre sans bordure agrandie couvrirait par défaut tout l'écran, barre des tâches comprise :
|
||||
// sa taille agrandie est ramenée à la zone utile de son écran (WM_GETMINMAXINFO).
|
||||
if (PresentationSource.FromVisual(window) is HwndSource source) source.AddHook(LimitMaximizedToWorkArea);
|
||||
FitToWorkArea(window);
|
||||
if (GetPersistenceKey(window) is { Length: > 0 } key && Store?.Get(key) == true) SetIsFullScreen(window, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Mode fenêtré : jamais plus grande que la zone utile de son écran, et entièrement visible. Une fenêtre placée
|
||||
/// volontairement hors des écrans (captures de tools\UiSnapshot) n'est pas déplacée.
|
||||
/// </summary>
|
||||
private static void FitToWorkArea(Window window)
|
||||
{
|
||||
var work = WorkArea(window);
|
||||
if (work.Width < 1 || work.Height < 1) return;
|
||||
if (window.Width > work.Width) window.Width = work.Width;
|
||||
if (window.Height > work.Height) window.Height = work.Height;
|
||||
if (double.IsNaN(window.Left) || double.IsNaN(window.Top)) return;
|
||||
if (!work.IntersectsWith(new Rect(window.Left, window.Top, window.Width, window.Height))) return;
|
||||
if (window.Left + window.Width > work.Right) window.Left = work.Right - window.Width;
|
||||
if (window.Top + window.Height > work.Bottom) window.Top = work.Bottom - window.Height;
|
||||
if (window.Left < work.Left) window.Left = work.Left;
|
||||
if (window.Top < work.Top) window.Top = work.Top;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Changement de mode
|
||||
|
||||
private static void OnIsFullScreenChanged(DependencyObject element, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (element is not Window window || window.GetValue(StateProperty) is not WindowModeState state) return;
|
||||
var fullScreen = e.NewValue is true;
|
||||
if (fullScreen && !GetAllowFullScreen(window))
|
||||
{
|
||||
SetIsFullScreen(window, false);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyChrome(window, state, fullScreen);
|
||||
if (!state.Syncing)
|
||||
{
|
||||
state.Syncing = true;
|
||||
try
|
||||
{
|
||||
// Fenêtre réduite : le mode s'appliquera à sa réapparition (RestoreFromMinimized).
|
||||
if (window.WindowState != WindowState.Minimized)
|
||||
window.WindowState = fullScreen ? WindowState.Maximized : WindowState.Normal;
|
||||
}
|
||||
finally
|
||||
{
|
||||
state.Syncing = false;
|
||||
}
|
||||
}
|
||||
if (GetPersistenceKey(window) is { Length: > 0 } key) Store?.Set(key, fullScreen);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// État changé par Windows (Win+Flèche bas, réduction depuis la barre des tâches…) : le mode suit. Une fenêtre réduite
|
||||
/// garde son mode ; une fenêtre agrandie n'existe qu'en plein écran (pas d'agrandissement partiel).
|
||||
/// </summary>
|
||||
private static void OnStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (sender is not Window window || window.GetValue(StateProperty) is not WindowModeState state || state.Syncing) return;
|
||||
if (window.WindowState == WindowState.Minimized) return;
|
||||
var fullScreen = window.WindowState == WindowState.Maximized;
|
||||
if (fullScreen == GetIsFullScreen(window)) return;
|
||||
state.Syncing = true;
|
||||
try
|
||||
{
|
||||
SetIsFullScreen(window, fullScreen);
|
||||
}
|
||||
finally
|
||||
{
|
||||
state.Syncing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ressources « WindowChrome.* » de la fenêtre : 0 en plein écran, valeurs d'origine en mode fenêtré.</summary>
|
||||
private static void ApplyChrome(Window window, WindowModeState state, bool fullScreen)
|
||||
{
|
||||
if (fullScreen)
|
||||
{
|
||||
foreach (var key in window.Resources.Keys.OfType<string>().Where(k => k.StartsWith(ChromeResourcePrefix, StringComparison.Ordinal)).ToList())
|
||||
{
|
||||
var value = window.Resources[key];
|
||||
object? flat = value switch
|
||||
{
|
||||
CornerRadius => new CornerRadius(0),
|
||||
Thickness => new Thickness(0),
|
||||
_ => null
|
||||
};
|
||||
if (flat is null) continue;
|
||||
state.WindowedChrome.TryAdd(key, value);
|
||||
window.Resources[key] = flat;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var (key, value) in state.WindowedChrome) window.Resources[key] = value;
|
||||
state.WindowedChrome.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Agrandie = zone utile de l'écran
|
||||
|
||||
private const int WmGetMinMaxInfo = 0x0024;
|
||||
|
||||
/// <summary>Marge laissée sur le bord d'une barre des tâches masquée automatiquement, pour qu'elle puisse réapparaître.</summary>
|
||||
private const int AutoHideMargin = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Taille et position de la fenêtre agrandie : la zone utile de l'écran qui la contient (hors barre des tâches et autres
|
||||
/// barres d'application). Barre des tâches en masquage automatique : un liseré de 2 pixels lui est laissé sur son bord,
|
||||
/// sinon la souris ne pourrait plus la faire apparaître.
|
||||
/// </summary>
|
||||
private static IntPtr LimitMaximizedToWorkArea(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg != WmGetMinMaxInfo || lParam == IntPtr.Zero) return IntPtr.Zero;
|
||||
var monitor = MonitorFromWindow(hwnd, MonitorDefaultToNearest);
|
||||
var info = new MonitorInfo { Size = Marshal.SizeOf<MonitorInfo>() };
|
||||
if (monitor == IntPtr.Zero || !GetMonitorInfo(monitor, ref info)) return IntPtr.Zero;
|
||||
|
||||
var work = info.Work;
|
||||
if (work.Left == info.Monitor.Left && work.Top == info.Monitor.Top &&
|
||||
work.Right == info.Monitor.Right && work.Bottom == info.Monitor.Bottom)
|
||||
{
|
||||
// Aucune place réservée : barre des tâches absente de cet écran, ou masquée automatiquement.
|
||||
if (HasAutoHideBar(AppBarEdgeLeft, info.Monitor)) work.Left += AutoHideMargin;
|
||||
else if (HasAutoHideBar(AppBarEdgeTop, info.Monitor)) work.Top += AutoHideMargin;
|
||||
else if (HasAutoHideBar(AppBarEdgeRight, info.Monitor)) work.Right -= AutoHideMargin;
|
||||
else if (HasAutoHideBar(AppBarEdgeBottom, info.Monitor)) work.Bottom -= AutoHideMargin;
|
||||
}
|
||||
|
||||
var minMax = Marshal.PtrToStructure<MinMaxInfo>(lParam);
|
||||
// Position relative au coin de l'écran (Windows l'applique à l'écran où la fenêtre est agrandie).
|
||||
minMax.MaxPosition = new NativePoint { X = work.Left - info.Monitor.Left, Y = work.Top - info.Monitor.Top };
|
||||
minMax.MaxSize = new NativePoint { X = work.Right - work.Left, Y = work.Bottom - work.Top };
|
||||
Marshal.StructureToPtr(minMax, lParam, false);
|
||||
handled = true;
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
private static bool HasAutoHideBar(int edge, NativeRect monitor)
|
||||
{
|
||||
var data = new AppBarData { Size = Marshal.SizeOf<AppBarData>(), Edge = edge, Rect = monitor };
|
||||
return SHAppBarMessage(AppBarGetAutoHideBarEx, ref data) != IntPtr.Zero;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Win32
|
||||
|
||||
private const int MonitorDefaultToNearest = 2;
|
||||
private const int AppBarGetAutoHideBarEx = 0x0000000B;
|
||||
private const int AppBarEdgeLeft = 0, AppBarEdgeTop = 1, AppBarEdgeRight = 2, AppBarEdgeBottom = 3;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativePoint
|
||||
{
|
||||
public int X, Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MinMaxInfo
|
||||
{
|
||||
public NativePoint Reserved;
|
||||
public NativePoint MaxSize;
|
||||
public NativePoint MaxPosition;
|
||||
public NativePoint MinTrackSize;
|
||||
public NativePoint MaxTrackSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct AppBarData
|
||||
{
|
||||
public int Size;
|
||||
public IntPtr Window;
|
||||
public uint CallbackMessage;
|
||||
public int Edge;
|
||||
public NativeRect Rect;
|
||||
public IntPtr Param;
|
||||
}
|
||||
|
||||
[DllImport("shell32.dll")]
|
||||
private static extern IntPtr SHAppBarMessage(int message, ref AppBarData data);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct NativeRect
|
||||
{
|
||||
public int Left, Top, Right, Bottom;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MonitorInfo
|
||||
{
|
||||
public int Size;
|
||||
public NativeRect Monitor;
|
||||
public NativeRect Work;
|
||||
public uint Flags;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr MonitorFromWindow(IntPtr hwnd, int flags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetMonitorInfo(IntPtr monitor, ref MonitorInfo info);
|
||||
}
|
||||
@@ -116,3 +116,21 @@ public sealed class NullToVisibilityConverter : IValueConverter
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hauteur d'une page « en deux colonnes » du menu d'administration : toute la zone visible (arrondie à l'inférieur, pour
|
||||
/// ne jamais faire apparaître d'ascenseur inutile), mais jamais moins que le paramètre (pixels) : sur un très petit écran,
|
||||
/// la page entière défile alors, et rien n'est rogné.
|
||||
/// </summary>
|
||||
public sealed class ViewportHeightConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
var viewport = value is double d && !double.IsNaN(d) && !double.IsInfinity(d) ? Math.Floor(d) : 0;
|
||||
var minimum = double.TryParse(parameter as string, NumberStyles.Float, CultureInfo.InvariantCulture, out var m) ? m : 0;
|
||||
return Math.Max(viewport, minimum);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) =>
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
(1.0.0 → 1.0.1 → 1.1.0…). Les launchers des joueurs ne se mettent à jour que vers
|
||||
une version STRICTEMENT supérieure à la leur.
|
||||
-->
|
||||
<Version>2.4.1</Version>
|
||||
<Version>2.5.0</Version>
|
||||
<Authors>Lanisys</Authors>
|
||||
<Company>LanisysGames</Company>
|
||||
<Copyright>© LanisysGames</Copyright>
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using GameLauncher.Core.Utils;
|
||||
|
||||
namespace GameLauncher.Platform;
|
||||
|
||||
/// <summary>
|
||||
/// Mode d'affichage mémorisé de chaque fenêtre qui le permet (plein écran ou fenêtré) :
|
||||
/// %AppData%\<Publisher>\GameLauncher\window-modes.json, ex. { "main": true, "admin": false }.
|
||||
/// Simple préférence d'affichage, sans donnée personnelle ; fichier illisible ou modifié à la main : ignoré.
|
||||
/// </summary>
|
||||
public sealed partial class WindowModeStore
|
||||
{
|
||||
public const string FileName = "window-modes.json";
|
||||
private const int MaxFileSize = 4 * 1024;
|
||||
private const int MaxEntries = 32;
|
||||
|
||||
private readonly string _path;
|
||||
private readonly Dictionary<string, bool> _modes;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public WindowModeStore(string path)
|
||||
{
|
||||
_path = path;
|
||||
_modes = Load(path);
|
||||
}
|
||||
|
||||
/// <summary>Plein écran mémorisé pour cette fenêtre (null : jamais choisi).</summary>
|
||||
public bool? Get(string key)
|
||||
{
|
||||
lock (_lock) return _modes.TryGetValue(key, out var fullScreen) ? fullScreen : null;
|
||||
}
|
||||
|
||||
public void Set(string key, bool fullScreen)
|
||||
{
|
||||
if (!IsValidKey(key)) return;
|
||||
Dictionary<string, bool> snapshot;
|
||||
lock (_lock)
|
||||
{
|
||||
if (_modes.TryGetValue(key, out var current) && current == fullScreen) return;
|
||||
if (!_modes.ContainsKey(key) && _modes.Count >= MaxEntries) return;
|
||||
_modes[key] = fullScreen;
|
||||
snapshot = new Dictionary<string, bool>(_modes);
|
||||
}
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(_path))!);
|
||||
FileUtil.WriteAllTextAtomic(_path, JsonSerializer.Serialize(snapshot, JsonDefaults.Options));
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
Log.Warn($"Mode d'affichage non mémorisé ({_path}) : {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, bool> Load(string path)
|
||||
{
|
||||
var modes = new Dictionary<string, bool>(StringComparer.Ordinal);
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path) || new FileInfo(path).Length > MaxFileSize) return modes;
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, bool>>(File.ReadAllText(path), JsonDefaults.Options);
|
||||
foreach (var (key, value) in loaded ?? new Dictionary<string, bool>())
|
||||
if (IsValidKey(key) && modes.Count < MaxEntries) modes[key] = value;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or JsonException)
|
||||
{
|
||||
Log.Warn($"Modes d'affichage illisibles ({path}) : {ex.Message}");
|
||||
}
|
||||
return modes;
|
||||
}
|
||||
|
||||
private static bool IsValidKey(string key) => KeyPattern().IsMatch(key);
|
||||
|
||||
[GeneratedRegex("^[a-z0-9][a-z0-9.-]{0,31}$")]
|
||||
private static partial Regex KeyPattern();
|
||||
}
|
||||
@@ -40,7 +40,7 @@ public sealed class WindowService : IWindowService
|
||||
public void Restore()
|
||||
{
|
||||
if (!_window.IsVisible) _window.Show(); // l'icône de la zone de notification disparaît (voir App)
|
||||
if (_window.WindowState == WindowState.Minimized) _window.WindowState = WindowState.Normal;
|
||||
Controls.WindowModeAssist.RestoreFromMinimized(_window); // dans son mode : plein écran ou fenêtré
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ public sealed class AdminWindowService : IDisposable
|
||||
{
|
||||
if (_window is not null)
|
||||
{
|
||||
if (_window.WindowState == WindowState.Minimized) _window.WindowState = WindowState.Normal;
|
||||
Controls.WindowModeAssist.RestoreFromMinimized(_window); // dans son mode : plein écran ou fenêtré
|
||||
_window.Activate();
|
||||
return;
|
||||
}
|
||||
|
||||
+108
-105
@@ -216,13 +216,14 @@
|
||||
<ControlTemplate TargetType="ListBoxItem">
|
||||
<Grid Background="Transparent">
|
||||
<Border x:Name="Bd" CornerRadius="12" Background="Transparent" Padding="12,10">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path x:Name="Icon" Width="18" Height="18" Stretch="Uniform" VerticalAlignment="Center"
|
||||
<DockPanel>
|
||||
<Path x:Name="Icon" DockPanel.Dock="Left" Width="18" Height="18" Stretch="Uniform" VerticalAlignment="Center"
|
||||
Fill="{DynamicResource TextSecondaryBrush}"
|
||||
Data="{Binding IconKey, Converter={StaticResource IconKey}}" />
|
||||
<TextBlock x:Name="Label" Text="{Binding Title}" Margin="12,0,0,0" VerticalAlignment="Center"
|
||||
FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource TextSecondaryBrush}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border x:Name="Indicator" Width="4" Height="20" CornerRadius="2" HorizontalAlignment="Left"
|
||||
Margin="-2,0,0,0" Background="{DynamicResource AccentGradientBrush}" Opacity="0" />
|
||||
@@ -359,10 +360,10 @@
|
||||
<StackPanel>
|
||||
<!-- Erreur de chargement -->
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Compte connecté -->
|
||||
@@ -376,12 +377,13 @@
|
||||
<TextBlock Text="{Binding Initials}" Style="{StaticResource AdminAvatarTextStyle}" FontSize="17" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1" Margin="16,0,0,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="{Binding DisplayName}" FontSize="17" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<Border Style="{StaticResource AdminChipStyle}" Margin="12,0,0,0" Padding="9,2" VerticalAlignment="Center">
|
||||
<WrapPanel>
|
||||
<TextBlock Text="{Binding DisplayName}" FontSize="17" FontWeight="SemiBold" VerticalAlignment="Center" Margin="0,0,12,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Style="{StaticResource AdminChipStyle}" Margin="0" Padding="9,2" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding RoleText}" FontSize="11" FontWeight="SemiBold" Foreground="{DynamicResource Accent2Brush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
<TextBlock Text="{Binding KeyText}" Style="{StaticResource AdminHintStyle}" Margin="0,4,0,0" />
|
||||
<TextBlock Text="{Binding SessionText}" Style="{StaticResource AdminHintStyle}" Margin="0,2,0,0" />
|
||||
<Button Style="{StaticResource LinkButtonStyle}" Content="Mes appareils" HorizontalAlignment="Left" Margin="0,6,0,12"
|
||||
@@ -492,7 +494,8 @@
|
||||
<Run Text="{Binding Name, Mode=OneWay}" FontWeight="SemiBold" />
|
||||
<Run Text="{Binding GameId, Mode=OneWay}" Foreground="{DynamicResource TextMutedBrush}" />
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Column="1" Text="{Binding ManifestHost}" FontSize="12" Margin="12,0,0,0"
|
||||
<TextBlock Grid.Column="1" Text="{Binding ManifestHost}" FontSize="12" Margin="12,0,0,0" MaxWidth="320"
|
||||
TextTrimming="CharacterEllipsis" ToolTip="{Binding ManifestHost}"
|
||||
Foreground="{DynamicResource TextMutedBrush}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
@@ -567,21 +570,21 @@
|
||||
<StackPanel DockPanel.Dock="Top" Margin="0,0,14,0">
|
||||
<!-- Erreur (chargement, publication refusée par le serveur…) -->
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Publication non configurée sur le serveur -->
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource WarningBrush}"
|
||||
Visibility="{Binding ShowPublishingDisabled, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Alert}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Alert}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620"
|
||||
<TextBlock Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5"
|
||||
Text="Publication désactivée : le fichier public du catalogue n'est pas configuré sur le serveur (LauncherApi:Catalog:PublicFile). Les modifications peuvent être préparées mais pas publiées." />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Publié entre-temps par un autre administrateur -->
|
||||
@@ -600,11 +603,11 @@
|
||||
<!-- Résultat de la dernière publication -->
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
@@ -984,7 +987,7 @@
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource Accent2Brush}" Data="{StaticResource Icon.Gamepad}" />
|
||||
</Border>
|
||||
<TextBlock DockPanel.Dock="Left" Text="Jeu" Style="{StaticResource AdminFieldLabelStyle}" Margin="0,0,12,0" VerticalAlignment="Center" />
|
||||
<ComboBox Style="{StaticResource ModernComboBoxStyle}" Width="360" HorizontalAlignment="Left"
|
||||
<ComboBox Style="{StaticResource ModernComboBoxStyle}" MaxWidth="360" HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding GameChoices}" SelectedItem="{Binding SelectedGame, Mode=TwoWay}" DisplayMemberPath="Label"
|
||||
IsEnabled="{Binding IsGameSwitchEnabled}" ToolTip="Changer de jeu" />
|
||||
</DockPanel>
|
||||
@@ -998,10 +1001,10 @@
|
||||
<StackPanel DockPanel.Dock="Top" Margin="0,0,14,0">
|
||||
<!-- Erreur (chargement, jeu non configuré sur le serveur, refus…) -->
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Manifest modifié entre-temps -->
|
||||
@@ -1020,11 +1023,11 @@
|
||||
<!-- Résultat du dernier enregistrement -->
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Jeu : à choisir avant tout (plusieurs jeux), puis liste pour en changer -->
|
||||
@@ -1227,21 +1230,21 @@
|
||||
<StackPanel>
|
||||
<!-- Erreur (contrôle du dossier, refus du serveur…) -->
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Version publiée -->
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Envoi interrompu : reprise ou abandon -->
|
||||
@@ -1277,16 +1280,16 @@
|
||||
<TextBlock Text="Dossier de publication" Style="{StaticResource AdminCardTitleStyle}" />
|
||||
<TextBlock Text="{Binding PublishedText}" Style="{StaticResource AdminHintStyle}" Margin="0,3,0,0" />
|
||||
<!-- Signature de la version en ligne (phase 7) -->
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4,0,0"
|
||||
<DockPanel Margin="0,4,0,0"
|
||||
Visibility="{Binding HasOnlineSignatureText, Converter={StaticResource BoolToVisibility}}">
|
||||
<Path Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.ShieldCheck}"
|
||||
<Path DockPanel.Dock="Left" Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.ShieldCheck}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0"
|
||||
Visibility="{Binding IsOnlineSigned, Converter={StaticResource BoolToVisibility}}" />
|
||||
<Path Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Alert}"
|
||||
<Path DockPanel.Dock="Left" Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Alert}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0"
|
||||
Visibility="{Binding IsOnlineSigned, Converter={StaticResource BoolToVisibility}, ConverterParameter=Invert}" />
|
||||
<TextBlock Text="{Binding OnlineSignatureText}" Style="{StaticResource AdminHintStyle}" Margin="8,0,0,0" MaxWidth="600" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding OnlineSignatureText}" Style="{StaticResource AdminHintStyle}" Margin="8,0,0,0" />
|
||||
</DockPanel>
|
||||
<DockPanel Margin="0,12,0,0">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Margin="12,0,0,0">
|
||||
<Button Style="{StaticResource IconButtonStyle}" Width="30" Height="30" Padding="8" Margin="0,0,6,0"
|
||||
@@ -1348,11 +1351,11 @@
|
||||
Visibility="{Binding HasWarnings, Converter={StaticResource BoolToVisibility}}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
|
||||
<Path Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Alert}"
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<Path DockPanel.Dock="Left" Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Alert}"
|
||||
VerticalAlignment="Top" Margin="0,2,0,0" />
|
||||
<TextBlock Text="{Binding}" Margin="8,0,0,0" FontSize="12.5" TextWrapping="Wrap" MaxWidth="600" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding}" Margin="8,0,0,0" FontSize="12.5" TextWrapping="Wrap" />
|
||||
</DockPanel>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
@@ -1484,19 +1487,19 @@
|
||||
<DataTemplate DataType="{x:Type aacc:MyDevicesPanelViewModel}">
|
||||
<StackPanel>
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<ContentControl Content="{Binding CreatedInvitation}" Focusable="False"
|
||||
@@ -1600,19 +1603,19 @@
|
||||
<DataTemplate DataType="{x:Type aacc:AdminsPanelViewModel}">
|
||||
<StackPanel>
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<ContentControl Content="{Binding CreatedInvitation}" Focusable="False"
|
||||
@@ -1747,11 +1750,11 @@
|
||||
<!-- Groupe système, ou votre propre groupe -->
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource WarningBrush}"
|
||||
Visibility="{Binding HasNotice, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Lock}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource WarningBrush}" Data="{StaticResource Icon.Lock}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding Notice}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="560" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Notice}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Nom du groupe" Style="{StaticResource AdminFieldLabelStyle}" />
|
||||
@@ -1842,19 +1845,19 @@
|
||||
<DockPanel>
|
||||
<StackPanel DockPanel.Dock="Top" Margin="0,0,14,0">
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
@@ -1986,11 +1989,11 @@
|
||||
<DataTemplate DataType="{x:Type aaud:AuditPanelViewModel}">
|
||||
<StackPanel>
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Filtres -->
|
||||
@@ -2004,7 +2007,7 @@
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<StackPanel>
|
||||
<TextBlock Text="Rechercher (nom, adresse IP, clé, version…)" Style="{StaticResource AdminFieldLabelStyle}" />
|
||||
<TextBlock Text="Rechercher (nom, appareil, adresse IP, clé…)" Style="{StaticResource AdminFieldLabelStyle}" />
|
||||
<TextBox Style="{StaticResource ModernTextBoxStyle}" MaxLength="100"
|
||||
Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged, Delay=250}" />
|
||||
</StackPanel>
|
||||
@@ -2142,19 +2145,19 @@
|
||||
<DataTemplate DataType="{x:Type asig:SigningPanelViewModel}">
|
||||
<StackPanel>
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource WarningBrush}"
|
||||
Visibility="{Binding ShowPublishingDisabled, Converter={StaticResource BoolToVisibility}}">
|
||||
@@ -2175,14 +2178,14 @@
|
||||
<StackPanel Grid.Column="1" Margin="14,0,0,0">
|
||||
<TextBlock Text="Trousseau publié" Style="{StaticResource AdminCardTitleStyle}" />
|
||||
<TextBlock Text="{Binding KeyringText}" Style="{StaticResource AdminHintStyle}" Margin="0,3,0,0" />
|
||||
<StackPanel Orientation="Horizontal" Margin="0,6,0,0" Visibility="{Binding HasKeyring, Converter={StaticResource BoolToVisibility}}">
|
||||
<Path Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.ShieldCheck}"
|
||||
<DockPanel Margin="0,6,0,0" Visibility="{Binding HasKeyring, Converter={StaticResource BoolToVisibility}}">
|
||||
<Path DockPanel.Dock="Left" Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.ShieldCheck}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" Visibility="{Binding RootValid, Converter={StaticResource BoolToVisibility}}" />
|
||||
<Path Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<Path DockPanel.Dock="Left" Width="13" Height="13" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0"
|
||||
Visibility="{Binding RootValid, Converter={StaticResource BoolToVisibility}, ConverterParameter=Invert}" />
|
||||
<TextBlock Text="{Binding RootText}" FontSize="12" Margin="8,0,0,0" TextWrapping="Wrap" MaxWidth="600" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding RootText}" FontSize="12" Margin="8,0,0,0" TextWrapping="Wrap" />
|
||||
</DockPanel>
|
||||
<TextBlock FontSize="12" Margin="0,8,0,0" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"
|
||||
Visibility="{Binding KeyringUrl, Converter={StaticResource StringToVisibility}}">
|
||||
<Run Text="Adresse à mettre dans « keyringUrl » de launcher.json :" />
|
||||
@@ -2321,21 +2324,21 @@
|
||||
<StackPanel>
|
||||
<!-- Erreur (refus du serveur : fichier utilisé, droits du dossier…) -->
|
||||
<Border Style="{StaticResource AdminErrorBannerStyle}" Visibility="{Binding HasError, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="16" Height="16" Stretch="Uniform" Fill="{DynamicResource DangerBrush}" Data="{StaticResource Icon.AlertCircle}"
|
||||
VerticalAlignment="Top" />
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding ErrorMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Résultat de la dernière suppression -->
|
||||
<Border Style="{StaticResource AdminInfoBannerStyle}" BorderBrush="{DynamicResource SuccessBrush}"
|
||||
Visibility="{Binding HasStatus, Converter={StaticResource BoolToVisibility}}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Path Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
<DockPanel>
|
||||
<Path DockPanel.Dock="Left" Width="15" Height="15" Stretch="Uniform" Fill="{DynamicResource SuccessBrush}" Data="{StaticResource Icon.CheckCircle}"
|
||||
VerticalAlignment="Top" Margin="0,1,0,0" />
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" MaxWidth="620" />
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding StatusMessage}" Margin="10,0,0,0" TextWrapping="Wrap" FontSize="12.5" />
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Espace disque -->
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<conv:StringToVisibilityConverter x:Key="StringToVisibility" />
|
||||
<conv:IconKeyConverter x:Key="IconKey" />
|
||||
<conv:KindToResourceConverter x:Key="KindToResource" />
|
||||
<conv:ViewportHeightConverter x:Key="ViewportHeight" />
|
||||
|
||||
<!-- ============================================================ Focus clavier discret -->
|
||||
<Style x:Key="FocusVisualStyle">
|
||||
@@ -69,6 +70,26 @@
|
||||
<Setter Property="ui:IconAssist.HoverBrush" Value="{DynamicResource DangerBrush}" />
|
||||
</Style>
|
||||
|
||||
<!--
|
||||
Plein écran / mode fenêtré (F11), pour toute fenêtre inscrite avec ui:WindowModeAssist.AllowFullScreen="True" :
|
||||
icône et infobulle suivent le mode ; bouton masqué dans une fenêtre qui ne le permet pas.
|
||||
Usage : <Button Style="{StaticResource WindowModeButtonStyle}" />, entre « Réduire » et « Fermer ».
|
||||
-->
|
||||
<Style x:Key="WindowModeButtonStyle" TargetType="Button" BasedOn="{StaticResource IconButtonStyle}">
|
||||
<Setter Property="Command" Value="{x:Static ui:WindowModeAssist.ToggleFullScreenCommand}" />
|
||||
<Setter Property="ui:IconAssist.Icon" Value="{StaticResource Icon.Fullscreen}" />
|
||||
<Setter Property="ToolTip" Value="Plein écran (F11)" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=(ui:WindowModeAssist.IsFullScreen), RelativeSource={RelativeSource AncestorType=Window}}" Value="True">
|
||||
<Setter Property="ui:IconAssist.Icon" Value="{StaticResource Icon.FullscreenExit}" />
|
||||
<Setter Property="ToolTip" Value="Quitter le plein écran (F11)" />
|
||||
</DataTrigger>
|
||||
<DataTrigger Binding="{Binding Path=(ui:WindowModeAssist.AllowFullScreen), RelativeSource={RelativeSource AncestorType=Window}}" Value="False">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ Bouton principal (JOUER / INSTALLER…) -->
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Height" Value="60" />
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
<Geometry x:Key="Icon.Cog">M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z</Geometry>
|
||||
<Geometry x:Key="Icon.Close">M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z</Geometry>
|
||||
<Geometry x:Key="Icon.Minimize">M20,14H4V10H20</Geometry>
|
||||
<!-- Plein écran (F11) / quitter le plein écran : bouton des fenêtres qui le permettent (WindowModeAssist) -->
|
||||
<Geometry x:Key="Icon.Fullscreen">M5,5H10V7H7V10H5V5M14,5H19V10H17V7H14V5M17,14H19V19H14V17H17V14M10,17V19H5V14H7V17H10Z</Geometry>
|
||||
<Geometry x:Key="Icon.FullscreenExit">M14,14H19V16H16V19H14V14M5,14H10V19H8V16H5V14M8,5H10V10H5V8H8V5M19,8V10H14V5H16V8H19Z</Geometry>
|
||||
<Geometry x:Key="Icon.WindowRestore">M4,8H8V4H20V16H16V20H4V8M16,8V14H18V6H10V8H16M6,12V18H14V12H6Z</Geometry>
|
||||
<Geometry x:Key="Icon.ExitToApp">M19,3H5C3.89,3 3,3.89 3,5V9H5V5H19V19H5V15H3V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3M10.08,15.58L11.5,17L16.5,12L11.5,7L10.08,8.41L12.67,11H3V13H12.67L10.08,15.58Z</Geometry>
|
||||
<Geometry x:Key="Icon.Folder">M10,4H4C2.89,4 2,4.89 2,6V18A2,2 0 0,0 4,20H20A2,2 0 0,0 22,18V8C22,6.89 21.1,6 20,6H12L10,4Z</Geometry>
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||
UseLayoutRounding="True"
|
||||
TextOptions.TextFormattingMode="Ideal"
|
||||
ui:WheelAssist.IsEnabled="True">
|
||||
ui:WheelAssist.IsEnabled="True"
|
||||
ui:WindowModeAssist.AllowFullScreen="True"
|
||||
ui:WindowModeAssist.PersistenceKey="admin">
|
||||
|
||||
<!--
|
||||
Menu d'administration (accès discret : Ctrl+Maj+Alt+A, ou 5 clics rapides sur le logo).
|
||||
@@ -27,6 +29,29 @@
|
||||
• AdminWorkspaceViewModel → navigation entre les pages des modules + page affichée.
|
||||
-->
|
||||
<Window.Resources>
|
||||
<!--
|
||||
Mode fenêtré : ombre et coins arrondis. En plein écran (F11, bouton de la barre de titre), WindowModeAssist met ces
|
||||
ressources « WindowChrome.* » à 0. Toute page d'un module (présente ou future) s'affiche dans cette fenêtre et
|
||||
profite donc du plein écran sans rien déclarer.
|
||||
-->
|
||||
<Thickness x:Key="WindowChrome.ShadowMargin">20</Thickness>
|
||||
<CornerRadius x:Key="WindowChrome.Radius">20</CornerRadius>
|
||||
<CornerRadius x:Key="WindowChrome.NavRadius">0,0,0,20</CornerRadius>
|
||||
|
||||
<!-- Ombre portée : inutile (et coûteuse) en plein écran, où elle serait hors de l'écran -->
|
||||
<Style x:Key="WindowShadowStyle" TargetType="Border">
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect BlurRadius="32" ShadowDepth="8" Direction="270" Opacity="0.6" Color="Black" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=(ui:WindowModeAssist.IsFullScreen), RelativeSource={RelativeSource AncestorType=Window}}" Value="True">
|
||||
<Setter Property="Effect" Value="{x:Null}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<DataTemplate DataType="{x:Type avm:LoginViewModel}">
|
||||
<local:LoginView />
|
||||
</DataTemplate>
|
||||
@@ -44,7 +69,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Navigation -->
|
||||
<Border Background="{DynamicResource RailBrush}" CornerRadius="0,0,0,20"
|
||||
<Border Background="{DynamicResource RailBrush}" CornerRadius="{DynamicResource WindowChrome.NavRadius}"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,1,0">
|
||||
<DockPanel Margin="16,20,16,18">
|
||||
<!-- Compte connecté -->
|
||||
@@ -93,7 +118,7 @@
|
||||
<!-- Page affichée -->
|
||||
<DockPanel Grid.Column="1" Margin="32,22,18,18">
|
||||
<StackPanel DockPanel.Dock="Top" Margin="0,0,14,18">
|
||||
<TextBlock Text="{Binding PanelTitle}" FontSize="22" FontWeight="SemiBold" />
|
||||
<TextBlock Text="{Binding PanelTitle}" FontSize="22" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding PanelDescription}" FontSize="12.5" Margin="0,4,0,0" TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
@@ -103,41 +128,54 @@
|
||||
Content="{Binding SelectedPanel.ViewModel}"
|
||||
ContentTemplateSelector="{StaticResource AdminFooterSelector}" />
|
||||
<!-- Retour en haut : page changée, ou demandé par la page (après « Recharger », « Publier »…).
|
||||
Page en deux colonnes (HasSplitLayout) : la fenêtre ne fait plus défiler la page entière ; la page reçoit
|
||||
la hauteur disponible et chaque colonne a son propre ascenseur (Themes/Admin.xaml). -->
|
||||
<ScrollViewer Focusable="False"
|
||||
Page en deux colonnes (HasSplitLayout) : la page reçoit la hauteur visible et chaque colonne a son propre
|
||||
ascenseur (Themes/Admin.xaml) ; sur un très petit écran (moins de 360 px de haut), la page entière défile
|
||||
en plus, pour que rien ne soit jamais rogné (bandeaux en haut compris). -->
|
||||
<ScrollViewer Focusable="False" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"
|
||||
ui:ScrollAssist.ResetOn="{Binding SelectedPanel}"
|
||||
ui:ScrollAssist.ScrollToTopToken="{Binding SelectedPanel.ViewModel.ScrollToTopToken}">
|
||||
<ScrollViewer.Style>
|
||||
<Style TargetType="ScrollViewer">
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Auto" />
|
||||
<Setter Property="Padding" Value="0,0,14,0" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedPanel.ViewModel.HasSplitLayout}" Value="True">
|
||||
<Setter Property="VerticalScrollBarVisibility" Value="Disabled" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ScrollViewer.Style>
|
||||
<!-- Modèle choisi selon le type de la page (Themes/Admin.xaml) -->
|
||||
<ContentControl Content="{Binding SelectedPanel.ViewModel}" Focusable="False" />
|
||||
<ContentControl Content="{Binding SelectedPanel.ViewModel}" Focusable="False">
|
||||
<ContentControl.Style>
|
||||
<Style TargetType="ContentControl">
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding SelectedPanel.ViewModel.HasSplitLayout}" Value="True">
|
||||
<Setter Property="Height"
|
||||
Value="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}, Converter={StaticResource ViewportHeight}, ConverterParameter=360}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</ContentControl.Style>
|
||||
</ContentControl>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="20">
|
||||
<Grid Margin="{DynamicResource WindowChrome.ShadowMargin}">
|
||||
<!-- Ombre portée -->
|
||||
<Border CornerRadius="20" Background="{DynamicResource BackgroundBrush}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="32" ShadowDepth="8" Direction="270" Opacity="0.6" Color="Black" />
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" Background="{DynamicResource BackgroundBrush}"
|
||||
Style="{StaticResource WindowShadowStyle}" />
|
||||
|
||||
<Border CornerRadius="20" Background="{DynamicResource SurfaceSolidBrush}"
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" Background="{DynamicResource SurfaceSolidBrush}"
|
||||
BorderBrush="{DynamicResource WindowBorderBrush}" BorderThickness="1">
|
||||
<!--
|
||||
Responsive (ScaleToFitDecorator) : 1038 × 678 en mode fenêtré. Plus grand (plein écran) : les pages s'étalent.
|
||||
Plus petit : jusqu'à 960 × 560 la mise en page s'adapte (les pages défilent) ; en dessous (petit écran, mise à
|
||||
l'échelle 125-150 %), tout est réduit d'un bloc, sans rien rogner.
|
||||
-->
|
||||
<ui:ScaleToFitDecorator DesignWidth="960" DesignHeight="560">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="64" />
|
||||
@@ -172,6 +210,8 @@
|
||||
</Border>
|
||||
<Button Style="{StaticResource IconButtonStyle}" ui:IconAssist.Icon="{StaticResource Icon.Minimize}"
|
||||
ToolTip="Réduire" Click="OnMinimize" />
|
||||
<!-- Plein écran / mode fenêtré (F11) -->
|
||||
<Button Style="{StaticResource WindowModeButtonStyle}" Padding="9" Margin="4,0,0,0" />
|
||||
<Button Style="{StaticResource CloseButtonStyle}" ui:IconAssist.Icon="{StaticResource Icon.Close}"
|
||||
ToolTip="Fermer (déconnexion)" Margin="4,0,0,0" Click="OnClose" />
|
||||
</StackPanel>
|
||||
@@ -181,6 +221,7 @@
|
||||
<!-- Écran courant -->
|
||||
<ContentControl Grid.Row="1" Content="{Binding Screen}" Focusable="False" />
|
||||
</Grid>
|
||||
</ui:ScaleToFitDecorator>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -29,7 +29,7 @@ public partial class AdminWindow : Window
|
||||
PreviewMouseDown += (_, _) => _shell.NotifyActivity();
|
||||
PreviewMouseWheel += (_, _) => _shell.NotifyActivity();
|
||||
PreviewKeyDown += OnPreviewKeyDown;
|
||||
Loaded += (_, _) => FitToScreen();
|
||||
// Taille adaptée à l'écran (mode fenêtré) et plein écran : ui:WindowModeAssist (AdminWindow.xaml).
|
||||
|
||||
// Session Windows verrouillée (Win+L, mise en veille) : l'administration se verrouille aussi.
|
||||
SystemEvents.SessionSwitch += OnSessionSwitch;
|
||||
@@ -54,16 +54,6 @@ public partial class AdminWindow : Window
|
||||
_shell.NotifyActivity();
|
||||
}
|
||||
|
||||
/// <summary>Jamais plus haute que la zone utile de l'écran (petits écrans, mise à l'échelle 125-150 %).</summary>
|
||||
private void FitToScreen()
|
||||
{
|
||||
var work = SystemParameters.WorkArea;
|
||||
if (Height > work.Height) Height = work.Height;
|
||||
if (Width > work.Width) Width = work.Width;
|
||||
if (Top < work.Top) Top = work.Top;
|
||||
if (Left < work.Left) Left = work.Left;
|
||||
}
|
||||
|
||||
private void OnPreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
_shell.NotifyActivity();
|
||||
@@ -87,10 +77,11 @@ public partial class AdminWindow : Window
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Déplacement par la barre de titre (mode fenêtré seulement).</summary>
|
||||
private void OnTitleBarMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed)
|
||||
DragMove();
|
||||
Controls.WindowModeAssist.DragMove(this);
|
||||
}
|
||||
|
||||
private void OnMinimize(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||
|
||||
@@ -13,14 +13,17 @@ public partial class ChangelogWindow : Window
|
||||
InitializeComponent();
|
||||
SubtitleText.Text = string.IsNullOrWhiteSpace(gameName) ? "Nouveautés et correctifs" : $"{gameName} · nouveautés et correctifs";
|
||||
Entries.ItemsSource = entries;
|
||||
FitToScreen();
|
||||
SourceInitialized += (_, _) => FitToScreen();
|
||||
}
|
||||
|
||||
/// <summary>La fenêtre ne dépasse jamais la zone utile de l'écran : au-delà, les notes défilent.</summary>
|
||||
/// <summary>
|
||||
/// La fenêtre ne dépasse jamais la zone utile de l'écran de sa fenêtre parente (celle-ci peut être en plein écran sur
|
||||
/// un autre écran que l'écran principal) : au-delà, les notes défilent.
|
||||
/// </summary>
|
||||
private void FitToScreen()
|
||||
{
|
||||
const double chrome = 250; // en-tête, bouton, marges et ombre
|
||||
EntriesScroll.MaxHeight = Math.Max(240, SystemParameters.WorkArea.Height - chrome);
|
||||
EntriesScroll.MaxHeight = Math.Max(240, Controls.WindowModeAssist.WorkArea(Owner ?? (System.Windows.Media.Visual)this).Height - chrome);
|
||||
}
|
||||
|
||||
private void OnClose(object sender, RoutedEventArgs e) => Close();
|
||||
|
||||
@@ -14,9 +14,34 @@
|
||||
FontFamily="{DynamicResource UiFont}"
|
||||
Foreground="{DynamicResource TextPrimaryBrush}"
|
||||
UseLayoutRounding="True"
|
||||
TextOptions.TextFormattingMode="Ideal">
|
||||
TextOptions.TextFormattingMode="Ideal"
|
||||
ui:WindowModeAssist.AllowFullScreen="True"
|
||||
ui:WindowModeAssist.PersistenceKey="main">
|
||||
|
||||
<Window.Resources>
|
||||
<!--
|
||||
Mode fenêtré : ombre autour de la fenêtre et coins arrondis. En plein écran (F11, bouton de la barre de titre),
|
||||
WindowModeAssist met ces ressources « WindowChrome.* » à 0 : la fenêtre occupe toute la zone utile de l'écran
|
||||
(barre des tâches visible), sans marge ni arrondi.
|
||||
-->
|
||||
<Thickness x:Key="WindowChrome.ShadowMargin">16</Thickness>
|
||||
<CornerRadius x:Key="WindowChrome.Radius">22</CornerRadius>
|
||||
<CornerRadius x:Key="WindowChrome.RailRadius">22,0,0,22</CornerRadius>
|
||||
|
||||
<!-- Ombre portée : inutile (et coûteuse) en plein écran, où elle serait hors de l'écran -->
|
||||
<Style x:Key="WindowShadowStyle" TargetType="Border">
|
||||
<Setter Property="Effect">
|
||||
<Setter.Value>
|
||||
<DropShadowEffect BlurRadius="28" ShadowDepth="0" Opacity="0.7" Color="Black" />
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding Path=(ui:WindowModeAssist.IsFullScreen), RelativeSource={RelativeSource AncestorType=Window}}" Value="True">
|
||||
<Setter Property="Effect" Value="{x:Null}" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ChipStyle" TargetType="Border">
|
||||
<Setter Property="CornerRadius" Value="16" />
|
||||
<Setter Property="Background" Value="{DynamicResource ChipBrush}" />
|
||||
@@ -166,24 +191,21 @@
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid Margin="16">
|
||||
<Grid Margin="{DynamicResource WindowChrome.ShadowMargin}">
|
||||
<Grid x:Name="Root">
|
||||
<!-- ===================================================== Fond (thème) : ombre, image, lueur, dégradés -->
|
||||
<Border CornerRadius="22" Background="{DynamicResource BackgroundBrush}">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="28" ShadowDepth="0" Opacity="0.7" Color="Black" />
|
||||
</Border.Effect>
|
||||
</Border>
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" Background="{DynamicResource BackgroundBrush}"
|
||||
Style="{StaticResource WindowShadowStyle}" />
|
||||
<!-- Illustration du thème (Default : Assets/background.png, Exovival : Assets/Themes/exovival-background.jpg) -->
|
||||
<Border x:Name="BackgroundLayer" CornerRadius="22">
|
||||
<Border x:Name="BackgroundLayer" CornerRadius="{DynamicResource WindowChrome.Radius}">
|
||||
<Border.Background>
|
||||
<ImageBrush ImageSource="{DynamicResource BackgroundImage}" Stretch="UniformToFill" AlignmentX="Right" />
|
||||
</Border.Background>
|
||||
</Border>
|
||||
<Border CornerRadius="22" Background="{DynamicResource AmbientGlowBrush}" />
|
||||
<Border CornerRadius="22" Background="{DynamicResource HeroOverlayBrush}" />
|
||||
<Border CornerRadius="22" Background="{DynamicResource VignetteBrush}" />
|
||||
<Border CornerRadius="22" BorderBrush="{DynamicResource WindowBorderBrush}" BorderThickness="1" />
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" Background="{DynamicResource AmbientGlowBrush}" />
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" Background="{DynamicResource HeroOverlayBrush}" />
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" Background="{DynamicResource VignetteBrush}" />
|
||||
<Border CornerRadius="{DynamicResource WindowChrome.Radius}" BorderBrush="{DynamicResource WindowBorderBrush}" BorderThickness="1" />
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -192,7 +214,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- ===================================================== Liste des jeux (si plusieurs jeux) -->
|
||||
<Border Width="88" CornerRadius="22,0,0,22" Background="{DynamicResource RailBrush}"
|
||||
<Border Width="88" CornerRadius="{DynamicResource WindowChrome.RailRadius}" Background="{DynamicResource RailBrush}"
|
||||
MouseLeftButtonDown="OnTitleBarMouseDown"
|
||||
BorderBrush="{DynamicResource BorderBrush}" BorderThickness="0,0,1,0"
|
||||
Visibility="{Binding IsMultiGame, Converter={StaticResource BoolToVisibility}}">
|
||||
@@ -208,7 +230,13 @@
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Column="1" DataContext="{Binding SelectedGame}">
|
||||
<!--
|
||||
Responsive (ScaleToFitDecorator) : zone du jeu de 1168 × 708 en mode fenêtré. Plus grand (plein écran) : le contenu
|
||||
s'étale (colonnes fluides). Plus petit : jusqu'à 1100 × 620 la mise en page s'adapte (la présentation et les notes
|
||||
défilent) ; en dessous (petit écran, mise à l'échelle 125-150 %), tout est réduit d'un bloc, sans rien rogner.
|
||||
-->
|
||||
<ui:ScaleToFitDecorator Grid.Column="1" DesignWidth="1100" DesignHeight="620">
|
||||
<Grid DataContext="{Binding SelectedGame}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="76" />
|
||||
<RowDefinition Height="*" />
|
||||
@@ -255,6 +283,8 @@
|
||||
<Button Style="{StaticResource IconButtonStyle}" ToolTip="Réduire" Padding="12"
|
||||
ui:IconAssist.Icon="{StaticResource Icon.Minimize}"
|
||||
Command="{Binding DataContext.MinimizeCommand, RelativeSource={RelativeSource AncestorType=Window}}" />
|
||||
<!-- Plein écran / mode fenêtré (F11) -->
|
||||
<Button Style="{StaticResource WindowModeButtonStyle}" Padding="11" Margin="4,0,0,0" />
|
||||
<Button Style="{StaticResource CloseButtonStyle}" ToolTip="{Binding DataContext.CloseToolTip, RelativeSource={RelativeSource AncestorType=Window}}" Padding="12" Margin="4,0,0,0"
|
||||
ui:IconAssist.Icon="{StaticResource Icon.Close}"
|
||||
Command="{Binding DataContext.CloseCommand, RelativeSource={RelativeSource AncestorType=Window}}" />
|
||||
@@ -265,11 +295,13 @@
|
||||
<Grid Grid.Row="1" Margin="58,6,34,20">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="370" />
|
||||
<!-- Notes de version : 370 en mode fenêtré, plus larges en plein écran (jusqu'à 560) -->
|
||||
<ColumnDefinition Width="0.5*" MinWidth="370" MaxWidth="560" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Présentation du jeu -->
|
||||
<StackPanel VerticalAlignment="Center" Margin="0,0,40,0">
|
||||
<!-- Présentation du jeu : défile si elle ne tient pas en hauteur (description, liens et bandeau à la fois) -->
|
||||
<ScrollViewer Margin="0,0,24,0" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Focusable="False">
|
||||
<StackPanel VerticalAlignment="Center" Margin="0,0,16,0">
|
||||
<!-- Badge d'état -->
|
||||
<Border HorizontalAlignment="Left" CornerRadius="14" Padding="12,6"
|
||||
Background="{DynamicResource BadgeBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1">
|
||||
@@ -404,6 +436,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Notes de version -->
|
||||
<Border Grid.Column="1" Margin="0,4" Padding="22,20,10,18" CornerRadius="20"
|
||||
@@ -537,6 +570,7 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ui:ScaleToFitDecorator>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -544,6 +578,6 @@
|
||||
<Image x:Name="TransitionImage" IsHitTestVisible="False" Visibility="Collapsed" Stretch="Fill" />
|
||||
|
||||
<!-- Voile sombre affiché derrière les boîtes de dialogue -->
|
||||
<Border x:Name="DimOverlay" CornerRadius="22" Background="#A6000000" Visibility="Collapsed" />
|
||||
<Border x:Name="DimOverlay" CornerRadius="{DynamicResource WindowChrome.Radius}" Background="#A6000000" Visibility="Collapsed" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -54,11 +54,11 @@ public partial class MainWindow : Window, IThemeTransition
|
||||
AdminRequested?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>Déplacement de la fenêtre sans bordure par sa barre de titre.</summary>
|
||||
/// <summary>Déplacement de la fenêtre sans bordure par sa barre de titre (mode fenêtré seulement).</summary>
|
||||
private void OnTitleBarMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
if (e.ChangedButton == MouseButton.Left && e.ButtonState == MouseButtonState.Pressed)
|
||||
DragMove();
|
||||
Controls.WindowModeAssist.DragMove(this);
|
||||
}
|
||||
|
||||
/// <summary>Ouvre le menu "…" au-dessus du bouton.</summary>
|
||||
|
||||
@@ -33,8 +33,12 @@
|
||||
FontSize="19" FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock x:Name="MessageText" Margin="0,18,0,0" FontSize="14" LineHeight="21"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}" TextWrapping="Wrap" />
|
||||
<!-- Message long (détail d'une erreur…) : il défile, la fenêtre ne dépasse jamais l'écran -->
|
||||
<ScrollViewer x:Name="MessageScroll" Margin="0,18,0,0" VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled" Focusable="False">
|
||||
<TextBlock x:Name="MessageText" FontSize="14" LineHeight="21"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}" TextWrapping="Wrap" />
|
||||
</ScrollViewer>
|
||||
|
||||
<Border x:Name="OptionPanel" Margin="0,20,0,0" Padding="16,12" CornerRadius="14"
|
||||
Background="{DynamicResource InputBrush}" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1"
|
||||
|
||||
@@ -12,6 +12,12 @@ public partial class ModernDialog : Window
|
||||
private ModernDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
// Jamais plus haute que la zone utile de l'écran de sa fenêtre parente (au-delà, le message défile).
|
||||
SourceInitialized += (_, _) =>
|
||||
{
|
||||
const double chrome = 280; // titre, option, boutons, marges et ombre
|
||||
MessageScroll.MaxHeight = Math.Max(120, Controls.WindowModeAssist.WorkArea(Owner ?? (Visual)this).Height - chrome);
|
||||
};
|
||||
}
|
||||
|
||||
public static void ShowMessage(Window? owner, string title, string message, DialogKind kind)
|
||||
|
||||
@@ -10,17 +10,17 @@ public partial class SettingsWindow : Window
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
Loaded += (_, _) => FitToScreen();
|
||||
SourceInitialized += (_, _) => FitToScreen(); // avant le centrage sur la fenêtre parente
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// La fenêtre ne dépasse jamais la zone utile de l'écran (mise à l'échelle 125-150 %) :
|
||||
/// au-delà, les sections défilent.
|
||||
/// La fenêtre ne dépasse jamais la zone utile de l'écran de sa fenêtre parente (mise à l'échelle 125-150 %, autre
|
||||
/// écran) : au-delà, les sections défilent.
|
||||
/// </summary>
|
||||
private void FitToScreen()
|
||||
{
|
||||
const double chrome = 260; // en-tête, boutons, marges et ombre
|
||||
SectionsScroll.MaxHeight = Math.Max(240, SystemParameters.WorkArea.Height - chrome);
|
||||
SectionsScroll.MaxHeight = Math.Max(240, Controls.WindowModeAssist.WorkArea(Owner ?? (System.Windows.Media.Visual)this).Height - chrome);
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
|
||||
|
||||
@@ -187,7 +187,12 @@ public class AuditPanelTests
|
||||
ActorName = "Paul,=1+1",
|
||||
Details = "nom ; avec \"guillemets\"\nsur deux lignes"
|
||||
};
|
||||
var line = AuditTexts.ToCsv(new[] { new AuditItem(entry) }).Split(Environment.NewLine)[1];
|
||||
var csv = AuditTexts.ToCsv(new[] { new AuditItem(entry), new AuditItem(entry with { KeyLabel = "PC bureau" }) }).Split(Environment.NewLine);
|
||||
Assert.Equal("Date;Action;Code;Résultat;Administrateur;Appareil;IP;Clé;Cible;Détails", csv[0]);
|
||||
Assert.Contains(";\"PC bureau\";\"'@SUM(A1)\";", csv[2]);
|
||||
Assert.Equal("Appareil « PC bureau »", new AuditItem(entry with { KeyLabel = "PC bureau" }).DeviceText);
|
||||
Assert.StartsWith("Appareil « PC bureau » · IP", new AuditItem(entry with { KeyLabel = "PC bureau" }).OriginText);
|
||||
var line = csv[1];
|
||||
Assert.Contains(";\"'@SUM(A1)\";", line);
|
||||
Assert.Contains(";\"'=HYPERLINK(\"\"http://x\"\")\";", line);
|
||||
Assert.Contains(";\"' -2+3\";", line);
|
||||
|
||||
@@ -52,6 +52,30 @@ public class AuditModuleTests
|
||||
Assert.Contains("entrées plus anciennes", views[0].Details);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Entries_name_the_device_chosen_at_enrollment_never_the_windows_name()
|
||||
{
|
||||
await using var api = await TestApi.StartAsync();
|
||||
using var ownerKey = api.AddOwner();
|
||||
await using var owner = await api.LoginAsync(ownerKey);
|
||||
var (editorKey, _) = await api.AddAdminAsync(owner, "Éditrice", AdminRoles.Editor); // appareil « poste de test »
|
||||
|
||||
// Launcher 2.4.1 ou plus ancien : nom Windows du PC entre parenthèses dans la description du logiciel.
|
||||
await using (await api.CreateClient().LoginAsync(editorKey, "GameLauncher 2.4.1 (DESKTOP-ABC123)"))
|
||||
{
|
||||
}
|
||||
|
||||
var login = (await owner.GetAuditAsync(50)).Entries.First(e => e.Action == "auth.login" && e.KeyId == editorKey.KeyId);
|
||||
Assert.Equal("poste de test", login.KeyLabel);
|
||||
Assert.Equal("appareil « poste de test » · GameLauncher 2.4.1", login.Details);
|
||||
Assert.DoesNotContain(Journal(api, "auth.login"), e => e.Details?.Contains("DESKTOP-ABC123") == true);
|
||||
|
||||
// Le nom de l'appareil est ajouté à la lecture : il n'est jamais écrit dans le fichier du journal.
|
||||
Assert.All(api.Services.GetRequiredService<AuditLog>().Read(200, null).Entries, e => Assert.Null(e.KeyLabel));
|
||||
// Entrée sans clé : pas d'appareil.
|
||||
Assert.All((await owner.GetAuditAsync(50)).Entries.Where(e => e.KeyId is null), e => Assert.Null(e.KeyLabel));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Exports_are_recorded_and_checked()
|
||||
{
|
||||
|
||||
+81
-45
@@ -1,67 +1,103 @@
|
||||
# Launcher 2.4.1
|
||||
# Launcher 2.5.0
|
||||
LAUNCHER - NOTES DE MISE À JOUR
|
||||
Avertissement avant une publication signée - Mise à jour du 7 novembre 2026
|
||||
Plein écran et nom des appareils dans l'audit - Mise à jour du 8 novembre 2026
|
||||
==================================================================
|
||||
|
||||
Petite version du menu d'administration : avant de publier le catalogue ou une version, le launcher vérifie que la clé de votre PC est bien reconnue par les launchers des joueurs, et vous prévient sinon.
|
||||
La fenêtre du launcher et le menu d'administration peuvent maintenant s'agrandir sur tout l'écran, la barre des tâches restant visible. Leur mise en page s'adapte à la place disponible, sur grand comme sur petit écran.
|
||||
|
||||
Rien ne change dans le fonctionnement : le serveur accepte toujours ces publications et les launchers des joueurs les traitent comme avant. Il s'agit uniquement d'un avertissement. Pas de serveur à redéployer.
|
||||
Dans le menu d'administration, le journal d'audit indique désormais chaque appareil sous le nom choisi lors de l'invitation, et non plus sous le nom Windows du PC. Le serveur est à redéployer pour en profiter.
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
EN BREF
|
||||
------------------------------------------------------------------
|
||||
- NOUVEAU : avertissement « Clé non reconnue par les launchers des
|
||||
joueurs » avant de publier le catalogue (page « Jeux »).
|
||||
- NOUVEAU : même avertissement avant de publier une version et
|
||||
avant « Signer la version en ligne » (page « Publier une
|
||||
version »).
|
||||
- AMÉLIORÉ : le message explique la conséquence pour les joueurs
|
||||
et la marche à suivre.
|
||||
- SERVEUR : aucun changement, rien à redéployer.
|
||||
- QUALITÉ : 5 nouveaux tests automatisés (281 au total).
|
||||
- NOUVEAU : plein écran pour la fenêtre du launcher et le menu
|
||||
d'administration (bouton entre « Réduire » et « Fermer », ou F11).
|
||||
- NOUVEAU : le mode choisi (plein écran ou fenêtré) est retenu pour
|
||||
la fois suivante.
|
||||
- AMÉLIORÉ : mise en page adaptée à la taille de l'écran, sans
|
||||
texte ni bouton coupé.
|
||||
- CORRIGÉ : le journal d'audit affichait le nom Windows du PC au
|
||||
lieu du nom de l'appareil choisi à l'invitation.
|
||||
- SERVEUR : à redéployer (nom des appareils dans le journal
|
||||
d'audit).
|
||||
- QUALITÉ : 1 nouveau test automatisé (282 au total).
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
AVERTISSEMENT AVANT PUBLICATION
|
||||
PLEIN ÉCRAN
|
||||
------------------------------------------------------------------
|
||||
- Juste avant la signature, le launcher télécharge le trousseau
|
||||
publié et le vérifie comme les launchers des joueurs.
|
||||
- L'avertissement s'affiche si :
|
||||
* la clé du PC n'est pas dans le trousseau publié ;
|
||||
* la clé y est, mais sans l'autorisation voulue (catalogue ou
|
||||
versions du jeu) ;
|
||||
* le trousseau est refusé (autre clé racine, trop ancien,
|
||||
illisible) ou injoignable ;
|
||||
* aucun trousseau n'est configuré dans le launcher.
|
||||
- Le message indique ce qui se passera chez les joueurs :
|
||||
* vérification obligatoire : catalogue ignoré (les joueurs
|
||||
gardent le précédent) ou version refusée (ni installation ni
|
||||
mise à jour, le jeu déjà installé reste jouable) ;
|
||||
* vérification en mode « warn » : contenu accepté, avec une
|
||||
alerte dans le journal ;
|
||||
* vérification désactivée : aucun avertissement.
|
||||
- Deux choix : « Annuler » (rien n'est envoyé) ou « Publier quand
|
||||
même » (« Signer quand même » pour une version déjà en ligne).
|
||||
- Pour régler le problème : un propriétaire ajoute la clé du PC au
|
||||
trousseau (page « Signature »), le fait signer avec la clé
|
||||
racine puis le publie ; les joueurs acceptent alors le contenu
|
||||
sans nouvelle publication.
|
||||
- Concerne la fenêtre principale du launcher et le menu
|
||||
d'administration ; les autres fenêtres (paramètres, notes de
|
||||
version agrandies, messages) gardent leur taille.
|
||||
- Bascule : bouton entre « Réduire » et « Fermer », ou touche F11.
|
||||
- La fenêtre s'agrandit comme une fenêtre Windows : elle occupe tout
|
||||
l'écran où elle se trouve, la barre des tâches reste visible (même
|
||||
en masquage automatique).
|
||||
- Le mode de chaque fenêtre est retenu pour la fois suivante.
|
||||
- Les fenêtres ne se redimensionnent toujours pas par les bords ou
|
||||
les coins : deux modes seulement, fenêtré ou plein écran.
|
||||
- Une fenêtre réduite dans la barre des tâches ou près de l'horloge
|
||||
revient dans son mode.
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
MISE EN PAGE ADAPTÉE À L'ÉCRAN
|
||||
------------------------------------------------------------------
|
||||
- Plein écran : les notes de version s'élargissent, les pages du
|
||||
menu d'administration utilisent toute la largeur.
|
||||
- Petit écran ou mise à l'échelle Windows 125-150 % : la fenêtre ne
|
||||
dépasse plus de l'écran ; si la place manque, le contenu est
|
||||
réduit d'un bloc, sans rien couper.
|
||||
- Présentation du jeu : elle défile si elle ne tient pas en hauteur
|
||||
(description, liens et bandeau à la fois).
|
||||
- Menu d'administration :
|
||||
* messages d'erreur et de résultat sur toute la largeur ;
|
||||
* pages en deux colonnes (Jeux, Notes de version, Groupes) : sur
|
||||
un très petit écran, la page entière défile, rien n'est coupé ;
|
||||
* libellés trop longs abrégés proprement (navigation, titres).
|
||||
- Paramètres, notes de version agrandies et messages : taille
|
||||
limitée à l'écran de la fenêtre du launcher (utile avec plusieurs
|
||||
écrans) ; un message long défile.
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
JOURNAL D'AUDIT : NOM DES APPAREILS
|
||||
------------------------------------------------------------------
|
||||
- Chaque entrée indique l'appareil sous le nom choisi lors de
|
||||
l'invitation (« J'ai une invitation »), jamais le nom Windows du
|
||||
PC.
|
||||
- Les anciennes entrées en profitent aussi ; seules les lignes
|
||||
« Connexion » déjà écrites gardent leur texte d'origine (le
|
||||
journal n'est jamais réécrit).
|
||||
- Export CSV : nouvelle colonne « Appareil ».
|
||||
- Recherche : le nom de l'appareil est pris en compte.
|
||||
- SÉCURITÉ :
|
||||
* aucun changement côté serveur ni côté joueur ;
|
||||
* trousseau vérifié avec la clé racine intégrée au launcher ;
|
||||
* clé du PC identifiée localement, à partir de la clé qui
|
||||
signe ;
|
||||
* téléchargement sans cache, limité en taille et en durée ;
|
||||
* une erreur imprévue du contrôle ne bloque pas la publication.
|
||||
* le nom Windows du PC n'est plus envoyé au serveur ni conservé ;
|
||||
le serveur retire celui qu'envoient les launchers plus anciens ;
|
||||
* le nom de l'appareil est ajouté par le serveur à la lecture, il
|
||||
n'est jamais écrit dans les fichiers du journal.
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
SERVEUR
|
||||
------------------------------------------------------------------
|
||||
- À redéployer : server/deploy/LISEZMOI.md, étape 8 (étapes 1 et
|
||||
2). Aucun réglage nouveau.
|
||||
- Sans redéploiement, la colonne « Appareil » reste vide.
|
||||
|
||||
|
||||
------------------------------------------------------------------
|
||||
DÉVELOPPEMENT
|
||||
------------------------------------------------------------------
|
||||
- Tests : nouveau fichier PlayerSigningCheckTests.cs.
|
||||
- Documentation : docs/ADMIN-ARCHITECTURE.md mis à jour
|
||||
(avertissement avant publication).
|
||||
- Nouveaux fichiers : Controls/WindowModeAssist.cs (plein écran,
|
||||
F11, mode retenu), Controls/ScaleToFitDecorator.cs (mise en page
|
||||
adaptée), Platform/WindowModeStore.cs (window-modes.json).
|
||||
- Une future fenêtre ou page d'administration en profite sans code
|
||||
supplémentaire (règles dans docs/ADMIN-ARCHITECTURE.md, § 5.3).
|
||||
- Tests : AuditModuleTests.cs et AuditPanelTests.cs complétés.
|
||||
- Documentation : README.md et docs/ADMIN-ARCHITECTURE.md mis à
|
||||
jour.
|
||||
|
||||
|
||||
La sécurité des mises à jour du launcher ne change pas : la signature numérique et l'empreinte SHA-256 de chaque nouvelle version sont vérifiées avant toute installation.
|
||||
Reference in new issue
Block a user