Files
DeskCore/MainWindow.Extras.cs
M_ViperandClaude Opus 5.5 1ae6e1abc2 Fortschritt, .NET-Prüfung, Updates, Fenster merken, Box-Funktionen
- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen;
  abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert
- Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien
- Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime
  und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft)
- Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart
- Fenstergröße und -position werden gemerkt
- Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen
- Fenster und Boxen kehren auf sichtbare Bildschirme zurück
- README ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 20:46:48 +02:00

245 lines
9.7 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using System.Windows;
using System.Windows.Threading;
namespace DeskCore;
/// <summary>Fortschrittsanzeige für lange Vorgänge, Fensterposition merken und Update-Hinweis.</summary>
public partial class MainWindow
{
// =====================================================================
// Fortschritt mit Abbrechen
// =====================================================================
/// <summary>Meldet Fortschritt aus dem Hintergrund (threadsicher).</summary>
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;
/// <summary>
/// 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).
/// </summary>
private async Task<T> RunWithProgress<T>(string title, string detail, Func<ProgressSink, CancellationToken, Task<T>> 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 …";
}
/// <summary>Beim Beenden: laufenden Vorgang abbrechen und kurz aufs Aufräumen warten.</summary>
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();
}
/// <summary>Bildschirm abgesteckt/umgestellt: Fenster zurückholen, falls es nicht mehr erreichbar ist.</summary>
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;
/// <summary>Beim Start: höchstens einmal am Tag nachsehen, leise im Hintergrund.</summary>
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}"); }
}
}