diff --git a/src/MikrotikInstaller.App/App.xaml.cs b/src/MikrotikInstaller.App/App.xaml.cs
index 53c9860..dbba879 100644
--- a/src/MikrotikInstaller.App/App.xaml.cs
+++ b/src/MikrotikInstaller.App/App.xaml.cs
@@ -14,11 +14,8 @@ public partial class App : Application
{
base.OnStartup(e);
- // Übernimmt Hell/Dunkel vom Windows-Systemthema; die Akzentfarbe setzen wir
- // anschließend selbst auf MikroTik-Grün statt auf die Windows-Akzentfarbe.
- ApplicationThemeManager.ApplySystemTheme(updateAccent: false);
- var appliedTheme = ApplicationThemeManager.GetAppTheme();
- ApplicationAccentColorManager.Apply(BrandAccent, appliedTheme, systemGlassColor: false, systemAccentColor: false);
+ // Hell/Dunkel laut Einstellungen (Standard: wie Windows); die Akzentfarbe ist immer MikroTik-Grün.
+ UserSettings.ApplyTheme();
DispatcherUnhandledException += OnDispatcherUnhandledException;
}
diff --git a/src/MikrotikInstaller.App/MainWindow.xaml b/src/MikrotikInstaller.App/MainWindow.xaml
index 0940189..dfb1bb2 100644
--- a/src/MikrotikInstaller.App/MainWindow.xaml
+++ b/src/MikrotikInstaller.App/MainWindow.xaml
@@ -44,6 +44,7 @@
+
@@ -189,6 +190,13 @@
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/MainWindow.xaml.cs b/src/MikrotikInstaller.App/MainWindow.xaml.cs
index 62ff983..870945a 100644
--- a/src/MikrotikInstaller.App/MainWindow.xaml.cs
+++ b/src/MikrotikInstaller.App/MainWindow.xaml.cs
@@ -16,11 +16,24 @@ public partial class MainWindow : FluentWindow
viewModel.CloseRequested += (_, _) => Close();
}
- // Folgt Hell/Dunkel-Wechseln des Windows-Systemthemes zur Laufzeit;
+ // Folgt Hell/Dunkel-Wechseln des Windows-Systemthemes zur Laufzeit — nur bei "Wie System";
// updateAccents:false, damit unser MikroTik-Grün-Akzent dabei erhalten bleibt.
- SystemThemeWatcher.Watch(this, WindowBackdropType.Mica, updateAccents: false);
+ WatchSystemThemeIfWanted();
+ UserSettings.Changed += (_, _) => WatchSystemThemeIfWanted();
}
+ private void WatchSystemThemeIfWanted()
+ {
+ SystemThemeWatcher.UnWatch(this);
+ if (UserSettings.Current.Theme == Core.Settings.AppTheme.System)
+ {
+ SystemThemeWatcher.Watch(this, WindowBackdropType.Mica, updateAccents: false);
+ }
+ }
+
+ private void OnOpenSettings(object sender, System.Windows.RoutedEventArgs e) =>
+ new Views.SettingsWindow { Owner = this }.ShowDialog();
+
private async void OnClosing(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (DataContext is MainViewModel viewModel)
diff --git a/src/MikrotikInstaller.App/UserSettings.cs b/src/MikrotikInstaller.App/UserSettings.cs
new file mode 100644
index 0000000..e9e2842
--- /dev/null
+++ b/src/MikrotikInstaller.App/UserSettings.cs
@@ -0,0 +1,57 @@
+using System.Windows;
+using MikrotikInstaller.App.Credentials;
+using MikrotikInstaller.Core.Settings;
+using Wpf.Ui.Appearance;
+
+namespace MikrotikInstaller.App;
+
+/// Die geladenen Einstellungen für die ganze App, plus das Backup-Passwort aus dem Anmeldeinformationsspeicher.
+public static class UserSettings
+{
+ private const string BackupPasswordId = "backup-password";
+ private static readonly AppSettingsStore Store = new();
+
+ public static AppSettings Current { get; private set; } = Store.Load();
+
+ public static event EventHandler? Changed;
+
+ public static void Save(AppSettings settings)
+ {
+ Store.Save(settings);
+ Current = settings;
+ ApplyTheme();
+ Changed?.Invoke(null, EventArgs.Empty);
+ }
+
+ /// Leer/null = Backups unverschlüsselt.
+ public static string? BackupPassword
+ {
+ get => WindowsCredentialStore.TryRead(BackupPasswordId) is { Length: > 0 } password ? password : null;
+ }
+
+ /// Gibt false zurück, wenn Windows das Passwort nicht speichern konnte (Mac v1.11.4: dann nicht als gesetzt anzeigen).
+ public static bool SetBackupPassword(string? password)
+ {
+ if (string.IsNullOrEmpty(password))
+ {
+ WindowsCredentialStore.Delete(BackupPasswordId);
+ return true;
+ }
+
+ return WindowsCredentialStore.Save(BackupPasswordId, "backup", password);
+ }
+
+ public static void ApplyTheme()
+ {
+ if (Current.Theme == AppTheme.System)
+ {
+ ApplicationThemeManager.ApplySystemTheme(updateAccent: false);
+ }
+ else
+ {
+ ApplicationThemeManager.Apply(Current.Theme == AppTheme.Dark ? ApplicationTheme.Dark : ApplicationTheme.Light, updateAccent: false);
+ }
+
+ ApplicationAccentColorManager.Apply(App.BrandAccent, ApplicationThemeManager.GetAppTheme(), systemGlassColor: false, systemAccentColor: false);
+ }
+}
diff --git a/src/MikrotikInstaller.App/ViewModels/SettingsViewModel.cs b/src/MikrotikInstaller.App/ViewModels/SettingsViewModel.cs
new file mode 100644
index 0000000..c30c51f
--- /dev/null
+++ b/src/MikrotikInstaller.App/ViewModels/SettingsViewModel.cs
@@ -0,0 +1,92 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Win32;
+using MikrotikInstaller.Core.Settings;
+
+namespace MikrotikInstaller.App.ViewModels;
+
+/// Einstellungen-Fenster (Mac: Einstellungen → Allgemein/Darstellung). Änderungen gelten erst mit „Speichern".
+public partial class SettingsViewModel : ObservableObject
+{
+ public SettingsViewModel()
+ {
+ var settings = UserSettings.Current;
+ _theme = settings.Theme;
+ _checkUpdatesOnConnect = settings.CheckUpdatesOnConnect;
+ _professionalMode = settings.ProfessionalMode;
+ _testEnvironment = settings.TestEnvironment;
+ _backupFolder = settings.EffectiveBackupFolder;
+ _hasBackupPassword = UserSettings.BackupPassword is not null;
+ }
+
+ [ObservableProperty]
+ private AppTheme _theme;
+
+ [ObservableProperty]
+ private bool _checkUpdatesOnConnect;
+
+ [ObservableProperty]
+ private bool _professionalMode;
+
+ [ObservableProperty]
+ private bool _testEnvironment;
+
+ [ObservableProperty]
+ private string _backupFolder;
+
+ /// Neues Backup-Passwort; leer lassen = unverändert.
+ [ObservableProperty]
+ private string _newBackupPassword = string.Empty;
+
+ [ObservableProperty]
+ private bool _hasBackupPassword;
+
+ [ObservableProperty]
+ private bool _removeBackupPassword;
+
+ [ObservableProperty]
+ private bool _restartIntro;
+
+ [ObservableProperty]
+ private string? _errorMessage;
+
+ /// Wird nach erfolgreichem Speichern ausgelöst, damit das Fenster sich schließt.
+ public event EventHandler? Saved;
+
+ [RelayCommand]
+ private void ChooseBackupFolder()
+ {
+ var dialog = new OpenFolderDialog { Title = "Ordner für Backups wählen", InitialDirectory = BackupFolder };
+ if (dialog.ShowDialog() == true)
+ {
+ BackupFolder = dialog.FolderName;
+ }
+ }
+
+ [RelayCommand]
+ private void Save()
+ {
+ ErrorMessage = null;
+ if (RemoveBackupPassword)
+ {
+ UserSettings.SetBackupPassword(null);
+ }
+ else if (NewBackupPassword.Length > 0 && !UserSettings.SetBackupPassword(NewBackupPassword))
+ {
+ ErrorMessage = "Das Backup-Passwort konnte nicht gespeichert werden. Neue Backups bleiben deshalb unverändert.";
+ return;
+ }
+
+ var current = UserSettings.Current;
+ UserSettings.Save(current with
+ {
+ Theme = Theme,
+ CheckUpdatesOnConnect = CheckUpdatesOnConnect,
+ ProfessionalMode = ProfessionalMode,
+ TestEnvironment = TestEnvironment,
+ BackupFolder = BackupFolder == new AppSettings().EffectiveBackupFolder ? null : BackupFolder,
+ IntroCompleted = current.IntroCompleted && !RestartIntro,
+ });
+ Saved?.Invoke(this, EventArgs.Empty);
+ }
+}
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/DeviceOverviewStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/DeviceOverviewStepViewModel.cs
index 886900b..efbed4d 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/DeviceOverviewStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/DeviceOverviewStepViewModel.cs
@@ -74,7 +74,10 @@ public partial class DeviceOverviewStepViewModel : WizardStepViewModelBase
// Läuft im Hintergrund weiter, ohne die Navigation zu blockieren — ein frisches Gerät
// hat an dieser Stelle im Assistenten meist noch keinen Internetzugang, die Prüfung
// schlägt dann einfach fehl (kein Fehlerzustand) und wird nach dem Anwenden wiederholt.
- _ = CheckForUpdatesInBackgroundAsync(_session.Client);
+ if (UserSettings.Current.CheckUpdatesOnConnect)
+ {
+ _ = CheckForUpdatesInBackgroundAsync(_session.Client);
+ }
}
catch (RouterOsException ex)
{
diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs
index 2290c9c..fc677b6 100644
--- a/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs
+++ b/src/MikrotikInstaller.App/ViewModels/Steps/ModeSelectionStepViewModel.cs
@@ -24,7 +24,8 @@ public partial class ModeSelectionStepViewModel : WizardStepViewModelBase
[ObservableProperty]
private bool _isExpertMode;
- public override bool ShouldSkip => _session.SelectedArea == AppArea.BackupManagement;
+ /// Ohne „Professioneller Netzwerker" gibt es nichts zu wählen — dann immer der geführte Assistent.
+ public override bool ShouldSkip => _session.SelectedArea == AppArea.BackupManagement || !UserSettings.Current.ProfessionalMode;
public override Task OnActivatedAsync()
{
diff --git a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
index e0c8bae..df3436e 100644
--- a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
+++ b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs
@@ -27,7 +27,14 @@ public sealed class WizardSession
/// Ob der Nutzer im Moduswahl-Schritt den Experten-Modus gewählt hat. Steuert, ob die
/// nachfolgenden Schritte zusätzliche, technischere Felder anzeigen. Standard: Laien-Modus (false).
///
- public bool IsExpertMode { get; set; }
+ public bool IsExpertMode
+ {
+ // Ohne "Professioneller Netzwerker" (Einstellungen) gibt es keinen Experten-Modus (Mac v1.3.0).
+ get => _isExpertMode && UserSettings.Current.ProfessionalMode;
+ set => _isExpertMode = value;
+ }
+
+ private bool _isExpertMode;
///
/// Direkt nach dem Verbinden gewählter Bereich (siehe ).
diff --git a/src/MikrotikInstaller.App/Views/SettingsWindow.xaml b/src/MikrotikInstaller.App/Views/SettingsWindow.xaml
new file mode 100644
index 0000000..4db8464
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/SettingsWindow.xaml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MikrotikInstaller.App/Views/SettingsWindow.xaml.cs b/src/MikrotikInstaller.App/Views/SettingsWindow.xaml.cs
new file mode 100644
index 0000000..b91f4d3
--- /dev/null
+++ b/src/MikrotikInstaller.App/Views/SettingsWindow.xaml.cs
@@ -0,0 +1,16 @@
+using System.Windows;
+using MikrotikInstaller.App.ViewModels;
+using Wpf.Ui.Controls;
+
+namespace MikrotikInstaller.App.Views;
+
+public partial class SettingsWindow : FluentWindow
+{
+ public SettingsWindow()
+ {
+ InitializeComponent();
+ ((SettingsViewModel)DataContext).Saved += (_, _) => Close();
+ }
+
+ private void OnCancel(object sender, RoutedEventArgs e) => Close();
+}
diff --git a/src/MikrotikInstaller.Core.Tests/Settings/AppSettingsStoreTests.cs b/src/MikrotikInstaller.Core.Tests/Settings/AppSettingsStoreTests.cs
new file mode 100644
index 0000000..0602be5
--- /dev/null
+++ b/src/MikrotikInstaller.Core.Tests/Settings/AppSettingsStoreTests.cs
@@ -0,0 +1,47 @@
+using MikrotikInstaller.Core.Settings;
+
+namespace MikrotikInstaller.Core.Tests.Settings;
+
+public class AppSettingsStoreTests : IDisposable
+{
+ private readonly string _filePath = Path.Combine(Path.GetTempPath(), $"settings-test-{Guid.NewGuid()}.json");
+
+ public void Dispose()
+ {
+ if (File.Exists(_filePath))
+ {
+ File.Delete(_filePath);
+ }
+ }
+
+ [Fact]
+ public void Load_NoFile_ReturnsDefaults()
+ {
+ var settings = new AppSettingsStore(_filePath).Load();
+
+ Assert.Equal(AppTheme.System, settings.Theme);
+ Assert.True(settings.TestEnvironment);
+ Assert.False(settings.ProfessionalMode);
+ }
+
+ [Fact]
+ public void SaveThenLoad_RoundTrips()
+ {
+ var store = new AppSettingsStore(_filePath);
+ store.Save(new AppSettings { Theme = AppTheme.Dark, ProfessionalMode = true, BackupFolder = @"D:\Sicherungen" });
+
+ var loaded = store.Load();
+
+ Assert.Equal(AppTheme.Dark, loaded.Theme);
+ Assert.True(loaded.ProfessionalMode);
+ Assert.Equal(@"D:\Sicherungen", loaded.EffectiveBackupFolder);
+ }
+
+ [Fact]
+ public void Load_CorruptFile_ReturnsDefaults()
+ {
+ File.WriteAllText(_filePath, "{ kaputt");
+
+ Assert.Equal(new AppSettings(), new AppSettingsStore(_filePath).Load());
+ }
+}
diff --git a/src/MikrotikInstaller.Core/Settings/AppSettings.cs b/src/MikrotikInstaller.Core/Settings/AppSettings.cs
new file mode 100644
index 0000000..83cda00
--- /dev/null
+++ b/src/MikrotikInstaller.Core/Settings/AppSettings.cs
@@ -0,0 +1,75 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace MikrotikInstaller.Core.Settings;
+
+public enum AppTheme
+{
+ System,
+ Light,
+ Dark,
+}
+
+///
+/// Lokale Einstellungen der App (analog zu den Einstellungen der Mac-Version, ohne App-Store-Teil).
+/// Enthält bewusst kein Backup-Passwort — das liegt wie Router-Passwörter im Windows-Anmeldeinformationsspeicher.
+///
+public sealed record AppSettings
+{
+ [JsonConverter(typeof(JsonStringEnumConverter))]
+ public AppTheme Theme { get; init; } = AppTheme.System;
+
+ /// Beim Verbinden im Hintergrund nach einer neueren RouterOS-Version suchen.
+ public bool CheckUpdatesOnConnect { get; init; } = true;
+
+ /// „Professioneller Netzwerker": blendet den Experten-Modus im Assistenten (und später den Experte-Bereich) ein.
+ public bool ProfessionalMode { get; init; }
+
+ /// Simulierte Testgeräte im Verbindungs-Schritt anbieten.
+ public bool TestEnvironment { get; init; } = true;
+
+ /// Ordner für auf den PC geladene Backups; null = Standard (Dokumente\MikroTik-Backups).
+ public string? BackupFolder { get; init; }
+
+ /// Die Einführung beim ersten Start wurde abgeschlossen oder übersprungen.
+ public bool IntroCompleted { get; init; }
+
+ public string EffectiveBackupFolder => BackupFolder ?? Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "MikroTik-Backups");
+}
+
+/// Liest/schreibt als JSON unter %AppData%\MikroTik-Installer\settings.json.
+public sealed class AppSettingsStore
+{
+ private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
+
+ private readonly string _filePath;
+
+ /// Nur zum Überschreiben in Tests.
+ public AppSettingsStore(string? filePath = null)
+ {
+ _filePath = filePath ?? Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "MikroTik-Installer", "settings.json");
+ }
+
+ /// Fehlende oder beschädigte Datei ergibt die Standardwerte — Einstellungen blockieren nie den Start.
+ public AppSettings Load()
+ {
+ try
+ {
+ return File.Exists(_filePath)
+ ? JsonSerializer.Deserialize(File.ReadAllText(_filePath), JsonOptions) ?? new AppSettings()
+ : new AppSettings();
+ }
+ catch (Exception ex) when (ex is JsonException or IOException)
+ {
+ return new AppSettings();
+ }
+ }
+
+ public void Save(AppSettings settings)
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
+ File.WriteAllText(_filePath, JsonSerializer.Serialize(settings, JsonOptions));
+ }
+}