feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar
This commit is contained in:
@@ -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);
|
||||
Reference in New Issue
Block a user