@@ -26,6 +26,8 @@ public class App : Application
|
||||
public static IServiceProvider Services { get; private set; } = null!;
|
||||
private static ServiceProvider? _serviceProvider;
|
||||
private static bool _exitHandlerAttached;
|
||||
private static Task _initialPolls = Task.CompletedTask;
|
||||
private static readonly CancellationTokenSource StartupCancellation = new();
|
||||
|
||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||
|
||||
@@ -65,7 +67,11 @@ public class App : Application
|
||||
promptVm.OnUnlocked = async password =>
|
||||
{
|
||||
AppBootstrapper.DbPassword = password;
|
||||
await StartMainAppAsync(desktop, promptWindow);
|
||||
var unlockedSplash = new SplashWindow();
|
||||
desktop.MainWindow = unlockedSplash;
|
||||
unlockedSplash.Show();
|
||||
promptWindow.Close();
|
||||
await StartMainAppAsync(desktop, unlockedSplash);
|
||||
};
|
||||
desktop.MainWindow = promptWindow;
|
||||
promptWindow.Show();
|
||||
@@ -79,14 +85,21 @@ public class App : Application
|
||||
private static async Task StartMainAppAsync(
|
||||
IClassicDesktopStyleApplicationLifetime desktop, Window? windowToClose = null)
|
||||
{
|
||||
_serviceProvider = AppBootstrapper.BuildServices();
|
||||
var splash = windowToClose as SplashWindow;
|
||||
var timer = System.Diagnostics.Stopwatch.StartNew();
|
||||
var progress = new Progress<(int Value, string Text)>(step =>
|
||||
splash?.SetProgress(step.Value, step.Text));
|
||||
_serviceProvider = await Task.Run(() => AppBootstrapper.BuildServices(
|
||||
(value, text) => ((IProgress<(int, string)>)progress).Report((value, text))));
|
||||
Services = _serviceProvider;
|
||||
Services.GetRequiredService<AppLogger>().Info("Anwendung gestartet.");
|
||||
GlobalExceptionHandler.AttachNotifications(Services.GetRequiredService<NotificationService>());
|
||||
// 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<ITrashRepository>().PurgeOlderThan(DateTime.UtcNow.AddDays(-30));
|
||||
splash?.SetProgress(60, "Papierkorb aufräumen …");
|
||||
await Task.Run(() => Services.GetRequiredService<ITrashRepository>()
|
||||
.PurgeOlderThan(DateTime.UtcNow.AddDays(-30)));
|
||||
|
||||
if (!_exitHandlerAttached)
|
||||
{
|
||||
@@ -99,19 +112,13 @@ public class App : Application
|
||||
// Opt-in - ein Neustart der App richtet den Pipe-Listener neu ein.
|
||||
Services.GetRequiredService<Services.Mcp.McpServerHostedService>().Start();
|
||||
|
||||
splash?.SetProgress(75, "Übersicht vorbereiten …");
|
||||
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background);
|
||||
var mainVm = Services.GetRequiredService<MainWindowViewModel>();
|
||||
WireCallbacks(mainVm);
|
||||
|
||||
// Beide optionalen Erstabgleiche noch unter dem Splashscreen abschließen. Ihre
|
||||
// CPU-/Datenbankarbeit läuft innerhalb der Dienste im Threadpool; dadurch öffnet das
|
||||
// Hauptfenster mit fertigem Datenstand und friert nicht kurz danach ein. Das Auflösen
|
||||
// aktiviert zugleich die periodischen Timer.
|
||||
var initialPolls = new List<Task>();
|
||||
if (Services.GetService<UntisSyncService>() is { } untisSync)
|
||||
initialPolls.Add(untisSync.PollAsync());
|
||||
if (Services.GetService<AnnualPlanSyncService>() is { } annualPlanSync)
|
||||
initialPolls.Add(annualPlanSync.PollAsync());
|
||||
await Task.WhenAll(initialPolls);
|
||||
splash?.SetProgress(90, "Hauptfenster öffnen …");
|
||||
await Dispatcher.UIThread.InvokeAsync(() => { }, DispatcherPriority.Background);
|
||||
|
||||
var main = new MainWindow { DataContext = mainVm };
|
||||
main.EnableWindowSizePersistence(Services.GetRequiredService<WindowSettingsService>());
|
||||
@@ -119,7 +126,30 @@ public class App : Application
|
||||
main.EnableFinalSync(syncEngine);
|
||||
desktop.MainWindow = main;
|
||||
main.Show();
|
||||
splash?.SetProgress(100, "Bereit");
|
||||
windowToClose?.Close();
|
||||
AppBootstrapper.Logger.Info($"Start: Hauptfenster nach {timer.ElapsedMilliseconds} ms geöffnet.");
|
||||
|
||||
// Vorhandene lokale Daten sind sofort nutzbar; Netzwerkzugriffe blockieren den Start nicht.
|
||||
var untis = Services.GetService<UntisSyncService>();
|
||||
var annualPlan = Services.GetService<AnnualPlanSyncService>();
|
||||
if (untis is not null)
|
||||
untis.DataChanged += () => Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
Services.GetRequiredService<TimetableViewModel>().Load();
|
||||
Services.GetRequiredService<DashboardViewModel>().RefreshCommand.Execute(null);
|
||||
});
|
||||
_initialPolls = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(
|
||||
untis?.PollAsync(StartupCancellation.Token) ?? Task.CompletedTask,
|
||||
annualPlan?.PollAsync(StartupCancellation.Token) ?? Task.CompletedTask);
|
||||
}
|
||||
catch (OperationCanceledException) when (StartupCancellation.IsCancellationRequested) { }
|
||||
catch (Exception ex) { AppBootstrapper.Logger.Error("Erstabgleich fehlgeschlagen.", ex); }
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>Wechselt die Darstellung sofort, ohne Neustart (12.4) — Avalonia stylt den
|
||||
@@ -140,6 +170,8 @@ public class App : Application
|
||||
|
||||
try
|
||||
{
|
||||
StartupCancellation.Cancel();
|
||||
_initialPolls.GetAwaiter().GetResult();
|
||||
serviceProvider.GetService<LiteDbContext>()?.Checkpoint();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
Reference in New Issue
Block a user