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
+72 -12
View File
@@ -3,7 +3,7 @@ using System.IO;
namespace DeskCore;
public enum TransferStatus { Ok, Skipped, Error }
public enum TransferStatus { Ok, Skipped, Error, Cancelled }
/// <param name="Copied">true = kopiert (Original bleibt), false = verschoben</param>
public record TransferResult(TransferStatus Status, string Source, string? Dest = null, bool Copied = false, string? Message = null);
@@ -140,8 +140,10 @@ public static class Storage
/// wird kopiert – das Original kann danach gelöscht werden. Innerhalb der App und zurück auf den
/// Desktop wird verschoben. Gleiche Elemente werden nicht doppelt angelegt.
/// </summary>
public static TransferResult Transfer(string source, string targetDir, bool forceMove = false)
public static TransferResult Transfer(string source, string targetDir, bool forceMove = false,
Action<long>? onBytes = null, CancellationToken ct = default)
{
if (ct.IsCancellationRequested) return new(TransferStatus.Cancelled, source);
var name = Path.GetFileName(source.TrimEnd('\\'));
try
{
@@ -160,8 +162,18 @@ public static class Storage
Directory.CreateDirectory(targetDir);
var dest = UniquePath(targetDir, name);
if (copy) CopyAny(source, dest);
else MoveAny(source, dest);
try
{
if (copy) CopyAny(source, dest, onBytes, ct);
else MoveAny(source, dest, onBytes, ct);
}
catch (OperationCanceledException)
{
// halb kopiertes Ziel wieder entfernen – die Quelle ist unverändert
// (beim Verschieben zwischen Laufwerken wird die Quelle erst nach vollständiger Kopie gelöscht)
DeletePartial(dest);
return new(TransferStatus.Cancelled, source);
}
return new(TransferStatus.Ok, source, dest, copy);
}
catch (UnauthorizedAccessException)
@@ -176,7 +188,7 @@ public static class Storage
}
/// <summary>Verschiebt Datei oder Ordner – auch zwischen Laufwerken.</summary>
public static void MoveAny(string source, string dest)
public static void MoveAny(string source, string dest, Action<long>? onBytes = null, CancellationToken ct = default)
{
NoteCreated(dest);
if (File.Exists(source))
@@ -189,15 +201,15 @@ public static class Storage
Directory.Move(source, dest);
return;
}
CopyDirectory(source, dest);
CopyDirectory(source, dest, onBytes, ct);
Directory.Delete(source, recursive: true);
}
public static void CopyAny(string source, string dest)
public static void CopyAny(string source, string dest, Action<long>? onBytes = null, CancellationToken ct = default)
{
NoteCreated(dest);
if (Directory.Exists(source)) CopyDirectory(source, dest);
else File.Copy(source, dest);
if (Directory.Exists(source)) CopyDirectory(source, dest, onBytes, ct);
else CopyFile(source, dest, onBytes, ct);
}
/// <summary>Benennt ein Element um. Gibt den neuen Pfad zurück.</summary>
@@ -321,12 +333,60 @@ public static class Storage
}
}
private static void CopyDirectory(string source, string dest)
private static void CopyDirectory(string source, string dest, Action<long>? onBytes, CancellationToken ct)
{
Directory.CreateDirectory(dest);
foreach (var f in Directory.GetFiles(source))
File.Copy(f, Path.Combine(dest, Path.GetFileName(f)));
CopyFile(f, Path.Combine(dest, Path.GetFileName(f)), onBytes, ct);
foreach (var d in Directory.GetDirectories(source))
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)));
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)), onBytes, ct);
}
/// <summary>Kopiert in 1-MB-Stücken, damit Fortschritt angezeigt und abgebrochen werden kann.</summary>
private static void CopyFile(string source, string dest, Action<long>? onBytes, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
const int Chunk = 1 << 20;
using (var input = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, Chunk, FileOptions.SequentialScan))
using (var output = new FileStream(dest, FileMode.CreateNew, FileAccess.Write, FileShare.None, Chunk))
{
var buffer = new byte[Chunk];
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ct.ThrowIfCancellationRequested();
output.Write(buffer, 0, read);
onBytes?.Invoke(read);
}
}
// Zeitstempel und Attribute wie beim normalen Kopieren übernehmen
try
{
File.SetCreationTimeUtc(dest, File.GetCreationTimeUtc(source));
File.SetLastWriteTimeUtc(dest, File.GetLastWriteTimeUtc(source));
File.SetAttributes(dest, File.GetAttributes(source) & ~FileAttributes.ReadOnly);
}
catch { }
}
private static void DeletePartial(string path)
{
try
{
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
else if (File.Exists(path)) File.Delete(path);
}
catch { }
}
/// <summary>Wie viele Bytes müssen für diese Übertragung tatsächlich kopiert werden? (0 = nur umbenennen)</summary>
public static long BytesToCopy(string source, string targetDir, bool forceMove)
{
bool copy = !forceMove && !IsInStorage(source) && IsInStorage(targetDir);
bool crossDrive = !string.Equals(Path.GetPathRoot(Path.GetFullPath(source)), Path.GetPathRoot(Path.GetFullPath(targetDir)),
StringComparison.OrdinalIgnoreCase);
if (!copy && !crossDrive) return 0;
try { return Directory.Exists(source) ? DirectorySize(source) : new FileInfo(source).Length; }
catch { return 0; }
}
}