feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
@@ -0,0 +1,124 @@
using System.Text.Json;
using ClawdDotNet.Core.Security;
using ClawdDotNet.Core.Storage;
namespace ClawdDotNet.App.Settings;
/// <summary>
/// Lädt und speichert die anwendungsweiten Einstellungen.
///
/// <para><b>Ablageort.</b> Bis zur Linux-Portierung lag <c>Settings.json</c> neben der
/// Programmdatei. Unter Windows in einem Benutzerverzeichnis ging das; unter Linux liegt
/// die Anwendung in <c>/opt</c> oder <c>/usr/lib</c> und ist für den Dienstbenutzer nicht
/// beschreibbar. Jetzt entscheidet <see cref="AppPaths.ConfigDirectory"/> — XDG unter
/// Linux, <c>%APPDATA%</c> unter Windows, per <c>CLAWD_CONFIG_DIR</c> überschreibbar.</para>
///
/// <para><b>Kein Migrationspfad.</b> Bewusst: Zum Zeitpunkt der Umstellung lief noch
/// keine Installation produktiv. Eine bestehende <c>Settings.json</c> neben der
/// Programmdatei wird also <em>nicht</em> übernommen — der Ort wechselt einmal sauber,
/// statt eine Ausweichlogik zu hinterlassen, die niemand mehr anfasst.</para>
///
/// <para><b>Keine Meldungsfenster.</b> Diese Schicht kennt keine Oberfläche. Ein
/// Speicherfehler kommt als <see cref="SettingsPersistenceException"/> heraus; ob daraus
/// ein Dialog, ein Logeintrag oder ein Rückgabewert wird, entscheidet der Aufrufer.</para>
/// </summary>
public sealed class SettingsManager
{
private const string SettingsFileName = "Settings.json";
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
ReadCommentHandling = JsonCommentHandling.Skip,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true
};
private readonly string _settingsPath;
public AppSettings AppSettings { get; private set; } = new();
/// <summary>Der Ort der Einstellungsdatei — für Meldungen und Diagnose.</summary>
public string SettingsPath => _settingsPath;
public SettingsManager(string? basePath = null)
{
var dir = basePath ?? AppPaths.ConfigDirectory;
_settingsPath = Path.Combine(dir, SettingsFileName);
}
public void Load()
{
if (!File.Exists(_settingsPath))
{
AppSettings = new AppSettings();
Save(); // Vorgaben festschreiben, damit der Ort sichtbar wird
return;
}
try
{
var json = AtomicFile.ReadAllText(_settingsPath);
AppSettings = JsonSerializer.Deserialize<AppSettings>(json, JsonOptions)
?? new AppSettings();
}
catch (Exception ex) when (ex is JsonException or IOException)
{
// Eine unlesbare Datei darf den Start nicht verhindern — mit Vorgaben
// weiterzumachen ist besser, als gar nicht zu starten.
AppSettings = new AppSettings();
return;
}
// Geheimnisse liegen in der Datei verschlüsselt und werden zur Laufzeit im
// Klartext gehalten.
AppSettings.LicenseKey = TryUnprotect(AppSettings.LicenseKey);
AppSettings.DeploymentcenterToken = TryUnprotect(AppSettings.DeploymentcenterToken);
}
/// <summary>
/// Entschlüsselt einen Wert; bei Nicht-Lesbarkeit (anderer Benutzer, anderer Rechner,
/// Umzug von Windows) leer, damit der Nutzer ihn neu eintragen kann statt einen
/// unbrauchbaren Wert an eine Gegenstelle zu schicken.
/// </summary>
private static string TryUnprotect(string value)
{
try { return SecretProtector.Unprotect(value) ?? ""; }
catch (SecretProtectionException) { return ""; }
}
/// <exception cref="SettingsPersistenceException">Wenn die Datei nicht geschrieben werden kann.</exception>
public void Save()
{
// Nur zum Schreiben verschlüsseln; die laufende Instanz braucht Klartext.
var plainLicenseKey = AppSettings.LicenseKey;
var plainDeploymentcenterToken = AppSettings.DeploymentcenterToken;
try
{
AppSettings.LicenseKey = SecretProtector.Protect(plainLicenseKey) ?? "";
AppSettings.DeploymentcenterToken = SecretProtector.Protect(plainDeploymentcenterToken) ?? "";
AppPaths.EnsureDirectory(Path.GetDirectoryName(_settingsPath)!);
var json = JsonSerializer.Serialize(AppSettings, JsonOptions);
AtomicFile.WriteAllText(_settingsPath, json);
AppPaths.RestrictToOwner(_settingsPath);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
or SecretProtectionException)
{
throw new SettingsPersistenceException(
$"Die Einstellungen konnten nicht nach {_settingsPath} geschrieben werden: {ex.Message}",
ex);
}
finally
{
AppSettings.LicenseKey = plainLicenseKey;
AppSettings.DeploymentcenterToken = plainDeploymentcenterToken;
}
}
}
public sealed class SettingsPersistenceException(string message, Exception inner)
: Exception(message, inner);