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
+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);
}
}