304 lines
13 KiB
C#
304 lines
13 KiB
C#
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 oder ein beliebiger Ordner ("Ordner-Portal").</summary>
|
||
public class FenceTab
|
||
{
|
||
public const string CategoryKind = "Kategorie", FolderKind = "Ordner";
|
||
public string Kind { get; set; } = CategoryKind;
|
||
/// <summary>Kategoriename bzw. Ordnerpfad.</summary>
|
||
public string Value { get; set; } = "";
|
||
[JsonIgnore] public bool IsFolder => Kind == FolderKind;
|
||
[JsonIgnore] public string Title => IsFolder ? Path.GetFileName(Value.TrimEnd('\\')) is { Length: > 0 } n ? n : Value : Value;
|
||
}
|
||
|
||
/// <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" oder "Datum"</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>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 ??= [];
|
||
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>Wird diese Kategorie in irgendeiner Box angezeigt?</summary>
|
||
public bool HasFence(string category) =>
|
||
Fences.Any(f => f.Tabs.Any(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
|
||
|
||
/// <summary>Kategorie als eigene Box anzeigen bzw. aus allen Boxen entfernen.</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)
|
||
f.Tabs.RemoveAll(t => !t.IsFolder && 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; }
|
||
if (changed) Save();
|
||
}
|
||
|
||
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));
|
||
}
|
||
}
|