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:
2026-10-01 20:19:32 +02:00
co-authored by Claude Opus 5.5
commit 77bb3e6b9d
36 changed files with 5894 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
# Build-Ausgaben
bin/
obj/
artifacts/
Setup/bin/
Setup/obj/
# Visual Studio / Rider
.vs/
.idea/
*.user
*.suo
+19
View File
@@ -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
View File
@@ -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);
}
}
+108
View File
@@ -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();
}
}
+39
View File
@@ -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>
+129
View File
@@ -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);
}
}
}
+6
View File
@@ -0,0 +1,6 @@
<Project>
<!-- Eine Versionsnummer für App und Installer – nur hier ändern -->
<PropertyGroup>
<Version>1.0.0</Version>
</PropertyGroup>
</Project>
+65
View File
@@ -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="&#xE712;" 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>
+278
View File
@@ -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();
}
}
+138
View File
@@ -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;
}
}
+49
View File
@@ -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
View File
@@ -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
View File
@@ -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="&#xE8A1;" 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="&#xE700;" 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="&#xE73E;" 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="&#xE76C;" 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="&#xE70D;" 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="&#xE74D;" 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="&#xE921;" Click="Minimize_Click"
WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Minimieren"/>
<Button x:Name="MaxButton" Style="{StaticResource CaptionButton}" Content="&#xE922;" Click="Maximize_Click"
WindowChrome.IsHitTestVisibleInChrome="True" ToolTip="Maximieren"/>
<Button Style="{StaticResource CloseCaptionButton}" Content="&#xE8BB;" 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="&#xE710;" 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="&#xE710;" 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="&#xE713;" 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="&#xE721;" 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="&#xE711;" 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="&#xE7A7;" 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="&#xE8A9;" 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="&#xE72C;" 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="&#xE70F;" 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="&#xE8A1;" 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="&#xED25;" 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="&#xE74D;" 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="&#xE946;" 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="&#xE706;" 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="&#xE80A;" 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 &amp; 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="&#xE7E8;" 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="&#xE8A1;" 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="&#xE8BB;" 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="&#xE765;" 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="&#xE9E9;" 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="&#xE74D;" 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="&#xE710;" 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="&#xE8A1;" 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="&#xE898;" 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="&#xE896;" FontFamily="{DynamicResource IconFont}" FontSize="14" Margin="0,0,8,0" VerticalAlignment="Center"/>
<TextBlock Text="Wiederherstellen…"/>
</StackPanel>
</Button>
</StackPanel>
<TextBlock DockPanel.Dock="Left" Text="&#xE8C8;" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Kategorien sichern &amp; 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="&#xE7E8;" 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="&#xE73E;" 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
View File
File diff suppressed because it is too large Load Diff
+76
View File
@@ -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]);
}
}
+97
View File
@@ -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;
}
+138
View File
@@ -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
View File
@@ -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));
}
}
+18
View File
@@ -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>
+70
View File
@@ -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));
}
+32
View File
@@ -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>
+368
View File
@@ -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",
};
}
+165
View File
@@ -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="&#xE921;" Click="Minimize_Click"
WindowChrome.IsHitTestVisibleInChrome="True"/>
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="&#xE8BB;" 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="&#xE946;" 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="&#xE73E;" 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>
+249
View File
@@ -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
View File
@@ -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
View File
@@ -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)));
}
}
+74
View File
@@ -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;
}
}
+27
View File
@@ -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>
+27
View File
@@ -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>
+651
View File
@@ -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="&#xE76C;" 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="&#xE73E;" 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>
+49
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+19
View File
@@ -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