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; }
}
/// Eine Start-Gruppe: ein Klick startet mehrere Programme nacheinander.
public class LaunchGroup
{
public string Name { get; set; } = "";
/// Pfade in Startreihenfolge (meist Verknüpfungen aus den Kategorien).
public List Items { get; set; } = [];
/// Wartezeit zwischen zwei Programmen in Sekunden.
public double DelaySeconds { get; set; } = 1;
public string? Color { get; set; }
}
public class SortRule
{
/// "Name" = Name enthält …, "Typ" = Dateityp ist … (z. B. "Bilder" oder "pdf, docx")
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";
}
/// Ein Tab einer Desktop-Box: eine Kategorie, ein beliebiger Ordner ("Ordner-Portal") oder "Zuletzt benutzt".
public class FenceTab
{
public const string CategoryKind = "Kategorie", FolderKind = "Ordner", RecentKind = "Zuletzt";
public string Kind { get; set; } = CategoryKind;
/// Kategoriename bzw. Ordnerpfad (bei "Zuletzt benutzt" leer).
public string Value { get; set; } = "";
/// Eigene Reihenfolge (Dateinamen), wenn die Box nach "Eigene" sortiert ist.
public List 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;
}
/// Zusatzeinstellungen einer Desktop-Seite: Zeitplan und Hintergrundbild.
public class PageOption
{
/// Seite automatisch zu bestimmten Zeiten anzeigen.
public bool Scheduled { get; set; }
/// Wochentage als Bits: Montag = 1, Dienstag = 2 … Sonntag = 64.
public int Days { get; set; } = 0b0011111; // Mo–Fr
public string From { get; set; } = "08:00";
public string To { get; set; } = "17:00";
/// Eigenes Hintergrundbild für diese Seite (null = nicht ändern).
public string? Wallpaper { get; set; }
public static int DayBit(DayOfWeek d) => 1 << (((int)d + 6) % 7); // Montag = Bit 0
/// Gilt der Zeitplan zu diesem Zeitpunkt? Zeiträume über Mitternacht (z. B. 22:00–06:00) gehen auch.
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;
}
}
/// Eine Desktop-Box mit Position, Tabs und Aussehen.
public class FenceDef
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");
public List 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; }
/// Gesperrt: nicht verschieb- oder vergrößerbar.
public bool Locked { get; set; }
/// Hintergrundfarbe (#RRGGBB) oder null = passend zum Design.
public string? Background { get; set; }
/// Deckkraft des Hintergrunds 0.2–1.
public double Opacity { get; set; } = 0.92;
/// Milchglas-Effekt (Hintergrund verschwommen).
public bool Blur { get; set; }
public ViewMode IconSize { get; set; } = ViewMode.Medium;
/// "Name", "Typ", "Datum", "Häufig" (meistgenutzt zuerst) oder "Eigene" (per Ziehen angeordnet)
public string Sort { get; set; } = "Name";
/// Icon-Tönung (#RRGGBB) oder null.
public string? Tint { get; set; }
/// Desktop-Seite, auf der die Box liegt (0 = erste Seite).
public int Page { get; set; }
[JsonIgnore] public FenceTab? Active => Tabs.Count == 0 ? null : Tabs[Math.Clamp(ActiveTab, 0, Tabs.Count - 1)];
}
/// Gespeicherte Anordnung aller Desktop-Boxen.
public class FenceSnapshot
{
public string Name { get; set; } = "";
public DateTime Created { get; set; } = DateTime.Now;
public List Fences { get; set; } = [];
[JsonIgnore] public string Info => $"{Fences.Count} {(Fences.Count == 1 ? "Box" : "Boxen")} · gespeichert am {Created:dd.MM.yyyy HH:mm}";
}
/// Alle Einstellungen, gespeichert in %APPDATA%\DeskCore\settings.json.
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 Rules { get; set; } = [];
public bool FencesEnabled { get; set; } = true;
public List Fences { get; set; } = [];
public List Snapshots { get; set; } = [];
/// Box-Positionen je Bildschirm-Anordnung (Signatur → Box-Id → x, y, w, h).
public Dictionary> ScreenLayouts { get; set; } = [];
/// Chamäleon: Boxen blenden sich aus, bis die Maus darüberfährt.
public bool FenceChameleon { get; set; }
/// Doppelklick auf eine freie Desktop-Stelle blendet Icons und Boxen aus/ein.
public bool QuickHide { get; set; } = true;
/// Peek: Tastenkürzel holt alle Boxen vor die Fenster.
public bool PeekEnabled { get; set; } = true;
public int PeekModifiers { get; set; } = Hotkey.ModWin | Hotkey.ModShift;
public int PeekKey { get; set; } = 0x42; // B wie "Boxen"
/// Desktop-Seiten: jede Seite hat ihre eigenen Boxen.
public List Pages { get; set; } = ["Seite 1"];
public int CurrentPage { get; set; }
/// Mausrad über einer freien Desktop-Stelle wechselt die Seite.
public bool PageWheel { get; set; } = true;
/// Zeitplan und Hintergrundbild je Seite (Schlüssel = Seitenname).
public Dictionary PageOptions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
/// Seite, die außerhalb aller Zeitpläne angezeigt wird (null = keine).
public string? PageFallback { get; set; }
/// Hintergrundbild, das vor dem ersten Seiten-Hintergrund eingestellt war (zum Zurückholen).
public string? SavedWallpaper { get; set; }
/// Schnellstart-Suche per Tastenkürzel.
public bool QuickSearchEnabled { get; set; } = true;
public int QuickSearchModifiers { get; set; } = Hotkey.ModAlt | Hotkey.ModShift;
public int QuickSearchKey { get; set; } = 0x20; // Leertaste
/// Programme aus dem Windows-Startmenü mit durchsuchen.
public bool QuickSearchStartMenu { get; set; } = true;
/// Sortierung im Hauptfenster: "Name" oder "Häufig".
public string MainSort { get; set; } = "Name";
/// Rechte Maustaste auf freier Desktop-Fläche ziehen → neue Box.
public bool DrawFences { get; set; } = true;
/// Tönung der echten Desktop-Icons (#RRGGBB) oder null = aus.
public string? DesktopTint { get; set; }
public double DesktopTintStrength { get; set; } = 0.55;
/// Verknüpfungen zu deinstallierten Programmen automatisch entfernen.
public bool RemoveDeadShortcuts { get; set; } = true;
/// Tote Verknüpfungen, die der Nutzer per Rückgängig behalten wollte.
public List 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 CategoryOrder { get; set; } = [];
public List LaunchGroups { get; set; } = [];
public Dictionary 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(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..];
}
/// Liegt diese Kategorie auf der gerade angezeigten Desktop-Seite in einer Box?
public bool HasFence(string category) =>
Fences.Any(f => f.Page == CurrentPage &&
f.Tabs.Any(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
/// Kategorie auf der aktuellen Seite als eigene Box anzeigen bzw. von dieser Seite entfernen (andere Seiten bleiben).
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);
}
}
/// Tiefe Kopie (für Snapshots).
public static List CloneFences(List fences) =>
JsonSerializer.Deserialize>(JsonSerializer.Serialize(fences, Json), Json) ?? [];
/// Datei wurde umbenannt/verschoben → Start-Gruppen zeigen auf den neuen Pfad.
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;
}
/// Aufräumen nach dem Löschen einer Seite (Boxen verschiebt der FenceManager).
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));
}
}