Sicherungscommit vor dem Aufraeumen des Repos, damit nachvollziehbar bleibt, welcher Stand vor der Bereinigung galt. Build gruen, 476 Tests gruen. Zwei Straenge, die sich ueber .csproj, Program.cs und appsettings.json ueberschneiden und darum gemeinsam abgelegt werden: Deploymentcenter-Integration (P3c, Plan D-0 bis D-5 code-seitig fertig): - Deploymentcenter.Client 2.5.0 als lokales Paket, Source-Mapping erweitert - DeploymentcenterOptions, DeploymentcenterErrorReporter, LicenseGate/LicenseCli - WatchdogHeartbeatService auf die Deploymentcenter-API umgestellt (version, os, checks, metrics, status stopped) - Security: MasterKeyResolver, SecretRedactor, FilePermissions - Directory.Build.props mit zentraler Version 0.1.0 (Packager-Versionsdisziplin) - deploy/: Packager-Vorlage und systemd-Unit; echte Zugangsdaten bleiben ueber .gitignore aussen vor - setup.json fuer die Erstinstallation - UMSETZUNGSPLAN-Deploymentcenter-Integration.md; ANALYSE-Linux-Portierung.md verweist auf den neuen Plan Einfenster-Shell (UI-Redesign): - ShellWindow + ShellNavModel als Seitenleisten-Shell - WindowMenuBar und WindowMenuModel entfallen - Modul- und Kernfenster auf die Shell-Einbettung angepasst Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
129 lines
5.6 KiB
C#
129 lines
5.6 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Avalonia.Controls;
|
||
using PolyTrader.Core.Modularity;
|
||
|
||
namespace PolyTrader.App.Avalonia.Shell
|
||
{
|
||
/// <summary>
|
||
/// Avalonia-Umsetzung von <see cref="IModuleUiHost"/>.
|
||
///
|
||
/// <para><b>Umstellung auf die Einfenster-Shell:</b> Früher öffnete jede View ein eigenes
|
||
/// Fenster. Jetzt gibt es genau ein Anwendungsfenster (<see cref="Views.ShellWindow"/>); eine
|
||
/// View ist eine <see cref="UserControl"/>-Seite, die in dessen Inhaltsbereich gehängt wird.
|
||
/// Eine Seite wird beim ersten Aufruf erzeugt und bleibt danach für die Sitzung bestehen –
|
||
/// das entspricht dem alten „Fenster bleibt offen": Timer, Log-Abos und der Bildlaufstand
|
||
/// überleben den Seitenwechsel.</para>
|
||
///
|
||
/// <para>Der Core-Contract ist toolkit-neutral (<see cref="ModuleView.CreateView"/> liefert
|
||
/// <see cref="object"/>) – hier wird auf <see cref="Control"/> gecastet. Ein falscher Typ ist ein
|
||
/// Programmierfehler und soll laut scheitern, nicht still eine leere Seite erzeugen.</para>
|
||
/// </summary>
|
||
public sealed class AvaloniaUiHost : IModuleUiHost
|
||
{
|
||
/// <summary>ID der Startseite (frühere LauncherForm/LauncherWindow).</summary>
|
||
public const string StartViewId = "core.start";
|
||
|
||
private readonly List<ModuleView> _views = new();
|
||
private readonly List<UnavailableView> _unavailable = new();
|
||
private readonly Dictionary<string, Control> _pages = new();
|
||
private Views.ShellWindow? _shell;
|
||
private string? _activeViewId;
|
||
private bool _shutdownDialogOpen;
|
||
|
||
/// <summary>True, sobald das Herunterfahren über die Sicherheitsabfrage bestätigt wurde.</summary>
|
||
public bool ShutdownConfirmed { get; private set; }
|
||
|
||
public event Action? OpenStateChanged;
|
||
|
||
public IReadOnlyList<ModuleView> Views => _views;
|
||
|
||
/// <summary>ID der gerade angezeigten Seite – die Seitenleiste hebt sie hervor.</summary>
|
||
public string? ActiveViewId => _activeViewId;
|
||
|
||
public void RegisterView(ModuleView view) => _views.Add(view);
|
||
|
||
/// <summary>
|
||
/// Ein bekanntes, in dieser Sitzung aber nicht geladenes Modul. Solche Module haben keine
|
||
/// Seite – die Seitenleiste zeigt sie trotzdem, ausgegraut und mit dem Grund als Hinweis.
|
||
/// </summary>
|
||
/// <param name="Reason">Warum das Modul fehlt (Lizenz, Abschaltung, fehlender API-Key …).</param>
|
||
public sealed record UnavailableView(string Title, string? IconKey, int Order, string Reason);
|
||
|
||
/// <summary>
|
||
/// Meldet ein nicht geladenes Modul an. Ohne das verschwände es spurlos aus der Oberfläche –
|
||
/// in der Mehrfenster-Fassung fiel das kaum auf, in einer Seitenleiste ist eine unerklärt
|
||
/// leere Rubrik dagegen schlicht ein Fehler.
|
||
/// </summary>
|
||
public void RegisterUnavailable(UnavailableView view) => _unavailable.Add(view);
|
||
|
||
/// <summary>Bekannte, aber nicht geladene Module (siehe <see cref="RegisterUnavailable"/>).</summary>
|
||
public IReadOnlyList<UnavailableView> UnavailableViews => _unavailable;
|
||
|
||
/// <summary>Setzt das Shell-Fenster – Ziel aller Seitenwechsel und Besitzer der Dialoge.</summary>
|
||
public void SetShell(Views.ShellWindow shell) => _shell = shell;
|
||
|
||
/// <summary>Das Shell-Fenster, oder <c>null</c> vor dessen Aufbau (z.B. im Smoke-Lauf).</summary>
|
||
public Window? ShellWindow => _shell;
|
||
|
||
/// <summary>
|
||
/// Wurde die Seite in dieser Sitzung schon einmal aufgerufen? Erbt die Bedeutung des früheren
|
||
/// „Fenster ist offen": die Seite existiert und arbeitet weiter, auch wenn gerade eine andere
|
||
/// angezeigt wird.
|
||
/// </summary>
|
||
public bool IsOpen(string viewId) => _pages.ContainsKey(viewId);
|
||
|
||
/// <summary>Zurück auf die Startseite.</summary>
|
||
public void ActivateMain() => OpenView(StartViewId);
|
||
|
||
public void OpenView(string viewId)
|
||
{
|
||
var view = _views.FirstOrDefault(v => v.Id == viewId);
|
||
if (view != null) OpenView(view);
|
||
}
|
||
|
||
public void OpenView(ModuleView view)
|
||
{
|
||
if (!_pages.TryGetValue(view.Id, out var page))
|
||
{
|
||
page = (Control)view.CreateView();
|
||
_pages[view.Id] = page;
|
||
}
|
||
|
||
_activeViewId = view.Id;
|
||
_shell?.ShowPage(view.Title, page);
|
||
OpenStateChanged?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Zeigt die Sicherheitsabfrage (Beenden-Schaltfläche 10 s gesperrt) und fährt bei Bestätigung
|
||
/// herunter. Aus jeder Seite aufrufbar – auch aus Modul-Seiten, die die App nicht kennen.
|
||
/// </summary>
|
||
public async void RequestShutdown()
|
||
{
|
||
if (ShutdownConfirmed || _shutdownDialogOpen) return;
|
||
_shutdownDialogOpen = true;
|
||
try
|
||
{
|
||
var dlg = new Views.ShutdownConfirmWindow();
|
||
bool confirmed = _shell != null
|
||
? await dlg.ShowDialog<bool>(_shell)
|
||
: await dlg.ShowDialog<bool>(new Window());
|
||
if (!confirmed) return;
|
||
|
||
ShutdownConfirmed = true;
|
||
if (global::Avalonia.Application.Current?.ApplicationLifetime
|
||
is global::Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop)
|
||
{
|
||
desktop.Shutdown();
|
||
}
|
||
}
|
||
finally
|
||
{
|
||
_shutdownDialogOpen = false;
|
||
}
|
||
}
|
||
}
|
||
}
|