Files
2026-10-04 14:48:53 +00:00

385 lines
18 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.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace DeskCore;
public enum DesignMode { System, Light, Dark }
public enum ViewMode { Small, Medium, Large, List }
public class CategoryMeta
{
public string? Glyph { get; set; }
public string? Color { get; set; }
// Die folgenden Box-Felder stammen aus Version 1.0.x und werden nur noch beim Umstieg auf FenceDef gelesen
public bool Fence { get; set; }
public double? FenceX { get; set; }
public double? FenceY { get; set; }
public double FenceW { get; set; } = 340;
public double FenceH { get; set; } = 260;
public bool FenceCollapsed { get; set; }
}
/// <summary>Eine Start-Gruppe: ein Klick startet mehrere Programme nacheinander.</summary>
public class LaunchGroup
{
public string Name { get; set; } = "";
/// <summary>Pfade in Startreihenfolge (meist Verknüpfungen aus den Kategorien).</summary>
public List<string> Items { get; set; } = [];
/// <summary>Wartezeit zwischen zwei Programmen in Sekunden.</summary>
public double DelaySeconds { get; set; } = 1;
public string? Color { get; set; }
}
public class SortRule
{
/// <summary>"Name" = Name enthält …, "Typ" = Dateityp ist … (z. B. "Bilder" oder "pdf, docx")</summary>
public string Kind { get; set; } = "Name";
public string Contains { get; set; } = "";
public string Category { get; set; } = "";
[JsonIgnore] public string KindText => Kind == "Typ" ? "Dateityp ist" : "Name enthält";
}
/// <summary>Ein Tab einer Desktop-Box: eine Kategorie, ein beliebiger Ordner ("Ordner-Portal") oder "Zuletzt benutzt".</summary>
public class FenceTab
{
public const string CategoryKind = "Kategorie", FolderKind = "Ordner", RecentKind = "Zuletzt";
public string Kind { get; set; } = CategoryKind;
/// <summary>Kategoriename bzw. Ordnerpfad (bei "Zuletzt benutzt" leer).</summary>
public string Value { get; set; } = "";
/// <summary>Eigene Reihenfolge (Dateinamen), wenn die Box nach "Eigene" sortiert ist.</summary>
public List<string> Order { get; set; } = [];
[JsonIgnore] public bool IsFolder => Kind == FolderKind;
[JsonIgnore] public bool IsRecent => Kind == RecentKind;
[JsonIgnore] public bool IsCategory => Kind == CategoryKind;
[JsonIgnore] public string Title => IsRecent ? "Zuletzt benutzt"
: IsFolder ? Path.GetFileName(Value.TrimEnd('\\')) is { Length: > 0 } n ? n : Value : Value;
}
/// <summary>Zusatzeinstellungen einer Desktop-Seite: Zeitplan und Hintergrundbild.</summary>
public class PageOption
{
/// <summary>Seite automatisch zu bestimmten Zeiten anzeigen.</summary>
public bool Scheduled { get; set; }
/// <summary>Wochentage als Bits: Montag = 1, Dienstag = 2 … Sonntag = 64.</summary>
public int Days { get; set; } = 0b0011111; // Mo–Fr
public string From { get; set; } = "08:00";
public string To { get; set; } = "17:00";
/// <summary>Eigenes Hintergrundbild für diese Seite (null = nicht ändern).</summary>
public string? Wallpaper { get; set; }
public static int DayBit(DayOfWeek d) => 1 << (((int)d + 6) % 7); // Montag = Bit 0
/// <summary>Gilt der Zeitplan zu diesem Zeitpunkt? Zeiträume über Mitternacht (z. B. 22:00–06:00) gehen auch.</summary>
public bool Matches(DateTime now)
{
if (!Scheduled || !TimeSpan.TryParse(From, out var from) || !TimeSpan.TryParse(To, out var to)) return false;
var t = now.TimeOfDay;
if (from <= to) return (Days & DayBit(now.DayOfWeek)) != 0 && t >= from && t < to;
// über Mitternacht: der Tag zählt, an dem der Zeitraum beginnt
return t >= from ? (Days & DayBit(now.DayOfWeek)) != 0 : t < to && (Days & DayBit(now.AddDays(-1).DayOfWeek)) != 0;
}
}
/// <summary>Eine Desktop-Box mit Position, Tabs und Aussehen.</summary>
public class FenceDef
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public List<FenceTab> Tabs { get; set; } = [];
public int ActiveTab { get; set; }
public double? X { get; set; }
public double? Y { get; set; }
public double W { get; set; } = 340;
public double H { get; set; } = 260;
public bool Collapsed { get; set; }
/// <summary>Gesperrt: nicht verschieb- oder vergrößerbar.</summary>
public bool Locked { get; set; }
/// <summary>Hintergrundfarbe (#RRGGBB) oder null = passend zum Design.</summary>
public string? Background { get; set; }
/// <summary>Deckkraft des Hintergrunds 0.2–1.</summary>
public double Opacity { get; set; } = 0.92;
/// <summary>Milchglas-Effekt (Hintergrund verschwommen).</summary>
public bool Blur { get; set; }
public ViewMode IconSize { get; set; } = ViewMode.Medium;
/// <summary>"Name", "Typ", "Datum", "Häufig" (meistgenutzt zuerst) oder "Eigene" (per Ziehen angeordnet)</summary>
public string Sort { get; set; } = "Name";
/// <summary>Icon-Tönung (#RRGGBB) oder null.</summary>
public string? Tint { get; set; }
/// <summary>Desktop-Seite, auf der die Box liegt (0 = erste Seite).</summary>
public int Page { get; set; }
[JsonIgnore] public FenceTab? Active => Tabs.Count == 0 ? null : Tabs[Math.Clamp(ActiveTab, 0, Tabs.Count - 1)];
}
/// <summary>Gespeicherte Anordnung aller Desktop-Boxen.</summary>
public class FenceSnapshot
{
public string Name { get; set; } = "";
public DateTime Created { get; set; } = DateTime.Now;
public List<FenceDef> Fences { get; set; } = [];
[JsonIgnore] public string Info => $"{Fences.Count} {(Fences.Count == 1 ? "Box" : "Boxen")} · gespeichert am {Created:dd.MM.yyyy HH:mm}";
}
/// <summary>Alle Einstellungen, gespeichert in %APPDATA%\DeskCore\settings.json.</summary>
public class AppSettings
{
public DesignMode Design { get; set; } = DesignMode.System;
public ViewMode View { get; set; } = ViewMode.Medium;
public bool TrayEnabled { get; set; } = true;
public bool CloseToTray { get; set; } = true;
public bool TrayHintShown { get; set; }
public bool HotkeyEnabled { get; set; } = true;
public int HotkeyModifiers { get; set; } = Hotkey.ModWin | Hotkey.ModShift;
public int HotkeyKey { get; set; } = 0x44; // D
public bool AutoSort { get; set; }
public bool AutoSortRemoveFromDesktop { get; set; }
public List<SortRule> Rules { get; set; } = [];
public bool FencesEnabled { get; set; } = true;
public List<FenceDef> Fences { get; set; } = [];
public List<FenceSnapshot> Snapshots { get; set; } = [];
/// <summary>Box-Positionen je Bildschirm-Anordnung (Signatur → Box-Id → x, y, w, h).</summary>
public Dictionary<string, Dictionary<string, double[]>> ScreenLayouts { get; set; } = [];
/// <summary>Chamäleon: Boxen blenden sich aus, bis die Maus darüberfährt.</summary>
public bool FenceChameleon { get; set; }
/// <summary>Doppelklick auf eine freie Desktop-Stelle blendet Icons und Boxen aus/ein.</summary>
public bool QuickHide { get; set; } = true;
/// <summary>Peek: Tastenkürzel holt alle Boxen vor die Fenster.</summary>
public bool PeekEnabled { get; set; } = true;
public int PeekModifiers { get; set; } = Hotkey.ModWin | Hotkey.ModShift;
public int PeekKey { get; set; } = 0x42; // B wie "Boxen"
/// <summary>Desktop-Seiten: jede Seite hat ihre eigenen Boxen.</summary>
public List<string> Pages { get; set; } = ["Seite 1"];
public int CurrentPage { get; set; }
/// <summary>Mausrad über einer freien Desktop-Stelle wechselt die Seite.</summary>
public bool PageWheel { get; set; } = true;
/// <summary>Zeitplan und Hintergrundbild je Seite (Schlüssel = Seitenname).</summary>
public Dictionary<string, PageOption> PageOptions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// <summary>Seite, die außerhalb aller Zeitpläne angezeigt wird (null = keine).</summary>
public string? PageFallback { get; set; }
/// <summary>Hintergrundbild, das vor dem ersten Seiten-Hintergrund eingestellt war (zum Zurückholen).</summary>
public string? SavedWallpaper { get; set; }
/// <summary>Schnellstart-Suche per Tastenkürzel.</summary>
public bool QuickSearchEnabled { get; set; } = true;
public int QuickSearchModifiers { get; set; } = Hotkey.ModAlt | Hotkey.ModShift;
public int QuickSearchKey { get; set; } = 0x20; // Leertaste
/// <summary>Programme aus dem Windows-Startmenü mit durchsuchen.</summary>
public bool QuickSearchStartMenu { get; set; } = true;
/// <summary>Sortierung im Hauptfenster: "Name" oder "Häufig".</summary>
public string MainSort { get; set; } = "Name";
/// <summary>Rechte Maustaste auf freier Desktop-Fläche ziehen → neue Box.</summary>
public bool DrawFences { get; set; } = true;
/// <summary>Tönung der echten Desktop-Icons (#RRGGBB) oder null = aus.</summary>
public string? DesktopTint { get; set; }
public double DesktopTintStrength { get; set; } = 0.55;
/// <summary>Verknüpfungen zu deinstallierten Programmen automatisch entfernen.</summary>
public bool RemoveDeadShortcuts { get; set; } = true;
/// <summary>Tote Verknüpfungen, die der Nutzer per Rückgängig behalten wollte.</summary>
public List<string> KeptShortcuts { get; set; } = [];
// Fenster merken (WPF-Einheiten)
public double? WindowLeft { get; set; }
public double? WindowTop { get; set; }
public double? WindowWidth { get; set; }
public double? WindowHeight { get; set; }
public bool WindowMaximized { get; set; }
// Updates
public bool AutoUpdateCheck { get; set; } = true;
public DateTime? LastUpdateCheck { get; set; }
public string? SkippedVersion { get; set; }
public List<string> CategoryOrder { get; set; } = [];
public List<LaunchGroup> LaunchGroups { get; set; } = [];
public Dictionary<string, CategoryMeta> Categories { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// ---------- Laden/Speichern ----------
public static readonly string Folder = Migration.Pick(Environment.SpecialFolder.ApplicationData);
private static readonly string FilePath = Path.Combine(Folder, "settings.json");
public static readonly JsonSerializerOptions Json = new()
{
WriteIndented = true,
Converters = { new JsonStringEnumConverter() },
};
public static AppSettings Current { get; private set; } = Load();
public static event Action? Saved;
private static AppSettings Load()
{
AppSettings s;
try
{
s = File.Exists(FilePath)
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(FilePath), Json) ?? new()
: new();
}
catch { s = new(); }
s.Categories = new(s.Categories ?? [], StringComparer.OrdinalIgnoreCase);
s.Rules ??= [];
s.CategoryOrder ??= [];
s.KeptShortcuts ??= [];
s.LaunchGroups ??= [];
s.Fences ??= [];
s.Snapshots ??= [];
s.ScreenLayouts ??= [];
s.PageOptions = new(s.PageOptions ?? [], StringComparer.OrdinalIgnoreCase);
foreach (var t in s.Fences.SelectMany(f => f.Tabs)) t.Order ??= [];
if (s.Pages is null || s.Pages.Count == 0) s.Pages = ["Seite 1"];
s.CurrentPage = Math.Clamp(s.CurrentPage, 0, s.Pages.Count - 1);
// Umstieg von 1.0.x: Box gehörte fest zu einer Kategorie → eigenständige Box mit einem Tab
foreach (var (name, meta) in s.Categories)
{
if (!meta.Fence) continue;
s.Fences.Add(new FenceDef
{
Tabs = [new FenceTab { Value = name }],
X = meta.FenceX, Y = meta.FenceY, W = meta.FenceW, H = meta.FenceH, Collapsed = meta.FenceCollapsed,
});
meta.Fence = false;
}
// Übernahme der alten Einstellung aus Version 2
var old = Path.Combine(Folder, "design.txt");
if (File.Exists(old))
{
try
{
var v = File.ReadAllText(old).Trim();
s.Design = v == "Light" ? DesignMode.Light : v == "Dark" ? DesignMode.Dark : DesignMode.System;
File.Delete(old);
}
catch { }
}
return s;
}
public void Save()
{
try
{
Directory.CreateDirectory(Folder);
File.WriteAllText(FilePath, JsonSerializer.Serialize(this, Json));
}
catch { }
Saved?.Invoke();
}
public CategoryMeta Meta(string category)
{
if (!Categories.TryGetValue(category, out var m))
Categories[category] = m = new CategoryMeta();
return m;
}
public void RenameCategory(string oldName, string newName)
{
if (Categories.Remove(oldName, out var m)) Categories[newName] = m;
for (int i = 0; i < CategoryOrder.Count; i++)
if (string.Equals(CategoryOrder[i], oldName, StringComparison.OrdinalIgnoreCase)) CategoryOrder[i] = newName;
foreach (var r in Rules)
if (string.Equals(r.Category, oldName, StringComparison.OrdinalIgnoreCase)) r.Category = newName;
foreach (var t in Fences.SelectMany(f => f.Tabs))
if (!t.IsFolder && string.Equals(t.Value, oldName, StringComparison.OrdinalIgnoreCase)) t.Value = newName;
// Start-Gruppen zeigen auf Dateien in der Kategorie → Pfade mit umbenennen
var oldDir = Storage.CategoryPath(oldName).TrimEnd('\\') + "\\";
var newDir = Storage.CategoryPath(newName).TrimEnd('\\') + "\\";
foreach (var g in LaunchGroups)
for (int i = 0; i < g.Items.Count; i++)
if (g.Items[i].StartsWith(oldDir, StringComparison.OrdinalIgnoreCase))
g.Items[i] = newDir + g.Items[i][oldDir.Length..];
}
/// <summary>Liegt diese Kategorie auf der gerade angezeigten Desktop-Seite in einer Box?</summary>
public bool HasFence(string category) =>
Fences.Any(f => f.Page == CurrentPage &&
f.Tabs.Any(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
/// <summary>Kategorie auf der aktuellen Seite als eigene Box anzeigen bzw. von dieser Seite entfernen (andere Seiten bleiben).</summary>
public void SetFence(string category, bool on)
{
if (on)
{
if (!HasFence(category)) Fences.Add(new FenceDef { Page = CurrentPage, Tabs = [new FenceTab { Value = category }] });
FencesEnabled = true;
}
else
{
foreach (var f in Fences.Where(f => f.Page == CurrentPage))
f.Tabs.RemoveAll(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
Fences.RemoveAll(f => f.Tabs.Count == 0);
}
}
/// <summary>Tiefe Kopie (für Snapshots).</summary>
public static List<FenceDef> CloneFences(List<FenceDef> fences) =>
JsonSerializer.Deserialize<List<FenceDef>>(JsonSerializer.Serialize(fences, Json), Json) ?? [];
/// <summary>Datei wurde umbenannt/verschoben → Start-Gruppen zeigen auf den neuen Pfad.</summary>
public void ReplaceLaunchPath(string oldPath, string newPath)
{
bool changed = false;
foreach (var g in LaunchGroups)
for (int i = 0; i < g.Items.Count; i++)
if (string.Equals(g.Items[i], oldPath, StringComparison.OrdinalIgnoreCase)) { g.Items[i] = newPath; changed = true; }
// Umbenannt im selben Ordner → Platz in der eigenen Box-Reihenfolge behalten
if (Storage.SamePath(Path.GetDirectoryName(oldPath), Path.GetDirectoryName(newPath)))
{
string oldName = Path.GetFileName(oldPath), newName = Path.GetFileName(newPath);
foreach (var t in Fences.SelectMany(f => f.Tabs))
for (int i = 0; i < t.Order.Count; i++)
if (string.Equals(t.Order[i], oldName, StringComparison.OrdinalIgnoreCase)) { t.Order[i] = newName; changed = true; }
}
Usage.Rename(oldPath, newPath);
if (changed) Save();
}
// ---------- Desktop-Seiten ----------
public PageOption PageOption(string page)
{
if (!PageOptions.TryGetValue(page, out var o)) PageOptions[page] = o = new PageOption();
return o;
}
public void RenamePage(int index, string newName)
{
var old = Pages[index];
Pages[index] = newName;
if (PageOptions.Remove(old, out var o)) PageOptions[newName] = o;
if (string.Equals(PageFallback, old, StringComparison.OrdinalIgnoreCase)) PageFallback = newName;
}
/// <summary>Aufräumen nach dem Löschen einer Seite (Boxen verschiebt der FenceManager).</summary>
public void ForgetPage(string name)
{
PageOptions.Remove(name);
if (string.Equals(PageFallback, name, StringComparison.OrdinalIgnoreCase)) PageFallback = null;
}
public void RemoveCategory(string name)
{
foreach (var f in Fences)
f.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, name, StringComparison.OrdinalIgnoreCase));
Fences.RemoveAll(f => f.Tabs.Count == 0);
Categories.Remove(name);
CategoryOrder.RemoveAll(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase));
Rules.RemoveAll(r => string.Equals(r.Category, name, StringComparison.OrdinalIgnoreCase));
}
}