using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.Styling; using LehrerApp.Core.Interfaces; using LehrerApp.Core.Services; using LehrerApp.Data; using LehrerApp.Desktop.Services; using LehrerApp.Desktop.ViewModels; using LehrerApp.Desktop.ViewModels.Groups; using LehrerApp.Desktop.ViewModels.Planning; using LehrerApp.Desktop.ViewModels.Students; using LehrerApp.Desktop.Views; using LehrerApp.Sync; using Microsoft.Extensions.DependencyInjection; namespace LehrerApp.Desktop; public class App : Application { public static IServiceProvider Services { get; private set; } = null!; private static ServiceProvider? _serviceProvider; private static bool _exitHandlerAttached; public override void Initialize() => AvaloniaXamlLoader.Load(this); public override void OnFrameworkInitializationCompleted() { // Vor jedem Fenster (auch dem DB-Passwort-Prompt) anwenden, damit kein kurzes Aufblitzen // der Systemvorgabe zu sehen ist, falls Hell/Dunkel manuell erzwungen wurde. Eigenständige // Instanz statt über den DI-Container (der erst in BuildServices() entsteht, die den // Passwort-Prompt bereits verzögert) - dieselbe appData-Datei wie später der // DI-Singleton, rein lesend also unproblematisch doppelt konstruiert. ApplyTheme(new AppearanceSettingsService(AppBootstrapper.ResolveAppDataPath()).Load()); if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { // Verschlüsselte Datenbank (13.3.4): Passwort abfragen, bevor die Datenbank // (und damit BuildServices, das sie öffnet) angefasst wird. if (AppBootstrapper.IsDbEncrypted()) { var promptVm = new DbPasswordPromptViewModel( new DatabaseEncryptionService(), AppBootstrapper.ResolveDbPath()); var promptWindow = new DbPasswordPromptWindow { DataContext = promptVm }; promptVm.OnUnlocked = password => { AppBootstrapper.DbPassword = password; StartMainApp(desktop, showImmediately: true); promptWindow.Close(); }; desktop.MainWindow = promptWindow; } else { StartMainApp(desktop, showImmediately: false); } } base.OnFrameworkInitializationCompleted(); } private static void StartMainApp( IClassicDesktopStyleApplicationLifetime desktop, bool showImmediately) { _serviceProvider = AppBootstrapper.BuildServices(); Services = _serviceProvider; Services.GetRequiredService().Info("Anwendung gestartet."); GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService()); // Papierkorb (14.3): Einträge älter als 30 Tage endgültig entfernen. Beim Start statt per // Timer - reicht für ein Werkzeug, das ohnehin nur "Fehlklick eben rückgängig machen" sein // soll, kein dauerhaftes Archiv. Services.GetRequiredService().PurgeOlderThan(DateTime.UtcNow.AddDays(-30)); if (!_exitHandlerAttached) { desktop.Exit += (_, _) => DisposeServices(); _exitHandlerAttached = true; } var mainVm = Services.GetRequiredService(); WireCallbacks(mainVm); // DI-Singletons werden erst beim ersten Auflösen erzeugt. Ohne dieses explizite // Auflösen entstand der Timer des optionalen WebUntis-Abgleichs erst, wenn die // Einstellungen geöffnet oder ein manueller Abruf gestartet wurde. Den Dienst beim // Anwendungsstart aktivieren und wie den Jahresplan sofort einmal abgleichen; danach // übernimmt sein stündlicher Timer. if (Services.GetService() is { } untisSync) _ = untisSync.PollAsync(); var main = new MainWindow { DataContext = mainVm }; main.EnableWindowSizePersistence(Services.GetRequiredService()); if (Services.GetService() is { } syncEngine) main.EnableFinalSync(syncEngine); desktop.MainWindow = main; if (showImmediately) main.Show(); } /// Wechselt die Darstellung sofort, ohne Neustart (12.4) — Avalonia stylt den /// gesamten sichtbaren Baum automatisch neu, sobald sich /// ändert. public static void ApplyTheme(AppTheme theme) => Current!.RequestedThemeVariant = theme switch { AppTheme.Light => ThemeVariant.Light, AppTheme.Dark => ThemeVariant.Dark, _ => ThemeVariant.Default, }; private static void DisposeServices() { var serviceProvider = Interlocked.Exchange(ref _serviceProvider, null); if (serviceProvider is null) return; try { serviceProvider.GetService()?.Checkpoint(); } catch (Exception ex) { AppBootstrapper.Logger.Error("Datenbank-Checkpoint beim Beenden fehlgeschlagen.", ex); } finally { // Der Exit-Handler läuft synchron auf dem Avalonia-UI-Thread. Die asynchrone // Entsorgung darf dort nicht mit GetResult() gestartet werden: Fortsetzungen aus // WebUntis/HttpClient könnten sonst auf den blockierten UI-Kontext zurückwarten. try { Task.Run(async () => await serviceProvider.DisposeAsync().ConfigureAwait(false)) .GetAwaiter().GetResult(); } catch (Exception ex) { AppBootstrapper.Logger.Error("Dienste konnten beim Beenden nicht vollständig freigegeben werden.", ex); } } } private static void WireCallbacks(MainWindowViewModel main) { // GroupList → GroupDetail (OnAddGroup wird in GroupListView.axaml.cs verdrahtet) var gl = Services.GetRequiredService(); gl.OnNavigateToDetail = (id, tab) => main.NavigateToGroupDetail(id, tab); // Dashboard → GroupDetail (Chips) / StudentDetail (Fehlzeiten-/Förderplan-Hinweise) var dash = Services.GetRequiredService(); dash.OnNavigateToGroup = id => main.NavigateToGroupDetail(id); dash.OnNavigateToStudent = id => main.NavigateToStudent(id); dash.OnNavigateToLesson = id => main.NavigateToGroupDetail(id, 3); // Tab "Mitarbeit" dash.OnNavigateToExam = id => main.NavigateToGroupDetail(id, 4); // Tab "Klausuren" dash.OnNavigateToUnplannedLesson = id => main.NavigateToGroupDetail(id, 6); // Tab "Planung" // StudentList → StudentDetail + Anlegen var sl = Services.GetRequiredService(); sl.OnNavigateToDetail = id => main.NavigateToStudent(id); sl.OnAddStudent = () => ShowAddStudentDialog(); // Stundenplan "Heute" → GroupDetail (Tab "Planung") / Einstellungen (Zahnrad, Tab "Ferien & Feiertage") var timetable = Services.GetRequiredService(); timetable.OnNavigateToSettings = tab => main.NavigateToSettings(tab); timetable.OnNavigateToGroup = id => main.NavigateToGroupDetail(id, 6); } private static async Task ShowAddStudentDialog() { var vm = new ViewModels.Students.AddStudentDialogViewModel( Services.GetRequiredService()); var dialog = new Views.Students.AddStudentDialog { DataContext = vm }; if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) await dialog.ShowDialog(owner); } }