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>
This commit is contained in:
2026-10-01 20:46:48 +02:00
co-authored by Claude Opus 5.5
parent 77bb3e6b9d
commit 1ae6e1abc2
19 changed files with 1098 additions and 95 deletions
+84 -15
View File
@@ -32,6 +32,7 @@ public partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
RestorePlacement();
StateChanged += (_, _) => UpdateWindowState();
Closing += OnClosing;
PreviewKeyDown += OnPreviewKeyDown;
@@ -73,6 +74,7 @@ public partial class MainWindow : Window
private void OnClosing(object? sender, CancelEventArgs e)
{
if (App.Quitting) return;
SavePlacement();
if (S.TrayEnabled && S.CloseToTray)
{
e.Cancel = true;
@@ -189,7 +191,7 @@ public partial class MainWindow : Window
/// <summary>Vom Watcher: Dateien haben sich geändert.</summary>
public void OnStorageChanged()
{
if (!_dragging) Refresh();
if (!_dragging && !IsBusy) Refresh(); // während großer Kopien nicht ständig neu laden
}
private void Refresh()
@@ -792,7 +794,7 @@ public partial class MainWindow : Window
IconMenu.Items.Add(MenuItem(fromApp ? "Aus der App löschen" : "Vom Desktop löschen", "", () => DeleteIcons(sel), gesture: "Entf"));
}
private void RenameIcon(IconItem item)
public void RenameIcon(IconItem item)
{
var ext = Storage.HiddenExtension(item.Path);
var newName = Dialog.Prompt(this, "Umbenennen", "Neuer Name:", item.Name, "Umbenennen", text =>
@@ -816,7 +818,7 @@ public partial class MainWindow : Window
catch (Exception ex) { Error("Umbenennen fehlgeschlagen", ex.Message); }
}
private void DeleteIcons(List<IconItem> sel)
public void DeleteIcons(List<IconItem> sel)
{
// Ungesicherte Desktop-Icons: nachfragen. Alles andere geht direkt (Strg+Z holt es zurück).
var unsorted = sel.Count(i => !Storage.IsInStorage(i.Path) && !i.IsSorted);
@@ -842,19 +844,31 @@ public partial class MainWindow : Window
else ShowToast($"{Plural(sel.Count, "Element", "Elemente")} gelöscht", "", undoable: true);
}
/// <summary>Kopiert/verschiebt Dateien in einen Ordner – mit Rückgängig und Rückmeldung.</summary>
public void TransferAll(IEnumerable<string> paths, string targetDir)
/// <summary>
/// Kopiert/verschiebt Dateien in einen Ordner – mit Rückgängig und Rückmeldung.
/// Läuft im Hintergrund; bei großen Datenmengen erscheint ein Fortschritt mit Abbrechen.
/// </summary>
public async void TransferAll(IEnumerable<string> paths, string targetDir)
{
if (IsBusy) { ShowToast("Bitte warten, bis der laufende Vorgang fertig ist", "\uE946", info: true); return; }
var list = paths.ToList();
if (list.Count == 0) return;
var target = Storage.IsDesktopDir(targetDir) ? DesktopName : Path.GetFileName(targetDir.TrimEnd('\\'));
// Große Ordner würden beim Kopieren doppelt Platz belegen → lieber verschieben?
var bigFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (Storage.IsInStorage(targetDir))
{
const long Limit = 200L * 1024 * 1024;
var big = list.Where(p => Directory.Exists(p) && !Storage.IsInStorage(p))
.Select(p => (Path: p, Size: Storage.DirectorySize(p, Limit * 50)))
.Where(x => x.Size > Limit).ToList();
Mouse.OverrideCursor = Cursors.Wait;
List<(string Path, long Size)> big;
try
{
big = await Task.Run(() => list.Where(p => Directory.Exists(p) && !Storage.IsInStorage(p))
.Select(p => (Path: p, Size: Storage.DirectorySize(p, Limit * 50)))
.Where(x => x.Size > Limit).ToList());
}
finally { Mouse.OverrideCursor = null; }
if (big.Count > 0)
{
var what = big.Count == 1
@@ -867,8 +881,31 @@ public partial class MainWindow : Window
}
}
bool anyCopy = list.Any(p => !bigFolders.Contains(p) && !Storage.IsInStorage(p) && Storage.IsInStorage(targetDir));
var detail = list.Count == 1 ? $"„{Storage.DisplayName(list[0])}“ → {target}" : $"{list.Count} Elemente → {target}";
List<TransferResult> results;
try
{
results = await RunWithProgress(anyCopy ? "Wird kopiert …" : "Wird verschoben …", detail, (sink, ct) =>
{
sink.SetTotal(list.Sum(p => Storage.BytesToCopy(p, targetDir, bigFolders.Contains(p))));
var r = new List<TransferResult>();
foreach (var p in list)
{
if (list.Count > 1) sink.Detail = $"„{Storage.DisplayName(p)}“ → {target}";
r.Add(Storage.Transfer(p, targetDir, bigFolders.Contains(p), sink.Add, ct));
}
return Task.FromResult(r);
});
}
catch (Exception ex)
{
Refresh();
Error("Übertragen fehlgeschlagen", ex.Message);
return;
}
var entry = new UndoEntry($"{Plural(list.Count, "Element", "Elemente")} nach „{target}“");
var results = list.Select(p => Storage.Transfer(p, targetDir, forceMove: bigFolders.Contains(p))).ToList();
foreach (var r in results.Where(r => r.Status == TransferStatus.Ok))
{
if (r.Copied) entry.AddCopy(r.Source, r.Dest!);
@@ -876,25 +913,33 @@ public partial class MainWindow : Window
}
Undo.Push(entry);
Refresh();
FenceManager.RefreshAll();
var ok = results.Where(r => r.Status == TransferStatus.Ok).ToList();
var skipped = results.Where(r => r.Status == TransferStatus.Skipped).ToList();
var cancelled = results.Count(r => r.Status == TransferStatus.Cancelled);
var errors = results.Where(r => r.Status == TransferStatus.Error).Select(r => r.Message!).ToList();
if (errors.Count > 0) { ShowErrors(errors); return; }
if (cancelled > 0)
{
ShowToast(ok.Count > 0 ? $"Abgebrochen – {Plural(ok.Count, "Element", "Elemente")} wurden noch übertragen" : "Abgebrochen – es wurde nichts verändert",
"\uE711", undoable: ok.Count > 0, info: true);
return;
}
if (ok.Count == 0 && skipped.Count > 0)
{
ShowToast(skipped.Count == 1 ? skipped[0].Message! : $"Alle {skipped.Count} Elemente sind schon in „{target}“", "", info: true);
ShowToast(skipped.Count == 1 ? skipped[0].Message! : $"Alle {skipped.Count} Elemente sind schon in „{target}“", "\uE946", info: true);
return;
}
var n = Plural(ok.Count, "Element", "Elemente");
var extra = skipped.Count > 0 ? $" · {skipped.Count} waren schon da" : "";
if (Storage.IsDesktopDir(targetDir))
ShowToast($"{n} zurück auf den Desktop gelegt{extra}", "", undoable: true);
ShowToast($"{n} zurück auf den Desktop gelegt{extra}", "\uE7F4", undoable: true);
else if (ok.All(r => r.Copied))
ShowToast($"{n} in „{target}“ gesichert{extra}", undoable: true);
else
ShowToast($"{n} nach „{target}“ verschoben{extra}", "", undoable: true);
ShowToast($"{n} nach „{target}“ verschoben{extra}", "\uE8DE", undoable: true);
}
// ---------- Rückgängig ----------
@@ -1095,6 +1140,7 @@ public partial class MainWindow : Window
HotkeyBox.IsEnabled = S.HotkeyEnabled;
AutoSortToggle.IsChecked = S.AutoSort;
AutoUpdateToggle.IsChecked = S.AutoUpdateCheck;
AutoRemoveToggle.IsChecked = S.AutoSortRemoveFromDesktop;
RulesList.ItemsSource = null;
RulesList.ItemsSource = S.Rules;
@@ -1258,18 +1304,30 @@ public partial class MainWindow : Window
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
};
if (dlg.ShowDialog(this) != true) return;
if (Storage.IsUnder(dlg.FileName, Storage.Root))
{
Error("Anderen Ort wählen", "Die Sicherung kann nicht im DeskCore-Ordner selbst gespeichert werden – sie würde sich sonst mitsichern. " +
"Am besten z. B. unter Dokumente oder auf einem USB-Stick speichern.");
return;
}
var size = await Task.Run(Backup.StorageSize);
if (size > 2L * 1024 * 1024 * 1024 &&
!Dialog.Confirm(this, "Große Sicherung",
$"Deine Kategorien sind zusammen {Backup.FormatSize(size)} groß (vor allem durch Ordner). Das Sichern kann eine Weile dauern " +
"und braucht entsprechend Platz.", "Trotzdem sichern"))
return;
ShowToast("Sicherung läuft …", "", info: true);
if (IsBusy) return;
try
{
var count = await Task.Run(() => Backup.Export(dlg.FileName));
var file = dlg.FileName;
var count = await RunWithProgress("Sicherung wird erstellt …", Path.GetFileName(file), (sink, ct) =>
{
sink.SetTotal(size);
return Task.FromResult(Backup.Export(file, sink.Add, ct));
});
ShowToast($"{Plural(count, "Datei", "Dateien")} gesichert in {Path.GetFileName(dlg.FileName)}", "");
}
catch (OperationCanceledException) { ShowToast("Sicherung abgebrochen – es wurde keine Datei angelegt", "\uE711", info: true); }
catch (Exception ex) { Error("Sichern fehlgeschlagen", ex.Message); }
}
@@ -1286,14 +1344,25 @@ public partial class MainWindow : Window
"Die Kategorien aus der Sicherung werden zu deinen vorhandenen hinzugefügt. Vorhandene Dateien werden nicht überschrieben.",
"Wiederherstellen"))
return;
if (IsBusy) return;
try
{
var (added, skipped) = await Task.Run(() => Backup.Import(dlg.FileName));
var file = dlg.FileName;
var (added, skipped) = await RunWithProgress("Sicherung wird wiederhergestellt …", Path.GetFileName(file), (sink, ct) =>
{
sink.SetTotal(Backup.ArchiveSize(file));
return Task.FromResult(Backup.Import(file, sink.Add, ct));
});
Refresh();
FenceManager.Sync();
LoadSettingsView();
ShowToast($"{Plural(added, "Datei", "Dateien")} wiederhergestellt" + (skipped > 0 ? $" · {skipped} waren schon da" : ""), "");
}
catch (OperationCanceledException)
{
Refresh();
ShowToast("Wiederherstellen abgebrochen – bereits übernommene Dateien bleiben erhalten", "\uE711", info: true);
}
catch (Exception ex) { Error("Wiederherstellen fehlgeschlagen", $"Die Datei ist keine gültige Sicherung.\n\n{ex.Message}"); }
}