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:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
# Build-Ausgaben
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
artifacts/
|
||||||
|
Setup/bin/
|
||||||
|
Setup/obj/
|
||||||
|
|
||||||
|
# Visual Studio / Rider
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<Application x:Class="DeskCore.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
>
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- [0] wird von Theme.Apply() je nach Hell/Dunkel ersetzt -->
|
||||||
|
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#0067C0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#1975C5"/>
|
||||||
|
<SolidColorBrush x:Key="OnAccentBrush" Color="White"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#220067C0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleStrokeBrush" Color="#550067C0"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
private const string MutexName = "DeskCore.Instanz";
|
||||||
|
private const string ShowEventName = "DeskCore.Anzeigen";
|
||||||
|
|
||||||
|
public static MainWindow? Win { get; private set; }
|
||||||
|
public static Tray? TrayIcon { get; private set; }
|
||||||
|
public static bool Quitting { get; private set; }
|
||||||
|
|
||||||
|
private static Hotkey? _hotkey;
|
||||||
|
private static Watcher? _watcher;
|
||||||
|
private Mutex? _mutex;
|
||||||
|
private EventWaitHandle? _showEvent;
|
||||||
|
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
// Nur eine Instanz: eine zweite holt einfach das vorhandene Fenster nach vorne
|
||||||
|
_mutex = new Mutex(true, MutexName, out bool first);
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
try { EventWaitHandle.OpenExisting(ShowEventName).Set(); } catch { }
|
||||||
|
Shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Migration.Run(); // alte "Desktop Sortierer"-Ordner übernehmen – vor allem anderen
|
||||||
|
_showEvent = new EventWaitHandle(false, EventResetMode.AutoReset, ShowEventName);
|
||||||
|
new Thread(() =>
|
||||||
|
{
|
||||||
|
while (_showEvent.WaitOne())
|
||||||
|
Dispatcher.BeginInvoke(() => Win?.ShowFromBackground());
|
||||||
|
}) { IsBackground = true, Name = "ShowListener" }.Start();
|
||||||
|
|
||||||
|
DispatcherUnhandledException += (_, args) =>
|
||||||
|
{
|
||||||
|
var log = Path.Combine(Storage.Root, "fehler.log");
|
||||||
|
try { File.AppendAllText(log, $"[{DateTime.Now}] {args.Exception}\n\n"); } catch { }
|
||||||
|
MessageBox.Show($"Unerwarteter Fehler:\n\n{args.Exception.Message}\n\nDetails: {log}", "DeskCore",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Error);
|
||||||
|
args.Handled = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
base.OnStartup(e);
|
||||||
|
ShutdownMode = ShutdownMode.OnExplicitShutdown;
|
||||||
|
|
||||||
|
Storage.FlushHolding(); // Reste eines Absturzes in den Papierkorb
|
||||||
|
Theme.Init();
|
||||||
|
Theme.Changed += FenceManager.RefreshAll;
|
||||||
|
|
||||||
|
Win = new MainWindow();
|
||||||
|
MainWindow = Win;
|
||||||
|
var hwnd = new WindowInteropHelper(Win).EnsureHandle();
|
||||||
|
|
||||||
|
_hotkey = new Hotkey(hwnd);
|
||||||
|
_hotkey.Pressed += () => Win.ToggleFromHotkey();
|
||||||
|
ApplyHotkey();
|
||||||
|
|
||||||
|
TrayIcon = new Tray();
|
||||||
|
TrayIcon.Clicked += () => Win.ShowFromBackground();
|
||||||
|
TrayIcon.BalloonClicked += () => Win.ShowFromBackground();
|
||||||
|
TrayIcon.MenuRequested += () => Win.ShowTrayMenu();
|
||||||
|
ApplyTray();
|
||||||
|
|
||||||
|
_watcher = new Watcher(Dispatcher);
|
||||||
|
_watcher.Changed += () =>
|
||||||
|
{
|
||||||
|
Win.OnStorageChanged();
|
||||||
|
FenceManager.RefreshAll();
|
||||||
|
};
|
||||||
|
_watcher.NewOnDesktop += Win.AutoSortNew;
|
||||||
|
|
||||||
|
Autostart.RefreshPath();
|
||||||
|
|
||||||
|
bool minimized = e.Args.Contains("--minimized", StringComparer.OrdinalIgnoreCase);
|
||||||
|
if (!minimized || !AppSettings.Current.TrayEnabled) Win.Show();
|
||||||
|
FenceManager.Sync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tastenkürzel neu registrieren. false = von anderem Programm belegt.</summary>
|
||||||
|
public static bool ApplyHotkey()
|
||||||
|
{
|
||||||
|
if (_hotkey is null) return false;
|
||||||
|
var s = AppSettings.Current;
|
||||||
|
if (!s.HotkeyEnabled) { _hotkey.Unregister(); return true; }
|
||||||
|
return _hotkey.Register(s.HotkeyModifiers, s.HotkeyKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplyTray()
|
||||||
|
{
|
||||||
|
if (TrayIcon is not null) TrayIcon.Visible = AppSettings.Current.TrayEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Quit()
|
||||||
|
{
|
||||||
|
if (Quitting) return;
|
||||||
|
Quitting = true;
|
||||||
|
Win?.Undo.FlushAll();
|
||||||
|
FenceManager.CloseAll();
|
||||||
|
_watcher?.Dispose();
|
||||||
|
_hotkey?.Dispose();
|
||||||
|
TrayIcon?.Dispose();
|
||||||
|
Win?.Close();
|
||||||
|
Current.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnExit(ExitEventArgs e)
|
||||||
|
{
|
||||||
|
TrayIcon?.Dispose();
|
||||||
|
_mutex?.Dispose();
|
||||||
|
base.OnExit(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Export/Import aller Kategorien samt Einstellungen als ZIP.</summary>
|
||||||
|
public static class Backup
|
||||||
|
{
|
||||||
|
private const string CategoriesPrefix = "Kategorien/";
|
||||||
|
private const string SettingsEntry = "einstellungen.json";
|
||||||
|
|
||||||
|
public static long StorageSize()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Directory.EnumerateFiles(Storage.Root, "*", SearchOption.AllDirectories)
|
||||||
|
.Sum(f => { try { return new FileInfo(f).Length; } catch { return 0L; } });
|
||||||
|
}
|
||||||
|
catch { return 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatSize(long bytes) => bytes switch
|
||||||
|
{
|
||||||
|
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
|
||||||
|
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
|
||||||
|
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Gibt die Anzahl gesicherter Dateien zurück.</summary>
|
||||||
|
public static int Export(string zipPath)
|
||||||
|
{
|
||||||
|
if (File.Exists(zipPath)) File.Delete(zipPath);
|
||||||
|
int count = 0;
|
||||||
|
using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create);
|
||||||
|
foreach (var cat in Storage.Categories())
|
||||||
|
{
|
||||||
|
var dir = Storage.CategoryPath(cat);
|
||||||
|
zip.CreateEntry(CategoriesPrefix + cat + "/"); // auch leere Kategorien sichern
|
||||||
|
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
var rel = Path.GetRelativePath(Storage.Root, f).Replace('\\', '/');
|
||||||
|
zip.CreateEntryFromFile(f, CategoriesPrefix + rel, CompressionLevel.Optimal);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
foreach (var d in Directory.EnumerateDirectories(dir, "*", SearchOption.AllDirectories))
|
||||||
|
if (!Directory.EnumerateFileSystemEntries(d).Any())
|
||||||
|
zip.CreateEntry(CategoriesPrefix + Path.GetRelativePath(Storage.Root, d).Replace('\\', '/') + "/");
|
||||||
|
}
|
||||||
|
var entry = zip.CreateEntry(SettingsEntry);
|
||||||
|
using (var w = new StreamWriter(entry.Open()))
|
||||||
|
w.Write(JsonSerializer.Serialize(AppSettings.Current, AppSettings.Json));
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Führt eine Sicherung mit dem Bestand zusammen. Vorhandenes wird nicht überschrieben.</summary>
|
||||||
|
public static (int Added, int Skipped) Import(string zipPath)
|
||||||
|
{
|
||||||
|
int added = 0, skipped = 0;
|
||||||
|
using var zip = ZipFile.OpenRead(zipPath);
|
||||||
|
var root = Path.GetFullPath(Storage.Root);
|
||||||
|
|
||||||
|
foreach (var e in zip.Entries)
|
||||||
|
{
|
||||||
|
if (!e.FullName.StartsWith(CategoriesPrefix, StringComparison.OrdinalIgnoreCase)) continue;
|
||||||
|
var rel = e.FullName[CategoriesPrefix.Length..];
|
||||||
|
if (rel.Length == 0) continue;
|
||||||
|
var target = Path.GetFullPath(Path.Combine(root, rel));
|
||||||
|
if (!Storage.IsUnder(target, root)) continue; // Schutz vor manipulierten ZIPs
|
||||||
|
|
||||||
|
if (e.FullName.EndsWith('/'))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(target);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (File.Exists(target)) { skipped++; continue; }
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(target)!);
|
||||||
|
e.ExtractToFile(target);
|
||||||
|
added++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zip.GetEntry(SettingsEntry) is { } se)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var r = new StreamReader(se.Open());
|
||||||
|
var imported = JsonSerializer.Deserialize<AppSettings>(r.ReadToEnd(), AppSettings.Json);
|
||||||
|
if (imported is not null) Merge(imported);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return (added, skipped);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Merge(AppSettings from)
|
||||||
|
{
|
||||||
|
var s = AppSettings.Current;
|
||||||
|
foreach (var (name, meta) in from.Categories ?? [])
|
||||||
|
if (!s.Categories.ContainsKey(name)) s.Categories[name] = meta;
|
||||||
|
foreach (var n in from.CategoryOrder ?? [])
|
||||||
|
if (!s.CategoryOrder.Contains(n, StringComparer.OrdinalIgnoreCase)) s.CategoryOrder.Add(n);
|
||||||
|
foreach (var r in from.Rules ?? [])
|
||||||
|
if (!s.Rules.Any(x => string.Equals(x.Contains, r.Contains, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(x.Category, r.Category, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
s.Rules.Add(r);
|
||||||
|
s.Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<!-- nur für das Symbol im Infobereich (NotifyIcon) -->
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AssemblyName>DeskCore</AssemblyName>
|
||||||
|
<RootNamespace>DeskCore</RootNamespace>
|
||||||
|
<ApplicationIcon>app.ico</ApplicationIcon>
|
||||||
|
<!-- Angaben für Datei-Eigenschaften → Details -->
|
||||||
|
<Product>DeskCore</Product>
|
||||||
|
<AssemblyTitle>DeskCore</AssemblyTitle>
|
||||||
|
<Description>Desktop-Icons in eigene Kategorien sortieren</Description>
|
||||||
|
<Company>M_Viper</Company>
|
||||||
|
<Authors>M_Viper</Authors>
|
||||||
|
<Copyright>Copyright (c) M_Viper. Alle Rechte vorbehalten.</Copyright>
|
||||||
|
<NeutralLanguage>de-DE</NeutralLanguage>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- Installer-Projekt und Build-Ausgaben gehören nicht zur App -->
|
||||||
|
<Compile Remove="Setup\**;artifacts\**" />
|
||||||
|
<Page Remove="Setup\**;artifacts\**" />
|
||||||
|
<None Remove="Setup\**;artifacts\**" />
|
||||||
|
<EmbeddedResource Remove="Setup\**;artifacts\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- WinForms/Drawing-Namespaces nicht global, sonst Namenskonflikte mit WPF -->
|
||||||
|
<Using Remove="System.Windows.Forms" />
|
||||||
|
<Using Remove="System.Drawing" />
|
||||||
|
<Resource Include="app.ico" />
|
||||||
|
<!-- Lizenz: in der App eingebettet und als Datei neben der .exe -->
|
||||||
|
<Resource Include="LICENSE.txt" />
|
||||||
|
<None Include="LICENSE.txt" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest"
|
||||||
|
ExcludeFromSingleFile="true" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Dialoge im Stil eines Windows-11-ContentDialogs.</summary>
|
||||||
|
public static class Dialog
|
||||||
|
{
|
||||||
|
public static bool Confirm(Window owner, string title, string message, string primary = "OK", string? secondary = "Abbrechen") =>
|
||||||
|
Show(owner, title, Text(message), primary, secondary, null);
|
||||||
|
|
||||||
|
public static void Info(Window owner, string title, string message) =>
|
||||||
|
Show(owner, title, Text(message), "OK", null, null);
|
||||||
|
|
||||||
|
/// <summary>Eingabedialog. validate gibt eine Fehlermeldung oder null zurück.</summary>
|
||||||
|
public static string? Prompt(Window owner, string title, string label, string initial, string primary, Func<string, string?> validate)
|
||||||
|
{
|
||||||
|
var box = new TextBox { Text = initial, Margin = new Thickness(0, 8, 0, 0) };
|
||||||
|
var panel = new StackPanel();
|
||||||
|
panel.Children.Add(Text(label, secondary: true));
|
||||||
|
panel.Children.Add(box);
|
||||||
|
var ok = Show(owner, title, panel, primary, "Abbrechen", () => validate(box.Text), box);
|
||||||
|
return ok ? box.Text.Trim() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TextBlock Text(string text, bool secondary = false) => new()
|
||||||
|
{
|
||||||
|
Text = text, TextWrapping = TextWrapping.Wrap, LineHeight = 20,
|
||||||
|
Foreground = (Brush)Application.Current.Resources[secondary ? "TextSecondaryBrush" : "TextBrush"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>Allgemeiner Dialog mit beliebigem Inhalt. validate: Fehlermeldung oder null.</summary>
|
||||||
|
public static bool Show(Window owner, string title, UIElement content, string primary, string? secondary,
|
||||||
|
Func<string?>? validate, IInputElement? focus = null, double width = 448)
|
||||||
|
{
|
||||||
|
var res = Application.Current.Resources;
|
||||||
|
|
||||||
|
var error = new TextBlock
|
||||||
|
{
|
||||||
|
Foreground = (Brush)res["DangerBrush"], FontSize = 12, Margin = new Thickness(0, 8, 0, 0),
|
||||||
|
TextWrapping = TextWrapping.Wrap, Visibility = Visibility.Collapsed,
|
||||||
|
};
|
||||||
|
|
||||||
|
var body = new StackPanel { Margin = new Thickness(24, 22, 24, 24) };
|
||||||
|
body.Children.Add(new TextBlock
|
||||||
|
{
|
||||||
|
Text = title, FontSize = 20, FontWeight = FontWeights.SemiBold, Margin = new Thickness(0, 0, 0, 12),
|
||||||
|
FontFamily = (FontFamily)res["DisplayFont"], TextWrapping = TextWrapping.Wrap,
|
||||||
|
});
|
||||||
|
body.Children.Add(content);
|
||||||
|
body.Children.Add(error);
|
||||||
|
|
||||||
|
var ok = new Button { Content = primary, IsDefault = true, Style = (Style)res["AccentButton"] };
|
||||||
|
var footer = new Grid { Margin = new Thickness(24) };
|
||||||
|
footer.ColumnDefinitions.Add(new ColumnDefinition());
|
||||||
|
footer.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(8) });
|
||||||
|
footer.ColumnDefinitions.Add(new ColumnDefinition());
|
||||||
|
if (secondary is not null)
|
||||||
|
{
|
||||||
|
var cancel = new Button { Content = secondary, IsCancel = true };
|
||||||
|
Grid.SetColumn(cancel, 2);
|
||||||
|
footer.Children.Add(cancel);
|
||||||
|
}
|
||||||
|
else Grid.SetColumn(ok, 2); // einzelner Button rechts, wie bei Windows 11
|
||||||
|
footer.Children.Add(ok);
|
||||||
|
|
||||||
|
var layout = new DockPanel();
|
||||||
|
var footerBorder = new Border
|
||||||
|
{
|
||||||
|
Background = (Brush)res["DialogFooterBrush"], Child = footer,
|
||||||
|
BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(0, 1, 0, 0),
|
||||||
|
CornerRadius = new CornerRadius(0, 0, 8, 8),
|
||||||
|
};
|
||||||
|
DockPanel.SetDock(footerBorder, Dock.Bottom);
|
||||||
|
layout.Children.Add(footerBorder);
|
||||||
|
layout.Children.Add(new ScrollViewer { Content = body, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, MaxHeight = 560 });
|
||||||
|
|
||||||
|
var dlg = new Window
|
||||||
|
{
|
||||||
|
Owner = owner.IsVisible ? owner : null, Title = title, Width = width + 64, SizeToContent = SizeToContent.Height,
|
||||||
|
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
|
||||||
|
ResizeMode = ResizeMode.NoResize, ShowInTaskbar = !owner.IsVisible, Topmost = !owner.IsVisible,
|
||||||
|
WindowStartupLocation = owner.IsVisible ? WindowStartupLocation.CenterOwner : WindowStartupLocation.CenterScreen,
|
||||||
|
FontFamily = (FontFamily)res["UiFont"], FontSize = 14,
|
||||||
|
Foreground = (Brush)res["TextBrush"],
|
||||||
|
Content = new Border
|
||||||
|
{
|
||||||
|
Margin = new Thickness(32), CornerRadius = new CornerRadius(8),
|
||||||
|
Background = (Brush)res["LayerBrush"],
|
||||||
|
BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(1),
|
||||||
|
Effect = (System.Windows.Media.Effects.Effect)res["DialogShadow"], Child = layout,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
ok.Click += (_, _) =>
|
||||||
|
{
|
||||||
|
if (validate?.Invoke() is { } err)
|
||||||
|
{
|
||||||
|
error.Text = err;
|
||||||
|
error.Visibility = Visibility.Visible;
|
||||||
|
(focus as UIElement)?.Focus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dlg.DialogResult = true;
|
||||||
|
};
|
||||||
|
if (focus is TextBox tb) tb.TextChanged += (_, _) => error.Visibility = Visibility.Collapsed;
|
||||||
|
|
||||||
|
dlg.PreviewKeyDown += (_, e) => { if (e.Key == Key.Escape) dlg.Close(); };
|
||||||
|
dlg.MouseLeftButtonDown += (_, e) => { if (e.ButtonState == MouseButtonState.Pressed) dlg.DragMove(); };
|
||||||
|
dlg.Loaded += (_, _) =>
|
||||||
|
{
|
||||||
|
if (focus is TextBox t) { t.Focus(); t.SelectAll(); }
|
||||||
|
else if (focus is UIElement u) u.Focus();
|
||||||
|
else ok.Focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
(owner as MainWindow)?.SetDimmed(true);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return dlg.ShowDialog() == true;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
(owner as MainWindow)?.SetDimmed(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<!-- Eine Versionsnummer für App und Installer – nur hier ändern -->
|
||||||
|
<PropertyGroup>
|
||||||
|
<Version>1.0.0</Version>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
<Window x:Class="DeskCore.FenceWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
WindowStyle="None" AllowsTransparency="True" Background="Transparent" ResizeMode="CanResize"
|
||||||
|
ShowInTaskbar="False" ShowActivated="False" MinWidth="180" MinHeight="40"
|
||||||
|
FontFamily="{DynamicResource UiFont}" FontSize="12" Foreground="{DynamicResource TextBrush}"
|
||||||
|
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True">
|
||||||
|
|
||||||
|
<WindowChrome.WindowChrome>
|
||||||
|
<WindowChrome CaptionHeight="36" ResizeBorderThickness="6" GlassFrameThickness="0" CornerRadius="0"/>
|
||||||
|
</WindowChrome.WindowChrome>
|
||||||
|
|
||||||
|
<Border CornerRadius="8" Background="{DynamicResource FenceBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}"
|
||||||
|
BorderThickness="1" ClipToBounds="True">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- Titel (zum Verschieben ziehen, Doppelklick = ein-/ausklappen) -->
|
||||||
|
<Grid DockPanel.Dock="Top" Height="36" Background="Transparent" x:Name="TitleBar">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock x:Name="GlyphText" FontFamily="{DynamicResource IconFont}" FontSize="14" VerticalAlignment="Center" Margin="12,0,10,0"/>
|
||||||
|
<TextBlock x:Name="TitleText" Grid.Column="1" FontSize="13" FontWeight="SemiBold" VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock x:Name="CountText" Grid.Column="2" Foreground="{DynamicResource TextTertiaryBrush}" VerticalAlignment="Center" Margin="8,0"/>
|
||||||
|
<Button x:Name="MenuButton" Grid.Column="3" Style="{StaticResource SubtleButton}" Padding="7,5" MinHeight="0" Margin="0,0,4,0"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True" Click="MenuButton_Click" ToolTip="Optionen">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="12"/>
|
||||||
|
<Button.ContextMenu>
|
||||||
|
<ContextMenu x:Name="FenceMenu" Placement="Bottom"/>
|
||||||
|
</Button.ContextMenu>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid x:Name="Body">
|
||||||
|
<ListBox x:Name="Items" Padding="6,0,6,8" SelectionMode="Extended" ItemContainerStyle="{StaticResource TileItem}"
|
||||||
|
AllowDrop="True" DragOver="Items_DragOver" Drop="Items_Drop"
|
||||||
|
MouseDoubleClick="Items_MouseDoubleClick" KeyDown="Items_KeyDown"
|
||||||
|
ContextMenuOpening="Items_ContextMenuOpening">
|
||||||
|
<ListBox.ContextMenu>
|
||||||
|
<ContextMenu x:Name="ItemMenu"/>
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
<ListBox.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListBox.ItemsPanel>
|
||||||
|
<ListBox.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<StackPanel Width="78" Height="80" ToolTip="{Binding Name}">
|
||||||
|
<Image Source="{Binding Icon}" Width="32" Height="32" Margin="0,8,0,0" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock Text="{Binding Name}" TextAlignment="Center" TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
|
||||||
|
MaxHeight="30" LineHeight="15" Margin="4,6,4,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
<TextBlock x:Name="EmptyText" Text="Leer – Icons hierher ziehen" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource TextTertiaryBrush}" IsHitTestVisible="False" Margin="0,0,0,10"/>
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Eine "Box" auf dem Desktop, die den Inhalt einer Kategorie zeigt.</summary>
|
||||||
|
public partial class FenceWindow : Window
|
||||||
|
{
|
||||||
|
private const double CollapsedHeight = 38;
|
||||||
|
private readonly DispatcherTimer _saveTimer;
|
||||||
|
private bool _applying;
|
||||||
|
|
||||||
|
public string Category { get; private set; }
|
||||||
|
|
||||||
|
public FenceWindow(string category)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
Category = category;
|
||||||
|
_saveTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(700), DispatcherPriority.Background, (_, _) =>
|
||||||
|
{
|
||||||
|
_saveTimer!.Stop();
|
||||||
|
AppSettings.Current.Save();
|
||||||
|
}, Dispatcher);
|
||||||
|
_saveTimer.Stop();
|
||||||
|
|
||||||
|
ApplyBounds();
|
||||||
|
LocationChanged += (_, _) => RememberBounds();
|
||||||
|
SizeChanged += (_, _) => RememberBounds();
|
||||||
|
SourceInitialized += (_, _) => PinToDesktop();
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryMeta Meta => AppSettings.Current.Meta(Category);
|
||||||
|
|
||||||
|
public void Rename(string newName)
|
||||||
|
{
|
||||||
|
Category = newName;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Position/Größe ----------
|
||||||
|
|
||||||
|
private void ApplyBounds()
|
||||||
|
{
|
||||||
|
_applying = true;
|
||||||
|
var m = Meta;
|
||||||
|
var wa = SystemParameters.WorkArea;
|
||||||
|
Width = Math.Max(MinWidth, m.FenceW);
|
||||||
|
Height = m.FenceCollapsed ? CollapsedHeight : Math.Max(120, m.FenceH);
|
||||||
|
Left = Math.Clamp(m.FenceX ?? wa.Left + 24, wa.Left - Width + 80, wa.Right - 80);
|
||||||
|
Top = Math.Clamp(m.FenceY ?? wa.Top + 24, wa.Top, wa.Bottom - 40);
|
||||||
|
Body.Visibility = m.FenceCollapsed ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
_applying = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RememberBounds()
|
||||||
|
{
|
||||||
|
if (_applying || !IsLoaded) return;
|
||||||
|
var m = Meta;
|
||||||
|
m.FenceX = Left;
|
||||||
|
m.FenceY = Top;
|
||||||
|
m.FenceW = Width;
|
||||||
|
if (!m.FenceCollapsed) m.FenceH = Height;
|
||||||
|
_saveTimer.Stop();
|
||||||
|
_saveTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ToggleCollapsed()
|
||||||
|
{
|
||||||
|
Meta.FenceCollapsed = !Meta.FenceCollapsed;
|
||||||
|
ApplyBounds();
|
||||||
|
AppSettings.Current.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Inhalt ----------
|
||||||
|
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
var meta = Meta;
|
||||||
|
TitleText.Text = Category;
|
||||||
|
GlyphText.Text = meta.Glyph ?? "";
|
||||||
|
GlyphText.Foreground = CategoryColors.Brush(meta.Color) ?? (Brush)FindResource("TextBrush");
|
||||||
|
|
||||||
|
var items = Storage.Entries(Storage.CategoryPath(Category))
|
||||||
|
.Select(p => new IconItem { Path = p })
|
||||||
|
.OrderBy(i => !Directory.Exists(i.Path)).ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
int px = (int)Math.Round(32 * VisualTreeHelper.GetDpi(this).DpiScaleX);
|
||||||
|
foreach (var i in items) IconLoader.Load(i, px);
|
||||||
|
Items.ItemsSource = items;
|
||||||
|
CountText.Text = items.Count.ToString();
|
||||||
|
EmptyText.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<IconItem> Selected => Items.SelectedItems.Cast<IconItem>().ToList();
|
||||||
|
|
||||||
|
private static void Open(string path)
|
||||||
|
{
|
||||||
|
try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_MouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (MainWindow.FindParent<ListBoxItem>(e.OriginalSource as DependencyObject)?.DataContext is IconItem i) Open(i.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_KeyDown(object sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Key == Key.Enter) Selected.ForEach(i => Open(i.Path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||||
|
{
|
||||||
|
var sel = Selected;
|
||||||
|
if (sel.Count == 0) { e.Handled = true; return; }
|
||||||
|
ItemMenu.Items.Clear();
|
||||||
|
ItemMenu.Items.Add(MainWindow.MenuItem("Öffnen", "", () => sel.ForEach(i => Open(i.Path)), bold: true));
|
||||||
|
if (sel.Count == 1)
|
||||||
|
ItemMenu.Items.Add(MainWindow.MenuItem("Dateispeicherort öffnen", "", () =>
|
||||||
|
Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\"")));
|
||||||
|
ItemMenu.Items.Add(new Separator());
|
||||||
|
ItemMenu.Items.Add(MainWindow.MenuItem("In DeskCore zeigen", "", ShowInApp));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MenuButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FenceMenu.Items.Clear();
|
||||||
|
FenceMenu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", ShowInApp));
|
||||||
|
FenceMenu.Items.Add(MainWindow.MenuItem(Meta.FenceCollapsed ? "Ausklappen" : "Einklappen",
|
||||||
|
Meta.FenceCollapsed ? "" : "", ToggleCollapsed));
|
||||||
|
FenceMenu.Items.Add(new Separator());
|
||||||
|
FenceMenu.Items.Add(MainWindow.MenuItem("Box ausblenden", "", () =>
|
||||||
|
{
|
||||||
|
Meta.Fence = false;
|
||||||
|
AppSettings.Current.Save();
|
||||||
|
FenceManager.Sync();
|
||||||
|
}));
|
||||||
|
FenceMenu.PlacementTarget = MenuButton;
|
||||||
|
FenceMenu.IsOpen = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowInApp() => App.Win?.ShowCategory(Category);
|
||||||
|
|
||||||
|
private void Items_DragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop)
|
||||||
|
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
|
||||||
|
: DragDropEffects.None;
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Items_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Data.GetData(DataFormats.FileDrop) is string[] files)
|
||||||
|
App.Win?.TransferAll(files, Storage.CategoryPath(Category));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Auf dem Desktop festhalten ----------
|
||||||
|
|
||||||
|
private void PinToDesktop()
|
||||||
|
{
|
||||||
|
var hwnd = new WindowInteropHelper(this).Handle;
|
||||||
|
// Besitzer = Desktop (Progman): bleibt bei Win+D sichtbar und liegt über den Desktop-Icons
|
||||||
|
var progman = FindWindow("Progman", null);
|
||||||
|
if (progman != IntPtr.Zero) SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, progman);
|
||||||
|
// Nicht in Alt+Tab
|
||||||
|
var ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
|
||||||
|
SetWindowLongPtr(hwnd, GWL_EXSTYLE, new IntPtr((ex | WS_EX_TOOLWINDOW) & ~WS_EX_APPWINDOW));
|
||||||
|
HwndSource.FromHwnd(hwnd).AddHook(Hook);
|
||||||
|
SetWindowPos(hwnd, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
switch (msg)
|
||||||
|
{
|
||||||
|
case WM_WINDOWPOSCHANGING:
|
||||||
|
// immer ganz unten bleiben, nie über anderen Fenstern
|
||||||
|
var wp = Marshal.PtrToStructure<WINDOWPOS>(lParam);
|
||||||
|
wp.hwndInsertAfter = HWND_BOTTOM;
|
||||||
|
wp.flags &= ~SWP_NOZORDER;
|
||||||
|
Marshal.StructureToPtr(wp, lParam, false);
|
||||||
|
break;
|
||||||
|
case WM_NCLBUTTONDBLCLK:
|
||||||
|
ToggleCollapsed();
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
case WM_SYSCOMMAND when (wParam.ToInt32() & 0xFFF0) == SC_MAXIMIZE:
|
||||||
|
handled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const int GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8;
|
||||||
|
private const long WS_EX_TOOLWINDOW = 0x80, WS_EX_APPWINDOW = 0x40000;
|
||||||
|
private const int WM_WINDOWPOSCHANGING = 0x46, WM_NCLBUTTONDBLCLK = 0xA3, WM_SYSCOMMAND = 0x112, SC_MAXIMIZE = 0xF030;
|
||||||
|
private const uint SWP_NOSIZE = 0x1, SWP_NOMOVE = 0x2, SWP_NOZORDER = 0x4, SWP_NOACTIVATE = 0x10;
|
||||||
|
private static readonly IntPtr HWND_BOTTOM = new(1);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct WINDOWPOS
|
||||||
|
{
|
||||||
|
public IntPtr hwnd, hwndInsertAfter;
|
||||||
|
public int x, y, cx, cy;
|
||||||
|
public uint flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string? cls, string? title);
|
||||||
|
[DllImport("user32.dll")] private static extern IntPtr GetWindowLongPtr(IntPtr h, int i);
|
||||||
|
[DllImport("user32.dll")] private static extern IntPtr SetWindowLongPtr(IntPtr h, int i, IntPtr v);
|
||||||
|
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hält die Desktop-Boxen passend zu den Einstellungen.</summary>
|
||||||
|
public static class FenceManager
|
||||||
|
{
|
||||||
|
private static readonly List<FenceWindow> _open = [];
|
||||||
|
|
||||||
|
public static void Sync()
|
||||||
|
{
|
||||||
|
var s = AppSettings.Current;
|
||||||
|
var wanted = s.FencesEnabled
|
||||||
|
? Storage.Categories().Where(c => s.Categories.GetValueOrDefault(c)?.Fence == true).ToList()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach (var f in _open.Where(f => !wanted.Contains(f.Category, StringComparer.OrdinalIgnoreCase)).ToList())
|
||||||
|
{
|
||||||
|
f.Close();
|
||||||
|
_open.Remove(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
var wa = SystemParameters.WorkArea;
|
||||||
|
foreach (var c in wanted)
|
||||||
|
{
|
||||||
|
var f = _open.FirstOrDefault(x => string.Equals(x.Category, c, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (f is not null) { f.Refresh(); continue; }
|
||||||
|
|
||||||
|
// Neue Box: neben die vorhandenen setzen
|
||||||
|
var meta = s.Meta(c);
|
||||||
|
if (meta.FenceX is null)
|
||||||
|
{
|
||||||
|
// oben rechts beginnen – links liegen meist die Desktop-Icons
|
||||||
|
int perRow = Math.Max(1, (int)((wa.Width - 48) / (meta.FenceW + 16)));
|
||||||
|
int i = _open.Count;
|
||||||
|
meta.FenceX = wa.Right - 24 - meta.FenceW - (i % perRow) * (meta.FenceW + 16);
|
||||||
|
meta.FenceY = wa.Top + 24 + (i / perRow) * (meta.FenceH + 16);
|
||||||
|
}
|
||||||
|
f = new FenceWindow(c);
|
||||||
|
_open.Add(f);
|
||||||
|
f.Show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RefreshAll()
|
||||||
|
{
|
||||||
|
foreach (var f in _open) f.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Renamed(string oldName, string newName)
|
||||||
|
{
|
||||||
|
_open.FirstOrDefault(x => string.Equals(x.Category, oldName, StringComparison.OrdinalIgnoreCase))?.Rename(newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void CloseAll()
|
||||||
|
{
|
||||||
|
foreach (var f in _open) f.Close();
|
||||||
|
_open.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Systemweites Tastenkürzel (RegisterHotKey).</summary>
|
||||||
|
public sealed class Hotkey : IDisposable
|
||||||
|
{
|
||||||
|
public const int ModAlt = 0x1, ModCtrl = 0x2, ModShift = 0x4, ModWin = 0x8;
|
||||||
|
private const int ModNoRepeat = 0x4000, WmHotkey = 0x0312, Id = 0xB001;
|
||||||
|
|
||||||
|
private readonly IntPtr _hwnd;
|
||||||
|
private readonly HwndSource _source;
|
||||||
|
private bool _registered;
|
||||||
|
|
||||||
|
public event Action? Pressed;
|
||||||
|
|
||||||
|
public Hotkey(IntPtr hwnd)
|
||||||
|
{
|
||||||
|
_hwnd = hwnd;
|
||||||
|
_source = HwndSource.FromHwnd(hwnd);
|
||||||
|
_source.AddHook(Hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Registriert das Kürzel. false = schon von einem anderen Programm belegt.</summary>
|
||||||
|
public bool Register(int modifiers, int vk)
|
||||||
|
{
|
||||||
|
Unregister();
|
||||||
|
if (vk == 0) return false;
|
||||||
|
_registered = RegisterHotKey(_hwnd, Id, modifiers | ModNoRepeat, vk);
|
||||||
|
return _registered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister()
|
||||||
|
{
|
||||||
|
if (_registered) UnregisterHotKey(_hwnd, Id);
|
||||||
|
_registered = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||||
|
{
|
||||||
|
if (msg == WmHotkey && wParam.ToInt32() == Id)
|
||||||
|
{
|
||||||
|
Pressed?.Invoke();
|
||||||
|
handled = true;
|
||||||
|
}
|
||||||
|
return IntPtr.Zero;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Describe(int modifiers, int vk)
|
||||||
|
{
|
||||||
|
if (vk == 0) return "–";
|
||||||
|
var parts = new List<string>();
|
||||||
|
if ((modifiers & ModWin) != 0) parts.Add("Win");
|
||||||
|
if ((modifiers & ModCtrl) != 0) parts.Add("Strg");
|
||||||
|
if ((modifiers & ModAlt) != 0) parts.Add("Alt");
|
||||||
|
if ((modifiers & ModShift) != 0) parts.Add("Umschalt");
|
||||||
|
var key = KeyInterop.KeyFromVirtualKey(vk);
|
||||||
|
parts.Add(key switch
|
||||||
|
{
|
||||||
|
>= Key.D0 and <= Key.D9 => ((char)('0' + (key - Key.D0))).ToString(),
|
||||||
|
Key.Space => "Leertaste",
|
||||||
|
_ => key.ToString(),
|
||||||
|
});
|
||||||
|
return string.Join(" + ", parts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Unregister();
|
||||||
|
_source.RemoveHook(Hook);
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
|
||||||
|
[DllImport("user32.dll")] private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Start mit Windows über HKCU\...\Run.</summary>
|
||||||
|
public static class Autostart
|
||||||
|
{
|
||||||
|
private const string Key = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
private const string Name = "DeskCore";
|
||||||
|
// Frühere Namen dieser App. "DeskHub" heißt auch ein anderes Programm → nur übernehmen, wenn der Eintrag eindeutig von uns ist.
|
||||||
|
private static readonly string[] OldNames = ["DesktopSortierer", "DeskHub"];
|
||||||
|
|
||||||
|
public static bool Enabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(Key);
|
||||||
|
return k?.GetValue(Name) is string;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(Key, writable: true) ?? Registry.CurrentUser.CreateSubKey(Key);
|
||||||
|
if (value) k.SetValue(Name, $"\"{Environment.ProcessPath}\" --minimized");
|
||||||
|
else k.DeleteValue(Name, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Alten Autostart-Eintrag ("DesktopSortierer") auf den neuen Namen umstellen.</summary>
|
||||||
|
public static void Migrate()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(Key, writable: true);
|
||||||
|
if (k is null) return;
|
||||||
|
bool wasEnabled = false;
|
||||||
|
foreach (var old in OldNames)
|
||||||
|
{
|
||||||
|
if (k.GetValue(old) is not string cmd) continue;
|
||||||
|
bool ours = old == "DesktopSortierer" ||
|
||||||
|
(cmd.Contains($"{old}.exe", StringComparison.OrdinalIgnoreCase) && cmd.Contains("--minimized"));
|
||||||
|
if (!ours) continue;
|
||||||
|
k.DeleteValue(old, throwOnMissingValue: false);
|
||||||
|
wasEnabled = true;
|
||||||
|
}
|
||||||
|
if (wasEnabled) Enabled = true;
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Falls die exe verschoben wurde: Pfad im Autostart aktualisieren.</summary>
|
||||||
|
public static void RefreshPath()
|
||||||
|
{
|
||||||
|
if (Enabled) Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Lädt Icons auf einem eigenen STA-Thread (Shell-COM braucht STA) und cached sie.</summary>
|
||||||
|
public static class IconLoader
|
||||||
|
{
|
||||||
|
private static readonly ConcurrentDictionary<string, ImageSource?> _cache = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private static readonly BlockingCollection<(IconItem Item, int Size)> _queue = new();
|
||||||
|
|
||||||
|
static IconLoader()
|
||||||
|
{
|
||||||
|
var t = new Thread(Work) { IsBackground = true, Name = "IconLoader" };
|
||||||
|
t.SetApartmentState(ApartmentState.STA);
|
||||||
|
t.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Key(string path, int size)
|
||||||
|
{
|
||||||
|
DateTime stamp;
|
||||||
|
try { stamp = File.GetLastWriteTimeUtc(path); } catch { stamp = default; }
|
||||||
|
return $"{size}|{stamp.Ticks}|{path}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Setzt das Icon sofort aus dem Cache oder lädt es im Hintergrund nach.</summary>
|
||||||
|
public static void Load(IconItem item, int size)
|
||||||
|
{
|
||||||
|
if (_cache.TryGetValue(Key(item.Path, size), out var icon)) item.Icon = icon;
|
||||||
|
else _queue.Add((item, size));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Work()
|
||||||
|
{
|
||||||
|
foreach (var (item, size) in _queue.GetConsumingEnumerable())
|
||||||
|
{
|
||||||
|
var key = Key(item.Path, size);
|
||||||
|
if (!_cache.TryGetValue(key, out var icon))
|
||||||
|
{
|
||||||
|
icon = ShellIcon.Get(item.Path, size);
|
||||||
|
_cache[key] = icon;
|
||||||
|
}
|
||||||
|
var result = icon;
|
||||||
|
Application.Current?.Dispatcher.BeginInvoke(() => item.Icon = result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
@@ -0,0 +1,38 @@
|
|||||||
|
DeskCore – Lizenz- und Nutzungsbedingungen
|
||||||
|
|
||||||
|
Copyright (c) M_Viper. Alle Rechte vorbehalten.
|
||||||
|
|
||||||
|
Diese Software („DeskCore") sowie sämtlicher zugehöriger Quellcode, Ressourcen und
|
||||||
|
Dokumentation sind urheberrechtlich geschützt. Mit der Nutzung dieser Software
|
||||||
|
erkennst du die folgenden Bedingungen an.
|
||||||
|
|
||||||
|
1. Erlaubte Nutzung
|
||||||
|
Die Nutzung von DeskCore ist ausschließlich für den privaten, nicht-kommerziellen
|
||||||
|
Gebrauch gestattet.
|
||||||
|
|
||||||
|
2. Untersagt ist insbesondere
|
||||||
|
- Die Weitergabe dieser Software oder von Teilen davon an Dritte
|
||||||
|
- Das Verändern, Dekompilieren, Zurückentwickeln (Reverse Engineering) oder
|
||||||
|
Bearbeiten des Codes
|
||||||
|
- Die Veröffentlichung – auch in veränderter oder abgeleiteter Form –
|
||||||
|
an anderer Stelle als den offiziellen Kanälen des Autors
|
||||||
|
- Der Verkauf sowie jede sonstige kommerzielle Nutzung
|
||||||
|
|
||||||
|
3. Keine Gewährleistung
|
||||||
|
Die Software wird ohne jegliche Gewährleistung bereitgestellt, weder
|
||||||
|
ausdrücklich noch stillschweigend, einschließlich, aber nicht beschränkt auf
|
||||||
|
die Gewährleistung der Marktgängigkeit, der Eignung für einen bestimmten
|
||||||
|
Zweck und der Nichtverletzung von Rechten Dritter. Der Autor haftet nicht für
|
||||||
|
Schäden, die aus der Nutzung oder Unmöglichkeit der Nutzung der Software
|
||||||
|
entstehen.
|
||||||
|
|
||||||
|
4. Rechtsfolgen bei Verstoß
|
||||||
|
Wer gegen diese Bedingungen verstößt, macht sich strafbar und kann rechtlich
|
||||||
|
belangt werden. Alle Rechte, die hier nicht ausdrücklich eingeräumt werden,
|
||||||
|
bleiben beim Autor vorbehalten.
|
||||||
|
|
||||||
|
5. Markenhinweis
|
||||||
|
DeskCore ist kein offizielles Produkt von Microsoft. Windows ist
|
||||||
|
eine Marke der Microsoft Corporation.
|
||||||
|
|
||||||
|
Kontakt: https://m-viper.de
|
||||||
+625
@@ -0,0 +1,625 @@
|
|||||||
|
<Window x:Class="DeskCore.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||||
|
Title="DeskCore" Icon="app.ico" Width="1120" Height="720" MinWidth="760" MinHeight="480"
|
||||||
|
Background="{DynamicResource BaseBrush}" Foreground="{DynamicResource TextBrush}"
|
||||||
|
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||||
|
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<WindowChrome.WindowChrome>
|
||||||
|
<WindowChrome CaptionHeight="40" ResizeBorderThickness="6" GlassFrameThickness="0,0,0,1"
|
||||||
|
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||||
|
</WindowChrome.WindowChrome>
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<!-- Kachelgrößen – werden je nach Ansicht im Code umgestellt -->
|
||||||
|
<sys:Double x:Key="TileWidth">108</sys:Double>
|
||||||
|
<sys:Double x:Key="TileHeight">116</sys:Double>
|
||||||
|
<sys:Double x:Key="TileIcon">48</sys:Double>
|
||||||
|
<Thickness x:Key="TileIconMargin">0,14,0,0</Thickness>
|
||||||
|
|
||||||
|
<!-- Eintrag in der Seitenleiste -->
|
||||||
|
<DataTemplate x:Key="NavTemplate">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="{Binding Glyph}" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,14,0">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Foreground" Value="{Binding GlyphBrush}"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding GlyphBrush}" Value="{x:Null}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Name}" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="" FontFamily="{DynamicResource IconFont}" FontSize="12" VerticalAlignment="Center"
|
||||||
|
Margin="8,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}" ToolTip="Wird als Box auf dem Desktop angezeigt">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding HasFence}" Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding Count}" FontSize="12" Foreground="{DynamicResource TextTertiaryBrush}"
|
||||||
|
VerticalAlignment="Center" HorizontalAlignment="Right" Margin="8,0,0,0">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Style.Triggers>
|
||||||
|
<MultiDataTrigger>
|
||||||
|
<MultiDataTrigger.Conditions>
|
||||||
|
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True"/>
|
||||||
|
<Condition Binding="{Binding IsDesktop}" Value="False"/>
|
||||||
|
</MultiDataTrigger.Conditions>
|
||||||
|
<Setter Property="Visibility" Value="Hidden"/>
|
||||||
|
</MultiDataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
<!-- Griff zum Umsortieren (nur beim Überfahren) -->
|
||||||
|
<TextBlock Grid.Column="3" Text="" FontFamily="{DynamicResource IconFont}" FontSize="14" Cursor="SizeNS"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}" VerticalAlignment="Center" HorizontalAlignment="Right"
|
||||||
|
ToolTip="Ziehen zum Umsortieren (oder Alt+↑/↓)">
|
||||||
|
<TextBlock.Style>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<MultiDataTrigger>
|
||||||
|
<MultiDataTrigger.Conditions>
|
||||||
|
<Condition Binding="{Binding IsMouseOver, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True"/>
|
||||||
|
<Condition Binding="{Binding IsDesktop}" Value="False"/>
|
||||||
|
</MultiDataTrigger.Conditions>
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
</MultiDataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</TextBlock.Style>
|
||||||
|
</TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Kachel -->
|
||||||
|
<DataTemplate x:Key="TileTemplate">
|
||||||
|
<StackPanel Width="{DynamicResource TileWidth}" Height="{DynamicResource TileHeight}" ToolTip="{Binding ToolTip}">
|
||||||
|
<Grid Width="{DynamicResource TileIcon}" Height="{DynamicResource TileIcon}" Margin="{DynamicResource TileIconMargin}">
|
||||||
|
<Image Source="{Binding Icon}" RenderOptions.BitmapScalingMode="HighQuality">
|
||||||
|
<Image.Style>
|
||||||
|
<Style TargetType="Image">
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSorted}" Value="True">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Image.Style>
|
||||||
|
</Image>
|
||||||
|
<Border HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="0,0,-7,-5"
|
||||||
|
Width="20" Height="20" CornerRadius="10" Background="{DynamicResource SuccessBrush}"
|
||||||
|
BorderBrush="{DynamicResource LayerBrush}" BorderThickness="2">
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Visibility" Value="Collapsed"/>
|
||||||
|
<Style.Triggers>
|
||||||
|
<DataTrigger Binding="{Binding IsSorted}" Value="True">
|
||||||
|
<Setter Property="Visibility" Value="Visible"/>
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="9" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
<TextBlock Text="{Binding Name}" FontSize="12" TextAlignment="Center" TextWrapping="Wrap"
|
||||||
|
TextTrimming="CharacterEllipsis" MaxHeight="34" LineHeight="16" Margin="6,9,6,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<!-- Listenzeile -->
|
||||||
|
<DataTemplate x:Key="ListTemplate">
|
||||||
|
<Grid Height="40" Margin="14,0,10,0" ToolTip="{Binding ToolTip}">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
<ColumnDefinition Width="160"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Image Source="{Binding Icon}" Width="24" Height="24" Margin="0,0,14,0" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Name}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding LocationText}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12"/>
|
||||||
|
<TextBlock Grid.Column="3" Text="{Binding TypeText}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12"/>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
|
||||||
|
<ItemsPanelTemplate x:Key="WrapPanelTemplate">
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
<ItemsPanelTemplate x:Key="StackPanelTemplate">
|
||||||
|
<VirtualizingStackPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
|
||||||
|
<!-- Regelzeile in den Einstellungen -->
|
||||||
|
<DataTemplate x:Key="RuleTemplate">
|
||||||
|
<Grid Margin="0,0,0,8">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Name enthält" VerticalAlignment="Center" Margin="0,0,12,0" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBox Grid.Column="1" Text="{Binding Contains, UpdateSourceTrigger=PropertyChanged}" LostFocus="RuleText_LostFocus"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="" FontFamily="{DynamicResource IconFont}" FontSize="12" VerticalAlignment="Center"
|
||||||
|
Margin="12,0" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<Button Grid.Column="3" HorizontalContentAlignment="Stretch" Click="RuleCategory_Click" Tag="{Binding}">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Right" Text="" FontFamily="{DynamicResource IconFont}" FontSize="10"
|
||||||
|
VerticalAlignment="Center" Margin="8,0,0,0"/>
|
||||||
|
<TextBlock Text="{Binding Category}" TextTrimming="CharacterEllipsis"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="4" Style="{StaticResource SubtleButton}" Margin="6,0,0,0" Click="RuleDelete_Click" Tag="{Binding}"
|
||||||
|
ToolTip="Regel entfernen">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
</DataTemplate>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid x:Name="RootGrid">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="40"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="280"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- ===== Titelleiste ===== -->
|
||||||
|
<Grid Grid.ColumnSpan="2">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="16,0,0,0">
|
||||||
|
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock Text="DeskCore" FontSize="12" Margin="12,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
|
<Button x:Name="ThemeButton" Style="{StaticResource CaptionButton}" FontSize="14" Click="ThemeButton_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Design: Hell / Dunkel">
|
||||||
|
<Button.ContextMenu>
|
||||||
|
<ContextMenu x:Name="ThemeMenu" Placement="Bottom"/>
|
||||||
|
</Button.ContextMenu>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Minimieren"/>
|
||||||
|
<Button x:Name="MaxButton" Style="{StaticResource CaptionButton}" Content="" Click="Maximize_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Maximieren"/>
|
||||||
|
<Button Style="{StaticResource CloseCaptionButton}" Content="" Click="Close_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Schließen"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- ===== Seitenleiste ===== -->
|
||||||
|
<DockPanel Grid.Row="1">
|
||||||
|
<ListBox x:Name="DesktopNav" DockPanel.Dock="Top" Margin="8,6,8,0"
|
||||||
|
ItemContainerStyle="{StaticResource NavItem}" ItemTemplate="{StaticResource NavTemplate}"
|
||||||
|
SelectionChanged="Nav_SelectionChanged"
|
||||||
|
AllowDrop="True" DragOver="Nav_DragOver" DragLeave="Nav_DragLeave" Drop="Nav_Drop"/>
|
||||||
|
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="22,20,14,6">
|
||||||
|
<TextBlock Text="Kategorien" FontSize="12" FontWeight="SemiBold" Foreground="{DynamicResource TextSecondaryBrush}"
|
||||||
|
VerticalAlignment="Center" ToolTip="Reihenfolge ändern: Kategorie mit der Maus ziehen, Alt+↑/↓ oder Rechtsklick"/>
|
||||||
|
<Button Style="{StaticResource SubtleButton}" HorizontalAlignment="Right" Padding="6,4" MinHeight="0"
|
||||||
|
Click="NewCategory_Click" ToolTip="Neue Kategorie (Strg+N)">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="12"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel DockPanel.Dock="Bottom" Margin="8,8,8,12">
|
||||||
|
<Button Style="{StaticResource SubtleButton}" Padding="16,9" HorizontalContentAlignment="Left" Click="NewCategory_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16" Margin="0,0,14,0"
|
||||||
|
VerticalAlignment="Center" Foreground="{DynamicResource AccentBrush}"/>
|
||||||
|
<TextBlock Text="Neue Kategorie" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<ListBox x:Name="SettingsNav" Margin="0,2,0,0" ItemContainerStyle="{StaticResource NavItem}"
|
||||||
|
SelectionChanged="SettingsNav_SelectionChanged">
|
||||||
|
<ListBoxItem>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16" Margin="0,0,14,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Einstellungen" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ListBoxItem>
|
||||||
|
</ListBox>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<ListBox x:Name="CategoryList" Margin="8,0"
|
||||||
|
ItemContainerStyle="{StaticResource NavItem}" ItemTemplate="{StaticResource NavTemplate}"
|
||||||
|
SelectionChanged="Nav_SelectionChanged"
|
||||||
|
PreviewMouseLeftButtonDown="CategoryList_PreviewMouseLeftButtonDown" PreviewMouseMove="CategoryList_PreviewMouseMove"
|
||||||
|
ContextMenuOpening="CategoryList_ContextMenuOpening"
|
||||||
|
AllowDrop="True" DragOver="Nav_DragOver" DragLeave="Nav_DragLeave" Drop="Nav_Drop">
|
||||||
|
<ListBox.ContextMenu>
|
||||||
|
<ContextMenu x:Name="CategoryMenu"/>
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
</ListBox>
|
||||||
|
<TextBlock x:Name="NoCategoriesText" Margin="24,8,24,0" TextWrapping="Wrap" FontSize="12"
|
||||||
|
Foreground="{DynamicResource TextTertiaryBrush}" IsHitTestVisible="False"
|
||||||
|
Text="Noch keine Kategorien. Lege unten deine erste an – z. B. „Spiele“, „Arbeit“ oder „Tools“."/>
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- ===== Inhalt ===== -->
|
||||||
|
<Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource LayerBrush}"
|
||||||
|
BorderBrush="{DynamicResource LayerStrokeBrush}" BorderThickness="1,1,0,0" CornerRadius="8,0,0,0">
|
||||||
|
<Grid>
|
||||||
|
<DockPanel x:Name="BrowseView">
|
||||||
|
<!-- Kopfbereich -->
|
||||||
|
<Grid DockPanel.Dock="Top" Margin="36,26,30,0">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="HeaderText" FontFamily="{DynamicResource DisplayFont}" FontSize="28" FontWeight="SemiBold"
|
||||||
|
TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock x:Name="SubText" Foreground="{DynamicResource TextSecondaryBrush}" Margin="0,2,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<Grid Grid.Column="1" Width="280" VerticalAlignment="Center" Margin="24,0,0,0">
|
||||||
|
<TextBox x:Name="SearchBox" Padding="36,6,30,6" TextChanged="SearchBox_TextChanged"
|
||||||
|
ToolTip="Sucht in allen Kategorien und auf dem Desktop (Strg+F)"/>
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14" IsHitTestVisible="False"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}" VerticalAlignment="Center" Margin="12,0,0,0"/>
|
||||||
|
<TextBlock x:Name="SearchPlaceholder" Text="Suchen" IsHitTestVisible="False" VerticalAlignment="Center"
|
||||||
|
Margin="37,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}"/>
|
||||||
|
<Button x:Name="ClearSearchBtn" Style="{StaticResource SubtleButton}" HorizontalAlignment="Right" Margin="0,0,3,0"
|
||||||
|
Padding="6,4" MinHeight="0" Visibility="Collapsed" Click="ClearSearch_Click" ToolTip="Suche leeren (Esc)">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="10"/>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center" Margin="6,0,0,0">
|
||||||
|
<Button x:Name="UndoButton" Style="{StaticResource SubtleButton}" Padding="9" Click="Undo_Click" IsEnabled="False"
|
||||||
|
ToolTip="Rückgängig (Strg+Z)">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"/>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="ViewButton" Style="{StaticResource SubtleButton}" Padding="9" Click="ViewButton_Click" ToolTip="Ansicht">
|
||||||
|
<TextBlock x:Name="ViewGlyph" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"/>
|
||||||
|
<Button.ContextMenu>
|
||||||
|
<ContextMenu x:Name="ViewMenu" Placement="Bottom"/>
|
||||||
|
</Button.ContextMenu>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource SubtleButton}" Padding="9" Click="Refresh_Click" ToolTip="Aktualisieren (F5)">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Befehlsleiste (nur bei Kategorien) -->
|
||||||
|
<StackPanel x:Name="CategoryCommands" DockPanel.Dock="Top" Orientation="Horizontal" Margin="30,14,30,6">
|
||||||
|
<Button Style="{StaticResource SubtleButton}" Click="EditCategory_Click" Margin="0,0,4,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Bearbeiten" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button x:Name="FenceToggleButton" Style="{StaticResource SubtleButton}" Click="FenceToggle_Click" Margin="0,0,4,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock x:Name="FenceToggleText" Text="Als Box auf den Desktop" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource SubtleButton}" Click="OpenFolder_Click" Margin="0,0,4,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Im Explorer öffnen" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Style="{StaticResource SubtleButton}" Click="DeleteCategory_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="16" Margin="0,0,10,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Kategorie löschen" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Hinweis (InfoBar) -->
|
||||||
|
<Border x:Name="InfoBar" DockPanel.Dock="Top" Margin="36,20,36,8" Padding="16,12" CornerRadius="6"
|
||||||
|
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" VerticalAlignment="Top" Margin="0,1,14,0"/>
|
||||||
|
<TextBlock TextWrapping="Wrap" LineHeight="20">
|
||||||
|
<Run FontWeight="SemiBold" Text="So räumst du auf:"/>
|
||||||
|
<Run Text="Ziehe Icons auf eine Kategorie links – sie werden dort gesichert. Icons mit"/>
|
||||||
|
<Run Foreground="{DynamicResource SuccessBrush}" FontWeight="SemiBold" Text="✓"/>
|
||||||
|
<Run Text="sind gesichert und können vom Desktop gelöscht werden (Rechtsklick → Vom Desktop löschen)."/>
|
||||||
|
</TextBlock>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Spaltenköpfe der Listenansicht -->
|
||||||
|
<Grid x:Name="ListHeader" DockPanel.Dock="Top" Margin="42,10,48,4" Visibility="Collapsed">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="200"/>
|
||||||
|
<ColumnDefinition Width="160"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<TextBlock Text="Name" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}" Margin="38,0,0,0"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="Ort" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="Typ" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Kacheln / Liste -->
|
||||||
|
<Grid>
|
||||||
|
<ListBox x:Name="IconList" Padding="28,4,28,24" SelectionMode="Extended"
|
||||||
|
ItemContainerStyle="{StaticResource TileItem}" ItemTemplate="{StaticResource TileTemplate}"
|
||||||
|
ItemsPanel="{StaticResource WrapPanelTemplate}"
|
||||||
|
AllowDrop="True" Drop="IconList_Drop" DragOver="IconList_DragOver"
|
||||||
|
PreviewMouseLeftButtonDown="IconList_PreviewMouseLeftButtonDown" PreviewMouseMove="IconList_PreviewMouseMove"
|
||||||
|
MouseDoubleClick="IconList_MouseDoubleClick" KeyDown="IconList_KeyDown"
|
||||||
|
ContextMenuOpening="IconList_ContextMenuOpening">
|
||||||
|
<ListBox.ContextMenu>
|
||||||
|
<ContextMenu x:Name="IconMenu"/>
|
||||||
|
</ListBox.ContextMenu>
|
||||||
|
</ListBox>
|
||||||
|
|
||||||
|
<!-- Leerer Zustand -->
|
||||||
|
<StackPanel x:Name="EmptyState" HorizontalAlignment="Center" VerticalAlignment="Center" IsHitTestVisible="False"
|
||||||
|
Margin="0,0,0,60" MaxWidth="360">
|
||||||
|
<TextBlock x:Name="EmptyGlyph" FontFamily="{DynamicResource IconFont}" FontSize="48" HorizontalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource TextTertiaryBrush}"/>
|
||||||
|
<TextBlock x:Name="EmptyTitle" FontSize="18" FontWeight="SemiBold" HorizontalAlignment="Center" Margin="0,18,0,6"
|
||||||
|
FontFamily="{DynamicResource DisplayFont}"/>
|
||||||
|
<TextBlock x:Name="EmptyText" TextAlignment="Center" TextWrapping="Wrap" LineHeight="20"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- ===== Einstellungen ===== -->
|
||||||
|
<ScrollViewer x:Name="SettingsView" Visibility="Collapsed" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="36,26,36,36" MaxWidth="860" HorizontalAlignment="Stretch">
|
||||||
|
<TextBlock Text="Einstellungen" FontFamily="{DynamicResource DisplayFont}" FontSize="28" FontWeight="SemiBold"/>
|
||||||
|
|
||||||
|
<TextBlock Text="Darstellung" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Right" Style="{StaticResource SegmentGroup}" VerticalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<RadioButton x:Name="DesignLight" Style="{StaticResource SegmentRadio}" GroupName="Design" Content="Hell" Checked="Design_Checked"/>
|
||||||
|
<RadioButton x:Name="DesignDark" Style="{StaticResource SegmentRadio}" GroupName="Design" Content="Dunkel" Checked="Design_Checked"/>
|
||||||
|
<RadioButton x:Name="DesignSystem" Style="{StaticResource SegmentRadio}" GroupName="Design" Content="Wie Windows" Checked="Design_Checked"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Design"/>
|
||||||
|
<TextBlock Text="Helles oder dunkles Erscheinungsbild" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<Border DockPanel.Dock="Right" Style="{StaticResource SegmentGroup}" VerticalAlignment="Center">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<RadioButton x:Name="ViewSmall" Style="{StaticResource SegmentRadio}" GroupName="View" Content="Klein" Checked="View_Checked"/>
|
||||||
|
<RadioButton x:Name="ViewMedium" Style="{StaticResource SegmentRadio}" GroupName="View" Content="Mittel" Checked="View_Checked"/>
|
||||||
|
<RadioButton x:Name="ViewLarge" Style="{StaticResource SegmentRadio}" GroupName="View" Content="Groß" Checked="View_Checked"/>
|
||||||
|
<RadioButton x:Name="ViewList" Style="{StaticResource SegmentRadio}" GroupName="View" Content="Liste" Checked="View_Checked"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Ansicht"/>
|
||||||
|
<TextBlock Text="Größe der Kacheln oder Listenansicht" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="Start & Erreichbarkeit" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox x:Name="AutostartToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="AutostartToggle_Click"/>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Mit Windows starten"/>
|
||||||
|
<TextBlock Text="Startet unauffällig im Infobereich – Desktop-Boxen und Auto-Sortierung sind dann sofort aktiv" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox x:Name="TrayToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="TrayToggle_Click"/>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Symbol im Infobereich"/>
|
||||||
|
<TextBlock Text="Unten rechts neben der Uhr – Klick öffnet die App, Rechtsklick zeigt deine Kategorien" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox x:Name="CloseToTrayToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="CloseToTrayToggle_Click"/>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Beim Schließen weiterlaufen"/>
|
||||||
|
<TextBlock Text="Das X versteckt das Fenster nur – beenden über Rechtsklick auf das Infobereich-Symbol" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBox x:Name="HotkeyBox" Width="200" IsReadOnly="True" Margin="0,0,16,0" IsReadOnlyCaretVisible="False"
|
||||||
|
PreviewKeyDown="HotkeyBox_PreviewKeyDown" ToolTip="Hier klicken und neue Tastenkombination drücken"/>
|
||||||
|
<CheckBox x:Name="HotkeyToggle" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="HotkeyToggle_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Tastenkürzel"/>
|
||||||
|
<TextBlock x:Name="HotkeyHint" Text="Öffnet die App von überall – ins Feld klicken und neue Kombination drücken" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="Automatisch sortieren" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox x:Name="AutoSortToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="AutoSortToggle_Click"/>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Neue Desktop-Icons automatisch einsortieren"/>
|
||||||
|
<TextBlock Text="Sobald z. B. ein Installer eine Verknüpfung auf den Desktop legt, wird sie nach deinen Regeln gesichert" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox x:Name="AutoRemoveToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="AutoRemoveToggle_Click"/>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Danach vom Desktop entfernen"/>
|
||||||
|
<TextBlock Text="Das Original verschwindet vom Desktop (rückgängig machbar mit Strg+Z)" FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Regeln" Margin="0,0,0,4"/>
|
||||||
|
<TextBlock Text="Die erste passende Regel gewinnt. Groß-/Kleinschreibung ist egal." FontSize="12"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}" Margin="0,0,0,14"/>
|
||||||
|
<ItemsControl x:Name="RulesList" ItemTemplate="{StaticResource RuleTemplate}"/>
|
||||||
|
<TextBlock x:Name="NoRulesText" Text="Noch keine Regeln." FontSize="12" Foreground="{DynamicResource TextTertiaryBrush}" Margin="0,0,0,12"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Margin="0,4,0,0">
|
||||||
|
<Button Click="AddRule_Click" Margin="0,0,8,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="12" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Regel hinzufügen"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Click="ApplyRulesNow_Click" Content="Jetzt auf den Desktop anwenden"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="Desktop-Boxen" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<CheckBox x:Name="FencesToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="FencesToggle_Click"/>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Boxen auf dem Desktop anzeigen"/>
|
||||||
|
<TextBlock Text="Kategorien als verschiebbare Kästen direkt auf dem Desktop. Welche Kategorie eine Box bekommt, stellst du bei der Kategorie ein (Bearbeiten → „Als Box auf dem Desktop“). Doppelklick auf den Titel klappt eine Box ein." FontSize="12"
|
||||||
|
TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="Sicherung" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button Click="Export_Click" Margin="0,0,8,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Sichern…"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Click="Import_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Wiederherstellen…"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="Kategorien sichern & wiederherstellen"/>
|
||||||
|
<TextBlock x:Name="BackupHint" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Text="Info" Style="{StaticResource SectionTitle}"/>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<DockPanel>
|
||||||
|
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<Button Content="Lizenz" Click="License_Click" Margin="0,0,8,0"/>
|
||||||
|
<Button Content="Speicherort öffnen" Click="OpenStorage_Click" Margin="0,0,8,0"/>
|
||||||
|
<Button Click="Quit_Click">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="Beenden"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<Image DockPanel.Dock="Left" Source="app.ico" Width="24" Height="24" Margin="0,0,16,0" VerticalAlignment="Center"/>
|
||||||
|
<StackPanel VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="AboutTitle" Text="DeskCore"/>
|
||||||
|
<TextBlock x:Name="AboutCopyright" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<TextBlock x:Name="StoragePathText" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
<Border Style="{StaticResource Card}">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Tastenkürzel in der App" Margin="0,0,0,8"/>
|
||||||
|
<TextBlock FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}" LineHeight="20" TextWrapping="Wrap"
|
||||||
|
Text="Strg+F Suchen · Esc Suche leeren · Strg+A Alles markieren · Enter Öffnen · F2 Umbenennen · Entf Löschen · Strg+Z Rückgängig · Strg+N Neue Kategorie · Alt+↑/↓ Kategorie verschieben · F5 Aktualisieren · Strg+1…9 Kategorie wechseln"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Rückmeldung (Toast) -->
|
||||||
|
<Border x:Name="Toast" VerticalAlignment="Bottom" HorizontalAlignment="Center" Margin="0,0,0,24"
|
||||||
|
Padding="16,11" CornerRadius="8" Opacity="0" IsHitTestVisible="False"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<Border.RenderTransform>
|
||||||
|
<TranslateTransform x:Name="ToastShift" Y="12"/>
|
||||||
|
</Border.RenderTransform>
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border x:Name="ToastBadge" Width="20" Height="20" CornerRadius="10" Background="{DynamicResource SuccessBrush}" Margin="0,0,12,0">
|
||||||
|
<TextBlock x:Name="ToastGlyph" Text="" FontFamily="{DynamicResource IconFont}" FontSize="10" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="ToastText" VerticalAlignment="Center" MaxWidth="560" TextTrimming="CharacterEllipsis"/>
|
||||||
|
<Button x:Name="ToastUndo" Content="Rückgängig" Style="{StaticResource SubtleButton}" Margin="12,-4,-6,-4" Padding="10,4"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" Click="Undo_Click" Visibility="Collapsed"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Abdunkeln bei offenem Dialog -->
|
||||||
|
<Border x:Name="DimOverlay" Grid.RowSpan="2" Grid.ColumnSpan="2" Background="{DynamicResource OverlayBrush}" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
+1434
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Übernimmt Daten früherer Namen dieser App ("DesktopSortierer", "DeskHub") einmalig nach "DeskCore".
|
||||||
|
/// Darf Storage/AppSettings nicht anfassen – läuft, bevor deren Pfade festgelegt werden.
|
||||||
|
/// </summary>
|
||||||
|
public static class Migration
|
||||||
|
{
|
||||||
|
private const string NewName = "DeskCore";
|
||||||
|
|
||||||
|
/// <summary>Frühere Namen, neuester zuerst.</summary>
|
||||||
|
private static readonly string[] LegacyNames = ["DeskHub", "DesktopSortierer"];
|
||||||
|
|
||||||
|
private static readonly Environment.SpecialFolder[] Folders =
|
||||||
|
[
|
||||||
|
Environment.SpecialFolder.UserProfile, // Kategorien
|
||||||
|
Environment.SpecialFolder.ApplicationData, // Einstellungen
|
||||||
|
Environment.SpecialFolder.LocalApplicationData, // Zwischenablage für Rückgängig
|
||||||
|
];
|
||||||
|
|
||||||
|
public static void Run()
|
||||||
|
{
|
||||||
|
if (FindLegacy() is { } legacy)
|
||||||
|
{
|
||||||
|
foreach (var folder in Folders)
|
||||||
|
{
|
||||||
|
var baseDir = Environment.GetFolderPath(folder);
|
||||||
|
var oldPath = Path.Combine(baseDir, legacy);
|
||||||
|
var newPath = Path.Combine(baseDir, NewName);
|
||||||
|
if (!Directory.Exists(oldPath) || Directory.Exists(newPath)) continue;
|
||||||
|
try { Directory.Move(oldPath, newPath); } // gleiches Laufwerk → sofort, auch bei großen Ordnern
|
||||||
|
catch { /* z. B. Ordner gerade im Explorer geöffnet → Pick() nimmt den alten weiter */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Autostart.Migrate();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ältester noch vorhandener Datenbestand dieser App. "DeskHub" heißt auch ein anderes Programm –
|
||||||
|
/// deshalb zählt ein Ordner nur, wenn dort unsere Einstellungsdatei liegt.
|
||||||
|
/// </summary>
|
||||||
|
private static string? FindLegacy()
|
||||||
|
{
|
||||||
|
var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
if (Directory.Exists(Path.Combine(appData, NewName))) return null; // schon umgezogen
|
||||||
|
foreach (var name in LegacyNames)
|
||||||
|
if (IsOurs(Path.Combine(appData, name))) return name;
|
||||||
|
// Ganz alte Versionen hatten evtl. noch keine Einstellungsdatei – nur die Kategorien
|
||||||
|
var profile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
|
return Directory.Exists(Path.Combine(profile, "DesktopSortierer")) ? "DesktopSortierer" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsOurs(string settingsDir)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = Path.Combine(settingsDir, "settings.json");
|
||||||
|
if (!File.Exists(file)) return false;
|
||||||
|
var json = File.ReadAllText(file);
|
||||||
|
return json.Contains("\"CategoryOrder\"") && json.Contains("\"FencesEnabled\"");
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Neuer Ordner – außer der Umzug hat nicht geklappt und nur der alte existiert.</summary>
|
||||||
|
public static string Pick(Environment.SpecialFolder folder, params string[] sub)
|
||||||
|
{
|
||||||
|
var baseDir = Environment.GetFolderPath(folder);
|
||||||
|
if (!Directory.Exists(Path.Combine(baseDir, NewName)) && FindLegacy() is { } legacy
|
||||||
|
&& Directory.Exists(Path.Combine(baseDir, legacy)))
|
||||||
|
return Path.Combine([baseDir, legacy, .. sub]);
|
||||||
|
return Path.Combine([baseDir, NewName, .. sub]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows.Media;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
public class CategoryItem
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public bool IsDesktop { get; init; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public string Folder => IsDesktop ? Storage.UserDesktop : Storage.CategoryPath(Name);
|
||||||
|
|
||||||
|
public CategoryMeta? Meta => IsDesktop ? null : AppSettings.Current.Categories.GetValueOrDefault(Name);
|
||||||
|
public string Glyph => IsDesktop ? "" : Meta?.Glyph ?? "";
|
||||||
|
public Brush? GlyphBrush => CategoryColors.Brush(Meta?.Color);
|
||||||
|
public bool HasFence => Meta?.Fence == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CategoryColors
|
||||||
|
{
|
||||||
|
public static readonly (string Name, string Hex)[] All =
|
||||||
|
[
|
||||||
|
("Blau", "#3B82F6"), ("Türkis", "#12A594"), ("Grün", "#22A06B"), ("Gelb", "#E2A600"),
|
||||||
|
("Orange", "#F07A1A"), ("Rot", "#E5484D"), ("Pink", "#D6409F"), ("Lila", "#8E4EC6"), ("Grau", "#8B8D98"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public static Brush? Brush(string? hex)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(hex)) return null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var b = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex));
|
||||||
|
b.Freeze();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
catch { return null; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CategoryGlyphs
|
||||||
|
{
|
||||||
|
public static readonly (string Glyph, string Name)[] All =
|
||||||
|
[
|
||||||
|
("", "Ordner"), ("", "Spiele"), ("", "Arbeit"), ("", "Code"),
|
||||||
|
("", "Musik"), ("", "Video"), ("", "Bilder"), ("", "Internet"),
|
||||||
|
("", "Mail"), ("", "Shopping"), ("", "System"), ("", "Werkzeuge"),
|
||||||
|
("", "Dokumente"), ("", "Favoriten"), ("", "Herz"), ("", "Cloud"),
|
||||||
|
("", "Downloads"), ("", "Personen"), ("", "Bibliothek"), ("", "Rechner"),
|
||||||
|
("", "Ideen"), ("", "Zuhause"), ("", "Lernen"), ("", "Erweiterungen"),
|
||||||
|
("", "Computer"), ("", "Sicherheit"), ("", "Ebenen"),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public class IconItem : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public required string Path { get; init; }
|
||||||
|
public string? Category { get; init; }
|
||||||
|
|
||||||
|
private ImageSource? _icon;
|
||||||
|
public ImageSource? Icon
|
||||||
|
{
|
||||||
|
get => _icon;
|
||||||
|
set { _icon = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Icon))); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Nur für Desktop-Icons: Kategorie, in der bereits eine Kopie liegt.</summary>
|
||||||
|
public string? SortedInto { get; init; }
|
||||||
|
public bool IsSorted => SortedInto is not null;
|
||||||
|
|
||||||
|
public string Name => Storage.DisplayName(Path);
|
||||||
|
|
||||||
|
public string TypeText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (Directory.Exists(Path)) return "Ordner";
|
||||||
|
var ext = System.IO.Path.GetExtension(Path).ToLowerInvariant();
|
||||||
|
return ext switch
|
||||||
|
{
|
||||||
|
".lnk" => "Verknüpfung",
|
||||||
|
".url" => "Internetverknüpfung",
|
||||||
|
".exe" => "Programm",
|
||||||
|
"" => "Datei",
|
||||||
|
_ => ext.TrimStart('.').ToUpperInvariant() + "-Datei",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string LocationText => SortedInto is not null ? $"✓ gesichert in {SortedInto}" : Category ?? "";
|
||||||
|
|
||||||
|
public string ToolTip =>
|
||||||
|
SortedInto is not null ? $"{Name}\n✓ Bereits gesichert in „{SortedInto}“ – kann vom Desktop gelöscht werden." :
|
||||||
|
Category is not null ? $"{Name}\nOrt: {Category}" : $"{Name}\n{TypeText}";
|
||||||
|
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# DeskCore
|
||||||
|
|
||||||
|
**Endlich ein aufgeräumter Desktop.**
|
||||||
|
|
||||||
|
Mit DeskCore sortierst du deine Desktop-Icons in eigene Gruppen, zum Beispiel „Spiele“, „Arbeit“ oder „Tools“.
|
||||||
|
Danach kannst du die Icons vom Desktop löschen. In DeskCore bleiben sie erhalten und lassen sich mit einem Doppelklick starten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was du brauchst
|
||||||
|
|
||||||
|
- Windows 10 oder Windows 11
|
||||||
|
- Fehlt beim ersten Start etwas („.NET“), zeigt Windows automatisch einen Hinweis mit einem Download-Link. Einfach herunterladen, installieren und DeskCore nochmal starten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installieren
|
||||||
|
|
||||||
|
1. Doppelklick auf **`DeskCore-Setup.exe`**
|
||||||
|
2. **Weiter** klicken
|
||||||
|
3. Die Lizenz lesen, den Haken bei **„Ich akzeptiere …“** setzen, dann **Weiter**
|
||||||
|
4. Bei Bedarf noch auswählen:
|
||||||
|
- Verknüpfung auf dem Desktop (empfohlen)
|
||||||
|
- Mit Windows starten
|
||||||
|
5. **Installieren** klicken, und fertig.
|
||||||
|
|
||||||
|
Du brauchst dafür **keine Administratorrechte**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## So räumst du deinen Desktop auf
|
||||||
|
|
||||||
|
### 1. Gruppe anlegen
|
||||||
|
Unten links auf **„+ Neue Kategorie“** klicken und einen Namen eingeben, z. B. **Spiele**.
|
||||||
|
Auf Wunsch suchst du dir noch ein Symbol und eine Farbe aus.
|
||||||
|
|
||||||
|
### 2. Icons hineinziehen
|
||||||
|
Zieh ein Icon **vom Desktop** mit der Maus auf deine Kategorie links.
|
||||||
|
Es wird jetzt in DeskCore **gesichert**. Auf dem Desktop bleibt es vorerst liegen.
|
||||||
|
|
||||||
|
> Tipp: Du kannst auch mehrere Icons auf einmal markieren und ziehen.
|
||||||
|
|
||||||
|
### 3. Icons vom Desktop löschen
|
||||||
|
Klick links auf **„Desktop“**. Icons mit einem **grünen Haken ✓** sind schon gesichert.
|
||||||
|
Die kannst du ohne Sorge vom Desktop löschen:
|
||||||
|
**Rechtsklick → „Vom Desktop löschen“** oder ganz normal auf dem Desktop löschen.
|
||||||
|
|
||||||
|
**Fertig!** Dein Desktop ist leer, und deine Programme findest du jetzt in DeskCore.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Programme starten
|
||||||
|
|
||||||
|
- Kategorie links anklicken
|
||||||
|
- **Doppelklick** auf das Icon
|
||||||
|
|
||||||
|
Oder schneller: **Suchfeld** oben benutzen, den Namen eintippen und Doppelklick.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nützliche Extras
|
||||||
|
|
||||||
|
### DeskCore schnell öffnen
|
||||||
|
- **Win + Umschalt + D** drücken, von überall aus
|
||||||
|
- oder unten rechts neben der Uhr auf das **DeskCore-Symbol** klicken
|
||||||
|
|
||||||
|
Wenn du das Fenster mit dem **X** schließt, läuft DeskCore unten rechts weiter.
|
||||||
|
Richtig beenden: **Rechtsklick auf das Symbol neben der Uhr → Beenden**
|
||||||
|
|
||||||
|
### Hell oder Dunkel
|
||||||
|
Oben rechts auf die **Sonne bzw. den Mond** klicken und auswählen.
|
||||||
|
|
||||||
|
### Boxen auf dem Desktop
|
||||||
|
Eine Kategorie kann als kleine **Box direkt auf dem Desktop** erscheinen:
|
||||||
|
Kategorie anklicken und oben auf **„Als Box auf den Desktop“** klicken.
|
||||||
|
Die Box kannst du verschieben und in der Größe ändern. Ein Doppelklick auf ihren Titel klappt sie ein und aus.
|
||||||
|
|
||||||
|
### Automatisch sortieren
|
||||||
|
DeskCore kann neue Icons selbst einsortieren, z. B. alles mit „Steam“ im Namen → **Spiele**.
|
||||||
|
Einstellen kannst du das unter **Einstellungen → Automatisch sortieren → Regel hinzufügen**.
|
||||||
|
|
||||||
|
### Reihenfolge der Kategorien
|
||||||
|
Kategorie links mit der Maus **nach oben oder unten ziehen**.
|
||||||
|
Oder per Rechtsklick → **Nach oben / Nach unten**.
|
||||||
|
|
||||||
|
### Ansicht
|
||||||
|
Oben rechts gibt es den **Ansicht-Knopf**: kleine, mittlere oder große Symbole oder eine Liste.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Hoppla, falsch geklickt?
|
||||||
|
|
||||||
|
**Strg + Z** macht den letzten Schritt rückgängig.
|
||||||
|
Unten im Fenster erscheint nach vielen Aktionen auch kurz ein Knopf **„Rückgängig“**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Häufige Fragen
|
||||||
|
|
||||||
|
**Wo liegen meine Icons eigentlich?**
|
||||||
|
Im Ordner `C:\Benutzer\DEIN-NAME\DeskCore`. Jede Kategorie ist dort ein ganz normaler Ordner.
|
||||||
|
Auch wenn DeskCore mal nicht läuft, ist also nichts weg.
|
||||||
|
|
||||||
|
**Ich habe etwas gelöscht. Ist es für immer weg?**
|
||||||
|
Nein. Erst geht **Strg + Z**, und danach liegt es noch im **Papierkorb** von Windows.
|
||||||
|
|
||||||
|
**Ein Icon lässt sich nicht verschieben („Keine Berechtigung“).**
|
||||||
|
Manche Icons gehören allen Benutzern des PCs. Dafür DeskCore einmal mit
|
||||||
|
**Rechtsklick → „Als Administrator ausführen“** starten.
|
||||||
|
|
||||||
|
**Ich habe einen großen Ordner hineingezogen. Ist der jetzt doppelt da?**
|
||||||
|
DeskCore fragt bei großen Ordnern nach. Wählst du **„Verschieben“**, liegt er nur einmal da und belegt keinen doppelten Platz.
|
||||||
|
|
||||||
|
**Ich bekomme einen neuen PC. Was jetzt?**
|
||||||
|
**Einstellungen → Sicherung → „Sichern …“** speichert alles in eine ZIP-Datei.
|
||||||
|
Auf dem neuen PC installierst du DeskCore und wählst **„Wiederherstellen …“**.
|
||||||
|
|
||||||
|
**Das Tastenkürzel funktioniert nicht.**
|
||||||
|
Ein anderes Programm benutzt es vielleicht schon. Unter **Einstellungen → Tastenkürzel** ins Feld klicken und eine andere Kombination drücken.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deinstallieren
|
||||||
|
|
||||||
|
**Windows-Einstellungen → Apps → DeskCore → Deinstallieren**
|
||||||
|
|
||||||
|
Deine Kategorien bleiben dabei **standardmäßig erhalten**.
|
||||||
|
Willst du sie auch entfernen, setz beim Deinstallieren den Haken. Dann landen sie im Papierkorb.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lizenz
|
||||||
|
|
||||||
|
DeskCore ist nur für den **privaten, nicht-kommerziellen** Gebrauch.
|
||||||
|
Weitergeben, Verändern und Verkaufen ist nicht erlaubt.
|
||||||
|
Den vollständigen Text findest du in der Datei `LICENSE.txt` oder in DeskCore unter **Einstellungen → Info → Lizenz**.
|
||||||
|
|
||||||
|
© M_Viper · [m-viper.de](https://m-viper.de)
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
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; }
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SortRule
|
||||||
|
{
|
||||||
|
public string Contains { get; set; } = "";
|
||||||
|
public string Category { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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<string> CategoryOrder { 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 ??= [];
|
||||||
|
|
||||||
|
// Ü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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveCategory(string name)
|
||||||
|
{
|
||||||
|
Categories.Remove(name);
|
||||||
|
CategoryOrder.RemoveAll(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
Rules.RemoveAll(r => string.Equals(r.Category, name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<Application x:Class="DeskCoreSetup.App"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<!-- [0] wird je nach Hell/Dunkel ersetzt -->
|
||||||
|
<ResourceDictionary Source="Themes/Colors.Light.xaml"/>
|
||||||
|
<ResourceDictionary Source="Themes/Controls.xaml"/>
|
||||||
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#0067C0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#1975C5"/>
|
||||||
|
<SolidColorBrush x:Key="OnAccentBrush" Color="White"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleBrush" Color="#220067C0"/>
|
||||||
|
<SolidColorBrush x:Key="AccentSubtleStrokeBrush" Color="#550067C0"/>
|
||||||
|
</ResourceDictionary>
|
||||||
|
</Application.Resources>
|
||||||
|
</Application>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace DeskCoreSetup;
|
||||||
|
|
||||||
|
public partial class App : Application
|
||||||
|
{
|
||||||
|
protected override void OnStartup(StartupEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnStartup(e);
|
||||||
|
bool uninstall = e.Args.Any(a => a.Equals("--uninstall", StringComparison.OrdinalIgnoreCase));
|
||||||
|
bool silent = e.Args.Any(a => a.Equals("/S", StringComparison.OrdinalIgnoreCase));
|
||||||
|
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?[3..];
|
||||||
|
|
||||||
|
// Stille Installation/Deinstallation für Skripte: DeskCore-Setup.exe /S [/D=Ordner] bzw. --uninstall /S
|
||||||
|
if (silent)
|
||||||
|
{
|
||||||
|
var progress = new Progress<(double, string)>(_ => { });
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (uninstall) Installer.Uninstall(removeData: false, progress);
|
||||||
|
else Installer.Install(dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir,
|
||||||
|
desktopLink: true, autostart: Installer.AutostartEnabled, progress);
|
||||||
|
Shutdown(0);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// ohne Oberfläche: Fehler für Skripte nachvollziehbar ablegen
|
||||||
|
try { System.IO.File.WriteAllText(System.IO.Path.Combine(System.IO.Path.GetTempPath(), "DeskCore-Setup.log"), $"[{DateTime.Now}] {ex}"); } catch { }
|
||||||
|
Shutdown(1);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyTheme();
|
||||||
|
new MainWindow(uninstall).Show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Hell/Dunkel und Akzentfarbe wie in Windows eingestellt.</summary>
|
||||||
|
private void ApplyTheme()
|
||||||
|
{
|
||||||
|
bool light = ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0;
|
||||||
|
Resources.MergedDictionaries[0] = new ResourceDictionary
|
||||||
|
{
|
||||||
|
Source = new Uri($"pack://application:,,,/Themes/Colors.{(light ? "Light" : "Dark")}.xaml"),
|
||||||
|
};
|
||||||
|
int abgr = ReadDword(@"Software\Microsoft\Windows\DWM", "AccentColor") is var v and not -1 ? v : unchecked((int)0xFFD77800);
|
||||||
|
var accent = Color.FromRgb((byte)abgr, (byte)(abgr >> 8), (byte)(abgr >> 16));
|
||||||
|
var a = light ? Mix(accent, Colors.Black, 0.12) : Mix(accent, Colors.White, 0.45);
|
||||||
|
Resources["AccentBrush"] = new SolidColorBrush(a);
|
||||||
|
Resources["AccentHoverBrush"] = new SolidColorBrush(light ? Mix(a, Colors.White, 0.1) : Mix(a, Colors.Black, 0.1));
|
||||||
|
Resources["OnAccentBrush"] = new SolidColorBrush(light ? Colors.White : Colors.Black);
|
||||||
|
Resources["AccentSubtleBrush"] = new SolidColorBrush(Color.FromArgb(light ? (byte)0x22 : (byte)0x30, a.R, a.G, a.B));
|
||||||
|
Resources["AccentSubtleStrokeBrush"] = new SolidColorBrush(Color.FromArgb(light ? (byte)0x55 : (byte)0x60, a.R, a.G, a.B));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReadDword(string key, string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(key);
|
||||||
|
return k?.GetValue(name) is int i ? i : -1;
|
||||||
|
}
|
||||||
|
catch { return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color Mix(Color c, Color with, double t) => Color.FromRgb(
|
||||||
|
(byte)(c.R + (with.R - c.R) * t), (byte)(c.G + (with.G - c.G) * t), (byte)(c.B + (with.B - c.B) * t));
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
|
<UseWPF>true</UseWPF>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AssemblyName>DeskCore-Setup</AssemblyName>
|
||||||
|
<RootNamespace>DeskCoreSetup</RootNamespace>
|
||||||
|
<ApplicationIcon>..\app.ico</ApplicationIcon>
|
||||||
|
<Product>DeskCore Setup</Product>
|
||||||
|
<AssemblyTitle>DeskCore Setup</AssemblyTitle>
|
||||||
|
<Description>Installiert DeskCore</Description>
|
||||||
|
<Company>M_Viper</Company>
|
||||||
|
<Authors>M_Viper</Authors>
|
||||||
|
<Copyright>Copyright (c) M_Viper. Alle Rechte vorbehalten.</Copyright>
|
||||||
|
<NeutralLanguage>de-DE</NeutralLanguage>
|
||||||
|
<!-- fertig veröffentlichte DeskCore.exe, die eingepackt wird (build.ps1 legt sie hier ab) -->
|
||||||
|
<AppPayload Condition="'$(AppPayload)' == ''">..\artifacts\app\DeskCore.exe</AppPayload>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<!-- gleiches Design wie die App -->
|
||||||
|
<Page Include="..\Themes\Colors.Light.xaml" Link="Themes\Colors.Light.xaml" />
|
||||||
|
<Page Include="..\Themes\Colors.Dark.xaml" Link="Themes\Colors.Dark.xaml" />
|
||||||
|
<Page Include="..\Themes\Controls.xaml" Link="Themes\Controls.xaml" />
|
||||||
|
<Resource Include="..\app.ico" Link="app.ico" />
|
||||||
|
<Resource Include="..\app.png" Link="app.png" />
|
||||||
|
<!-- Nutzlast -->
|
||||||
|
<EmbeddedResource Include="$(AppPayload)" LogicalName="payload.DeskCore.exe" />
|
||||||
|
<EmbeddedResource Include="..\LICENSE.txt" LogicalName="payload.LICENSE.txt" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace DeskCoreSetup;
|
||||||
|
|
||||||
|
/// <summary>Installation/Deinstallation – nur für den aktuellen Benutzer, ohne Administratorrechte.</summary>
|
||||||
|
public static class Installer
|
||||||
|
{
|
||||||
|
public const string AppName = "DeskCore";
|
||||||
|
public const string Publisher = "M_Viper";
|
||||||
|
public const string ExeName = "DeskCore.exe";
|
||||||
|
public const string UninstallerName = "DeskCore-Uninstall.exe";
|
||||||
|
private const string UninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\DeskCore";
|
||||||
|
private const string RunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||||
|
|
||||||
|
public static string Version
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var v = typeof(Installer).Assembly.GetName().Version!;
|
||||||
|
return $"{v.Major}.{v.Minor}.{v.Build}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string DefaultDir =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
|
||||||
|
|
||||||
|
private static string StartMenuLink =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName + ".lnk");
|
||||||
|
|
||||||
|
private static string DesktopLink =>
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
|
||||||
|
|
||||||
|
/// <summary>Ordner einer vorhandenen Installation (oder null).</summary>
|
||||||
|
public static string? InstalledDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
||||||
|
return k?.GetValue("InstallLocation") as string is { } d && File.Exists(Path.Combine(d, ExeName)) ? d : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? InstalledVersion
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(UninstallKey);
|
||||||
|
return k?.GetValue("DisplayVersion") as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool AutostartEnabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(RunKey);
|
||||||
|
return k?.GetValue(AppName) is string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool DesktopLinkExists => File.Exists(DesktopLink);
|
||||||
|
|
||||||
|
public static string LicenseText => ReadResourceText("payload.LICENSE.txt");
|
||||||
|
|
||||||
|
// ---------- Installieren ----------
|
||||||
|
|
||||||
|
public static void Install(string dir, bool desktopLink, bool autostart, IProgress<(double, string)> progress)
|
||||||
|
{
|
||||||
|
progress.Report((0.05, "DeskCore wird beendet …"));
|
||||||
|
CloseRunningApp();
|
||||||
|
RemoveLegacyInstall();
|
||||||
|
|
||||||
|
progress.Report((0.2, "Dateien werden kopiert …"));
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var exe = Path.Combine(dir, ExeName);
|
||||||
|
WriteResource("payload.DeskCore.exe", exe);
|
||||||
|
WriteResource("payload.LICENSE.txt", Path.Combine(dir, "LICENSE.txt"));
|
||||||
|
|
||||||
|
// Setup selbst als Deinstallationsprogramm ablegen
|
||||||
|
var self = Environment.ProcessPath!;
|
||||||
|
var uninstaller = Path.Combine(dir, UninstallerName);
|
||||||
|
if (!string.Equals(Path.GetFullPath(self), Path.GetFullPath(uninstaller), StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
MoveAside(uninstaller);
|
||||||
|
File.Copy(self, uninstaller, overwrite: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((0.55, "Verknüpfungen werden angelegt …"));
|
||||||
|
CreateShortcut(StartMenuLink, exe);
|
||||||
|
if (desktopLink) CreateShortcut(DesktopLink, exe);
|
||||||
|
|
||||||
|
progress.Report((0.75, "Windows wird eingerichtet …"));
|
||||||
|
using (var run = Registry.CurrentUser.CreateSubKey(RunKey))
|
||||||
|
{
|
||||||
|
if (autostart) run.SetValue(AppName, $"\"{exe}\" --minimized");
|
||||||
|
else run.DeleteValue(AppName, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var k = Registry.CurrentUser.CreateSubKey(UninstallKey))
|
||||||
|
{
|
||||||
|
long size = new[] { exe, uninstaller, Path.Combine(dir, "LICENSE.txt") }
|
||||||
|
.Where(File.Exists).Sum(f => new FileInfo(f).Length) / 1024;
|
||||||
|
k.SetValue("DisplayName", AppName);
|
||||||
|
k.SetValue("DisplayVersion", Version);
|
||||||
|
k.SetValue("Publisher", Publisher);
|
||||||
|
k.SetValue("DisplayIcon", $"{exe},0");
|
||||||
|
k.SetValue("InstallLocation", dir);
|
||||||
|
k.SetValue("UninstallString", $"\"{uninstaller}\" --uninstall");
|
||||||
|
k.SetValue("QuietUninstallString", $"\"{uninstaller}\" --uninstall /S");
|
||||||
|
k.SetValue("URLInfoAbout", "https://m-viper.de");
|
||||||
|
k.SetValue("InstallDate", DateTime.Now.ToString("yyyyMMdd"));
|
||||||
|
k.SetValue("EstimatedSize", (int)size, RegistryValueKind.DWord);
|
||||||
|
k.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||||
|
k.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((1.0, "Fertig"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Frühere Version "DeskHub" entfernen ----------
|
||||||
|
|
||||||
|
private const string LegacyName = "DeskHub";
|
||||||
|
private const string LegacyUninstallKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall\DeskHub";
|
||||||
|
|
||||||
|
/// <summary>Ordner einer alten Installation unter dem früheren Namen "DeskHub" – nur unsere eigene.</summary>
|
||||||
|
public static string? LegacyInstallDir
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(LegacyUninstallKey);
|
||||||
|
// "DeskHub" heißt auch ein anderes Programm → nur mit unserem Herausgeber anfassen
|
||||||
|
if (k?.GetValue("Publisher") as string != Publisher) return null;
|
||||||
|
return k.GetValue("InstallLocation") as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RemoveLegacyInstall()
|
||||||
|
{
|
||||||
|
var dir = LegacyInstallDir;
|
||||||
|
if (dir is null) return;
|
||||||
|
var exe = Path.Combine(dir, LegacyName + ".exe");
|
||||||
|
|
||||||
|
// nur Prozesse beenden, die wirklich aus dem alten Ordner laufen
|
||||||
|
foreach (var p in Process.GetProcessesByName(LegacyName))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (p.MainModule?.FileName is { } f && Storage_IsUnder(f, dir))
|
||||||
|
{
|
||||||
|
p.Kill(entireProcessTree: true);
|
||||||
|
p.WaitForExit(10000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteIfPointsTo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), LegacyName + ".lnk"), exe);
|
||||||
|
DeleteIfPointsTo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), LegacyName + ".lnk"), exe);
|
||||||
|
using (var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true))
|
||||||
|
{
|
||||||
|
if (run?.GetValue(LegacyName) is string cmd && cmd.Contains(exe, StringComparison.OrdinalIgnoreCase))
|
||||||
|
run.DeleteValue(LegacyName, throwOnMissingValue: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var f in new[] { exe, Path.Combine(dir, "LICENSE.txt"), Path.Combine(dir, LegacyName + "-Uninstall.exe") })
|
||||||
|
MoveAside(f);
|
||||||
|
CleanupOldFiles(dir);
|
||||||
|
try { Directory.Delete(dir); } catch { } // nur wenn leer
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(LegacyUninstallKey, throwOnMissingSubKey: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Deinstallieren ----------
|
||||||
|
|
||||||
|
public static readonly string[] DataDirs =
|
||||||
|
[
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), AppName), // Kategorien
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), AppName), // Einstellungen
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), AppName), // Rückgängig-Zwischenablage
|
||||||
|
];
|
||||||
|
|
||||||
|
public static void Uninstall(bool removeData, IProgress<(double, string)> progress)
|
||||||
|
{
|
||||||
|
// Nur im eingetragenen Installationsordner löschen – nie dort, wo das Setup zufällig liegt (z. B. Downloads)
|
||||||
|
var dir = InstalledDir;
|
||||||
|
var exe = dir is null ? "" : Path.Combine(dir, ExeName);
|
||||||
|
|
||||||
|
progress.Report((0.1, "DeskCore wird beendet …"));
|
||||||
|
CloseRunningApp();
|
||||||
|
|
||||||
|
progress.Report((0.3, "Verknüpfungen werden entfernt …"));
|
||||||
|
DeleteIfPointsTo(StartMenuLink, exe);
|
||||||
|
DeleteIfPointsTo(DesktopLink, exe);
|
||||||
|
|
||||||
|
progress.Report((0.45, "Windows-Einträge werden entfernt …"));
|
||||||
|
using (var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true))
|
||||||
|
run?.DeleteValue(AppName, throwOnMissingValue: false);
|
||||||
|
Registry.CurrentUser.DeleteSubKeyTree(UninstallKey, throwOnMissingSubKey: false);
|
||||||
|
|
||||||
|
if (removeData)
|
||||||
|
{
|
||||||
|
progress.Report((0.6, "Kategorien und Einstellungen werden in den Papierkorb gelegt …"));
|
||||||
|
foreach (var d in DataDirs.Where(Directory.Exists))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Microsoft.VisualBasic.FileIO.FileSystem.DeleteDirectory(d,
|
||||||
|
Microsoft.VisualBasic.FileIO.UIOption.OnlyErrorDialogs,
|
||||||
|
Microsoft.VisualBasic.FileIO.RecycleOption.SendToRecycleBin);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.Report((0.85, "Dateien werden entfernt …"));
|
||||||
|
if (dir is null) { progress.Report((1.0, "Fertig")); return; }
|
||||||
|
TryDelete(exe);
|
||||||
|
TryDelete(Path.Combine(dir, "LICENSE.txt"));
|
||||||
|
CleanupOldFiles(dir);
|
||||||
|
|
||||||
|
// Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd.
|
||||||
|
// rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist.
|
||||||
|
var self = Environment.ProcessPath!;
|
||||||
|
if (Storage_IsUnder(self, dir))
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo("cmd.exe",
|
||||||
|
$"/c ping 127.0.0.1 -n 3 > nul & del /f /q \"{self}\" & del /f /q \"{Path.Combine(dir, "*.old")}\" & rmdir \"{dir}\"")
|
||||||
|
{
|
||||||
|
CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
TryDelete(Path.Combine(dir, UninstallerName));
|
||||||
|
try { Directory.Delete(dir); } catch { } // nur wenn leer
|
||||||
|
}
|
||||||
|
progress.Report((1.0, "Fertig"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer ----------
|
||||||
|
|
||||||
|
/// <summary>Laufendes DeskCore beenden (das X versteckt es nur im Infobereich).</summary>
|
||||||
|
private static void CloseRunningApp()
|
||||||
|
{
|
||||||
|
foreach (var p in Process.GetProcessesByName("DeskCore"))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
p.Kill(entireProcessTree: true);
|
||||||
|
p.WaitForExit(10000);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteResource(string name, string path)
|
||||||
|
{
|
||||||
|
CleanupOldFiles(Path.GetDirectoryName(path)!);
|
||||||
|
MoveAside(path);
|
||||||
|
for (int attempt = 1; ; attempt++)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var s = typeof(Installer).Assembly.GetManifestResourceStream(name)
|
||||||
|
?? throw new InvalidOperationException($"Installationsdatei {name} fehlt.");
|
||||||
|
using var f = File.Create(path);
|
||||||
|
s.CopyTo(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (IOException) when (attempt < 10)
|
||||||
|
{
|
||||||
|
Thread.Sleep(500);
|
||||||
|
}
|
||||||
|
catch (IOException) when (attempt >= 10)
|
||||||
|
{
|
||||||
|
throw new IOException($"„{Path.GetFileName(path)}“ wird noch verwendet. Bitte DeskCore beenden (Rechtsklick auf das Symbol im Infobereich → Beenden) und erneut versuchen.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Vorhandene Datei beiseite umbenennen. Das klappt unter Windows auch dann, wenn sie noch geöffnet ist
|
||||||
|
/// (z. B. vom Explorer für das Icon oder vom Virenscanner) – überschreiben würde dagegen fehlschlagen.
|
||||||
|
/// </summary>
|
||||||
|
private static void MoveAside(string path)
|
||||||
|
{
|
||||||
|
if (!File.Exists(path)) return;
|
||||||
|
try { File.Delete(path); return; } catch { }
|
||||||
|
try { File.Move(path, $"{path}.{Guid.NewGuid():N}.old"); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Beiseitegelegte Dateien früherer Installationen wegräumen (sofern nicht mehr in Benutzung).</summary>
|
||||||
|
private static void CleanupOldFiles(string dir)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
foreach (var f in Directory.GetFiles(dir, "*.old"))
|
||||||
|
try { File.Delete(f); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadResourceText(string name)
|
||||||
|
{
|
||||||
|
using var s = typeof(Installer).Assembly.GetManifestResourceStream(name);
|
||||||
|
if (s is null) return "";
|
||||||
|
using var r = new StreamReader(s);
|
||||||
|
return r.ReadToEnd().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CreateShortcut(string link, string target)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(link)!);
|
||||||
|
dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!;
|
||||||
|
dynamic lnk = shell.CreateShortcut(link);
|
||||||
|
lnk.TargetPath = target;
|
||||||
|
lnk.WorkingDirectory = Path.GetDirectoryName(target);
|
||||||
|
lnk.IconLocation = $"{target},0";
|
||||||
|
lnk.Description = "Desktop-Icons in Kategorien sortieren";
|
||||||
|
lnk.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DeleteIfPointsTo(string link, string target)
|
||||||
|
{
|
||||||
|
if (!File.Exists(link)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!;
|
||||||
|
string t = shell.CreateShortcut(link).TargetPath;
|
||||||
|
if (!string.Equals(t, target, StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
}
|
||||||
|
catch { return; }
|
||||||
|
// Explorer/Indexdienst halten Verknüpfungen manchmal kurz fest → mehrmals versuchen
|
||||||
|
for (int i = 0; i < 10 && File.Exists(link); i++)
|
||||||
|
{
|
||||||
|
try { File.Delete(link); }
|
||||||
|
catch { Thread.Sleep(300); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryDelete(string path)
|
||||||
|
{
|
||||||
|
try { if (File.Exists(path)) File.Delete(path); } catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Storage_IsUnder(string path, string dir) =>
|
||||||
|
(Path.GetFullPath(path)).StartsWith(Path.GetFullPath(dir).TrimEnd('\\') + "\\", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public static long DataSize()
|
||||||
|
{
|
||||||
|
long sum = 0;
|
||||||
|
foreach (var d in DataDirs.Where(Directory.Exists))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var f in Directory.EnumerateFiles(d, "*", SearchOption.AllDirectories))
|
||||||
|
try { sum += new FileInfo(f).Length; } catch { }
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string FormatSize(long bytes) => bytes switch
|
||||||
|
{
|
||||||
|
< 1024 * 1024 => $"{Math.Max(1, bytes / 1024)} KB",
|
||||||
|
< 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024:0.#} MB",
|
||||||
|
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
<Window x:Class="DeskCoreSetup.MainWindow"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
Title="DeskCore Setup" Icon="app.ico" Width="720" Height="500" ResizeMode="CanMinimize"
|
||||||
|
Background="{DynamicResource BaseBrush}" Foreground="{DynamicResource TextBrush}"
|
||||||
|
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||||
|
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||||
|
WindowStartupLocation="CenterScreen">
|
||||||
|
|
||||||
|
<WindowChrome.WindowChrome>
|
||||||
|
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
|
||||||
|
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||||
|
</WindowChrome.WindowChrome>
|
||||||
|
|
||||||
|
<Window.Resources>
|
||||||
|
<Style x:Key="StepText" TargetType="TextBlock">
|
||||||
|
<Setter Property="Margin" Value="0,0,0,14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextTertiaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="PageTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="24"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Body" TargetType="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
<Setter Property="LineHeight" Value="21"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Resources>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="36"/>
|
||||||
|
<RowDefinition Height="*"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="220"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<!-- Titelleiste -->
|
||||||
|
<Grid Grid.ColumnSpan="2">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||||
|
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock x:Name="WindowTitle" Text="DeskCore Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||||
|
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||||
|
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="" Click="Cancel_Click"
|
||||||
|
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Seitenleiste mit Schritten -->
|
||||||
|
<StackPanel Grid.Row="1" Grid.RowSpan="2" Margin="28,18,20,20">
|
||||||
|
<Image Source="app.png" Width="72" Height="72" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||||
|
<TextBlock Text="DeskCore" FontFamily="{DynamicResource DisplayFont}" FontSize="22" FontWeight="SemiBold" Margin="0,14,0,0"/>
|
||||||
|
<TextBlock x:Name="VersionText" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Margin="0,2,0,28"/>
|
||||||
|
<StackPanel x:Name="Steps">
|
||||||
|
<TextBlock x:Name="Step0" Text="Willkommen" Style="{StaticResource StepText}"/>
|
||||||
|
<TextBlock x:Name="Step1" Text="Lizenz" Style="{StaticResource StepText}"/>
|
||||||
|
<TextBlock x:Name="Step2" Text="Optionen" Style="{StaticResource StepText}"/>
|
||||||
|
<TextBlock x:Name="Step3" Text="Installation" Style="{StaticResource StepText}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Inhalt -->
|
||||||
|
<Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource LayerStrokeBrush}"
|
||||||
|
BorderThickness="1,1,0,1" CornerRadius="8,0,0,8" Padding="32,28,32,20">
|
||||||
|
<Grid>
|
||||||
|
<!-- 0: Willkommen -->
|
||||||
|
<StackPanel x:Name="PageWelcome">
|
||||||
|
<TextBlock Text="Willkommen bei DeskCore" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="DeskCore sortiert deine Desktop-Icons in eigene Kategorien – mit Suche, Desktop-Boxen, automatischem Einsortieren und Rückgängig-Funktion."/>
|
||||||
|
<TextBlock Style="{StaticResource Body}" Margin="0,14,0,0"
|
||||||
|
Text="Der Assistent installiert DeskCore nur für dein Benutzerkonto. Administratorrechte sind nicht nötig."/>
|
||||||
|
<Border x:Name="UpdateHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||||
|
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||||
|
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||||
|
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 1: Lizenz -->
|
||||||
|
<DockPanel x:Name="PageLicense" Visibility="Collapsed">
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="Lizenz- und Nutzungsbedingungen" Style="{StaticResource PageTitle}"/>
|
||||||
|
<CheckBox x:Name="AcceptLicense" DockPanel.Dock="Bottom" Margin="0,14,0,0" Click="AcceptLicense_Click"
|
||||||
|
Content="Ich akzeptiere die Lizenz- und Nutzungsbedingungen"/>
|
||||||
|
<Border Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="6">
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
|
<TextBlock x:Name="LicenseText" TextWrapping="Wrap" FontSize="12.5" LineHeight="18" Margin="14,10"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<!-- 2: Optionen -->
|
||||||
|
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,4,0,8"/>
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
|
||||||
|
<TextBox x:Name="DirBox"/>
|
||||||
|
</DockPanel>
|
||||||
|
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" Margin="0,24,0,0" IsChecked="True"/>
|
||||||
|
<CheckBox x:Name="StartMenuBox" Content="Eintrag im Startmenü" Margin="0,12,0,0" IsChecked="True" IsEnabled="False"/>
|
||||||
|
<CheckBox x:Name="AutostartBox" Content="Mit Windows starten (unauffällig im Infobereich)" Margin="0,12,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- 3: Fortschritt / Fertig -->
|
||||||
|
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
|
||||||
|
<TextBlock x:Name="ProgressTitle" Text="DeskCore wird installiert …" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Text="Einen Moment bitte."/>
|
||||||
|
<Border x:Name="ProgressTrack" Height="4" CornerRadius="2" Margin="0,22,0,0" Background="{DynamicResource SubtleSelectedBrush}">
|
||||||
|
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="2" Background="{DynamicResource AccentBrush}"/>
|
||||||
|
</Border>
|
||||||
|
<StackPanel x:Name="DonePanel" Visibility="Collapsed" Margin="0,26,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal">
|
||||||
|
<Border Width="28" Height="28" CornerRadius="14" Background="{DynamicResource SuccessBrush}">
|
||||||
|
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="13" Foreground="White"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<TextBlock x:Name="DoneText" VerticalAlignment="Center" Margin="12,0,0,0" FontWeight="SemiBold"/>
|
||||||
|
</StackPanel>
|
||||||
|
<CheckBox x:Name="LaunchBox" Content="DeskCore jetzt starten" IsChecked="True" Margin="0,22,0,0"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Deinstallation -->
|
||||||
|
<StackPanel x:Name="PageUninstall" Visibility="Collapsed">
|
||||||
|
<TextBlock Text="DeskCore deinstallieren" Style="{StaticResource PageTitle}"/>
|
||||||
|
<TextBlock Style="{StaticResource Body}"
|
||||||
|
Text="DeskCore wird von deinem PC entfernt: Programm, Verknüpfungen, Autostart und der Eintrag in den Windows-Einstellungen."/>
|
||||||
|
<Border Margin="0,22,0,0" Padding="16,14" CornerRadius="6" Background="{DynamicResource ControlFillBrush}"
|
||||||
|
BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Text="Deine Kategorien bleiben standardmäßig erhalten" FontWeight="SemiBold"/>
|
||||||
|
<TextBlock x:Name="DataInfo" Style="{StaticResource Body}" FontSize="12.5" Margin="0,4,0,12"/>
|
||||||
|
<CheckBox x:Name="RemoveDataBox" Content="Auch Kategorien und Einstellungen entfernen (Papierkorb)"
|
||||||
|
Click="RemoveDataBox_Click"/>
|
||||||
|
<TextBlock x:Name="RemoveDataWarning" Visibility="Collapsed" Margin="30,8,0,0" TextWrapping="Wrap" FontSize="12.5"
|
||||||
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
|
Text="Achtung: Dateien und Ordner, die nur noch in deinen Kategorien liegen, wandern damit in den Papierkorb."/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Fußleiste -->
|
||||||
|
<StackPanel Grid.Row="2" Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,20,18">
|
||||||
|
<Button x:Name="BackButton" Content="Zurück" Width="110" Margin="0,0,8,0" Click="Back_Click"/>
|
||||||
|
<Button x:Name="NextButton" Content="Weiter" Width="130" Margin="0,0,8,0" Style="{StaticResource AccentButton}" IsDefault="True" Click="Next_Click"/>
|
||||||
|
<Button x:Name="CancelButton" Content="Abbrechen" Width="110" Click="Cancel_Click"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Window>
|
||||||
@@ -0,0 +1,249 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Animation;
|
||||||
|
|
||||||
|
namespace DeskCoreSetup;
|
||||||
|
|
||||||
|
public partial class MainWindow : Window
|
||||||
|
{
|
||||||
|
private readonly bool _uninstall;
|
||||||
|
private int _page;
|
||||||
|
private bool _busy;
|
||||||
|
private bool _done;
|
||||||
|
private string? _installedExe;
|
||||||
|
|
||||||
|
public MainWindow(bool uninstall)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
_uninstall = uninstall;
|
||||||
|
VersionText.Text = $"Version {Installer.Version}";
|
||||||
|
LicenseText.Text = Installer.LicenseText;
|
||||||
|
|
||||||
|
DirBox.Text = Installer.InstalledDir ?? Installer.DefaultDir;
|
||||||
|
DesktopLinkBox.IsChecked = Installer.InstalledDir is null || Installer.DesktopLinkExists;
|
||||||
|
AutostartBox.IsChecked = Installer.AutostartEnabled;
|
||||||
|
|
||||||
|
if (Installer.InstalledDir is null && Installer.LegacyInstallDir is not null)
|
||||||
|
{
|
||||||
|
UpdateHint.Visibility = Visibility.Visible;
|
||||||
|
UpdateHintText.Text = "Die bisherige Version „DeskHub“ wird durch DeskCore ersetzt. Deine Kategorien und Einstellungen werden übernommen.";
|
||||||
|
}
|
||||||
|
else if (Installer.InstalledDir is not null)
|
||||||
|
{
|
||||||
|
UpdateHint.Visibility = Visibility.Visible;
|
||||||
|
UpdateHintText.Text = Installer.InstalledVersion == Installer.Version
|
||||||
|
? $"DeskCore {Installer.Version} ist bereits installiert und wird neu installiert. Deine Kategorien und Einstellungen bleiben erhalten."
|
||||||
|
: $"Die vorhandene Installation (Version {Installer.InstalledVersion}) wird durch Version {Installer.Version} ersetzt. Deine Kategorien und Einstellungen bleiben erhalten.";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_uninstall)
|
||||||
|
{
|
||||||
|
Title = WindowTitle.Text = "DeskCore deinstallieren";
|
||||||
|
Steps.Visibility = Visibility.Collapsed;
|
||||||
|
ShowOnly(PageUninstall);
|
||||||
|
NextButton.Content = "Deinstallieren";
|
||||||
|
BackButton.Visibility = Visibility.Collapsed;
|
||||||
|
LoadDataInfo();
|
||||||
|
}
|
||||||
|
else ShowPage(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Seiten ----------
|
||||||
|
|
||||||
|
private void ShowOnly(UIElement page)
|
||||||
|
{
|
||||||
|
foreach (var p in new UIElement[] { PageWelcome, PageLicense, PageOptions, PageProgress, PageUninstall })
|
||||||
|
p.Visibility = p == page ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowPage(int page)
|
||||||
|
{
|
||||||
|
_page = page;
|
||||||
|
ShowOnly(page switch { 0 => PageWelcome, 1 => PageLicense, 2 => PageOptions, _ => PageProgress });
|
||||||
|
|
||||||
|
var steps = new[] { Step0, Step1, Step2, Step3 };
|
||||||
|
for (int i = 0; i < steps.Length; i++)
|
||||||
|
{
|
||||||
|
var label = ((string)steps[i].Tag ?? (string)(steps[i].Tag = steps[i].Text));
|
||||||
|
steps[i].Text = i < page || (_done && i == page) ? $"✓ {label}" : label;
|
||||||
|
steps[i].FontWeight = i == page ? FontWeights.SemiBold : FontWeights.Normal;
|
||||||
|
steps[i].Foreground = (Brush)FindResource(i == page ? "TextBrush" : i < page ? "TextSecondaryBrush" : "TextTertiaryBrush");
|
||||||
|
}
|
||||||
|
|
||||||
|
BackButton.Visibility = page is 1 or 2 ? Visibility.Visible : Visibility.Hidden;
|
||||||
|
NextButton.Content = page switch { 2 => "Installieren", 3 => "Fertigstellen", _ => "Weiter" };
|
||||||
|
NextButton.IsEnabled = page != 1 || AcceptLicense.IsChecked == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AcceptLicense_Click(object sender, RoutedEventArgs e) => NextButton.IsEnabled = AcceptLicense.IsChecked == true;
|
||||||
|
|
||||||
|
private void Back_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!_busy && _page > 0) ShowPage(_page - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Next_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) return;
|
||||||
|
|
||||||
|
if (_uninstall)
|
||||||
|
{
|
||||||
|
if (_done) { Close(); return; }
|
||||||
|
await RunUninstall();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (_page)
|
||||||
|
{
|
||||||
|
case 0: ShowPage(1); break;
|
||||||
|
case 1 when AcceptLicense.IsChecked == true: ShowPage(2); break;
|
||||||
|
case 2:
|
||||||
|
var dir = DirBox.Text.Trim();
|
||||||
|
if (!Path.IsPathFullyQualified(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "Bitte einen gültigen Ordner angeben, z. B. C:\\Programme\\DeskCore.", "DeskCore Setup");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await RunInstall(dir);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
if (LaunchBox.IsChecked == true && _installedExe is not null)
|
||||||
|
Process.Start(new ProcessStartInfo(_installedExe) { UseShellExecute = true });
|
||||||
|
Close();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ChangeDir_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "Installationsordner wählen" };
|
||||||
|
if (dlg.ShowDialog(this) != true) return;
|
||||||
|
var chosen = dlg.FolderName;
|
||||||
|
// wie bei üblichen Installern: Unterordner "DeskCore" anhängen
|
||||||
|
DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals("DeskCore", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? chosen : Path.Combine(chosen, "DeskCore");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Installation ----------
|
||||||
|
|
||||||
|
private async Task RunInstall(string dir)
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
ShowPage(3);
|
||||||
|
SetButtons(false);
|
||||||
|
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||||
|
bool desktop = DesktopLinkBox.IsChecked == true, autostart = AutostartBox.IsChecked == true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(() => Installer.Install(dir, desktop, autostart, progress));
|
||||||
|
_installedExe = Path.Combine(dir, Installer.ExeName);
|
||||||
|
_done = true;
|
||||||
|
ProgressTitle.Text = "Installation abgeschlossen";
|
||||||
|
ProgressText.Text = $"DeskCore wurde in {dir} installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : ".");
|
||||||
|
DoneText.Text = "Alles bereit!";
|
||||||
|
DonePanel.Visibility = Visibility.Visible;
|
||||||
|
ShowPage(3);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProgressTitle.Text = "Installation fehlgeschlagen";
|
||||||
|
ProgressText.Text = ex.Message;
|
||||||
|
ProgressText.Foreground = (Brush)FindResource("DangerBrush");
|
||||||
|
NextButton.Content = "Schließen";
|
||||||
|
_installedExe = null;
|
||||||
|
}
|
||||||
|
_busy = false;
|
||||||
|
NextButton.IsEnabled = true;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
BackButton.Visibility = Visibility.Hidden;
|
||||||
|
CancelButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Deinstallation ----------
|
||||||
|
|
||||||
|
private async void LoadDataInfo()
|
||||||
|
{
|
||||||
|
var root = Installer.DataDirs[0];
|
||||||
|
if (!Directory.Exists(root))
|
||||||
|
{
|
||||||
|
DataInfo.Text = "Es wurden keine Kategorien gefunden.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DataInfo.Text = $"Sie liegen in {root}. Größe wird berechnet …";
|
||||||
|
var size = await Task.Run(Installer.DataSize);
|
||||||
|
DataInfo.Text = $"Sie liegen in {root} ({Installer.FormatSize(size)}) und bleiben dort, falls du DeskCore später wieder installierst.";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveDataBox_Click(object sender, RoutedEventArgs e) =>
|
||||||
|
RemoveDataWarning.Visibility = RemoveDataBox.IsChecked == true ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
|
||||||
|
private async Task RunUninstall()
|
||||||
|
{
|
||||||
|
_busy = true;
|
||||||
|
bool removeData = RemoveDataBox.IsChecked == true;
|
||||||
|
ShowOnly(PageProgress);
|
||||||
|
ProgressTitle.Text = "DeskCore wird entfernt …";
|
||||||
|
LaunchBox.Visibility = Visibility.Collapsed;
|
||||||
|
SetButtons(false);
|
||||||
|
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Run(() => Installer.Uninstall(removeData, progress));
|
||||||
|
ProgressTitle.Text = "Deinstallation abgeschlossen";
|
||||||
|
ProgressText.Text = removeData
|
||||||
|
? "DeskCore, deine Kategorien und Einstellungen wurden entfernt (die Daten liegen im Papierkorb)."
|
||||||
|
: "DeskCore wurde entfernt. Deine Kategorien liegen weiterhin in " + Installer.DataDirs[0] + ".";
|
||||||
|
DoneText.Text = "Fertig";
|
||||||
|
DonePanel.Visibility = Visibility.Visible;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProgressTitle.Text = "Deinstallation fehlgeschlagen";
|
||||||
|
ProgressText.Text = ex.Message;
|
||||||
|
}
|
||||||
|
_done = true;
|
||||||
|
_busy = false;
|
||||||
|
CloseButton.IsEnabled = true;
|
||||||
|
NextButton.Content = "Schließen";
|
||||||
|
NextButton.IsEnabled = true;
|
||||||
|
CancelButton.Visibility = Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Helfer ----------
|
||||||
|
|
||||||
|
private void SetButtons(bool enabled)
|
||||||
|
{
|
||||||
|
NextButton.IsEnabled = enabled;
|
||||||
|
BackButton.IsEnabled = enabled;
|
||||||
|
CancelButton.IsEnabled = enabled;
|
||||||
|
CloseButton.IsEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetProgress(double value, string text)
|
||||||
|
{
|
||||||
|
if (!_done) ProgressText.Text = text;
|
||||||
|
var target = Math.Max(0, ProgressTrack.ActualWidth * value);
|
||||||
|
ProgressFill.BeginAnimation(WidthProperty,
|
||||||
|
new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
|
||||||
|
|
||||||
|
private void Cancel_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) return;
|
||||||
|
if (!_done && !_uninstall && _page > 0 &&
|
||||||
|
MessageBox.Show(this, "Installation wirklich abbrechen?", "DeskCore Setup", MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
|
||||||
|
return;
|
||||||
|
Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
if (_busy) e.Cancel = true; // während der Installation nicht schließen
|
||||||
|
base.OnClosing(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
+100
@@ -0,0 +1,100 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Media.Imaging;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Holt das Icon (bzw. Vorschaubild) einer Datei so, wie der Explorer es anzeigt.</summary>
|
||||||
|
public static class ShellIcon
|
||||||
|
{
|
||||||
|
public static ImageSource? Get(string path, int size = 64)
|
||||||
|
{
|
||||||
|
IntPtr hbm = IntPtr.Zero;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SHCreateItemFromParsingName(path, IntPtr.Zero, typeof(IShellItemImageFactory).GUID, out var factory);
|
||||||
|
int hr = factory.GetImage(new SIZE { cx = size, cy = size }, 0, out hbm);
|
||||||
|
Marshal.ReleaseComObject(factory);
|
||||||
|
if (hr != 0 || hbm == IntPtr.Zero) return null;
|
||||||
|
return FromHBitmap(hbm);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (hbm != IntPtr.Zero) DeleteObject(hbm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBitmapSourceFromHBitmap verliert den Alphakanal, daher Pixel selbst kopieren.
|
||||||
|
private static BitmapSource? FromHBitmap(IntPtr hbm)
|
||||||
|
{
|
||||||
|
GetObject(hbm, Marshal.SizeOf<BITMAP>(), out BITMAP bm);
|
||||||
|
int w = bm.bmWidth, h = Math.Abs(bm.bmHeight);
|
||||||
|
if (w <= 0 || h <= 0) return null;
|
||||||
|
|
||||||
|
var bmi = new BITMAPINFOHEADER
|
||||||
|
{
|
||||||
|
biSize = Marshal.SizeOf<BITMAPINFOHEADER>(),
|
||||||
|
biWidth = w,
|
||||||
|
biHeight = -h, // top-down
|
||||||
|
biPlanes = 1,
|
||||||
|
biBitCount = 32,
|
||||||
|
};
|
||||||
|
var bits = new byte[w * h * 4];
|
||||||
|
IntPtr hdc = GetDC(IntPtr.Zero);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (GetDIBits(hdc, hbm, 0, (uint)h, bits, ref bmi, 0) == 0) return null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ReleaseDC(IntPtr.Zero, hdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasAlpha = false;
|
||||||
|
for (int i = 3; i < bits.Length; i += 4)
|
||||||
|
if (bits[i] != 0) { hasAlpha = true; break; }
|
||||||
|
|
||||||
|
var src = BitmapSource.Create(w, h, 96, 96, hasAlpha ? PixelFormats.Pbgra32 : PixelFormats.Bgr32, null, bits, w * 4);
|
||||||
|
src.Freeze();
|
||||||
|
return src;
|
||||||
|
}
|
||||||
|
|
||||||
|
[ComImport, Guid("bcc18b79-ba16-442f-80c4-8a59c30c463b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||||
|
private interface IShellItemImageFactory
|
||||||
|
{
|
||||||
|
[PreserveSig] int GetImage(SIZE size, int flags, out IntPtr phbm);
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct SIZE { public int cx, cy; }
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct BITMAP
|
||||||
|
{
|
||||||
|
public int bmType, bmWidth, bmHeight, bmWidthBytes;
|
||||||
|
public ushort bmPlanes, bmBitsPixel;
|
||||||
|
public IntPtr bmBits;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct BITMAPINFOHEADER
|
||||||
|
{
|
||||||
|
public int biSize, biWidth, biHeight;
|
||||||
|
public ushort biPlanes, biBitCount;
|
||||||
|
public int biCompression, biSizeImage, biXPelsPerMeter, biYPelsPerMeter, biClrUsed, biClrImportant;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
|
||||||
|
private static extern void SHCreateItemFromParsingName(string path, IntPtr pbc,
|
||||||
|
[MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItemImageFactory ppv);
|
||||||
|
|
||||||
|
[DllImport("gdi32.dll")] private static extern bool DeleteObject(IntPtr h);
|
||||||
|
[DllImport("gdi32.dll")] private static extern int GetObject(IntPtr h, int size, out BITMAP bm);
|
||||||
|
[DllImport("gdi32.dll")] private static extern int GetDIBits(IntPtr hdc, IntPtr hbm, uint start, uint lines, byte[] bits, ref BITMAPINFOHEADER bmi, uint usage);
|
||||||
|
[DllImport("user32.dll")] private static extern IntPtr GetDC(IntPtr hwnd);
|
||||||
|
[DllImport("user32.dll")] private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
|
||||||
|
}
|
||||||
+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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Media;
|
||||||
|
using Microsoft.Win32;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Übernimmt Hell/Dunkel-Modus und Akzentfarbe von Windows – auch live, wenn man sie ändert.</summary>
|
||||||
|
public static class Theme
|
||||||
|
{
|
||||||
|
public static bool IsLight { get; private set; } = true;
|
||||||
|
|
||||||
|
/// <summary>Vom Benutzer gewählter Modus; "System" folgt der Windows-Einstellung.</summary>
|
||||||
|
public static DesignMode Mode => AppSettings.Current.Design;
|
||||||
|
|
||||||
|
public static void Init()
|
||||||
|
{
|
||||||
|
Apply();
|
||||||
|
SystemEvents.UserPreferenceChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Category is UserPreferenceCategory.General or UserPreferenceCategory.Color or UserPreferenceCategory.VisualStyle)
|
||||||
|
Application.Current.Dispatcher.BeginInvoke(Apply);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Apply()
|
||||||
|
{
|
||||||
|
IsLight = Mode switch
|
||||||
|
{
|
||||||
|
DesignMode.Light => true,
|
||||||
|
DesignMode.Dark => false,
|
||||||
|
_ => ReadDword(@"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize", "AppsUseLightTheme") != 0,
|
||||||
|
};
|
||||||
|
var res = Application.Current.Resources;
|
||||||
|
res.MergedDictionaries[0] = new ResourceDictionary
|
||||||
|
{
|
||||||
|
Source = new Uri($"pack://application:,,,/Themes/Colors.{(IsLight ? "Light" : "Dark")}.xaml")
|
||||||
|
};
|
||||||
|
|
||||||
|
// DWM speichert die Akzentfarbe als ABGR
|
||||||
|
int abgr = ReadDword(@"Software\Microsoft\Windows\DWM", "AccentColor") is var v and not -1 ? v : unchecked((int)0xFFD77800);
|
||||||
|
var accent = Color.FromRgb((byte)abgr, (byte)(abgr >> 8), (byte)(abgr >> 16));
|
||||||
|
|
||||||
|
// Wie Windows 11: im Hellmodus etwas dunkler + weiße Schrift, im Dunkelmodus deutlich heller + schwarze Schrift
|
||||||
|
var a = IsLight ? Mix(accent, Colors.Black, 0.12) : Mix(accent, Colors.White, 0.45);
|
||||||
|
res["AccentBrush"] = Brush(a);
|
||||||
|
res["AccentHoverBrush"] = Brush(IsLight ? Mix(a, Colors.White, 0.1) : Mix(a, Colors.Black, 0.1));
|
||||||
|
res["OnAccentBrush"] = Brush(IsLight ? Colors.White : Colors.Black);
|
||||||
|
res["AccentSubtleBrush"] = Brush(Color.FromArgb(IsLight ? (byte)0x22 : (byte)0x30, a.R, a.G, a.B));
|
||||||
|
res["AccentSubtleStrokeBrush"] = Brush(Color.FromArgb(IsLight ? (byte)0x55 : (byte)0x60, a.R, a.G, a.B));
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static event Action? Changed;
|
||||||
|
|
||||||
|
private static int ReadDword(string key, string name)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var k = Registry.CurrentUser.OpenSubKey(key);
|
||||||
|
return k?.GetValue(name) is int i ? i : -1;
|
||||||
|
}
|
||||||
|
catch { return -1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Color Mix(Color c, Color with, double t) => Color.FromRgb(
|
||||||
|
(byte)(c.R + (with.R - c.R) * t), (byte)(c.G + (with.G - c.G) * t), (byte)(c.B + (with.B - c.B) * t));
|
||||||
|
|
||||||
|
private static SolidColorBrush Brush(Color c)
|
||||||
|
{
|
||||||
|
var b = new SolidColorBrush(c);
|
||||||
|
b.Freeze();
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="BaseBrush" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="LayerBrush" Color="#282828"/>
|
||||||
|
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#1A1A1A"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0FFFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#0AFFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#14FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="ControlFillBrush" Color="#2D2D2D"/>
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#323232"/>
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#272727"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#353535"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#8A8A8A"/>
|
||||||
|
<SolidColorBrush x:Key="InputFocusBrush" Color="#1F1F1F"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrush" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#C8C8C8"/>
|
||||||
|
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#9A9A9A"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutBrush" Color="#2C2C2C"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#404040"/>
|
||||||
|
<SolidColorBrush x:Key="DialogFooterBrush" Color="#202020"/>
|
||||||
|
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#9F9F9F"/>
|
||||||
|
<SolidColorBrush x:Key="OverlayBrush" Color="#66000000"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#4CAF50"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#FF99A4"/>
|
||||||
|
<SolidColorBrush x:Key="FenceBrush" Color="#F21C1C1C"/>
|
||||||
|
<Color x:Key="ShadowColor">#000000</Color>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<SolidColorBrush x:Key="BaseBrush" Color="#F3F3F3"/>
|
||||||
|
<SolidColorBrush x:Key="LayerBrush" Color="#FAFAFA"/>
|
||||||
|
<SolidColorBrush x:Key="LayerStrokeBrush" Color="#E5E5E5"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleHoverBrush" Color="#0A000000"/>
|
||||||
|
<SolidColorBrush x:Key="SubtlePressedBrush" Color="#06000000"/>
|
||||||
|
<SolidColorBrush x:Key="SubtleSelectedBrush" Color="#0F000000"/>
|
||||||
|
<SolidColorBrush x:Key="ControlFillBrush" Color="#FDFDFD"/>
|
||||||
|
<SolidColorBrush x:Key="ControlHoverBrush" Color="#F6F6F6"/>
|
||||||
|
<SolidColorBrush x:Key="ControlPressedBrush" Color="#F2F2F2"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrokeBrush" Color="#E3E3E3"/>
|
||||||
|
<SolidColorBrush x:Key="ControlStrongStrokeBrush" Color="#C4C4C4"/>
|
||||||
|
<SolidColorBrush x:Key="InputFocusBrush" Color="#FFFFFF"/>
|
||||||
|
<SolidColorBrush x:Key="TextBrush" Color="#1B1B1B"/>
|
||||||
|
<SolidColorBrush x:Key="TextSecondaryBrush" Color="#5D5D5D"/>
|
||||||
|
<SolidColorBrush x:Key="TextTertiaryBrush" Color="#8B8B8B"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutBrush" Color="#F9F9F9"/>
|
||||||
|
<SolidColorBrush x:Key="FlyoutStrokeBrush" Color="#DCDCDC"/>
|
||||||
|
<SolidColorBrush x:Key="DialogFooterBrush" Color="#F3F3F3"/>
|
||||||
|
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#8A8A8A"/>
|
||||||
|
<SolidColorBrush x:Key="OverlayBrush" Color="#4D000000"/>
|
||||||
|
<SolidColorBrush x:Key="SuccessBrush" Color="#0F7B0F"/>
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#C42B1C"/>
|
||||||
|
<SolidColorBrush x:Key="FenceBrush" Color="#F5F7F7F7"/>
|
||||||
|
<Color x:Key="ShadowColor">#000000</Color>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -0,0 +1,651 @@
|
|||||||
|
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
|
||||||
|
<!-- Schriften: Windows 11 nutzt "Segoe UI Variable" / "Segoe Fluent Icons", Windows 10 fällt auf Segoe UI / MDL2 zurück -->
|
||||||
|
<FontFamily x:Key="UiFont">Segoe UI Variable Text, Segoe UI</FontFamily>
|
||||||
|
<FontFamily x:Key="DisplayFont">Segoe UI Variable Display, Segoe UI</FontFamily>
|
||||||
|
<FontFamily x:Key="IconFont">Segoe Fluent Icons, Segoe MDL2 Assets</FontFamily>
|
||||||
|
|
||||||
|
<DropShadowEffect x:Key="FlyoutShadow" BlurRadius="24" ShadowDepth="6" Direction="270" Opacity="0.18"
|
||||||
|
Color="Black"/>
|
||||||
|
<DropShadowEffect x:Key="DialogShadow" BlurRadius="40" ShadowDepth="12" Direction="270" Opacity="0.28"
|
||||||
|
Color="Black"/>
|
||||||
|
|
||||||
|
<!-- ===== Buttons ===== -->
|
||||||
|
<Style TargetType="Button">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="Padding" Value="14,5"/>
|
||||||
|
<Setter Property="MinHeight" Value="32"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="4" Padding="{TemplateBinding Padding}"
|
||||||
|
SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlPressedBrush}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="AccentButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}"
|
||||||
|
BorderThickness="1" CornerRadius="4" Padding="{TemplateBinding Padding}" SnapsToDevicePixels="True">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
|
||||||
|
VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentHoverBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter Property="Opacity" Value="0.8"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Transparenter Button (Symbolleisten, Sidebar) -->
|
||||||
|
<Style x:Key="SubtleButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="Padding" Value="9,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4" Padding="{TemplateBinding Padding}">
|
||||||
|
<ContentPresenter HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
<Setter Property="Opacity" Value="0.75"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.35"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Titelleisten-Buttons -->
|
||||||
|
<Style x:Key="CaptionButton" TargetType="Button">
|
||||||
|
<Setter Property="Width" Value="46"/>
|
||||||
|
<Setter Property="Height" Value="32"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="10"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="IsTabStop" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="CloseCaptionButton" TargetType="Button" BasedOn="{StaticResource CaptionButton}">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Button">
|
||||||
|
<Border x:Name="Bd" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#C42B1C"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsPressed" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="#C83C30"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== TextBox ===== -->
|
||||||
|
<Style TargetType="TextBox">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="SelectionBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="Padding" Value="10,6"/>
|
||||||
|
<Setter Property="MinHeight" Value="32"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="TextBox">
|
||||||
|
<Grid>
|
||||||
|
<Border x:Name="Bd" Background="{TemplateBinding Background}" CornerRadius="4"
|
||||||
|
BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1"/>
|
||||||
|
<!-- Unterstrich wie bei Windows 11 -->
|
||||||
|
<Border x:Name="Line" Height="1" VerticalAlignment="Bottom" Margin="3,0" CornerRadius="0,0,4,4"
|
||||||
|
Background="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||||
|
<ScrollViewer x:Name="PART_ContentHost" Margin="{TemplateBinding Padding}" VerticalAlignment="Center"
|
||||||
|
Focusable="False" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource ControlHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsKeyboardFocused" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource InputFocusBrush}"/>
|
||||||
|
<Setter TargetName="Line" Property="Height" Value="2"/>
|
||||||
|
<Setter TargetName="Line" Property="Margin" Value="1,0"/>
|
||||||
|
<Setter TargetName="Line" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Scrollbar (schmal, wird beim Überfahren breiter) ===== -->
|
||||||
|
<Style x:Key="ScrollThumb" TargetType="Thumb">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Thumb">
|
||||||
|
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="3"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="ScrollBar">
|
||||||
|
<Setter Property="Width" Value="12"/>
|
||||||
|
<Setter Property="MinWidth" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Width="3" HorizontalAlignment="Center" Opacity="0.7"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Th" Property="Width" Value="6"/>
|
||||||
|
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="Orientation" Value="Horizontal">
|
||||||
|
<Setter Property="Width" Value="Auto"/>
|
||||||
|
<Setter Property="MinWidth" Value="0"/>
|
||||||
|
<Setter Property="Height" Value="12"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ScrollBar">
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<Track x:Name="PART_Track">
|
||||||
|
<Track.Thumb>
|
||||||
|
<Thumb x:Name="Th" Style="{StaticResource ScrollThumb}" Height="3" VerticalAlignment="Center" Opacity="0.7"/>
|
||||||
|
</Track.Thumb>
|
||||||
|
</Track>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Th" Property="Height" Value="6"/>
|
||||||
|
<Setter TargetName="Th" Property="Opacity" Value="1"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== ToolTip ===== -->
|
||||||
|
<Style TargetType="ToolTip">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="12"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="HasDropShadow" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ToolTip">
|
||||||
|
<Border Margin="8,4,8,12" Padding="9,6" CornerRadius="4" MaxWidth="320"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ContentPresenter>
|
||||||
|
<ContentPresenter.Resources>
|
||||||
|
<Style TargetType="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||||
|
</Style>
|
||||||
|
</ContentPresenter.Resources>
|
||||||
|
</ContentPresenter>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kontextmenü ===== -->
|
||||||
|
<Style TargetType="ContextMenu">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="HasDropShadow" Value="False"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ContextMenu">
|
||||||
|
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="200"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
|
||||||
|
Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||||
|
</Border>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="{x:Static MenuItem.SeparatorStyleKey}" TargetType="Separator">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="Separator">
|
||||||
|
<Border Height="1" Margin="0,4" Background="{DynamicResource FlyoutStrokeBrush}"/>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style TargetType="MenuItem">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="MenuItem">
|
||||||
|
<Border x:Name="Bd" Background="Transparent" CornerRadius="4" Margin="4,1" Padding="11,0" Height="34">
|
||||||
|
<Grid>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="28"/>
|
||||||
|
<ColumnDefinition Width="*"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<ContentPresenter ContentSource="Icon" VerticalAlignment="Center" HorizontalAlignment="Left"
|
||||||
|
TextElement.FontFamily="{DynamicResource IconFont}" TextElement.FontSize="16"/>
|
||||||
|
<ContentPresenter Grid.Column="1" ContentSource="Header" VerticalAlignment="Center" RecognizesAccessKey="True"/>
|
||||||
|
<TextBlock x:Name="Gesture" Grid.Column="2" Text="{TemplateBinding InputGestureText}" FontSize="12"
|
||||||
|
VerticalAlignment="Center" Margin="24,0,0,0" Foreground="{DynamicResource TextTertiaryBrush}"/>
|
||||||
|
<TextBlock x:Name="Arrow" Grid.Column="2" Text="" FontFamily="{DynamicResource IconFont}" FontSize="11"
|
||||||
|
VerticalAlignment="Center" Margin="24,0,0,0" Visibility="Collapsed"
|
||||||
|
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
<Popup x:Name="PART_Popup" Placement="Right" HorizontalOffset="-4" VerticalOffset="-12"
|
||||||
|
IsOpen="{Binding IsSubmenuOpen, RelativeSource={RelativeSource TemplatedParent}}"
|
||||||
|
AllowsTransparency="True" Focusable="False" PopupAnimation="Fade">
|
||||||
|
<Border Margin="12,4,12,16" Padding="0,4" CornerRadius="8" MinWidth="180"
|
||||||
|
Background="{DynamicResource FlyoutBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}"
|
||||||
|
BorderThickness="1" Effect="{StaticResource FlyoutShadow}">
|
||||||
|
<ScrollViewer MaxHeight="420" VerticalScrollBarVisibility="Auto">
|
||||||
|
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Cycle"/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Border>
|
||||||
|
</Popup>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsHighlighted" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Role" Value="SubmenuHeader">
|
||||||
|
<Setter TargetName="Arrow" Property="Visibility" Value="Visible"/>
|
||||||
|
<Setter TargetName="Gesture" Property="Visibility" Value="Collapsed"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Listen ===== -->
|
||||||
|
<Style TargetType="ListBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
<Setter Property="BorderThickness" Value="0"/>
|
||||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBox">
|
||||||
|
<ScrollViewer Focusable="False" Padding="{TemplateBinding Padding}">
|
||||||
|
<ItemsPresenter/>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Navigationseintrag in der Seitenleiste -->
|
||||||
|
<Style x:Key="NavItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Height" Value="38"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Grid Margin="4,1" Background="Transparent">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" BorderThickness="1" BorderBrush="Transparent" Background="Transparent"/>
|
||||||
|
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<ContentPresenter Margin="14,0,12,0" VerticalAlignment="Center"/>
|
||||||
|
<!-- Einfügelinie beim Umsortieren -->
|
||||||
|
<Border x:Name="InsertTop" Height="2" CornerRadius="1" VerticalAlignment="Top" Margin="6,-2,6,0"
|
||||||
|
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<Border x:Name="InsertBottom" Height="2" CornerRadius="1" VerticalAlignment="Bottom" Margin="6,0,6,-2"
|
||||||
|
Background="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleSelectedBrush}"/>
|
||||||
|
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="before">
|
||||||
|
<Setter TargetName="InsertTop" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="after">
|
||||||
|
<Setter TargetName="InsertBottom" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="Tag" Value="drop">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Icon-Kachel -->
|
||||||
|
<Style x:Key="TileItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Border x:Name="Bd" Margin="2" CornerRadius="6" BorderThickness="1" BorderBrush="Transparent" Background="Transparent">
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentSubtleStrokeBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Listenansicht-Zeile ===== -->
|
||||||
|
<Style x:Key="RowItem" TargetType="ListBoxItem">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="ListBoxItem">
|
||||||
|
<Grid Margin="0,1">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" Background="Transparent"/>
|
||||||
|
<Rectangle x:Name="Pill" Width="3" Height="16" RadiusX="1.5" RadiusY="1.5" HorizontalAlignment="Left"
|
||||||
|
Fill="{DynamicResource AccentBrush}" Visibility="Collapsed"/>
|
||||||
|
<ContentPresenter/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsSelected" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Pill" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Karte (Einstellungen) ===== -->
|
||||||
|
<Style x:Key="Card" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="18,14"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
<Setter Property="Margin" Value="2,24,0,8"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kippschalter (Windows 11) ===== -->
|
||||||
|
<Style x:Key="ToggleSwitch" TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||||
|
<TextBlock x:Name="State" Text="Aus" VerticalAlignment="Center" Margin="0,0,12,0" MinWidth="24" TextAlignment="Right"/>
|
||||||
|
<Grid Width="40" Height="20">
|
||||||
|
<Border x:Name="Track" CornerRadius="10" BorderThickness="1"
|
||||||
|
Background="{DynamicResource SubtleSelectedBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}"/>
|
||||||
|
<Ellipse x:Name="Knob" Width="12" Height="12" HorizontalAlignment="Left" Margin="4,0,0,0"
|
||||||
|
Fill="{DynamicResource TextSecondaryBrush}"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Track" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Track" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Knob" Property="Fill" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
<Setter TargetName="Knob" Property="HorizontalAlignment" Value="Right"/>
|
||||||
|
<Setter TargetName="Knob" Property="Margin" Value="0,0,4,0"/>
|
||||||
|
<Setter TargetName="State" Property="Text" Value="Ein"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Knob" Property="Width" Value="14"/>
|
||||||
|
<Setter TargetName="Knob" Property="Height" Value="14"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Kontrollkästchen ===== -->
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource UiFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="14"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="CheckBox">
|
||||||
|
<StackPanel Orientation="Horizontal" Background="Transparent">
|
||||||
|
<Border x:Name="Box" Width="20" Height="20" CornerRadius="4" BorderThickness="1" VerticalAlignment="Center"
|
||||||
|
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrongStrokeBrush}">
|
||||||
|
<TextBlock x:Name="Check" Text="" FontFamily="{DynamicResource IconFont}" FontSize="12"
|
||||||
|
Foreground="{DynamicResource OnAccentBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
|
Visibility="Collapsed"/>
|
||||||
|
</Border>
|
||||||
|
<ContentPresenter Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Box" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter TargetName="Check" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Box" Property="Opacity" Value="0.85"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsEnabled" Value="False">
|
||||||
|
<Setter Property="Opacity" Value="0.4"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Segment-Auswahl (z. B. Hell | Dunkel | System) ===== -->
|
||||||
|
<Style x:Key="SegmentRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Padding" Value="14,6"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Bd" CornerRadius="4" Padding="{TemplateBinding Padding}" Margin="2" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="SegmentGroup" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource SubtlePressedBrush}"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="1"/>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- ===== Farb- und Symbolauswahl ===== -->
|
||||||
|
<Style x:Key="SwatchRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Cursor" Value="Hand"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Grid Width="34" Height="34" Margin="0,0,6,6" Background="Transparent">
|
||||||
|
<Ellipse x:Name="Ring" Stroke="{DynamicResource TextBrush}" StrokeThickness="2" Visibility="Collapsed"/>
|
||||||
|
<Ellipse Margin="5" Fill="{TemplateBinding Background}" Stroke="{DynamicResource ControlStrokeBrush}"/>
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Ring" Property="Visibility" Value="Visible"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style x:Key="GlyphRadio" TargetType="RadioButton">
|
||||||
|
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource IconFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="16"/>
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate TargetType="RadioButton">
|
||||||
|
<Border x:Name="Bd" Width="36" Height="36" Margin="0,0,4,4" CornerRadius="4" BorderThickness="1"
|
||||||
|
BorderBrush="Transparent" Background="Transparent">
|
||||||
|
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||||
|
</Border>
|
||||||
|
<ControlTemplate.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource SubtleHoverBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
<Trigger Property="IsChecked" Value="True">
|
||||||
|
<Setter TargetName="Bd" Property="Background" Value="{DynamicResource AccentSubtleBrush}"/>
|
||||||
|
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
</Trigger>
|
||||||
|
</ControlTemplate.Triggers>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
</ResourceDictionary>
|
||||||
|
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Symbol im Infobereich der Taskleiste.</summary>
|
||||||
|
public sealed class Tray : IDisposable
|
||||||
|
{
|
||||||
|
private readonly System.Windows.Forms.NotifyIcon _icon;
|
||||||
|
|
||||||
|
/// <summary>Linksklick</summary>
|
||||||
|
public event Action? Clicked;
|
||||||
|
/// <summary>Rechtsklick – Kontextmenü wird von der WPF-Seite gezeigt (sieht moderner aus)</summary>
|
||||||
|
public event Action? MenuRequested;
|
||||||
|
public event Action? BalloonClicked;
|
||||||
|
|
||||||
|
public Tray()
|
||||||
|
{
|
||||||
|
var stream = Application.GetResourceStream(new Uri("pack://application:,,,/app.ico"))!.Stream;
|
||||||
|
_icon = new System.Windows.Forms.NotifyIcon
|
||||||
|
{
|
||||||
|
Icon = new System.Drawing.Icon(stream, System.Windows.Forms.SystemInformation.SmallIconSize),
|
||||||
|
Text = "DeskCore",
|
||||||
|
};
|
||||||
|
_icon.MouseUp += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.Button == System.Windows.Forms.MouseButtons.Left) Clicked?.Invoke();
|
||||||
|
else if (e.Button == System.Windows.Forms.MouseButtons.Right) MenuRequested?.Invoke();
|
||||||
|
};
|
||||||
|
_icon.BalloonTipClicked += (_, _) => BalloonClicked?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Visible
|
||||||
|
{
|
||||||
|
get => _icon.Visible;
|
||||||
|
set => _icon.Visible = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Notify(string title, string text)
|
||||||
|
{
|
||||||
|
if (!_icon.Visible) return;
|
||||||
|
_icon.ShowBalloonTip(3000, title, text, System.Windows.Forms.ToolTipIcon.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_icon.Visible = false;
|
||||||
|
_icon.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>Eine rückgängig machbare Aktion, bestehend aus mehreren Einzelschritten.</summary>
|
||||||
|
public class UndoEntry(string description)
|
||||||
|
{
|
||||||
|
public string Description { get; } = description;
|
||||||
|
private readonly List<Func<string?>> _steps = [];
|
||||||
|
|
||||||
|
/// <summary>Elemente, die beim Löschen zwischengeparkt wurden – gehen später in den Papierkorb.</summary>
|
||||||
|
public List<string> HeldPaths { get; } = [];
|
||||||
|
|
||||||
|
public bool IsEmpty => _steps.Count == 0;
|
||||||
|
|
||||||
|
/// <summary>Schritt zum Rückgängigmachen; gibt eine Fehlermeldung oder null zurück.</summary>
|
||||||
|
public void Add(Func<string?> undo) => _steps.Add(undo);
|
||||||
|
|
||||||
|
public void AddMove(string from, string to) => Add(() =>
|
||||||
|
{
|
||||||
|
if (!File.Exists(to) && !Directory.Exists(to)) return $"„{Path.GetFileName(to)}“ gibt es nicht mehr.";
|
||||||
|
var dir = Path.GetDirectoryName(from)!;
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
var back = File.Exists(from) || Directory.Exists(from) ? Storage.UniquePath(dir, Path.GetFileName(from)) : from;
|
||||||
|
Storage.MoveAny(to, back);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
public void AddCopy(string original, string copy) => Add(() =>
|
||||||
|
{
|
||||||
|
if (!File.Exists(copy) && !Directory.Exists(copy)) return null;
|
||||||
|
// Original noch da → Kopie weg; sonst Kopie dorthin zurück, wo das Original lag
|
||||||
|
if (File.Exists(original) || Directory.Exists(original)) Storage.Recycle(copy);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(original)!);
|
||||||
|
Storage.MoveAny(copy, original);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
public void AddDelete(string original, string held)
|
||||||
|
{
|
||||||
|
HeldPaths.Add(held);
|
||||||
|
AddMove(original, held);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<string> Run()
|
||||||
|
{
|
||||||
|
var errors = new List<string>();
|
||||||
|
for (int i = _steps.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
try { if (_steps[i]() is { } err) errors.Add(err); }
|
||||||
|
catch (Exception ex) { errors.Add(ex.Message); }
|
||||||
|
}
|
||||||
|
HeldPaths.Clear(); // wurden zurückgeholt
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UndoManager
|
||||||
|
{
|
||||||
|
private const int Limit = 30;
|
||||||
|
private readonly LinkedList<UndoEntry> _stack = new();
|
||||||
|
|
||||||
|
public event Action? Changed;
|
||||||
|
|
||||||
|
public bool CanUndo => _stack.Count > 0;
|
||||||
|
public string? NextDescription => _stack.Last?.Value.Description;
|
||||||
|
|
||||||
|
public void Push(UndoEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.IsEmpty) return;
|
||||||
|
_stack.AddLast(entry);
|
||||||
|
while (_stack.Count > Limit)
|
||||||
|
{
|
||||||
|
Flush(_stack.First!.Value);
|
||||||
|
_stack.RemoveFirst();
|
||||||
|
}
|
||||||
|
Changed?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public (string Description, List<string> Errors)? Undo()
|
||||||
|
{
|
||||||
|
if (_stack.Last is not { } last) return null;
|
||||||
|
_stack.RemoveLast();
|
||||||
|
var errors = last.Value.Run();
|
||||||
|
Changed?.Invoke();
|
||||||
|
return (last.Value.Description, errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Beim Beenden: alles Zwischengeparkte in den Papierkorb.</summary>
|
||||||
|
public void FlushAll()
|
||||||
|
{
|
||||||
|
foreach (var e in _stack) Flush(e);
|
||||||
|
_stack.Clear();
|
||||||
|
Storage.FlushHolding();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Flush(UndoEntry e)
|
||||||
|
{
|
||||||
|
foreach (var p in e.HeldPaths) Storage.Recycle(p);
|
||||||
|
e.HeldPaths.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace DeskCore;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Beobachtet Desktop und Kategorie-Ordner. Meldet Änderungen gebündelt (statt bei jedem einzelnen Ereignis)
|
||||||
|
/// und sammelt neu auf dem Desktop aufgetauchte Elemente für die automatische Sortierung.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Watcher : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<FileSystemWatcher> _watchers = [];
|
||||||
|
private readonly DispatcherTimer _changedTimer;
|
||||||
|
private readonly DispatcherTimer _newTimer;
|
||||||
|
private readonly HashSet<string> _newOnDesktop = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
/// <summary>Irgendetwas hat sich geändert (UI-Thread).</summary>
|
||||||
|
public event Action? Changed;
|
||||||
|
/// <summary>Neue Elemente auf dem Desktop (UI-Thread, nach kurzer Wartezeit).</summary>
|
||||||
|
public event Action<List<string>>? NewOnDesktop;
|
||||||
|
|
||||||
|
public Watcher(Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_changedTimer = new DispatcherTimer(TimeSpan.FromMilliseconds(350), DispatcherPriority.Background, (_, _) =>
|
||||||
|
{
|
||||||
|
_changedTimer!.Stop();
|
||||||
|
Changed?.Invoke();
|
||||||
|
}, dispatcher);
|
||||||
|
_changedTimer.Stop();
|
||||||
|
|
||||||
|
// Etwas länger warten: Installer legen Verknüpfungen oft in mehreren Schritten an
|
||||||
|
_newTimer = new DispatcherTimer(TimeSpan.FromSeconds(2), DispatcherPriority.Background, (_, _) =>
|
||||||
|
{
|
||||||
|
_newTimer!.Stop();
|
||||||
|
List<string> items;
|
||||||
|
lock (_lock) { items = _newOnDesktop.ToList(); _newOnDesktop.Clear(); }
|
||||||
|
items = items.Where(p => (File.Exists(p) || Directory.Exists(p)) && !Storage.CreatedByApp(p)).ToList();
|
||||||
|
if (items.Count > 0) NewOnDesktop?.Invoke(items);
|
||||||
|
}, dispatcher);
|
||||||
|
_newTimer.Stop();
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Storage.Root);
|
||||||
|
Add(Storage.UserDesktop, false, true);
|
||||||
|
Add(Storage.PublicDesktop, false, true);
|
||||||
|
Add(Storage.Root, true, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Add(string dir, bool subdirs, bool isDesktop)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(dir)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var w = new FileSystemWatcher(dir)
|
||||||
|
{
|
||||||
|
IncludeSubdirectories = subdirs,
|
||||||
|
NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName | NotifyFilters.LastWrite,
|
||||||
|
InternalBufferSize = 64 * 1024,
|
||||||
|
};
|
||||||
|
FileSystemEventHandler onChange = (_, e) =>
|
||||||
|
{
|
||||||
|
Poke();
|
||||||
|
if (isDesktop && e.ChangeType == WatcherChangeTypes.Created) NoteNew(e.FullPath);
|
||||||
|
};
|
||||||
|
w.Created += onChange;
|
||||||
|
w.Deleted += onChange;
|
||||||
|
w.Changed += onChange;
|
||||||
|
w.Renamed += (_, e) => { Poke(); if (isDesktop) NoteNew(e.FullPath); };
|
||||||
|
w.Error += (_, _) => Poke();
|
||||||
|
w.EnableRaisingEvents = true;
|
||||||
|
_watchers.Add(w);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NoteNew(string path)
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(path);
|
||||||
|
if (name.StartsWith('~') || name.EndsWith(".tmp", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
name.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase)) return;
|
||||||
|
lock (_lock) _newOnDesktop.Add(path);
|
||||||
|
_newTimer.Dispatcher.BeginInvoke(() => { _newTimer.Stop(); _newTimer.Start(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Poke() => _changedTimer.Dispatcher.BeginInvoke(() => { _changedTimer.Stop(); _changedTimer.Start(); });
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var w in _watchers) w.Dispose();
|
||||||
|
_changedTimer.Stop();
|
||||||
|
_newTimer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AutoSorter
|
||||||
|
{
|
||||||
|
/// <summary>Erste passende Regel → Kategoriename (nur existierende Kategorien).</summary>
|
||||||
|
public static string? Match(string path)
|
||||||
|
{
|
||||||
|
var name = Storage.DisplayName(path);
|
||||||
|
var cats = Storage.Categories();
|
||||||
|
foreach (var r in AppSettings.Current.Rules)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(r.Contains)) continue;
|
||||||
|
if (!name.Contains(r.Contains.Trim(), StringComparison.CurrentCultureIgnoreCase)) continue;
|
||||||
|
var cat = cats.FirstOrDefault(c => string.Equals(c, r.Category, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (cat is not null) return cat;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Baut DeskCore und den Installer.
|
||||||
|
# DeskCore.exe -> ..\DeskCore\ (direkt startbar)
|
||||||
|
# DeskCore-Setup.exe -> ..\DeskCore-Installer\ (zum Installieren/Weitergeben)
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
$root = $PSScriptRoot
|
||||||
|
$common = @("-c", "Release", "-r", "win-x64", "-p:SelfContained=false", "-p:PublishSingleFile=true", "-p:DebugType=none", "--nologo", "-v", "q")
|
||||||
|
|
||||||
|
Write-Host "1/2 DeskCore wird gebaut ..." -ForegroundColor Cyan
|
||||||
|
dotnet publish "$root\DeskCore.csproj" @common -o "$root\artifacts\app"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Build der App fehlgeschlagen" }
|
||||||
|
New-Item -ItemType Directory -Force "$root\..\DeskCore" | Out-Null
|
||||||
|
Copy-Item "$root\artifacts\app\*" "$root\..\DeskCore\" -Force
|
||||||
|
|
||||||
|
Write-Host "2/2 Installer wird gebaut ..." -ForegroundColor Cyan
|
||||||
|
dotnet publish "$root\Setup\DeskCoreSetup.csproj" @common -o "$root\..\DeskCore-Installer"
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "Build des Installers fehlgeschlagen" }
|
||||||
|
|
||||||
|
Get-Item "$root\..\DeskCore-Installer\DeskCore-Setup.exe" | Select-Object Name, @{n="Größe";e={"{0:N0} KB" -f ($_.Length/1KB)}}
|
||||||
|
Write-Host "Fertig." -ForegroundColor Green
|
||||||
Reference in New Issue
Block a user