- 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>
245 lines
9.7 KiB
C#
245 lines
9.7 KiB
C#
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}"); }
|
||
}
|
||
}
|