using System.Diagnostics; using System.Windows; using System.Windows.Threading; namespace DeskCore; /// Fortschrittsanzeige für lange Vorgänge, Fensterposition merken und Update-Hinweis. public partial class MainWindow { // ===================================================================== // Fortschritt mit Abbrechen // ===================================================================== /// Meldet Fortschritt aus dem Hintergrund (threadsicher). public sealed class ProgressSink { private long _done, _total; public long Done => Interlocked.Read(ref _done); public long Total => Interlocked.Read(ref _total); public void Add(long bytes) => Interlocked.Add(ref _done, bytes); public void SetTotal(long total) => Interlocked.Exchange(ref _total, total); public void SetDone(long done) => Interlocked.Exchange(ref _done, done); public volatile string? Detail; } private CancellationTokenSource? _busyCts; private Task? _busyTask; public bool IsBusy => _busyCts is not null; /// /// Führt einen langen Vorgang im Hintergrund aus. Das Fenster bleibt bedienbar; nach kurzer Zeit erscheint /// eine Fortschrittsanzeige mit Abbrechen-Knopf (bei schnellen Vorgängen gar nicht erst). /// private async Task RunWithProgress(string title, string detail, Func> work) { var cts = new CancellationTokenSource(); _busyCts = cts; var sink = new ProgressSink { Detail = detail }; var sw = Stopwatch.StartNew(); ProgressTitle.Text = title; ProgressDetail.Text = detail; ProgressStats.Text = ""; ProgressFill.Width = 0; ProgressCancel.IsEnabled = true; ProgressCancel.Content = "Abbrechen"; var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) }; timer.Tick += (_, _) => { if (ProgressOverlay.Visibility != Visibility.Visible && sw.ElapsedMilliseconds > 500) ProgressOverlay.Visibility = Visibility.Visible; if (sink.Detail is { } d) ProgressDetail.Text = d; long done = sink.Done, total = sink.Total; if (total > 0) { double frac = Math.Min(1, done / (double)total); ProgressFill.Width = ProgressTrack.ActualWidth * frac; ProgressStats.Text = $"{Backup.FormatSize(done)} von {Backup.FormatSize(total)}{Remaining(done, total, sw.Elapsed)}"; } else if (done > 0) ProgressStats.Text = Backup.FormatSize(done); }; timer.Start(); var task = Task.Run(() => work(sink, cts.Token)); _busyTask = task; try { return await task; } finally { timer.Stop(); ProgressOverlay.Visibility = Visibility.Collapsed; _busyCts = null; _busyTask = null; cts.Dispose(); } } private static string Remaining(long done, long total, TimeSpan elapsed) { if (done <= 0 || elapsed.TotalSeconds < 2) return ""; var rest = TimeSpan.FromSeconds((total - done) / (done / elapsed.TotalSeconds)); return rest.TotalSeconds < 5 ? " · gleich fertig" : rest.TotalMinutes < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalSeconds / 5) * 5:0} Sek." : rest.TotalHours < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalMinutes):0} Min." : $" · noch ca. {rest.TotalHours:0.#} Std."; } private void ProgressCancel_Click(object sender, RoutedEventArgs e) { _busyCts?.Cancel(); ProgressCancel.IsEnabled = false; ProgressCancel.Content = "Wird abgebrochen …"; } /// Beim Beenden: laufenden Vorgang abbrechen und kurz aufs Aufräumen warten. public void CancelBusyAndWait() { if (_busyCts is null) return; _busyCts.Cancel(); try { _busyTask?.Wait(TimeSpan.FromSeconds(5)); } catch { } } // ===================================================================== // Fensterposition merken // ===================================================================== private void RestorePlacement() { if (S.WindowLeft is double l && S.WindowTop is double t && S.WindowWidth is double w && S.WindowHeight is double h) { var r = new Rect(l, t, Math.Max(MinWidth, w), Math.Max(MinHeight, h)); // nur übernehmen, wenn das Fenster dort noch erreichbar ist (Monitor evtl. abgesteckt) if (Screens.IsReachable(r)) { WindowStartupLocation = WindowStartupLocation.Manual; Left = r.Left; Top = r.Top; Width = r.Width; Height = r.Height; } } if (S.WindowMaximized) WindowState = WindowState.Maximized; } public void SavePlacement() { if (!IsLoaded) return; // nie angezeigt (Start im Infobereich) → nichts zu merken var r = WindowState == WindowState.Normal ? new Rect(Left, Top, ActualWidth, ActualHeight) : RestoreBounds; if (r.IsEmpty || r.Width < 100) return; S.WindowLeft = r.Left; S.WindowTop = r.Top; S.WindowWidth = r.Width; S.WindowHeight = r.Height; S.WindowMaximized = WindowState == WindowState.Maximized; S.Save(); } /// Bildschirm abgesteckt/umgestellt: Fenster zurückholen, falls es nicht mehr erreichbar ist. public void EnsureOnScreen() { if (WindowState != WindowState.Normal) return; if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return; var wa = Screens.PrimaryWorkArea; Width = Math.Min(Width, wa.Width); Height = Math.Min(Height, wa.Height); Left = wa.Left + (wa.Width - Width) / 2; Top = wa.Top + (wa.Height - Height) / 2; } // ===================================================================== // Updates // ===================================================================== private UpdateInfo? _update; /// Beim Start: höchstens einmal am Tag nachsehen, leise im Hintergrund. public async void CheckForUpdatesOnStartup() { if (!S.AutoUpdateCheck) return; if (S.LastUpdateCheck is { } last && DateTime.Now - last < TimeSpan.FromHours(20)) return; await Task.Delay(TimeSpan.FromSeconds(8)); // Start nicht ausbremsen await CheckForUpdates(manual: false); } private async Task CheckForUpdates(bool manual) { if (manual) { CheckUpdateButton.IsEnabled = false; UpdateStatusText.Text = "Suche nach Updates …"; } try { var info = await UpdateChecker.CheckAsync(); S.LastUpdateCheck = DateTime.Now; S.Save(); if (info is null) { if (manual) UpdateStatusText.Text = $"Du hast die neueste Version ({UpdateChecker.Current})."; return; } if (!manual && S.SkippedVersion == info.Version.ToString()) return; _update = info; UpdateText.Text = $"DeskCore {info.Version} ist verfügbar – du hast Version {UpdateChecker.Current}."; UpdateNowButton.Content = info.SetupUrl is not null ? "Jetzt aktualisieren" : "Zur Download-Seite"; UpdateBar.Visibility = Visibility.Visible; if (manual) UpdateStatusText.Text = $"Version {info.Version} ist verfügbar – oben auf der Startseite aktualisieren."; else if (!IsVisible) App.TrayIcon?.Notify("Update verfügbar", $"DeskCore {info.Version} ist verfügbar. Zum Aktualisieren DeskCore öffnen."); } catch (Exception ex) { if (manual) UpdateStatusText.Text = $"Konnte nicht prüfen: {ex.Message}"; } finally { if (manual) CheckUpdateButton.IsEnabled = true; } } private async void CheckUpdate_Click(object sender, RoutedEventArgs e) => await CheckForUpdates(manual: true); private void AutoUpdateToggle_Click(object sender, RoutedEventArgs e) { S.AutoUpdateCheck = AutoUpdateToggle.IsChecked == true; S.Save(); } private void UpdateLater_Click(object sender, RoutedEventArgs e) => UpdateBar.Visibility = Visibility.Collapsed; private void UpdateSkip_Click(object sender, RoutedEventArgs e) { if (_update is not null) { S.SkippedVersion = _update.Version.ToString(); S.Save(); } UpdateBar.Visibility = Visibility.Collapsed; } private async void UpdateNow_Click(object sender, RoutedEventArgs e) { if (_update is not { } info || IsBusy) return; if (info.SetupUrl is null) { Process.Start(new ProcessStartInfo(info.PageUrl) { UseShellExecute = true }); return; } try { var setup = await RunWithProgress($"Update auf {info.Version}", "Neue Version wird heruntergeladen …", (sink, ct) => UpdateChecker.DownloadSetupAsync(info, (done, total) => { sink.SetTotal(total); sink.SetDone(done); }, ct)); // Installer still ausführen und DeskCore danach automatisch neu starten Process.Start(new ProcessStartInfo(setup, "/S /launch") { UseShellExecute = true }); App.Quit(); } catch (OperationCanceledException) { ShowToast("Update abgebrochen", "", info: true); } catch (Exception ex) { Error("Update fehlgeschlagen", $"{ex.Message}\n\nDu kannst die neue Version auch selbst herunterladen:\n{info.PageUrl}"); } } }