DeskCore 1.0.0
Desktop-Icons in eigene Kategorien sortieren: Windows-11-Design (hell/dunkel), Desktop-Boxen, automatisches Einsortieren nach Regeln, Rückgängig, Suche, Infobereich mit Tastenkürzel, Sicherung als ZIP und Installer ohne Adminrechte. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+332
@@ -0,0 +1,332 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
public enum TransferStatus { Ok, Skipped, Error }
|
||||
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Kategorien sind einfache Unterordner von %USERPROFILE%\DeskCore.
|
||||
/// Icons vom Desktop werden dorthin kopiert – es bleiben normale Dateien.
|
||||
/// </summary>
|
||||
public static class Storage
|
||||
{
|
||||
public static readonly string Root = Migration.Pick(Environment.SpecialFolder.UserProfile);
|
||||
|
||||
public static readonly string UserDesktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory);
|
||||
public static readonly string PublicDesktop = Environment.GetFolderPath(Environment.SpecialFolder.CommonDesktopDirectory);
|
||||
|
||||
/// <summary>Hier liegen gelöschte Elemente, solange man sie noch rückgängig machen kann.</summary>
|
||||
public static readonly string Holding = Migration.Pick(Environment.SpecialFolder.LocalApplicationData, "Geloescht");
|
||||
|
||||
// ---------- Kategorien ----------
|
||||
|
||||
public static List<string> Categories()
|
||||
{
|
||||
Directory.CreateDirectory(Root);
|
||||
var order = AppSettings.Current.CategoryOrder;
|
||||
return Directory.GetDirectories(Root)
|
||||
.Select(Path.GetFileName)
|
||||
.OfType<string>()
|
||||
.OrderBy(n => order.FindIndex(o => string.Equals(o, n, StringComparison.OrdinalIgnoreCase)) is var i and >= 0 ? i : int.MaxValue)
|
||||
.ThenBy(n => n, StringComparer.CurrentCultureIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static string CategoryPath(string name) => Path.Combine(Root, name);
|
||||
|
||||
public static IEnumerable<string> Entries(string dir)
|
||||
{
|
||||
if (!Directory.Exists(dir)) return [];
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFileSystemEntries(dir)
|
||||
.Where(p =>
|
||||
{
|
||||
var n = Path.GetFileName(p);
|
||||
if (n.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase)) return false;
|
||||
try
|
||||
{
|
||||
var attr = File.GetAttributes(p);
|
||||
return !attr.HasFlag(FileAttributes.Hidden) && !attr.HasFlag(FileAttributes.System);
|
||||
}
|
||||
catch { return false; }
|
||||
})
|
||||
.ToList();
|
||||
}
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
public static IEnumerable<string> DesktopEntries() =>
|
||||
Entries(UserDesktop).Concat(Entries(PublicDesktop));
|
||||
|
||||
public static bool IsDesktopDir(string dir) =>
|
||||
SamePath(dir, UserDesktop) || SamePath(dir, PublicDesktop);
|
||||
|
||||
public static bool IsInStorage(string path) =>
|
||||
path.StartsWith(Root.TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static string? ValidateName(string name)
|
||||
{
|
||||
name = name.Trim();
|
||||
if (name.Length == 0) return "Bitte einen Namen eingeben.";
|
||||
if (name.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) return "Der Name enthält ungültige Zeichen ( \\ / : * ? \" < > | ).";
|
||||
if (name is "." or ".." || name.EndsWith('.')) return "Ungültiger Name.";
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- Duplikate ----------
|
||||
|
||||
private static readonly ConcurrentDictionary<string, (DateTime Stamp, string? Target)> _lnkCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Ziel einer Verknüpfung (.lnk/.url), sonst null.</summary>
|
||||
public static string? ShortcutTarget(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path).ToLowerInvariant();
|
||||
if (ext is not (".lnk" or ".url")) return null;
|
||||
DateTime stamp;
|
||||
try { stamp = File.GetLastWriteTimeUtc(path); } catch { return null; }
|
||||
if (_lnkCache.TryGetValue(path, out var c) && c.Stamp == stamp) return c.Target;
|
||||
|
||||
string? target = null;
|
||||
try
|
||||
{
|
||||
if (ext == ".url")
|
||||
{
|
||||
target = File.ReadLines(path).FirstOrDefault(l => l.StartsWith("URL=", StringComparison.OrdinalIgnoreCase))?[4..];
|
||||
}
|
||||
else
|
||||
{
|
||||
dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!;
|
||||
dynamic lnk = shell.CreateShortcut(path);
|
||||
string t = lnk.TargetPath;
|
||||
string a = lnk.Arguments;
|
||||
target = string.IsNullOrEmpty(t) ? null : (t + " " + a).Trim();
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
if (string.IsNullOrWhiteSpace(target)) target = null;
|
||||
_lnkCache[path] = (stamp, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <summary>Gleicher Dateiname oder Verknüpfung mit gleichem Ziel?</summary>
|
||||
public static bool SameItem(string a, string b)
|
||||
{
|
||||
if (string.Equals(Path.GetFileName(a), Path.GetFileName(b), StringComparison.OrdinalIgnoreCase)) return true;
|
||||
var ta = ShortcutTarget(a);
|
||||
return ta is not null && string.Equals(ta, ShortcutTarget(b), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Liegt das Element (oder ein gleichwertiges) schon in diesem Ordner?</summary>
|
||||
public static string? FindEquivalent(string dir, string item) =>
|
||||
Entries(dir).FirstOrDefault(e => !SamePath(e, item) && SameItem(e, item));
|
||||
|
||||
/// <summary>In welcher Kategorie liegt das Element bereits? (für das ✓ auf dem Desktop)</summary>
|
||||
public static string? FindCategoryOf(string item, IReadOnlyDictionary<string, List<string>> categoryEntries)
|
||||
{
|
||||
foreach (var (cat, entries) in categoryEntries)
|
||||
if (entries.Any(e => SameItem(e, item))) return cat;
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- Übertragen ----------
|
||||
|
||||
/// <summary>
|
||||
/// Übernimmt Datei/Ordner nach targetDir. Kommt die Quelle von außerhalb der App (z. B. vom Desktop),
|
||||
/// 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)
|
||||
{
|
||||
var name = Path.GetFileName(source.TrimEnd('\\'));
|
||||
try
|
||||
{
|
||||
if (SamePath(Path.GetDirectoryName(source), targetDir))
|
||||
return new(TransferStatus.Skipped, source, Message: $"„{name}“ ist schon dort.");
|
||||
if (Directory.Exists(source) &&
|
||||
(targetDir.TrimEnd('\\') + "\\").StartsWith(source.TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase))
|
||||
return new(TransferStatus.Error, source, Message: $"„{name}“ kann nicht in sich selbst verschoben werden.");
|
||||
|
||||
bool copy = !forceMove && !IsInStorage(source) && IsInStorage(targetDir);
|
||||
if (FindEquivalent(targetDir, source) is { } existing)
|
||||
{
|
||||
var where = IsDesktopDir(targetDir) ? "auf dem Desktop" : $"in „{Path.GetFileName(targetDir)}“";
|
||||
return new(TransferStatus.Skipped, source, existing, Message: $"„{DisplayName(source)}“ ist schon {where}.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetDir);
|
||||
var dest = UniquePath(targetDir, name);
|
||||
if (copy) CopyAny(source, dest);
|
||||
else MoveAny(source, dest);
|
||||
return new(TransferStatus.Ok, source, dest, copy);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return new(TransferStatus.Error, source, Message: $"„{name}“: Keine Berechtigung. " +
|
||||
"(Icons auf dem öffentlichen Desktop brauchen Administratorrechte – App ggf. als Administrator starten.)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(TransferStatus.Error, source, Message: $"„{name}“: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verschiebt Datei oder Ordner – auch zwischen Laufwerken.</summary>
|
||||
public static void MoveAny(string source, string dest)
|
||||
{
|
||||
NoteCreated(dest);
|
||||
if (File.Exists(source))
|
||||
{
|
||||
File.Move(source, dest);
|
||||
return;
|
||||
}
|
||||
if (string.Equals(Path.GetPathRoot(source), Path.GetPathRoot(dest), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Directory.Move(source, dest);
|
||||
return;
|
||||
}
|
||||
CopyDirectory(source, dest);
|
||||
Directory.Delete(source, recursive: true);
|
||||
}
|
||||
|
||||
public static void CopyAny(string source, string dest)
|
||||
{
|
||||
NoteCreated(dest);
|
||||
if (Directory.Exists(source)) CopyDirectory(source, dest);
|
||||
else File.Copy(source, dest);
|
||||
}
|
||||
|
||||
/// <summary>Benennt ein Element um. Gibt den neuen Pfad zurück.</summary>
|
||||
public static string Rename(string path, string newName)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path)!;
|
||||
var dest = Path.Combine(dir, newName);
|
||||
if (string.Equals(path, dest, StringComparison.Ordinal)) return path;
|
||||
if (string.Equals(path, dest, StringComparison.OrdinalIgnoreCase))
|
||||
{ // nur Groß-/Kleinschreibung geändert
|
||||
var tmp = path + ".umbenennen";
|
||||
MoveAny(path, tmp);
|
||||
MoveAny(tmp, dest);
|
||||
return dest;
|
||||
}
|
||||
if (File.Exists(dest) || Directory.Exists(dest)) throw new IOException($"„{newName}“ gibt es dort schon.");
|
||||
MoveAny(path, dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// "Löscht" ein Element: es wandert erst in einen internen Ordner, damit Strg+Z funktioniert.
|
||||
/// Erst wenn es nicht mehr rückgängig gemacht werden kann, geht es in den Papierkorb.
|
||||
/// </summary>
|
||||
public static string Delete(string path)
|
||||
{
|
||||
var dir = Path.Combine(Holding, Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(dir);
|
||||
var dest = Path.Combine(dir, Path.GetFileName(path.TrimEnd('\\')));
|
||||
MoveAny(path, dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
/// <summary>Endgültig in den Windows-Papierkorb.</summary>
|
||||
public static void Recycle(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path))
|
||||
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(path,
|
||||
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
|
||||
else if (File.Exists(path))
|
||||
Microsoft.VisualBasic.FileIO.FileSystem.DeleteFile(path,
|
||||
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs, Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
|
||||
|
||||
// leeren Hilfsordner aufräumen
|
||||
var parent = Path.GetDirectoryName(path);
|
||||
if (parent is not null && IsUnder(parent, Holding) && Directory.Exists(parent) && !Directory.EnumerateFileSystemEntries(parent).Any())
|
||||
Directory.Delete(parent);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Reste von einem Absturz in den Papierkorb legen.</summary>
|
||||
public static void FlushHolding()
|
||||
{
|
||||
if (!Directory.Exists(Holding)) return;
|
||||
foreach (var d in Directory.GetDirectories(Holding))
|
||||
foreach (var e in Directory.EnumerateFileSystemEntries(d).ToList())
|
||||
Recycle(e);
|
||||
}
|
||||
|
||||
// ---------- Selbst angelegte Desktop-Elemente merken (damit die Auto-Sortierung sie ignoriert) ----------
|
||||
|
||||
private static readonly ConcurrentDictionary<string, DateTime> _created = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private static void NoteCreated(string path) => _created[path] = DateTime.UtcNow;
|
||||
|
||||
public static bool CreatedByApp(string path) =>
|
||||
_created.TryGetValue(path, out var t) && DateTime.UtcNow - t < TimeSpan.FromSeconds(15);
|
||||
|
||||
/// <summary>Größe eines Ordners in Bytes (bricht ab, sobald limit überschritten ist).</summary>
|
||||
public static long DirectorySize(string dir, long limit = long.MaxValue)
|
||||
{
|
||||
long sum = 0;
|
||||
try
|
||||
{
|
||||
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
try { sum += new FileInfo(f).Length; } catch { }
|
||||
if (sum > limit) break;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return sum;
|
||||
}
|
||||
|
||||
// ---------- Helfer ----------
|
||||
|
||||
public static string DisplayName(string path)
|
||||
{
|
||||
var n = Path.GetFileName(path.TrimEnd('\\'));
|
||||
var ext = Path.GetExtension(n).ToLowerInvariant();
|
||||
return ext is ".lnk" or ".url" or ".appref-ms" ? Path.GetFileNameWithoutExtension(n) : n;
|
||||
}
|
||||
|
||||
/// <summary>Endung, die beim Umbenennen erhalten bleibt (bei Verknüpfungen versteckt).</summary>
|
||||
public static string HiddenExtension(string path)
|
||||
{
|
||||
var ext = Path.GetExtension(path);
|
||||
return ext.ToLowerInvariant() is ".lnk" or ".url" or ".appref-ms" ? ext : "";
|
||||
}
|
||||
|
||||
public static bool SamePath(string? a, string? b) =>
|
||||
a is not null && b is not null &&
|
||||
string.Equals(Path.GetFullPath(a).TrimEnd('\\'), Path.GetFullPath(b).TrimEnd('\\'), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsUnder(string path, string dir) =>
|
||||
(Path.GetFullPath(path).TrimEnd('\\') + "\\").StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static string UniquePath(string dir, string fileName)
|
||||
{
|
||||
var path = Path.Combine(dir, fileName);
|
||||
if (!File.Exists(path) && !Directory.Exists(path)) return path;
|
||||
var stem = Path.GetFileNameWithoutExtension(fileName);
|
||||
var ext = Path.GetExtension(fileName);
|
||||
for (int i = 2; ; i++)
|
||||
{
|
||||
path = Path.Combine(dir, $"{stem} ({i}){ext}");
|
||||
if (!File.Exists(path) && !Directory.Exists(path)) return path;
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string source, string dest)
|
||||
{
|
||||
Directory.CreateDirectory(dest);
|
||||
foreach (var f in Directory.GetFiles(source))
|
||||
File.Copy(f, Path.Combine(dest, Path.GetFileName(f)));
|
||||
foreach (var d in Directory.GetDirectories(source))
|
||||
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user