Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f48097707 | ||
|
|
0d7ff9e9c3 | ||
|
|
70c3b8288b | ||
|
|
8efd5bc497 | ||
|
|
c0b552ef39 | ||
|
|
58461c1211 | ||
|
|
5c4fe0a029 | ||
|
|
51bfaa73b0 | ||
|
|
fcc0bedfab | ||
|
|
c78a8cc6f1 | ||
|
|
624d9c7a79 | ||
|
|
18128817d4 | ||
|
|
5b6f6881ee | ||
|
|
5058ceb6e5 | ||
|
|
c22bea3b39 | ||
|
|
56352ede19 | ||
|
|
e62822a544 | ||
|
|
100caee6ed | ||
|
|
3ed91b8406 | ||
|
|
d8c18425fe | ||
|
|
b7c38cd380 | ||
|
|
eb8c33e990 | ||
|
|
8638b115e8 | ||
|
|
33cfe843f1 | ||
|
|
f4ff96a0e4 | ||
|
|
483af97e6b | ||
|
|
6d610ca447 | ||
|
|
d87c360d2d | ||
|
|
33eaacc293 | ||
|
|
28b031ba58 | ||
|
|
99a1a45c7a | ||
|
|
6fcacbb577 | ||
|
|
eec5f2a4ca | ||
|
|
20b0812c82 | ||
|
|
0462d3fae2 | ||
|
|
020c83180c | ||
|
|
6791a114b2 | ||
|
|
7183e39d21 | ||
|
|
d8f6a70c2c | ||
|
|
ba0d990d69 | ||
|
|
9a5e954b73 | ||
|
|
5cb7224a2d | ||
|
|
0318a3dc20 | ||
|
|
6b713b08ad | ||
|
|
271701bfda | ||
|
|
85f3f597bb | ||
|
|
17dcfb16ea | ||
|
|
29fe9816a3 | ||
|
|
e170dcd90c | ||
|
|
077eba609e | ||
|
|
ff8b5b9484 | ||
|
|
1e8b538a71 | ||
|
|
a1c2bc38ba | ||
|
|
4c44b7769d | ||
|
|
54aee17a69 | ||
|
|
55baffb4d8 | ||
|
|
2df0e95fd6 | ||
|
|
84d835828c | ||
|
|
a434d48cb0 | ||
|
|
97e5b87ef3 | ||
|
|
fffc70d1fb | ||
|
|
1b0c3ee187 | ||
|
|
48ed7eef5c | ||
|
|
ee92f6cb9f | ||
|
|
efd6100ab9 | ||
|
|
beee89114f | ||
|
|
3e2b10ce6c | ||
|
|
65c293a0e7 | ||
|
|
67373a378f | ||
|
|
b21a03afad | ||
|
|
3739eb2c46 | ||
|
|
b1ef0c4190 | ||
|
|
2cf6ebe0a5 | ||
|
|
ddc30b0fe9 | ||
|
|
9b843ecab1 |
+209
-128
@@ -1,128 +1,209 @@
|
||||
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();
|
||||
|
||||
// Bildschirm abgesteckt/Auflösung geändert → Fenster und Boxen wieder in den sichtbaren Bereich holen
|
||||
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += (_, _) => Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
Win.EnsureOnScreen();
|
||||
FenceManager.EnsureOnScreen();
|
||||
});
|
||||
|
||||
Win.CheckForUpdatesOnStartup();
|
||||
}
|
||||
|
||||
/// <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?.CancelBusyAndWait(); // z. B. große Kopie abbrechen, halbe Dateien aufräumen
|
||||
Win?.SavePlacement();
|
||||
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);
|
||||
}
|
||||
}
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
private const string MutexName = "DeskCore.Instanz";
|
||||
|
||||
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 Hotkey? _peekHotkey;
|
||||
private static Hotkey? _pagePrev, _pageNext, _quickSearch;
|
||||
private static System.Windows.Threading.DispatcherTimer? _desktopTimer;
|
||||
private static Watcher? _watcher;
|
||||
private Mutex? _mutex;
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
// Nur eine Instanz: weitere Aufrufe reichen ihren Befehl (oder "Fenster zeigen") an die laufende weiter
|
||||
_mutex = new Mutex(true, MutexName, out bool first);
|
||||
if (!first)
|
||||
{
|
||||
Commands.Send(e.Args);
|
||||
Shutdown();
|
||||
return;
|
||||
}
|
||||
Migration.Run(); // alte "Desktop Sortierer"-Ordner übernehmen – vor allem anderen
|
||||
Commands.Listen(args => Dispatcher.BeginInvoke(() => HandleCommand(args)));
|
||||
|
||||
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();
|
||||
_peekHotkey = new Hotkey(hwnd, 0xB002);
|
||||
_peekHotkey.Pressed += FenceManager.TogglePeek;
|
||||
ApplyPeekHotkey();
|
||||
// Desktop-Seiten: Win + Alt + Pfeil links/rechts
|
||||
_pagePrev = new Hotkey(hwnd, 0xB003);
|
||||
_pagePrev.Pressed += () => FenceManager.NextPage(-1);
|
||||
_pagePrev.Register(Hotkey.ModWin | Hotkey.ModAlt, 0x25);
|
||||
_pageNext = new Hotkey(hwnd, 0xB004);
|
||||
_pageNext.Pressed += () => FenceManager.NextPage(1);
|
||||
_pageNext.Register(Hotkey.ModWin | Hotkey.ModAlt, 0x27);
|
||||
_quickSearch = new Hotkey(hwnd, 0xB005);
|
||||
_quickSearch.Pressed += QuickSearch.Toggle;
|
||||
ApplyQuickSearchHotkey();
|
||||
|
||||
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();
|
||||
Win.ScheduleDeadShortcutScan(); // z. B. Deinstallation hat die Desktop-Verknüpfung entfernt
|
||||
};
|
||||
_watcher.NewOnDesktop += Win.AutoSortNew;
|
||||
|
||||
Autostart.RefreshPath();
|
||||
|
||||
// Mit einem Befehl gestartet (z. B. Start-Gruppe per Desktop-Verknüpfung)? Dann ohne Fenster.
|
||||
if (Commands.WantsWindow(e.Args) || !AppSettings.Current.TrayEnabled) Win.Show();
|
||||
FenceManager.Sync();
|
||||
|
||||
// Bildschirm abgesteckt/Auflösung geändert → Fenster und Boxen wieder in den sichtbaren Bereich holen
|
||||
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += (_, _) => Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
Win.EnsureOnScreen();
|
||||
FenceManager.OnDisplayChanged();
|
||||
});
|
||||
|
||||
Win.CheckForUpdatesOnStartup();
|
||||
if (!Commands.WantsWindow(e.Args)) Dispatcher.BeginInvoke(() => HandleCommand(e.Args));
|
||||
Win.StartDeadShortcutWatch();
|
||||
DesktopToggle.Apply();
|
||||
|
||||
// Positionen der Desktop-Icons regelmäßig lesen (für Aufziehen, Doppelklick und die Icon-Tönung)
|
||||
_desktopTimer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(2) };
|
||||
_desktopTimer.Tick += (_, _) => RefreshDesktopIcons();
|
||||
_desktopTimer.Start();
|
||||
RefreshDesktopIcons();
|
||||
}
|
||||
|
||||
private static void RefreshDesktopIcons()
|
||||
{
|
||||
var s = AppSettings.Current;
|
||||
if (Quitting) return;
|
||||
PageScheduler.Tick();
|
||||
// Nur lesen, wenn eine Funktion die Positionen braucht
|
||||
if (s.DesktopTint is null && !s.DrawFences && !s.QuickHide) return;
|
||||
try { DesktopIcons.RefreshCache(); } catch { /* Explorer gerade neu gestartet o. Ä. */ }
|
||||
DesktopTint.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Befehl von der Befehlszeile oder einem weiteren Aufruf ausführen.</summary>
|
||||
private static void HandleCommand(string[] args)
|
||||
{
|
||||
if (Win is null || Quitting) return;
|
||||
if (Commands.Value(args, "--launch") is { } group) Win.LaunchGroupByName(group);
|
||||
else if (Commands.Value(args, "--snapshot") is { } snap) Win.RestoreSnapshotByName(snap);
|
||||
else if (Commands.Has(args, "--toggle-desktop")) DesktopToggle.Toggle();
|
||||
else if (Commands.Has(args, "--peek")) FenceManager.TogglePeek();
|
||||
else if (Commands.Has(args, "--search")) QuickSearch.Toggle();
|
||||
else if (Commands.Value(args, "--page") is { } page) SwitchPageByName(page);
|
||||
else if (Commands.WantsWindow(args)) Win.ShowFromBackground();
|
||||
}
|
||||
|
||||
/// <summary>DeskCore.exe --page "Name" oder --page 2.</summary>
|
||||
private static void SwitchPageByName(string page)
|
||||
{
|
||||
var pages = AppSettings.Current.Pages;
|
||||
int i = pages.FindIndex(p => string.Equals(p, page, StringComparison.OrdinalIgnoreCase));
|
||||
if (i < 0 && int.TryParse(page, out var n)) i = n - 1;
|
||||
if (i >= 0 && i < pages.Count) FenceManager.SwitchPage(i);
|
||||
else TrayIcon?.Notify("Seite nicht gefunden", $"Es gibt keine Desktop-Seite „{page}“.");
|
||||
}
|
||||
|
||||
public static bool QuickSearchRegistered { get; private set; }
|
||||
|
||||
/// <summary>Tastenkürzel der Schnellstart-Suche neu registrieren. false = von anderem Programm belegt.</summary>
|
||||
public static bool ApplyQuickSearchHotkey()
|
||||
{
|
||||
if (_quickSearch is null) return false;
|
||||
var s = AppSettings.Current;
|
||||
if (!s.QuickSearchEnabled) { _quickSearch.Unregister(); return QuickSearchRegistered = true; }
|
||||
return QuickSearchRegistered = _quickSearch.Register(s.QuickSearchModifiers, s.QuickSearchKey);
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
/// <summary>Peek-Kürzel neu registrieren. false = von anderem Programm belegt.</summary>
|
||||
public static bool ApplyPeekHotkey()
|
||||
{
|
||||
if (_peekHotkey is null) return false;
|
||||
var s = AppSettings.Current;
|
||||
if (!s.PeekEnabled) { _peekHotkey.Unregister(); return true; }
|
||||
return _peekHotkey.Register(s.PeekModifiers, s.PeekKey);
|
||||
}
|
||||
|
||||
public static void ApplyTray()
|
||||
{
|
||||
if (TrayIcon is not null) TrayIcon.Visible = AppSettings.Current.TrayEnabled;
|
||||
}
|
||||
|
||||
public static void Quit()
|
||||
{
|
||||
if (Quitting) return;
|
||||
Quitting = true;
|
||||
Win?.CancelBusyAndWait(); // z. B. große Kopie abbrechen, halbe Dateien aufräumen
|
||||
Win?.SavePlacement();
|
||||
_desktopTimer?.Stop();
|
||||
DesktopToggle.Stop();
|
||||
DesktopToggle.RestoreOnExit();
|
||||
_peekHotkey?.Dispose();
|
||||
_pagePrev?.Dispose();
|
||||
_quickSearch?.Dispose();
|
||||
Usage.Flush();
|
||||
_pageNext?.Dispose();
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,6 +170,12 @@ public static class Backup
|
||||
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);
|
||||
foreach (var g in from.LaunchGroups ?? [])
|
||||
if (!s.LaunchGroups.Any(x => string.Equals(x.Name, g.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
s.LaunchGroups.Add(g);
|
||||
foreach (var snap in from.Snapshots ?? [])
|
||||
if (!s.Snapshots.Any(x => string.Equals(x.Name, snap.Name, StringComparison.OrdinalIgnoreCase)))
|
||||
s.Snapshots.Add(snap);
|
||||
s.Save();
|
||||
}
|
||||
}
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>
|
||||
/// Auswahl nach dem Aufziehen einer Box: Welche Kategorie (oder welcher Ordner) soll hinein?
|
||||
/// Ein eigenes Fenster statt eines Kontextmenüs – ein Menü ohne aktives Fenster schließt sich sonst gelegentlich von selbst.
|
||||
/// </summary>
|
||||
public sealed class BoxPicker : Window
|
||||
{
|
||||
private static BoxPicker? _open;
|
||||
private readonly long _shownAt = Environment.TickCount64;
|
||||
private bool _done;
|
||||
|
||||
private static AppSettings S => AppSettings.Current;
|
||||
|
||||
/// <param name="area">Fläche der neuen Box in WPF-Einheiten.</param>
|
||||
/// <param name="create">Legt die Box mit dem gewählten Tab an.</param>
|
||||
public static void Show(Rect area, Action<FenceTab> create)
|
||||
{
|
||||
_open?.Finish(null);
|
||||
_open = new BoxPicker(area, create);
|
||||
_open.Show();
|
||||
}
|
||||
|
||||
private readonly Action<FenceTab> _create;
|
||||
|
||||
private BoxPicker(Rect area, Action<FenceTab> create)
|
||||
{
|
||||
_create = create;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
ShowInTaskbar = false;
|
||||
Topmost = true;
|
||||
SizeToContent = SizeToContent.WidthAndHeight;
|
||||
Title = "Neue Box";
|
||||
|
||||
var res = Application.Current.Resources;
|
||||
var list = new StackPanel();
|
||||
|
||||
list.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Neue Box hier", FontSize = 14, FontWeight = FontWeights.SemiBold, Margin = new Thickness(12, 8, 12, 0),
|
||||
Foreground = (Brush)res["TextBrush"],
|
||||
});
|
||||
list.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Was soll hinein?", FontSize = 12, Margin = new Thickness(12, 2, 12, 6),
|
||||
Foreground = (Brush)res["TextSecondaryBrush"],
|
||||
});
|
||||
|
||||
var cats = Storage.Categories();
|
||||
var catPanel = new StackPanel();
|
||||
foreach (var c in cats)
|
||||
{
|
||||
var meta = S.Categories.GetValueOrDefault(c);
|
||||
// Pro Seite steht eine Kategorie nur in einer Box – andere Seiten behalten ihre
|
||||
string? hint = S.HasFence(c) ? "wird aus ihrer Box auf dieser Seite hierher geholt" : null;
|
||||
catPanel.Children.Add(Row(meta?.Glyph ?? "", CategoryColors.Brush(meta?.Color), c, hint,
|
||||
() => Pick(new FenceTab { Value = c })));
|
||||
}
|
||||
if (cats.Count > 0)
|
||||
{
|
||||
list.Children.Add(new ScrollViewer
|
||||
{
|
||||
Content = catPanel, MaxHeight = 360, VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
});
|
||||
list.Children.Add(Divider());
|
||||
}
|
||||
list.Children.Add(Row("", null, "Zuletzt benutzt", "zeigt, was du zuletzt gestartet hast",
|
||||
() => Pick(new FenceTab { Kind = FenceTab.RecentKind })));
|
||||
list.Children.Add(Row("", null, "Neue Kategorie …", null, () => PickLater(() =>
|
||||
App.Win?.CreateCategoryInteractive() is { } name ? new FenceTab { Value = name } : null)));
|
||||
list.Children.Add(Row("", null, "Ordner-Portal …", null, () => PickLater(() =>
|
||||
FenceManager.PickFolder() is { } folder ? new FenceTab { Kind = FenceTab.FolderKind, Value = folder } : null)));
|
||||
list.Children.Add(Divider());
|
||||
list.Children.Add(Row("", null, "Abbrechen", null, () => Finish(null)));
|
||||
|
||||
Content = new Border
|
||||
{
|
||||
Margin = new Thickness(14), Padding = new Thickness(4), MinWidth = 240, CornerRadius = new CornerRadius(8),
|
||||
Background = (Brush)res["FlyoutBrush"], BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(1),
|
||||
Effect = (System.Windows.Media.Effects.Effect)res["FlyoutShadow"],
|
||||
Child = list,
|
||||
};
|
||||
|
||||
// Neben dem Mauszeiger öffnen, aber immer ganz auf dem Bildschirm
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
var mouse = GetCursorPos(out var p) ? new Point(p.X / Screens.Scale, p.Y / Screens.Scale) : area.BottomRight;
|
||||
var wa = Screens.WorkAreaAt(mouse);
|
||||
Left = Math.Clamp(mouse.X - 14, wa.Left, Math.Max(wa.Left, wa.Right - ActualWidth));
|
||||
Top = Math.Clamp(mouse.Y - 14, wa.Top, Math.Max(wa.Top, wa.Bottom - ActualHeight));
|
||||
BringToFront();
|
||||
};
|
||||
KeyDown += (_, e) => { if (e.Key == Key.Escape) Finish(null); };
|
||||
// Klick daneben schließt – aber nicht durch ein kurzes Flackern direkt nach dem Öffnen
|
||||
Deactivated += (_, _) =>
|
||||
{
|
||||
if (_done) return;
|
||||
if (Environment.TickCount64 - _shownAt < 400) BringToFront();
|
||||
else Finish(null);
|
||||
};
|
||||
Closed += (_, _) => { if (_open == this) _open = null; DrawBoxOverlay.Hide(); };
|
||||
}
|
||||
|
||||
private Button Row(string glyph, Brush? glyphBrush, string text, string? hint, Action click)
|
||||
{
|
||||
var res = Application.Current.Resources;
|
||||
var content = new DockPanel();
|
||||
var icon = new TextBlock
|
||||
{
|
||||
Text = glyph, FontFamily = (FontFamily)res["IconFont"], FontSize = 15, Width = 20, Margin = new Thickness(0, 0, 12, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center, Foreground = glyphBrush ?? (Brush)res["TextBrush"],
|
||||
};
|
||||
DockPanel.SetDock(icon, Dock.Left);
|
||||
content.Children.Add(icon);
|
||||
var texts = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
texts.Children.Add(new TextBlock { Text = text, Foreground = (Brush)res["TextBrush"] });
|
||||
if (hint is not null)
|
||||
texts.Children.Add(new TextBlock { Text = hint, FontSize = 11, Foreground = (Brush)res["TextTertiaryBrush"] });
|
||||
content.Children.Add(texts);
|
||||
|
||||
var b = new Button
|
||||
{
|
||||
Style = (Style)res["SubtleButton"], Content = content, HorizontalContentAlignment = HorizontalAlignment.Stretch,
|
||||
Padding = new Thickness(10, 7, 10, 7), Focusable = true,
|
||||
};
|
||||
b.Click += (_, _) => click();
|
||||
return b;
|
||||
}
|
||||
|
||||
private static Border Divider() => new()
|
||||
{
|
||||
Height = 1, Margin = new Thickness(4, 4, 4, 4), Background = (Brush)Application.Current.Resources["ControlStrokeBrush"],
|
||||
};
|
||||
|
||||
private void Pick(FenceTab tab) => Finish(tab);
|
||||
|
||||
/// <summary>Für Auswahlen mit eigenem Dialog: erst die Auswahl schließen, Rahmen aber stehen lassen.</summary>
|
||||
private void PickLater(Func<FenceTab?> ask)
|
||||
{
|
||||
_done = true;
|
||||
Hide();
|
||||
var tab = ask();
|
||||
if (tab is not null) _create(tab);
|
||||
Close();
|
||||
}
|
||||
|
||||
private void Finish(FenceTab? tab)
|
||||
{
|
||||
if (_done) return;
|
||||
_done = true;
|
||||
Close();
|
||||
if (tab is not null) _create(tab);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DeskCore läuft im Hintergrund – Windows lässt es nicht ohne Weiteres nach vorne. Ein kurzer Alt-Tastendruck
|
||||
/// gibt die Erlaubnis (bewährter Weg, der auch von Windows selbst so akzeptiert wird).
|
||||
/// </summary>
|
||||
private void BringToFront()
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
if (GetForegroundWindow() != hwnd)
|
||||
{
|
||||
// Loslassen erst nach dem Wechsel – so landet es bei uns und öffnet nirgends ein Menü
|
||||
keybd_event(VK_MENU, 0, 0, UIntPtr.Zero);
|
||||
SetForegroundWindow(hwnd);
|
||||
keybd_event(VK_MENU, 0, KEYEVENTF_KEYUP, UIntPtr.Zero);
|
||||
}
|
||||
Activate();
|
||||
Focus();
|
||||
}
|
||||
|
||||
private const byte VK_MENU = 0x12;
|
||||
private const uint KEYEVENTF_KEYUP = 0x2;
|
||||
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; }
|
||||
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT p);
|
||||
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
|
||||
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
|
||||
[DllImport("user32.dll")] private static extern void keybd_event(byte vk, byte scan, uint flags, UIntPtr extra);
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>
|
||||
/// Befehlszeile von DeskCore, z. B. für Desktop-Verknüpfungen und Skripte:
|
||||
/// DeskCore.exe --launch "Arbeit" Start-Gruppe starten
|
||||
/// DeskCore.exe --snapshot "Name" Box-Anordnung wiederherstellen
|
||||
/// DeskCore.exe --toggle-desktop Desktop-Icons und Boxen ein-/ausblenden
|
||||
/// DeskCore.exe --peek Boxen vor alle Fenster holen
|
||||
/// DeskCore.exe --minimized im Infobereich starten
|
||||
/// Läuft DeskCore schon, wird der Befehl an die laufende Instanz weitergereicht.
|
||||
/// </summary>
|
||||
public static class Commands
|
||||
{
|
||||
private const string PipeName = "DeskCore.Befehle";
|
||||
private const char Separator = '\u001F';
|
||||
|
||||
public static string? Value(string[] args, string name)
|
||||
{
|
||||
int i = Array.FindIndex(args, a => a.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
return i >= 0 && i + 1 < args.Length ? args[i + 1] : null;
|
||||
}
|
||||
|
||||
public static bool Has(string[] args, string name) => args.Any(a => a.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
/// <summary>Soll für diesen Aufruf das Hauptfenster erscheinen?</summary>
|
||||
public static bool WantsWindow(string[] args) =>
|
||||
!Has(args, "--minimized") && Value(args, "--launch") is null && Value(args, "--snapshot") is null &&
|
||||
!Has(args, "--toggle-desktop") && !Has(args, "--peek");
|
||||
|
||||
/// <summary>An die laufende Instanz senden.</summary>
|
||||
public static bool Send(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out);
|
||||
client.Connect(3000);
|
||||
using var w = new StreamWriter(client);
|
||||
w.Write(string.Join(Separator, args));
|
||||
w.Flush();
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empfängt Befehle weiterer Aufrufe (im Hintergrund). Die Pipe ist standardmäßig nur für den
|
||||
/// eigenen Benutzer beschreibbar – andere Benutzer können DeskCore keine Befehle schicken.
|
||||
/// </summary>
|
||||
public static void Listen(Action<string[]> onCommand)
|
||||
{
|
||||
new Thread(() =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var server = new NamedPipeServerStream(PipeName, PipeDirection.In, 1);
|
||||
server.WaitForConnection();
|
||||
using var r = new StreamReader(server);
|
||||
var text = r.ReadToEnd();
|
||||
onCommand(text.Length == 0 ? [] : text.Split(Separator));
|
||||
}
|
||||
catch { Thread.Sleep(500); }
|
||||
}
|
||||
}) { IsBackground = true, Name = "Befehle" }.Start();
|
||||
}
|
||||
}
|
||||
+234
@@ -0,0 +1,234 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Ein Icon auf dem echten Windows-Desktop (Bildschirm-Pixel).</summary>
|
||||
public record DesktopIcon(string Path, int X, int Y, int IconSize, int CellWidth);
|
||||
|
||||
/// <summary>
|
||||
/// Liest Positionen und Größe der echten Desktop-Icons über die offizielle Shell-Schnittstelle (IFolderView)
|
||||
/// aus – ohne etwas in den Explorer einzuschleusen. Nur auf dem UI-Thread (STA) aufrufen.
|
||||
/// </summary>
|
||||
public static class DesktopIcons
|
||||
{
|
||||
/// <summary>Alle Desktop-Icons mit Position – oder leer, wenn der Desktop gerade nicht erreichbar ist.</summary>
|
||||
public static List<DesktopIcon> Read()
|
||||
{
|
||||
var result = new List<DesktopIcon>();
|
||||
try
|
||||
{
|
||||
var view = DesktopFolderView(out var listView);
|
||||
if (view is null || listView == IntPtr.Zero) return result;
|
||||
var view2 = view as IFolderView2;
|
||||
int iconSize = 48;
|
||||
if (view2 is not null && view2.GetViewModeAndIconSize(out _, out var size) == 0) iconSize = size;
|
||||
view.GetSpacing(out var spacing);
|
||||
var origin = new POINT();
|
||||
ClientToScreen(listView, ref origin);
|
||||
|
||||
view.ItemCount(SVGIO_ALLVIEW, out int count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (view.Item(i, out var pidl) != 0 || pidl == IntPtr.Zero) continue;
|
||||
try
|
||||
{
|
||||
if (view.GetItemPosition(pidl, out var pos) != 0) continue;
|
||||
string path = "";
|
||||
if (view2 is not null && view2.GetItem(i, typeof(IShellItem).GUID, out var obj) == 0 && obj is IShellItem item)
|
||||
{
|
||||
if (item.GetDisplayName(SIGDN_FILESYSPATH, out var p) == 0)
|
||||
{
|
||||
path = Marshal.PtrToStringUni(p) ?? "";
|
||||
Marshal.FreeCoTaskMem(p);
|
||||
}
|
||||
Marshal.ReleaseComObject(item);
|
||||
}
|
||||
result.Add(new DesktopIcon(path, origin.X + pos.X, origin.Y + pos.Y, iconSize, spacing.X));
|
||||
}
|
||||
finally { Marshal.FreeCoTaskMem(pidl); }
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rechteck des Icon-Bildes in Bildschirm-Pixeln. Die Position aus IFolderView ist bereits die linke obere
|
||||
/// Ecke des Bildes (gemessen: Eintrag beginnt (Abstand − Bildgröße) / 2 weiter links, 3 px weiter oben).
|
||||
/// </summary>
|
||||
public static (int L, int T, int Size) IconRect(DesktopIcon i) => (i.X, i.Y + IconTopOffset, i.IconSize);
|
||||
|
||||
/// <summary>Feinkorrektur nach unten (Windows zeichnet das Bild mit kleinem Rand).</summary>
|
||||
public static int IconTopOffset { get; set; } = 3;
|
||||
|
||||
/// <summary>Gesamte Fläche eines Eintrags (Bild + Beschriftung), z. B. für Klick-Tests.</summary>
|
||||
public static (int L, int T, int W, int H) CellRect(DesktopIcon i)
|
||||
{
|
||||
int pad = Math.Max(0, (i.CellWidth - i.IconSize) / 2);
|
||||
return (i.X - pad, i.Y, i.CellWidth, i.IconSize + 44);
|
||||
}
|
||||
|
||||
/// <summary>Zuletzt gelesene Icons (für schnelle Prüfungen aus dem Maus-Hook, ohne COM-Aufruf).</summary>
|
||||
public static volatile IReadOnlyList<DesktopIcon> Cached = [];
|
||||
|
||||
/// <summary>Zwischenspeicher auffrischen (UI-Thread). Gibt true zurück, wenn sich etwas geändert hat.</summary>
|
||||
public static bool RefreshCache()
|
||||
{
|
||||
var fresh = Read();
|
||||
bool changed = fresh.Count != Cached.Count || fresh.Where((x, n) => x != Cached[n]).Any();
|
||||
Cached = fresh;
|
||||
return changed;
|
||||
}
|
||||
|
||||
/// <param name="margin">Zusätzlicher Rand in Pixeln, damit Klicks knapp neben Bild oder Beschriftung auch zählen.</param>
|
||||
public static bool HitsIcon(int x, int y, int margin = 0) => Cached.Any(i =>
|
||||
{
|
||||
var (l, t, w, h) = CellRect(i);
|
||||
return x >= l - margin && x < l + w + margin && y >= t - margin && y < t + h + margin;
|
||||
});
|
||||
|
||||
// ---------- Desktop-Ansicht finden ----------
|
||||
|
||||
private static IFolderView? DesktopFolderView(out IntPtr listView)
|
||||
{
|
||||
listView = IntPtr.Zero;
|
||||
var shellWindows = (IShellWindows)Activator.CreateInstance(Type.GetTypeFromCLSID(CLSID_ShellWindows)!)!;
|
||||
try
|
||||
{
|
||||
object loc = CSIDL_DESKTOP, empty = new object();
|
||||
var disp = shellWindows.FindWindowSW(ref loc, ref empty, SWC_DESKTOP, out _, SWFO_NEEDDISPATCH);
|
||||
if (disp is not IServiceProvider sp) return null;
|
||||
var sidBrowser = SID_STopLevelBrowser;
|
||||
var iidBrowser = typeof(IShellBrowser).GUID;
|
||||
if (sp.QueryService(ref sidBrowser, ref iidBrowser, out var browserObj) != 0) return null;
|
||||
var browser = (IShellBrowser)browserObj;
|
||||
if (browser.QueryActiveShellView(out var shellView) != 0 || shellView is null) return null;
|
||||
shellView.GetWindow(out var defView);
|
||||
listView = FindWindowEx(defView, IntPtr.Zero, "SysListView32", null);
|
||||
return shellView as IFolderView;
|
||||
}
|
||||
finally { Marshal.ReleaseComObject(shellWindows); }
|
||||
}
|
||||
|
||||
// ---------- COM-Schnittstellen (nur die benötigten Methoden in vtable-Reihenfolge) ----------
|
||||
|
||||
private const int CSIDL_DESKTOP = 0, SWC_DESKTOP = 8, SWFO_NEEDDISPATCH = 1, SVGIO_ALLVIEW = 2;
|
||||
private const uint SIGDN_FILESYSPATH = 0x80058000;
|
||||
private static readonly Guid CLSID_ShellWindows = new("9BA05972-F6A8-11CF-A442-00A0C90A8F39");
|
||||
private static readonly Guid SID_STopLevelBrowser = new("4C96BE40-915C-11CF-99D3-00AA004AE837");
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; }
|
||||
|
||||
[ComImport, Guid("85CB6900-4D95-11CF-960C-0080C7F4EE85"), InterfaceType(ComInterfaceType.InterfaceIsDual)]
|
||||
private interface IShellWindows
|
||||
{
|
||||
int Count { get; }
|
||||
[return: MarshalAs(UnmanagedType.IDispatch)] object Item(object index);
|
||||
[return: MarshalAs(UnmanagedType.IUnknown)] object _NewEnum();
|
||||
void Register([MarshalAs(UnmanagedType.IDispatch)] object pid, int hwnd, int swClass, out int plCookie);
|
||||
void RegisterPending(int lThreadId, ref object pvarloc, ref object pvarlocRoot, int swClass, out int plCookie);
|
||||
void Revoke(int lCookie);
|
||||
void OnNavigate(int lCookie, ref object pvarLoc);
|
||||
void OnActivated(int lCookie, short fActive);
|
||||
[return: MarshalAs(UnmanagedType.IDispatch)]
|
||||
object FindWindowSW(ref object pvarLoc, ref object pvarLocRoot, int swClass, out int pHWND, int swfwOptions);
|
||||
}
|
||||
|
||||
[ComImport, Guid("6D5140C1-7436-11CE-8034-00AA006009FA"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IServiceProvider
|
||||
{
|
||||
[PreserveSig] int QueryService(ref Guid guidService, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppvObject);
|
||||
}
|
||||
|
||||
[ComImport, Guid("000214E2-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IShellBrowser
|
||||
{
|
||||
void GetWindow(out IntPtr phwnd);
|
||||
void ContextSensitiveHelp(bool fEnterMode);
|
||||
void InsertMenusSB(IntPtr hmenuShared, IntPtr lpMenuWidths);
|
||||
void SetMenuSB(IntPtr hmenuShared, IntPtr holemenuRes, IntPtr hwndActiveObject);
|
||||
void RemoveMenusSB(IntPtr hmenuShared);
|
||||
void SetStatusTextSB(IntPtr pszStatusText);
|
||||
void EnableModelessSB(bool fEnable);
|
||||
void TranslateAcceleratorSB(IntPtr pmsg, ushort wID);
|
||||
void BrowseObject(IntPtr pidl, uint wFlags);
|
||||
void GetViewStateStream(uint grfMode, out IntPtr ppStrm);
|
||||
void GetControlWindow(uint id, out IntPtr phwnd);
|
||||
void SendControlMsg(uint id, uint uMsg, IntPtr wParam, IntPtr lParam, out IntPtr pret);
|
||||
[PreserveSig] int QueryActiveShellView(out IShellView ppshv);
|
||||
}
|
||||
|
||||
[ComImport, Guid("000214E3-0000-0000-C000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IShellView
|
||||
{
|
||||
void GetWindow(out IntPtr phwnd);
|
||||
}
|
||||
|
||||
[ComImport, Guid("CDE725B0-CCC9-4519-917E-325D72FAB4CE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IFolderView
|
||||
{
|
||||
[PreserveSig] int GetCurrentViewMode(out uint pViewMode);
|
||||
[PreserveSig] int SetCurrentViewMode(uint ViewMode);
|
||||
[PreserveSig] int GetFolder(ref Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int Item(int iItemIndex, out IntPtr ppidl);
|
||||
[PreserveSig] int ItemCount(uint uFlags, out int pcItems);
|
||||
[PreserveSig] int Items(uint uFlags, ref Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int GetSelectionMarkedItem(out int piItem);
|
||||
[PreserveSig] int GetFocusedItem(out int piItem);
|
||||
[PreserveSig] int GetItemPosition(IntPtr pidl, out POINT ppt);
|
||||
[PreserveSig] int GetSpacing(out POINT ppt);
|
||||
}
|
||||
|
||||
[ComImport, Guid("1AF3A467-214F-4298-908E-06B03E0B39F9"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IFolderView2
|
||||
{
|
||||
// IFolderView
|
||||
[PreserveSig] int GetCurrentViewMode(out uint pViewMode);
|
||||
[PreserveSig] int SetCurrentViewMode(uint ViewMode);
|
||||
[PreserveSig] int GetFolder(ref Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int Item(int iItemIndex, out IntPtr ppidl);
|
||||
[PreserveSig] int ItemCount(uint uFlags, out int pcItems);
|
||||
[PreserveSig] int Items(uint uFlags, ref Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int GetSelectionMarkedItem(out int piItem);
|
||||
[PreserveSig] int GetFocusedItem(out int piItem);
|
||||
[PreserveSig] int GetItemPosition(IntPtr pidl, out POINT ppt);
|
||||
[PreserveSig] int GetSpacing(out POINT ppt);
|
||||
[PreserveSig] int GetDefaultSpacing(out POINT ppt);
|
||||
[PreserveSig] int GetAutoArrange();
|
||||
[PreserveSig] int SelectItem(int iItem, uint dwFlags);
|
||||
[PreserveSig] int SelectAndPositionItems(uint cidl, IntPtr apidl, IntPtr apt, uint dwFlags);
|
||||
// IFolderView2
|
||||
[PreserveSig] int SetGroupBy(IntPtr key, bool fAscending);
|
||||
[PreserveSig] int GetGroupBy(IntPtr pkey, out bool pfAscending);
|
||||
[PreserveSig] int SetViewProperty(IntPtr pidl, IntPtr propkey, IntPtr propvar);
|
||||
[PreserveSig] int GetViewProperty(IntPtr pidl, IntPtr propkey, IntPtr ppropvar);
|
||||
[PreserveSig] int SetTileViewProperties(IntPtr pidl, IntPtr pszPropList);
|
||||
[PreserveSig] int SetExtendedTileViewProperties(IntPtr pidl, IntPtr pszPropList);
|
||||
[PreserveSig] int SetText(int iType, IntPtr pwszText);
|
||||
[PreserveSig] int SetCurrentFolderFlags(uint dwMask, uint dwFlags);
|
||||
[PreserveSig] int GetCurrentFolderFlags(out uint pdwFlags);
|
||||
[PreserveSig] int GetSortColumnCount(out int pcColumns);
|
||||
[PreserveSig] int SetSortColumns(IntPtr rgSortColumns, int cColumns);
|
||||
[PreserveSig] int GetSortColumns(IntPtr rgSortColumns, int cColumns);
|
||||
[PreserveSig] int GetItem(int iItem, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.Interface)] out object ppv);
|
||||
[PreserveSig] int GetVisibleItem(int iStart, bool fPrevious, out int piItem);
|
||||
[PreserveSig] int GetSelectedItem(int iStart, out int piItem);
|
||||
[PreserveSig] int GetSelection(bool fNoneImpliesFolder, out IntPtr ppsia);
|
||||
[PreserveSig] int GetSelectionState(IntPtr pidl, out uint pdwFlags);
|
||||
[PreserveSig] int InvokeVerbOnSelection(IntPtr pszVerb);
|
||||
[PreserveSig] int SetViewModeAndIconSize(int uViewMode, int iImageSize);
|
||||
[PreserveSig] int GetViewModeAndIconSize(out int puViewMode, out int piImageSize);
|
||||
}
|
||||
|
||||
[ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IShellItem
|
||||
{
|
||||
[PreserveSig] int BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv);
|
||||
[PreserveSig] int GetParent(out IShellItem ppsi);
|
||||
[PreserveSig] int GetDisplayName(uint sigdnName, out IntPtr ppszName);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string? cls, string? title);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Interop;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Gemeinsame Hilfen für klickdurchlässige Overlay-Fenster.</summary>
|
||||
internal static class Overlay
|
||||
{
|
||||
public static Window Create(bool topmost)
|
||||
{
|
||||
var w = new Window
|
||||
{
|
||||
WindowStyle = WindowStyle.None, AllowsTransparency = true, Background = Brushes.Transparent,
|
||||
ShowInTaskbar = false, ShowActivated = false, Topmost = topmost, ResizeMode = ResizeMode.NoResize,
|
||||
Focusable = false, IsHitTestVisible = false,
|
||||
};
|
||||
w.SourceInitialized += (_, _) =>
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(w).Handle;
|
||||
long ex = GetWindowLongPtr(hwnd, GWL_EXSTYLE).ToInt64();
|
||||
SetWindowLongPtr(hwnd, GWL_EXSTYLE, new IntPtr(ex | WS_EX_TRANSPARENT | WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE));
|
||||
};
|
||||
return w;
|
||||
}
|
||||
|
||||
/// <summary>Wie die Boxen: Besitzer = Desktop, immer ganz unten (über den Desktop-Icons, unter allen Fenstern).</summary>
|
||||
public static void PinToDesktop(Window w)
|
||||
{
|
||||
var hwnd = new WindowInteropHelper(w).Handle;
|
||||
var progman = FindWindow("Progman", null);
|
||||
if (progman != IntPtr.Zero) SetWindowLongPtr(hwnd, GWLP_HWNDPARENT, progman);
|
||||
HwndSource.FromHwnd(hwnd).AddHook((IntPtr h, int msg, IntPtr wp, IntPtr lp, ref bool handled) =>
|
||||
{
|
||||
if (msg == WM_WINDOWPOSCHANGING)
|
||||
{
|
||||
var pos = Marshal.PtrToStructure<WINDOWPOS>(lp);
|
||||
pos.hwndInsertAfter = new IntPtr(1); // HWND_BOTTOM
|
||||
pos.flags &= ~SWP_NOZORDER;
|
||||
Marshal.StructureToPtr(pos, lp, false);
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
});
|
||||
}
|
||||
|
||||
private const int GWL_EXSTYLE = -20, GWLP_HWNDPARENT = -8, WM_WINDOWPOSCHANGING = 0x46;
|
||||
private const long WS_EX_TRANSPARENT = 0x20, WS_EX_LAYERED = 0x80000, WS_EX_TOOLWINDOW = 0x80, WS_EX_NOACTIVATE = 0x8000000;
|
||||
private const uint SWP_NOZORDER = 0x4;
|
||||
|
||||
[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);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Seitenanzeige beim Wechseln der Desktop-Seite
|
||||
// =========================================================================
|
||||
|
||||
public static class PageIndicator
|
||||
{
|
||||
private static Window? _w;
|
||||
private static TextBlock? _name;
|
||||
private static StackPanel? _dots;
|
||||
private static DispatcherTimer? _hide;
|
||||
|
||||
public static void Show(string name, int index, int count)
|
||||
{
|
||||
if (_w is null)
|
||||
{
|
||||
_w = Overlay.Create(topmost: true);
|
||||
_w.SizeToContent = SizeToContent.WidthAndHeight;
|
||||
_name = new TextBlock { FontSize = 15, FontWeight = FontWeights.SemiBold, HorizontalAlignment = HorizontalAlignment.Center };
|
||||
_dots = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Center, Margin = new Thickness(0, 8, 0, 0) };
|
||||
_w.Content = new Border
|
||||
{
|
||||
Padding = new Thickness(22, 12, 22, 12), CornerRadius = new CornerRadius(10), Margin = new Thickness(12),
|
||||
Child = new StackPanel { Children = { _name, _dots } },
|
||||
};
|
||||
_hide = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(1300) };
|
||||
_hide.Tick += (_, _) =>
|
||||
{
|
||||
_hide.Stop();
|
||||
var fade = new DoubleAnimation(0, TimeSpan.FromMilliseconds(350));
|
||||
fade.Completed += (_, _) => { if (_w.Opacity == 0) _w.Hide(); };
|
||||
_w.BeginAnimation(UIElement.OpacityProperty, fade);
|
||||
};
|
||||
}
|
||||
var res = Application.Current.Resources;
|
||||
var border = (Border)_w.Content;
|
||||
border.Background = (Brush)res["FlyoutBrush"];
|
||||
border.BorderBrush = (Brush)res["FlyoutStrokeBrush"];
|
||||
border.BorderThickness = new Thickness(1);
|
||||
border.Effect = (System.Windows.Media.Effects.Effect)res["FlyoutShadow"];
|
||||
_name!.Text = name;
|
||||
_name.Foreground = (Brush)res["TextBrush"];
|
||||
_dots!.Children.Clear();
|
||||
for (int i = 0; i < count; i++)
|
||||
_dots.Children.Add(new Ellipse
|
||||
{
|
||||
Width = 7, Height = 7, Margin = new Thickness(4, 0, 4, 0),
|
||||
Fill = (Brush)res[i == index ? "AccentBrush" : "ControlStrongStrokeBrush"],
|
||||
});
|
||||
|
||||
_w.BeginAnimation(UIElement.OpacityProperty, null);
|
||||
_w.Opacity = 1;
|
||||
_w.Show();
|
||||
_w.UpdateLayout();
|
||||
var wa = Screens.PrimaryWorkArea;
|
||||
_w.Left = wa.Left + (wa.Width - _w.ActualWidth) / 2;
|
||||
_w.Top = wa.Bottom - _w.ActualHeight - 40;
|
||||
_hide!.Stop();
|
||||
_hide.Start();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Rahmen beim Aufziehen einer neuen Box (rechte Maustaste auf dem Desktop)
|
||||
// =========================================================================
|
||||
|
||||
public static class DrawBoxOverlay
|
||||
{
|
||||
private static Window? _w;
|
||||
|
||||
public static void Begin()
|
||||
{
|
||||
if (_w is null)
|
||||
{
|
||||
_w = Overlay.Create(topmost: true);
|
||||
var res = Application.Current.Resources;
|
||||
var accent = ((SolidColorBrush)res["AccentBrush"]).Color;
|
||||
_w.Content = new Grid
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new Rectangle
|
||||
{
|
||||
RadiusX = 8, RadiusY = 8, StrokeThickness = 2, StrokeDashArray = [4, 3],
|
||||
Stroke = new SolidColorBrush(accent), Fill = new SolidColorBrush(Color.FromArgb(0x38, accent.R, accent.G, accent.B)),
|
||||
},
|
||||
new TextBlock
|
||||
{
|
||||
Text = "Neue Box", Foreground = Brushes.White, FontSize = 14, FontWeight = FontWeights.SemiBold,
|
||||
HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
|
||||
Effect = new System.Windows.Media.Effects.DropShadowEffect { BlurRadius = 6, ShadowDepth = 0, Opacity = 0.8 },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
_w.Width = _w.Height = 1;
|
||||
_w.Show();
|
||||
}
|
||||
|
||||
/// <summary>Rechteck in Bildschirm-Pixeln.</summary>
|
||||
public static void Update(Rect px)
|
||||
{
|
||||
if (_w is null) return;
|
||||
double s = Screens.Scale;
|
||||
_w.Left = px.X / s;
|
||||
_w.Top = px.Y / s;
|
||||
_w.Width = Math.Max(1, px.Width / s);
|
||||
_w.Height = Math.Max(1, px.Height / s);
|
||||
}
|
||||
|
||||
public static void Finish(Rect px)
|
||||
{
|
||||
double s = Screens.Scale;
|
||||
var dip = new Rect(px.X / s, px.Y / s, px.Width / s, px.Height / s);
|
||||
if (dip.Width < 120 || dip.Height < 60) { Hide(); return; } // zu klein – war wohl nur ein Versehen
|
||||
Update(px); // Rahmen bleibt stehen, bis etwas ausgewählt ist
|
||||
FenceManager.ShowCreateMenu(dip);
|
||||
}
|
||||
|
||||
public static void Hide() => _w?.Hide();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Tönung der echten Desktop-Icons
|
||||
// =========================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Legt eine klickdurchlässige Farbschicht exakt über die Bilder der Desktop-Icons. Die Positionen stammen aus der
|
||||
/// offiziellen Shell-Schnittstelle; am Explorer selbst wird nichts verändert. Icons unter Boxen bleiben ausgespart.
|
||||
/// </summary>
|
||||
public static class DesktopTint
|
||||
{
|
||||
private static Window? _w;
|
||||
private static Canvas? _canvas;
|
||||
private static string _signature = "";
|
||||
|
||||
private static AppSettings S => AppSettings.Current;
|
||||
|
||||
/// <summary>Schicht neu aufbauen, wenn sich Icons, Boxen oder Einstellungen geändert haben.</summary>
|
||||
public static void Refresh(bool force = false)
|
||||
{
|
||||
bool on = S.DesktopTint is not null && !FenceManager.Hidden && DesktopToggle.IconsVisible();
|
||||
if (!on)
|
||||
{
|
||||
_w?.Hide();
|
||||
_signature = "";
|
||||
return;
|
||||
}
|
||||
var icons = DesktopIcons.Cached.Where(i => i.Path.Length > 0).ToList();
|
||||
var fences = FenceManager.OpenRects();
|
||||
var sig = $"{S.DesktopTint}|{S.DesktopTintStrength}|{DesktopIcons.IconTopOffset}|" +
|
||||
string.Join(";", icons.Select(i => $"{i.X},{i.Y},{i.IconSize}")) + "|" + string.Join(";", fences.Select(r => r.ToString()));
|
||||
if (!force && sig == _signature && _w?.IsVisible == true) return;
|
||||
_signature = sig;
|
||||
|
||||
if (_w is null)
|
||||
{
|
||||
_w = Overlay.Create(topmost: false);
|
||||
_canvas = new Canvas();
|
||||
_w.Content = _canvas;
|
||||
_w.SourceInitialized += (_, _) => Overlay.PinToDesktop(_w);
|
||||
}
|
||||
|
||||
// Fenster über die gesamte Desktop-Fläche (alle Bildschirme)
|
||||
double s = Screens.Scale;
|
||||
var vs = new Rect(SystemParameters.VirtualScreenLeft, SystemParameters.VirtualScreenTop,
|
||||
SystemParameters.VirtualScreenWidth, SystemParameters.VirtualScreenHeight);
|
||||
_w.Left = vs.Left;
|
||||
_w.Top = vs.Top;
|
||||
_w.Width = vs.Width;
|
||||
_w.Height = vs.Height;
|
||||
|
||||
var tint = CategoryColors.Brush(S.DesktopTint) ?? Brushes.Transparent;
|
||||
_canvas!.Children.Clear();
|
||||
foreach (var icon in icons)
|
||||
{
|
||||
var (l, t, size) = DesktopIcons.IconRect(icon);
|
||||
var r = new Rect(l / s, t / s, size / s, size / s);
|
||||
if (fences.Any(f => f.IntersectsWith(r))) continue; // liegt unter einer Box
|
||||
var item = new IconItem { Path = icon.Path };
|
||||
var rect = new Rectangle
|
||||
{
|
||||
Width = r.Width, Height = r.Height, Fill = tint, Opacity = Math.Clamp(S.DesktopTintStrength, 0.1, 0.95),
|
||||
Visibility = Visibility.Hidden, // erst zeigen, wenn die Form (das Icon) geladen ist
|
||||
};
|
||||
void Apply()
|
||||
{
|
||||
if (item.Icon is null) return;
|
||||
rect.OpacityMask = new ImageBrush(item.Icon) { Stretch = Stretch.Fill };
|
||||
rect.Visibility = Visibility.Visible;
|
||||
}
|
||||
item.PropertyChanged += (_, _) => Apply();
|
||||
Canvas.SetLeft(rect, r.X - vs.Left);
|
||||
Canvas.SetTop(rect, r.Y - vs.Top);
|
||||
_canvas.Children.Add(rect);
|
||||
IconLoader.Load(item, size);
|
||||
Apply();
|
||||
}
|
||||
if (!_w.IsVisible) _w.Show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>
|
||||
/// Maus-Funktionen direkt auf dem Windows-Desktop – ohne etwas in den Explorer einzuschleusen:
|
||||
/// • Doppelklick auf freie Stelle: Desktop-Icons und Boxen aus-/einblenden ("Schnell verstecken")
|
||||
/// • Rechte Maustaste auf freier Stelle ziehen: neue Box aufziehen (normaler Rechtsklick bleibt unverändert)
|
||||
/// • Mausrad über dem Desktop: Desktop-Seite wechseln
|
||||
/// </summary>
|
||||
public static class DesktopToggle
|
||||
{
|
||||
private static IntPtr _hook;
|
||||
private static LowLevelMouseProc? _proc; // Referenz halten, sonst räumt der GC den Callback weg
|
||||
private static long _lastDownTick;
|
||||
private static POINT _lastDownPt;
|
||||
|
||||
// Box aufziehen
|
||||
private static bool _rPending, _rDrawing, _updatePosted;
|
||||
private static POINT _rStart, _rCurrent;
|
||||
private static long _lastWheelTick;
|
||||
|
||||
private static AppSettings S => AppSettings.Current;
|
||||
|
||||
public static void Apply()
|
||||
{
|
||||
bool needed = S.QuickHide || S.DrawFences || (S.PageWheel && S.Pages.Count > 1);
|
||||
if (needed) Start();
|
||||
else Stop();
|
||||
}
|
||||
|
||||
private static void Start()
|
||||
{
|
||||
if (_hook != IntPtr.Zero) return;
|
||||
_proc = Proc;
|
||||
using var module = Process.GetCurrentProcess().MainModule!;
|
||||
_hook = SetWindowsHookEx(WH_MOUSE_LL, _proc, GetModuleHandle(module.ModuleName), 0);
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
{
|
||||
if (_hook == IntPtr.Zero) return;
|
||||
UnhookWindowsHookEx(_hook);
|
||||
_hook = IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>Muss sehr schnell sein: nur Fensterklasse und zwischengespeicherte Icon-Positionen prüfen.</summary>
|
||||
private static IntPtr Proc(int nCode, IntPtr wParam, IntPtr lParam)
|
||||
{
|
||||
if (nCode < 0) return CallNextHookEx(_hook, nCode, wParam, lParam);
|
||||
var info = Marshal.PtrToStructure<MSLLHOOKSTRUCT>(lParam);
|
||||
bool injected = (info.flags & LLMHF_INJECTED) != 0;
|
||||
int msg = wParam.ToInt32();
|
||||
var dispatcher = Application.Current?.Dispatcher;
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case WM_LBUTTONDOWN when S.QuickHide:
|
||||
{
|
||||
long now = Environment.TickCount64;
|
||||
bool isDouble = now - _lastDownTick <= GetDoubleClickTime() &&
|
||||
Math.Abs(info.pt.x - _lastDownPt.x) <= 4 && Math.Abs(info.pt.y - _lastDownPt.y) <= 4;
|
||||
if (isDouble)
|
||||
{
|
||||
_lastDownTick = 0;
|
||||
var pt = info.pt;
|
||||
var first = _lastDownPt;
|
||||
// Schon im Hook (ohne COM) aussortieren: Doppelklick auf ein Icon = Programmstart
|
||||
if (!DesktopIcons.HitsIcon(pt.x, pt.y, margin: 8) && !DesktopIcons.HitsIcon(first.x, first.y, margin: 8))
|
||||
dispatcher?.BeginInvoke(() => OnDoubleClick(pt, first));
|
||||
}
|
||||
else
|
||||
{
|
||||
_lastDownTick = now;
|
||||
_lastDownPt = info.pt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case WM_RBUTTONDOWN when !injected && S.DrawFences && IsDesktopAt(info.pt) && !DesktopIcons.HitsIcon(info.pt.x, info.pt.y):
|
||||
// Rechtsklick auf freie Desktop-Fläche abfangen – bleibt es ein Klick, wird er unten nachgereicht
|
||||
_rPending = true;
|
||||
_rDrawing = false;
|
||||
_rStart = _rCurrent = info.pt;
|
||||
return new IntPtr(1);
|
||||
|
||||
case WM_MOUSEMOVE when _rPending:
|
||||
_rCurrent = info.pt;
|
||||
if (!_rDrawing && (Math.Abs(info.pt.x - _rStart.x) > 8 || Math.Abs(info.pt.y - _rStart.y) > 8))
|
||||
{
|
||||
_rDrawing = true;
|
||||
dispatcher?.BeginInvoke(() => DrawBoxOverlay.Begin());
|
||||
}
|
||||
if (_rDrawing && !_updatePosted)
|
||||
{
|
||||
_updatePosted = true;
|
||||
dispatcher?.BeginInvoke(() => { _updatePosted = false; DrawBoxOverlay.Update(PixelRect(_rStart, _rCurrent)); });
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_RBUTTONUP when _rPending && !injected:
|
||||
{
|
||||
_rPending = false;
|
||||
var rect = PixelRect(_rStart, info.pt);
|
||||
if (_rDrawing) dispatcher?.BeginInvoke(() => DrawBoxOverlay.Finish(rect));
|
||||
else dispatcher?.BeginInvoke(ReplayRightClick); // normales Desktop-Kontextmenü
|
||||
return new IntPtr(1);
|
||||
}
|
||||
|
||||
case WM_MOUSEWHEEL when S.PageWheel && S.Pages.Count > 1 && !injected:
|
||||
{
|
||||
if ((GetKeyState(VK_CONTROL) & 0x8000) != 0) break; // Strg+Rad = Icon-Größe (Windows)
|
||||
long now = Environment.TickCount64;
|
||||
if (now - _lastWheelTick < 350 || !IsDesktopAt(info.pt)) break;
|
||||
_lastWheelTick = now;
|
||||
int delta = (short)((info.mouseData >> 16) & 0xFFFF);
|
||||
dispatcher?.BeginInvoke(() => FenceManager.NextPage(delta < 0 ? 1 : -1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(_hook, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
private static Rect PixelRect(POINT a, POINT b) =>
|
||||
new(Math.Min(a.x, b.x), Math.Min(a.y, b.y), Math.Abs(a.x - b.x), Math.Abs(a.y - b.y));
|
||||
|
||||
/// <summary>Liegt unter dem Punkt direkt der Desktop (kein Fenster darüber)?</summary>
|
||||
private static bool IsDesktopAt(POINT pt)
|
||||
{
|
||||
var cls = ClassName(WindowFromPoint(pt));
|
||||
return cls is "WorkerW" or "Progman" or "SHELLDLL_DefView" ||
|
||||
(cls == "SysListView32" && ClassName(GetParent(WindowFromPoint(pt))) == "SHELLDLL_DefView");
|
||||
}
|
||||
|
||||
/// <summary>Abgefangenen Rechtsklick an Windows weitergeben (wird als "eingespeist" markiert und nicht erneut abgefangen).</summary>
|
||||
private static void ReplayRightClick()
|
||||
{
|
||||
var inputs = new[]
|
||||
{
|
||||
new INPUT { type = INPUT_MOUSE, mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_RIGHTDOWN } },
|
||||
new INPUT { type = INPUT_MOUSE, mi = new MOUSEINPUT { dwFlags = MOUSEEVENTF_RIGHTUP } },
|
||||
};
|
||||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
private static void OnDoubleClick(POINT pt, POINT first)
|
||||
{
|
||||
if (!IsEmptyDesktopSpot(pt) || !IsEmptyDesktopSpot(first)) return;
|
||||
// Positionen frisch lesen – der Zwischenspeicher kann ein paar Sekunden alt sein (Icon gerade verschoben)
|
||||
if (IconsVisible())
|
||||
{
|
||||
try { DesktopIcons.RefreshCache(); } catch { return; }
|
||||
if (DesktopIcons.HitsIcon(pt.x, pt.y, margin: 8) || DesktopIcons.HitsIcon(first.x, first.y, margin: 8)) return;
|
||||
}
|
||||
Toggle();
|
||||
}
|
||||
|
||||
/// <summary>Liegt der Punkt auf dem Desktop – und zwar nicht auf einem Icon?</summary>
|
||||
private static bool IsEmptyDesktopSpot(POINT pt)
|
||||
{
|
||||
var hwnd = WindowFromPoint(pt);
|
||||
var cls = ClassName(hwnd);
|
||||
if (cls is "WorkerW" or "Progman" or "SHELLDLL_DefView") return true; // Icons gerade ausgeblendet
|
||||
if (cls != "SysListView32" || ClassName(GetParent(hwnd)) != "SHELLDLL_DefView") return false;
|
||||
// Auf ein Icon geklickt? Dann ist es ein normaler Programmstart, nichts tun.
|
||||
try
|
||||
{
|
||||
var el = AutomationElement.FromPoint(new Point(pt.x, pt.y));
|
||||
for (var e = el; e is not null; e = TreeWalker.RawViewWalker.GetParent(e))
|
||||
{
|
||||
var type = e.Current.ControlType;
|
||||
if (type == ControlType.ListItem) return false;
|
||||
if (type == ControlType.List) break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>Desktop-Icons und Boxen umschalten (auch per Befehl --toggle-desktop).</summary>
|
||||
public static void Toggle()
|
||||
{
|
||||
var view = DesktopView();
|
||||
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||||
bool iconsVisible = list != IntPtr.Zero && IsWindowVisible(list);
|
||||
if (view != IntPtr.Zero) SendMessage(view, WM_COMMAND, new IntPtr(0x7402), IntPtr.Zero); // "Desktopsymbole anzeigen" umschalten
|
||||
FenceManager.SetHidden(iconsVisible);
|
||||
DesktopTint.Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Beim Beenden: ausgeblendete Desktop-Icons wieder zeigen, damit niemand vor einem leeren Desktop sitzt.</summary>
|
||||
public static void RestoreOnExit()
|
||||
{
|
||||
if (!FenceManager.Hidden) return;
|
||||
var view = DesktopView();
|
||||
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||||
if (view != IntPtr.Zero && list != IntPtr.Zero && !IsWindowVisible(list))
|
||||
SendMessage(view, WM_COMMAND, new IntPtr(0x7402), IntPtr.Zero);
|
||||
}
|
||||
|
||||
/// <summary>Das Fenster mit den Desktop-Icons (liegt je nach Zustand unter Progman oder einem WorkerW).</summary>
|
||||
public static IntPtr DesktopView()
|
||||
{
|
||||
var progman = FindWindow("Progman", null);
|
||||
var view = FindWindowEx(progman, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||
if (view != IntPtr.Zero) return view;
|
||||
IntPtr worker = IntPtr.Zero;
|
||||
while ((worker = FindWindowEx(IntPtr.Zero, worker, "WorkerW", null)) != IntPtr.Zero)
|
||||
{
|
||||
view = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null);
|
||||
if (view != IntPtr.Zero) return view;
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>Sind die Desktop-Icons gerade sichtbar?</summary>
|
||||
public static bool IconsVisible()
|
||||
{
|
||||
var view = DesktopView();
|
||||
var list = view == IntPtr.Zero ? IntPtr.Zero : FindWindowEx(view, IntPtr.Zero, "SysListView32", null);
|
||||
return list != IntPtr.Zero && IsWindowVisible(list);
|
||||
}
|
||||
|
||||
private static string ClassName(IntPtr hwnd)
|
||||
{
|
||||
if (hwnd == IntPtr.Zero) return "";
|
||||
var sb = new StringBuilder(64);
|
||||
GetClassName(hwnd, sb, sb.Capacity);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private const int WH_MOUSE_LL = 14, WM_COMMAND = 0x0111, VK_CONTROL = 0x11;
|
||||
private const int WM_MOUSEMOVE = 0x0200, WM_LBUTTONDOWN = 0x0201, WM_RBUTTONDOWN = 0x0204, WM_RBUTTONUP = 0x0205, WM_MOUSEWHEEL = 0x020A;
|
||||
private const uint LLMHF_INJECTED = 0x1, INPUT_MOUSE = 0, MOUSEEVENTF_RIGHTDOWN = 0x0008, MOUSEEVENTF_RIGHTUP = 0x0010;
|
||||
|
||||
private delegate IntPtr LowLevelMouseProc(int nCode, IntPtr wParam, IntPtr lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int x, y; }
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MSLLHOOKSTRUCT
|
||||
{
|
||||
public POINT pt;
|
||||
public uint mouseData, flags, time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MOUSEINPUT
|
||||
{
|
||||
public int dx, dy;
|
||||
public uint mouseData, dwFlags, time;
|
||||
public IntPtr dwExtraInfo;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct INPUT
|
||||
{
|
||||
[FieldOffset(0)] public uint type;
|
||||
[FieldOffset(8)] public MOUSEINPUT mi; // 64-Bit: Union beginnt bei Offset 8
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelMouseProc lpfn, IntPtr hMod, uint dwThreadId);
|
||||
[DllImport("user32.dll")] private static extern bool UnhookWindowsHookEx(IntPtr hhk);
|
||||
[DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string lpModuleName);
|
||||
[DllImport("user32.dll")] private static extern uint GetDoubleClickTime();
|
||||
[DllImport("user32.dll")] private static extern short GetKeyState(int vk);
|
||||
[DllImport("user32.dll")] private static extern uint SendInput(uint n, INPUT[] inputs, int size);
|
||||
[DllImport("user32.dll")] private static extern IntPtr WindowFromPoint(POINT p);
|
||||
[DllImport("user32.dll")] private static extern IntPtr GetParent(IntPtr hWnd);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern int GetClassName(IntPtr hWnd, StringBuilder name, int max);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindow(string? cls, string? title);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr after, string? cls, string? title);
|
||||
[DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd);
|
||||
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!-- Eine Versionsnummer für App und Installer – nur hier ändern -->
|
||||
<!-- Eine Versionsnummer für App und Installer – nur hier ändern -->
|
||||
<PropertyGroup>
|
||||
<Version>1.0.1</Version>
|
||||
<Version>1.0.4</Version>
|
||||
<!-- keine Git-Kennung an die Produktversion anhängen (sonst "1.0.1+a1b2c3…") -->
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
+403
@@ -0,0 +1,403 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Verwaltet alle Desktop-Boxen: Anzeigen, Peek, Ausblenden, Snapshots und Anordnung je Bildschirm-Setup.</summary>
|
||||
public static class FenceManager
|
||||
{
|
||||
private static readonly List<FenceWindow> _open = [];
|
||||
private static AppSettings S => AppSettings.Current;
|
||||
private static DispatcherTimer? _peekWatch;
|
||||
private static string? _screenSignature;
|
||||
|
||||
/// <summary>Boxen gerade per "Schnell verstecken" ausgeblendet?</summary>
|
||||
public static bool Hidden { get; private set; }
|
||||
|
||||
/// <summary>Boxen gerade per Peek vor allen Fenstern?</summary>
|
||||
public static bool Peeking { get; private set; }
|
||||
|
||||
public static void Sync()
|
||||
{
|
||||
_screenSignature ??= ScreenSignature();
|
||||
// Boxen auf nicht (mehr) vorhandenen Seiten landen auf der letzten Seite
|
||||
foreach (var f in S.Fences) f.Page = Math.Clamp(f.Page, 0, S.Pages.Count - 1);
|
||||
var wanted = S.FencesEnabled && !Hidden
|
||||
? S.Fences.Where(f => f.Tabs.Count > 0 && f.Page == S.CurrentPage).ToList()
|
||||
: [];
|
||||
|
||||
foreach (var w in _open.Where(w => !wanted.Contains(w.Def)).ToList())
|
||||
{
|
||||
w.Close();
|
||||
_open.Remove(w);
|
||||
}
|
||||
|
||||
var wa = Screens.PrimaryWorkArea;
|
||||
foreach (var def in wanted)
|
||||
{
|
||||
var w = _open.FirstOrDefault(x => x.Def == def);
|
||||
if (w is not null) { w.Refresh(); continue; }
|
||||
|
||||
if (def.X is null)
|
||||
{
|
||||
// neue Box: oben rechts beginnen – links liegen meist die Desktop-Icons
|
||||
int perRow = Math.Max(1, (int)((wa.Width - 48) / (def.W + 16)));
|
||||
int i = _open.Count;
|
||||
def.X = wa.Right - 24 - def.W - (i % perRow) * (def.W + 16);
|
||||
def.Y = wa.Top + 24 + (i / perRow) * (def.H + 16);
|
||||
}
|
||||
w = new FenceWindow(def);
|
||||
_open.Add(w);
|
||||
w.Show();
|
||||
}
|
||||
}
|
||||
|
||||
public static void RefreshAll()
|
||||
{
|
||||
foreach (var w in _open) w.Refresh();
|
||||
}
|
||||
|
||||
public static void CloseAll()
|
||||
{
|
||||
foreach (var w in _open) w.Close();
|
||||
_open.Clear();
|
||||
}
|
||||
|
||||
public static void Remove(FenceDef def)
|
||||
{
|
||||
S.Fences.Remove(def);
|
||||
S.Save();
|
||||
Sync();
|
||||
App.Win?.RefreshFromFences();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kategorie aus den anderen Boxen derselben Seite entfernen – pro Seite steht eine Kategorie nur in einer Box.
|
||||
/// Auf anderen Seiten darf sie zusätzlich liegen (z. B. „Internet“ auf „Arbeit“ und auf „Freizeit“).
|
||||
/// </summary>
|
||||
public static void RemoveCategoryTab(string category, FenceDef? except, int page)
|
||||
{
|
||||
foreach (var f in S.Fences.Where(f => f != except && f.Page == page))
|
||||
f.Tabs.RemoveAll(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
|
||||
S.Fences.RemoveAll(f => f != except && f.Tabs.Count == 0);
|
||||
}
|
||||
|
||||
/// <summary>Nach Verschieben/Zusammenlegen: doppelte Kategorien auf einer Seite auflösen – „keep“ behält seine.</summary>
|
||||
private static void ResolveDuplicates(int page, FenceDef? keep)
|
||||
{
|
||||
if (keep is not null)
|
||||
foreach (var t in keep.Tabs.Where(t => t.IsCategory).ToList())
|
||||
RemoveCategoryTab(t.Value, keep, page);
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var f in S.Fences.Where(f => f.Page == page))
|
||||
f.Tabs.RemoveAll(t => t.IsCategory && !seen.Add(t.Value));
|
||||
S.Fences.RemoveAll(f => f.Tabs.Count == 0);
|
||||
foreach (var f in S.Fences) f.ActiveTab = Math.Clamp(f.ActiveTab, 0, Math.Max(0, f.Tabs.Count - 1));
|
||||
}
|
||||
|
||||
/// <summary>Tab als eigene Box daneben lösen.</summary>
|
||||
public static void DetachTab(FenceDef def, int index)
|
||||
{
|
||||
if (index < 0 || index >= def.Tabs.Count || def.Tabs.Count < 2) return;
|
||||
var tab = def.Tabs[index];
|
||||
def.Tabs.RemoveAt(index);
|
||||
def.ActiveTab = Math.Clamp(def.ActiveTab, 0, def.Tabs.Count - 1);
|
||||
S.Fences.Add(new FenceDef
|
||||
{
|
||||
Page = def.Page, Tabs = [tab], X = (def.X ?? 0) + 40, Y = (def.Y ?? 0) + 40, W = def.W, H = def.H,
|
||||
Background = def.Background, Opacity = def.Opacity, Blur = def.Blur, IconSize = def.IconSize, Sort = def.Sort, Tint = def.Tint,
|
||||
});
|
||||
S.Save();
|
||||
Sync();
|
||||
}
|
||||
|
||||
public static string? PickFolder()
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "Ordner für das Portal wählen" };
|
||||
return dlg.ShowDialog() == true ? dlg.FolderName : null;
|
||||
}
|
||||
|
||||
/// <summary>Neue Box, die live einen beliebigen Ordner zeigt (z. B. Downloads).</summary>
|
||||
public static void AddPortal()
|
||||
{
|
||||
if (PickFolder() is not { } folder) return;
|
||||
S.Fences.Add(new FenceDef { Page = S.CurrentPage, Tabs = [new FenceTab { Kind = FenceTab.FolderKind, Value = folder }] });
|
||||
S.FencesEnabled = true;
|
||||
S.Save();
|
||||
Sync();
|
||||
}
|
||||
|
||||
/// <summary>Flächen der sichtbaren Boxen (WPF-Einheiten), z. B. damit die Icon-Tönung sie ausspart.</summary>
|
||||
public static List<Rect> OpenRects() =>
|
||||
_open.Where(w => w.IsVisible).Select(w => new Rect(w.Left, w.Top, w.ActualWidth, w.ActualHeight)).ToList();
|
||||
|
||||
/// <summary>Nach dem Aufziehen: Auswahl "Neue Box hier" an der Mausposition.</summary>
|
||||
public static void ShowCreateMenu(Rect area)
|
||||
{
|
||||
BoxPicker.Show(area, tab =>
|
||||
{
|
||||
if (tab.IsCategory) RemoveCategoryTab(tab.Value, except: null, S.CurrentPage);
|
||||
S.Fences.Add(new FenceDef
|
||||
{
|
||||
Page = S.CurrentPage, Tabs = [tab],
|
||||
X = area.X, Y = area.Y, W = Math.Max(180, area.Width), H = Math.Max(120, area.Height),
|
||||
});
|
||||
S.FencesEnabled = true;
|
||||
S.Save();
|
||||
Sync();
|
||||
App.Win?.RefreshFromFences();
|
||||
});
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Desktop-Seiten
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Zur nächsten/vorigen Seite wechseln (rundum).</summary>
|
||||
public static void NextPage(int delta)
|
||||
{
|
||||
if (S.Pages.Count < 2) return;
|
||||
SwitchPage((S.CurrentPage + delta + S.Pages.Count) % S.Pages.Count);
|
||||
}
|
||||
|
||||
public static void SwitchPage(int index)
|
||||
{
|
||||
index = Math.Clamp(index, 0, S.Pages.Count - 1);
|
||||
if (Peeking) EndPeek();
|
||||
bool changed = index != S.CurrentPage;
|
||||
S.CurrentPage = index;
|
||||
S.Save();
|
||||
if (changed) Sync();
|
||||
PageScheduler.ApplyWallpaper(index);
|
||||
PageIndicator.Show(S.Pages[index], index, S.Pages.Count);
|
||||
App.Win?.RefreshFromFences();
|
||||
}
|
||||
|
||||
public static void MoveToPage(FenceDef def, int page)
|
||||
{
|
||||
def.Page = Math.Clamp(page, 0, S.Pages.Count - 1);
|
||||
ResolveDuplicates(def.Page, keep: def);
|
||||
S.Save();
|
||||
Sync();
|
||||
App.Win?.RefreshFromFences();
|
||||
}
|
||||
|
||||
/// <summary>Box mit gleichem Inhalt und Aussehen zusätzlich auf eine andere Seite legen.</summary>
|
||||
public static void CopyToPage(FenceDef def, int page)
|
||||
{
|
||||
var copy = AppSettings.CloneFences([def])[0];
|
||||
copy.Id = Guid.NewGuid().ToString("N");
|
||||
copy.Page = Math.Clamp(page, 0, S.Pages.Count - 1);
|
||||
S.Fences.Add(copy);
|
||||
ResolveDuplicates(copy.Page, keep: copy);
|
||||
S.Save();
|
||||
Sync();
|
||||
App.Win?.RefreshFromFences();
|
||||
App.TrayIcon?.Notify("Box kopiert", $"Die Box liegt jetzt auch auf „{S.Pages[copy.Page]}“ – an derselben Stelle.");
|
||||
}
|
||||
|
||||
/// <summary>Seite löschen: ihre Boxen wandern auf die Seite davor.</summary>
|
||||
public static void DeletePage(int index)
|
||||
{
|
||||
if (S.Pages.Count < 2 || index < 0 || index >= S.Pages.Count) return;
|
||||
S.Pages.RemoveAt(index);
|
||||
foreach (var f in S.Fences)
|
||||
if (f.Page == index) f.Page = Math.Max(0, index - 1);
|
||||
else if (f.Page > index) f.Page--;
|
||||
// Kategorien, die es auf beiden Seiten gab, nicht doppelt zeigen
|
||||
ResolveDuplicates(Math.Max(0, index - 1), keep: null);
|
||||
if (S.CurrentPage >= index && S.CurrentPage > 0) S.CurrentPage--;
|
||||
S.Save();
|
||||
CloseAll();
|
||||
Sync();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Aussehen
|
||||
// =====================================================================
|
||||
|
||||
public static void EditLook(FenceWindow w)
|
||||
{
|
||||
var def = w.Def;
|
||||
var res = Application.Current.Resources;
|
||||
var before = AppSettings.CloneFences([def])[0];
|
||||
var panel = new StackPanel();
|
||||
TextBlock Label(string t, double top = 16) => new() { Text = t, Margin = new Thickness(0, top, 0, 8), FontWeight = FontWeights.SemiBold, FontSize = 13 };
|
||||
|
||||
// Hintergrund
|
||||
panel.Children.Add(Label("Hintergrund", 0));
|
||||
var bgPanel = new WrapPanel();
|
||||
void AddSwatch(WrapPanel p, string group, string? hex, Brush brush, string tip, Func<string?> get, Action<string?> set)
|
||||
{
|
||||
var rb = new RadioButton { Style = (Style)res["SwatchRadio"], GroupName = group, Background = brush, ToolTip = tip, IsChecked = get() == hex };
|
||||
rb.Checked += (_, _) => { set(hex); w.ApplyLook(); };
|
||||
p.Children.Add(rb);
|
||||
}
|
||||
AddSwatch(bgPanel, "bg", null, (Brush)res["FenceBrush"], "Passend zum Design", () => def.Background, v => def.Background = v);
|
||||
foreach (var (name, hex) in CategoryColors.All.Concat([("Schwarz", "#101010"), ("Weiß", "#F5F5F5")]))
|
||||
AddSwatch(bgPanel, "bg", hex, CategoryColors.Brush(hex)!, name, () => def.Background, v => def.Background = v);
|
||||
panel.Children.Add(bgPanel);
|
||||
|
||||
// Deckkraft
|
||||
panel.Children.Add(Label("Deckkraft"));
|
||||
var opacity = new Slider { Minimum = 20, Maximum = 100, Value = Math.Round(def.Opacity * 100), TickFrequency = 5, IsSnapToTickEnabled = true };
|
||||
opacity.ValueChanged += (_, _) => { def.Opacity = opacity.Value / 100; w.ApplyLook(); };
|
||||
panel.Children.Add(opacity);
|
||||
|
||||
var blur = new CheckBox { Content = "Milchglas (Hintergrund verschwommen)", IsChecked = def.Blur, Margin = new Thickness(0, 12, 0, 0) };
|
||||
blur.Click += (_, _) => { def.Blur = blur.IsChecked == true; w.ApplyLook(); };
|
||||
panel.Children.Add(blur);
|
||||
|
||||
// Icon-Größe und Sortierung als Segmente
|
||||
StackPanel Segments(string group, (string Label, string Value)[] options, string current, Action<string> set)
|
||||
{
|
||||
var sp = new StackPanel { Orientation = Orientation.Horizontal };
|
||||
foreach (var (label, value) in options)
|
||||
{
|
||||
var rb = new RadioButton { Style = (Style)res["SegmentRadio"], GroupName = group, Content = label, IsChecked = value == current };
|
||||
rb.Checked += (_, _) => { set(value); w.Refresh(); };
|
||||
sp.Children.Add(rb);
|
||||
}
|
||||
return sp;
|
||||
}
|
||||
panel.Children.Add(Label("Icon-Größe"));
|
||||
panel.Children.Add(new Border
|
||||
{
|
||||
Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = Segments("size", [("Klein", "Small"), ("Mittel", "Medium"), ("Groß", "Large")], def.IconSize.ToString(),
|
||||
v => def.IconSize = Enum.Parse<ViewMode>(v)),
|
||||
});
|
||||
panel.Children.Add(Label("Sortieren nach"));
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "„Eigene“: Icons in der Box einfach an die gewünschte Stelle ziehen.", FontSize = 12, Margin = new Thickness(0, -4, 0, 8),
|
||||
TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextSecondaryBrush"],
|
||||
});
|
||||
panel.Children.Add(new Border
|
||||
{
|
||||
Style = (Style)res["SegmentGroup"], HorizontalAlignment = HorizontalAlignment.Left,
|
||||
Child = Segments("sort", [("Name", "Name"), ("Typ", "Typ"), ("Datum", "Datum"), ("Meistgenutzt", "Häufig"), ("Eigene", "Eigene")],
|
||||
def.Sort, v => def.Sort = v),
|
||||
});
|
||||
|
||||
// Icon-Tönung
|
||||
panel.Children.Add(Label("Icon-Tönung"));
|
||||
var tintPanel = new WrapPanel();
|
||||
AddSwatch(tintPanel, "tint", null, Brushes.Transparent, "Keine Tönung", () => def.Tint, v => def.Tint = v);
|
||||
foreach (var (name, hex) in CategoryColors.All)
|
||||
AddSwatch(tintPanel, "tint", hex, CategoryColors.Brush(hex)!, name, () => def.Tint, v => def.Tint = v);
|
||||
panel.Children.Add(tintPanel);
|
||||
|
||||
var owner = App.Win!;
|
||||
if (Dialog.Show(owner, "Aussehen der Box", panel, "Speichern", "Abbrechen", null, width: 470))
|
||||
S.Save();
|
||||
else
|
||||
{
|
||||
// Vorschau zurücknehmen
|
||||
(def.Background, def.Opacity, def.Blur, def.IconSize, def.Sort, def.Tint) =
|
||||
(before.Background, before.Opacity, before.Blur, before.IconSize, before.Sort, before.Tint);
|
||||
w.Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Peek & Schnell verstecken
|
||||
// =====================================================================
|
||||
|
||||
public static void TogglePeek()
|
||||
{
|
||||
if (Peeking) EndPeek();
|
||||
else StartPeek();
|
||||
}
|
||||
|
||||
private static void StartPeek()
|
||||
{
|
||||
if (_open.Count == 0) return;
|
||||
if (Hidden) SetHidden(false);
|
||||
Peeking = true;
|
||||
foreach (var w in _open) w.SetPeek(true);
|
||||
_open[0].Activate();
|
||||
// Peek endet, sobald ein anderes Fenster aktiv wird
|
||||
_peekWatch ??= new DispatcherTimer(TimeSpan.FromMilliseconds(300), DispatcherPriority.Background, (_, _) =>
|
||||
{
|
||||
if (!_open.Any(w => w.IsActive) && !_open.Any(w => w.IsMouseOver)) EndPeek();
|
||||
}, Application.Current.Dispatcher);
|
||||
_peekWatch.Start();
|
||||
}
|
||||
|
||||
public static void EndPeek()
|
||||
{
|
||||
_peekWatch?.Stop();
|
||||
if (!Peeking) return;
|
||||
Peeking = false;
|
||||
foreach (var w in _open) w.SetPeek(false);
|
||||
}
|
||||
|
||||
/// <summary>Alle Boxen aus- bzw. wieder einblenden (Schnell verstecken).</summary>
|
||||
public static void SetHidden(bool hidden)
|
||||
{
|
||||
if (Hidden == hidden) return;
|
||||
Hidden = hidden;
|
||||
if (hidden) EndPeek();
|
||||
Sync();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Bildschirme & Snapshots
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Kennung der aktuellen Bildschirm-Anordnung (Anzahl, Größen, Positionen).</summary>
|
||||
private static string ScreenSignature() =>
|
||||
string.Join("|", System.Windows.Forms.Screen.AllScreens
|
||||
.OrderBy(s => s.Bounds.X).ThenBy(s => s.Bounds.Y)
|
||||
.Select(s => $"{s.Bounds.X},{s.Bounds.Y},{s.Bounds.Width}x{s.Bounds.Height}"));
|
||||
|
||||
private static Dictionary<string, double[]> CurrentPositions() =>
|
||||
S.Fences.Where(f => f.X is not null).ToDictionary(f => f.Id, f => new[] { f.X!.Value, f.Y!.Value, f.W, f.H });
|
||||
|
||||
/// <summary>
|
||||
/// Bildschirm angesteckt/abgesteckt oder Auflösung geändert: Anordnung der alten Konstellation merken
|
||||
/// und – falls bekannt – die zur neuen Konstellation passende wiederherstellen.
|
||||
/// </summary>
|
||||
public static void OnDisplayChanged()
|
||||
{
|
||||
var newSig = ScreenSignature();
|
||||
if (_screenSignature is { } oldSig && oldSig != newSig)
|
||||
{
|
||||
S.ScreenLayouts[oldSig] = CurrentPositions();
|
||||
if (S.ScreenLayouts.TryGetValue(newSig, out var layout))
|
||||
{
|
||||
foreach (var f in S.Fences)
|
||||
if (layout.TryGetValue(f.Id, out var r) && r.Length == 4)
|
||||
(f.X, f.Y, f.W, f.H) = (r[0], r[1], r[2], r[3]);
|
||||
foreach (var w in _open) w.Reapply();
|
||||
}
|
||||
S.Save();
|
||||
}
|
||||
_screenSignature = newSig;
|
||||
foreach (var w in _open) w.EnsureOnScreen();
|
||||
}
|
||||
|
||||
public static void SaveSnapshot(string name)
|
||||
{
|
||||
S.Snapshots.RemoveAll(s => string.Equals(s.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
S.Snapshots.Add(new FenceSnapshot { Name = name, Fences = AppSettings.CloneFences(S.Fences) });
|
||||
S.Save();
|
||||
}
|
||||
|
||||
/// <summary>Anordnung wiederherstellen. Gibt die Anordnung davor zurück (für Rückgängig).</summary>
|
||||
public static List<FenceDef> RestoreSnapshot(FenceSnapshot snap) => ApplyFences(AppSettings.CloneFences(snap.Fences));
|
||||
|
||||
public static List<FenceDef> ApplyFences(List<FenceDef> fences)
|
||||
{
|
||||
var before = AppSettings.CloneFences(S.Fences);
|
||||
CloseAll();
|
||||
S.Fences = fences;
|
||||
S.FencesEnabled = true;
|
||||
S.Save();
|
||||
Sync();
|
||||
return before;
|
||||
}
|
||||
}
|
||||
+43
-14
@@ -1,29 +1,37 @@
|
||||
<Window x:Class="DeskCore.FenceWindow"
|
||||
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"
|
||||
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 x:Name="Chrome" 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">
|
||||
<Window.Resources>
|
||||
<!-- Kachelgrößen je Box (im Code je nach Einstellung gesetzt) -->
|
||||
<sys:Double x:Key="FTileW">78</sys:Double>
|
||||
<sys:Double x:Key="FTileH">80</sys:Double>
|
||||
<sys:Double x:Key="FIcon">32</sys:Double>
|
||||
</Window.Resources>
|
||||
|
||||
<Border x:Name="Root" CornerRadius="8" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1" ClipToBounds="True">
|
||||
<DockPanel x:Name="Content_">
|
||||
<!-- Titel / Tabs (zum Verschieben ziehen, Doppelklick = ein-/ausklappen) -->
|
||||
<Grid DockPanel.Dock="Top" Height="36" Background="Transparent" x:Name="TitleBar" MouseLeftButtonDown="TitleBar_MouseLeftButtonDown">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<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"/>
|
||||
<StackPanel x:Name="TabStrip" Orientation="Horizontal" Margin="6,0,0,0" VerticalAlignment="Center" ClipToBounds="True"/>
|
||||
<TextBlock x:Name="LockGlyph" Grid.Column="1" Text="" FontFamily="{DynamicResource IconFont}" FontSize="11"
|
||||
Foreground="{DynamicResource TextTertiaryBrush}" VerticalAlignment="Center" Margin="6,0,0,0"
|
||||
Visibility="Collapsed" ToolTip="Box ist gesperrt"/>
|
||||
<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">
|
||||
@@ -34,9 +42,18 @@
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- Ordner-Portal: Unterordner-Navigation -->
|
||||
<DockPanel x:Name="PortalBar" DockPanel.Dock="Top" Margin="6,0,8,4" Visibility="Collapsed">
|
||||
<Button DockPanel.Dock="Left" Style="{StaticResource SubtleButton}" Padding="6,3" MinHeight="0" Click="PortalUp_Click" ToolTip="Zurück">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="11"/>
|
||||
</Button>
|
||||
<TextBlock x:Name="PortalPath" VerticalAlignment="Center" Margin="6,0,0,0" TextTrimming="CharacterEllipsis"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
</DockPanel>
|
||||
|
||||
<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"
|
||||
AllowDrop="True" DragOver="Items_DragOver" DragLeave="Items_DragLeave" Drop="Items_Drop"
|
||||
MouseDoubleClick="Items_MouseDoubleClick" KeyDown="Items_KeyDown"
|
||||
PreviewMouseLeftButtonDown="Items_PreviewMouseLeftButtonDown" PreviewMouseMove="Items_PreviewMouseMove"
|
||||
ContextMenuOpening="Items_ContextMenuOpening">
|
||||
@@ -50,8 +67,16 @@
|
||||
</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"/>
|
||||
<StackPanel Width="{DynamicResource FTileW}" Height="{DynamicResource FTileH}" ToolTip="{Binding Name}">
|
||||
<Grid Width="{DynamicResource FIcon}" Height="{DynamicResource FIcon}" Margin="0,8,0,0">
|
||||
<Image Source="{Binding Icon}" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<!-- Icon-Tönung: Farbfläche in Form des Icons -->
|
||||
<Rectangle Fill="{DynamicResource FTint}" Opacity="0.6" IsHitTestVisible="False">
|
||||
<Rectangle.OpacityMask>
|
||||
<ImageBrush ImageSource="{Binding Icon}"/>
|
||||
</Rectangle.OpacityMask>
|
||||
</Rectangle>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Name}" TextAlignment="Center" TextWrapping="Wrap" TextTrimming="CharacterEllipsis"
|
||||
MaxHeight="30" LineHeight="15" Margin="4,6,4,0"/>
|
||||
</StackPanel>
|
||||
@@ -59,7 +84,11 @@
|
||||
</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"/>
|
||||
Foreground="{DynamicResource TextTertiaryBrush}" IsHitTestVisible="False" Margin="16,0,16,10"
|
||||
TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
<!-- Einfügestelle beim Umsortieren per Ziehen -->
|
||||
<Rectangle x:Name="DropMarker" Width="3" RadiusX="1.5" RadiusY="1.5" Fill="{DynamicResource AccentBrush}"
|
||||
HorizontalAlignment="Left" VerticalAlignment="Top" IsHitTestVisible="False" Visibility="Collapsed"/>
|
||||
</Grid>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
+732
-346
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,101 @@
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>
|
||||
/// Erkennt, ob ein Spiel aus einem Steam- oder Epic-Link (.url) noch installiert ist.
|
||||
/// Ergebnis null = "weiß ich nicht" → DeskCore lässt den Link dann sicherheitshalber stehen.
|
||||
/// </summary>
|
||||
public static class GameLibraries
|
||||
{
|
||||
private static readonly Regex SteamRun = new(@"^steam://(?:rungameid|run|launch)/(\d+)", RegexOptions.IgnoreCase);
|
||||
private const string EpicPrefix = "com.epicgames.launcher://apps/";
|
||||
|
||||
/// <summary>Ist das Spiel hinter diesem Launcher-Link noch installiert? (null = nicht feststellbar)</summary>
|
||||
public static bool? IsInstalled(string url)
|
||||
{
|
||||
url = url.Trim();
|
||||
var m = SteamRun.Match(url);
|
||||
if (m.Success) return SteamAppInstalled(m.Groups[1].Value);
|
||||
if (url.StartsWith(EpicPrefix, StringComparison.OrdinalIgnoreCase)) return EpicAppInstalled(url);
|
||||
return null;
|
||||
}
|
||||
|
||||
// ---------- Steam ----------
|
||||
|
||||
private static (DateTime When, List<string>? Libraries) _steamCache;
|
||||
|
||||
/// <summary>Alle Steam-Bibliotheksordner – oder null, wenn Steam fehlt bzw. eine Bibliothek gerade nicht erreichbar ist.</summary>
|
||||
private static List<string>? SteamLibraries()
|
||||
{
|
||||
if (DateTime.UtcNow - _steamCache.When < TimeSpan.FromSeconds(30)) return _steamCache.Libraries;
|
||||
List<string>? result = null;
|
||||
try
|
||||
{
|
||||
using var k = Registry.CurrentUser.OpenSubKey(@"Software\Valve\Steam");
|
||||
if (k?.GetValue("SteamPath") is string sp && sp.Length > 0)
|
||||
{
|
||||
var steamPath = Path.GetFullPath(sp.Replace('/', '\\'));
|
||||
var libs = new List<string> { steamPath };
|
||||
var vdf = Path.Combine(steamPath, "steamapps", "libraryfolders.vdf");
|
||||
if (File.Exists(vdf))
|
||||
{
|
||||
foreach (Match lm in Regex.Matches(File.ReadAllText(vdf), "\"path\"\\s+\"([^\"]+)\""))
|
||||
libs.Add(Path.GetFullPath(lm.Groups[1].Value.Replace(@"\\", @"\")));
|
||||
}
|
||||
libs = libs.Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||
// Liegt eine Bibliothek auf einem abgesteckten Laufwerk, könnte das Spiel dort sein → nichts entscheiden
|
||||
result = libs.All(l => Directory.Exists(Path.Combine(l, "steamapps"))) ? libs : null;
|
||||
}
|
||||
}
|
||||
catch { result = null; }
|
||||
_steamCache = (DateTime.UtcNow, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool? SteamAppInstalled(string appId)
|
||||
{
|
||||
var libs = SteamLibraries();
|
||||
if (libs is null) return null;
|
||||
return libs.Any(l => File.Exists(Path.Combine(l, "steamapps", $"appmanifest_{appId}.acf")));
|
||||
}
|
||||
|
||||
// ---------- Epic Games ----------
|
||||
|
||||
private static bool? EpicAppInstalled(string url)
|
||||
{
|
||||
// Format: com.epicgames.launcher://apps/<namespace>%3A<katalog>%3A<AppName>?action=launch
|
||||
// oder com.epicgames.launcher://apps/<AppName>?action=launch
|
||||
var rest = url[EpicPrefix.Length..];
|
||||
var q = rest.IndexOf('?');
|
||||
if (q >= 0) rest = rest[..q];
|
||||
var appName = Uri.UnescapeDataString(rest).Split(':').Last().Trim();
|
||||
if (appName.Length == 0) return null;
|
||||
|
||||
var manifests = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
|
||||
"Epic", "EpicGamesLauncher", "Data", "Manifests");
|
||||
if (!Directory.Exists(manifests)) return null; // Epic-Launcher nicht (mehr) vorhanden → nicht entscheiden
|
||||
try
|
||||
{
|
||||
foreach (var f in Directory.EnumerateFiles(manifests, "*.item"))
|
||||
{
|
||||
using var doc = JsonDocument.Parse(File.ReadAllText(f));
|
||||
var root = doc.RootElement;
|
||||
if (!root.TryGetProperty("AppName", out var an) ||
|
||||
!string.Equals(an.GetString(), appName, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
// Installationsort auf abgestecktem Laufwerk? → nicht entscheiden
|
||||
if (root.TryGetProperty("InstallLocation", out var loc) && loc.GetString() is { Length: > 0 } dir)
|
||||
{
|
||||
var drive = new DriveInfo(Path.GetPathRoot(dir)!);
|
||||
if (!drive.IsReady) return null;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
@@ -1,138 +1,140 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
private readonly int Id;
|
||||
|
||||
private readonly IntPtr _hwnd;
|
||||
private readonly HwndSource _source;
|
||||
private bool _registered;
|
||||
|
||||
public event Action? Pressed;
|
||||
|
||||
public Hotkey(IntPtr hwnd, int id = 0xB001)
|
||||
{
|
||||
Id = id;
|
||||
_hwnd = hwnd;
|
||||
_source = HwndSource.FromHwnd(hwnd);
|
||||
_source.AddHook(Hook);
|
||||
}
|
||||
|
||||
/// <summary>Registriert das Kürzel. false = schon von einem anderen Programm belegt.</summary>
|
||||
public bool Register(int modifiers, int vk)
|
||||
{
|
||||
Unregister();
|
||||
if (vk == 0) return false;
|
||||
_registered = RegisterHotKey(_hwnd, Id, modifiers | ModNoRepeat, vk);
|
||||
return _registered;
|
||||
}
|
||||
|
||||
public void Unregister()
|
||||
{
|
||||
if (_registered) UnregisterHotKey(_hwnd, Id);
|
||||
_registered = false;
|
||||
}
|
||||
|
||||
private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
|
||||
{
|
||||
if (msg == WmHotkey && wParam.ToInt32() == Id)
|
||||
{
|
||||
Pressed?.Invoke();
|
||||
handled = true;
|
||||
}
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
public static string Describe(int modifiers, int vk)
|
||||
{
|
||||
if (vk == 0) return "–";
|
||||
var parts = new List<string>();
|
||||
if ((modifiers & ModWin) != 0) parts.Add("Win");
|
||||
if ((modifiers & ModCtrl) != 0) parts.Add("Strg");
|
||||
if ((modifiers & ModAlt) != 0) parts.Add("Alt");
|
||||
if ((modifiers & ModShift) != 0) parts.Add("Umschalt");
|
||||
var key = KeyInterop.KeyFromVirtualKey(vk);
|
||||
parts.Add(key switch
|
||||
{
|
||||
>= Key.D0 and <= Key.D9 => ((char)('0' + (key - Key.D0))).ToString(),
|
||||
Key.Space => "Leertaste",
|
||||
_ => key.ToString(),
|
||||
});
|
||||
return string.Join(" + ", parts);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Unregister();
|
||||
_source.RemoveHook(Hook);
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")] private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
|
||||
[DllImport("user32.dll")] private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
|
||||
}
|
||||
|
||||
/// <summary>Start mit Windows über HKCU\...\Run.</summary>
|
||||
public static class Autostart
|
||||
{
|
||||
private const string Key = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
private const string Name = "DeskCore";
|
||||
// Frühere Namen dieser App. "DeskHub" heißt auch ein anderes Programm → nur übernehmen, wenn der Eintrag eindeutig von uns ist.
|
||||
private static readonly string[] OldNames = ["DesktopSortierer", "DeskHub"];
|
||||
|
||||
public static bool Enabled
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
using var k = Registry.CurrentUser.OpenSubKey(Key);
|
||||
return k?.GetValue(Name) is string;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
set
|
||||
{
|
||||
try
|
||||
{
|
||||
using var k = Registry.CurrentUser.OpenSubKey(Key, writable: true) ?? Registry.CurrentUser.CreateSubKey(Key);
|
||||
if (value) k.SetValue(Name, $"\"{Environment.ProcessPath}\" --minimized");
|
||||
else k.DeleteValue(Name, throwOnMissingValue: false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Alten Autostart-Eintrag ("DesktopSortierer") auf den neuen Namen umstellen.</summary>
|
||||
public static void Migrate()
|
||||
{
|
||||
try
|
||||
{
|
||||
using var k = Registry.CurrentUser.OpenSubKey(Key, writable: true);
|
||||
if (k is null) return;
|
||||
bool wasEnabled = false;
|
||||
foreach (var old in OldNames)
|
||||
{
|
||||
if (k.GetValue(old) is not string cmd) continue;
|
||||
bool ours = old == "DesktopSortierer" ||
|
||||
(cmd.Contains($"{old}.exe", StringComparison.OrdinalIgnoreCase) && cmd.Contains("--minimized"));
|
||||
if (!ours) continue;
|
||||
k.DeleteValue(old, throwOnMissingValue: false);
|
||||
wasEnabled = true;
|
||||
}
|
||||
if (wasEnabled) Enabled = true;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Falls die exe verschoben wurde: Pfad im Autostart aktualisieren.</summary>
|
||||
public static void RefreshPath()
|
||||
{
|
||||
if (Enabled) Enabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Verknüpfungen zu deinstallierten Programmen automatisch aus den Kategorien entfernen.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
private DispatcherTimer? _deadScanTimer;
|
||||
private DispatcherTimer? _deadScanPeriodic;
|
||||
private bool _deadScanRunning;
|
||||
private DispatcherTimer? _deadConfirmTimer;
|
||||
private readonly Dictionary<string, DateTime> _deadSince = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly TimeSpan ConfirmAfter = TimeSpan.FromSeconds(60);
|
||||
|
||||
/// <summary>Zweite Prüfung kurz nach Ablauf der Wartezeit.</summary>
|
||||
private void ScheduleConfirmScan()
|
||||
{
|
||||
if (_deadConfirmTimer is null)
|
||||
{
|
||||
_deadConfirmTimer = new DispatcherTimer { Interval = ConfirmAfter + TimeSpan.FromSeconds(5) };
|
||||
_deadConfirmTimer.Tick += (_, _) => { _deadConfirmTimer.Stop(); CleanupDeadShortcuts(); };
|
||||
}
|
||||
if (!_deadConfirmTimer.IsEnabled) _deadConfirmTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>Einmal beim Start einrichten: Prüfung kurz nach dem Start und danach alle 10 Minuten.</summary>
|
||||
public void StartDeadShortcutWatch()
|
||||
{
|
||||
_deadScanTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(4) };
|
||||
_deadScanTimer.Tick += (_, _) => { _deadScanTimer.Stop(); CleanupDeadShortcuts(); };
|
||||
// Deinstallationen über die Windows-Einstellungen ändern unsere Ordner nicht → zusätzlich regelmäßig prüfen
|
||||
_deadScanPeriodic = new DispatcherTimer { Interval = TimeSpan.FromMinutes(10) };
|
||||
_deadScanPeriodic.Tick += (_, _) => CleanupDeadShortcuts();
|
||||
_deadScanPeriodic.Start();
|
||||
ScheduleDeadShortcutScan();
|
||||
}
|
||||
|
||||
/// <summary>Nach Änderungen (z. B. Deinstallation entfernt die Desktop-Verknüpfung) gebündelt prüfen.</summary>
|
||||
public void ScheduleDeadShortcutScan()
|
||||
{
|
||||
if (_deadScanTimer is null) return;
|
||||
_deadScanTimer.Stop();
|
||||
_deadScanTimer.Start();
|
||||
}
|
||||
|
||||
private async void CleanupDeadShortcuts()
|
||||
{
|
||||
if (!S.RemoveDeadShortcuts || _deadScanRunning || IsBusy) return;
|
||||
_deadScanRunning = true;
|
||||
try
|
||||
{
|
||||
var keep = S.KeptShortcuts.ToList();
|
||||
var found = await Task.Run(() => Storage.FindDeadShortcuts(keep));
|
||||
|
||||
// Updates ersetzen ihre Programmdatei manchmal kurz → erst entfernen, wenn sie mindestens
|
||||
// eine Minute lang fehlt (zweite Prüfung), sonst würde ein laufendes Update fälschlich aufgeräumt
|
||||
var now = DateTime.UtcNow;
|
||||
foreach (var p in _deadSince.Keys.Where(k => !found.Contains(k, StringComparer.OrdinalIgnoreCase)).ToList())
|
||||
_deadSince.Remove(p);
|
||||
foreach (var p in found) _deadSince.TryAdd(p, now);
|
||||
var dead = found.Where(p => now - _deadSince[p] >= ConfirmAfter).ToList();
|
||||
if (dead.Count < found.Count) ScheduleConfirmScan();
|
||||
if (dead.Count == 0) return;
|
||||
foreach (var p in dead) _deadSince.Remove(p);
|
||||
|
||||
var entry = new UndoEntry(dead.Count == 1
|
||||
? $"„{Storage.DisplayName(dead[0])}“ entfernen (Programm deinstalliert)"
|
||||
: $"{dead.Count} Verknüpfungen deinstallierter Programme entfernen");
|
||||
var removed = new List<string>();
|
||||
foreach (var p in dead)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Reihenfolge beim Rückgängigmachen: erst zurückholen, dann merken, dass sie bleiben soll
|
||||
var path = p;
|
||||
entry.Add(() =>
|
||||
{
|
||||
if (!S.KeptShortcuts.Contains(path, StringComparer.OrdinalIgnoreCase)) S.KeptShortcuts.Add(path);
|
||||
S.Save();
|
||||
return null;
|
||||
});
|
||||
entry.AddDelete(p, Storage.Delete(p));
|
||||
removed.Add(p);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
if (removed.Count == 0) return;
|
||||
Undo.Push(entry);
|
||||
Refresh();
|
||||
FenceManager.RefreshAll();
|
||||
|
||||
var text = removed.Count == 1
|
||||
? $"„{Storage.DisplayName(removed[0])}“ entfernt – das Programm ist nicht mehr installiert"
|
||||
: $"{removed.Count} Verknüpfungen entfernt – die Programme sind nicht mehr installiert";
|
||||
if (IsVisible && IsActive) ShowToast(text, "", undoable: true);
|
||||
else App.TrayIcon?.Notify("Aufgeräumt", text);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_deadScanRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeadShortcutsToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.RemoveDeadShortcuts = DeadShortcutsToggle.IsChecked == true;
|
||||
// beim Einschalten: früher behaltene Verknüpfungen wieder prüfen lassen
|
||||
if (S.RemoveDeadShortcuts) S.KeptShortcuts.RemoveAll(p => !File.Exists(p));
|
||||
S.Save();
|
||||
if (S.RemoveDeadShortcuts) ScheduleDeadShortcutScan();
|
||||
}
|
||||
}
|
||||
+249
-244
@@ -1,244 +1,249 @@
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Fortschrittsanzeige für lange Vorgänge, Fensterposition merken und Update-Hinweis.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
// =====================================================================
|
||||
// Fortschritt mit Abbrechen
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Meldet Fortschritt aus dem Hintergrund (threadsicher).</summary>
|
||||
public sealed class ProgressSink
|
||||
{
|
||||
private long _done, _total;
|
||||
public long Done => Interlocked.Read(ref _done);
|
||||
public long Total => Interlocked.Read(ref _total);
|
||||
public void Add(long bytes) => Interlocked.Add(ref _done, bytes);
|
||||
public void SetTotal(long total) => Interlocked.Exchange(ref _total, total);
|
||||
public void SetDone(long done) => Interlocked.Exchange(ref _done, done);
|
||||
public volatile string? Detail;
|
||||
}
|
||||
|
||||
private CancellationTokenSource? _busyCts;
|
||||
private Task? _busyTask;
|
||||
|
||||
public bool IsBusy => _busyCts is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Führt einen langen Vorgang im Hintergrund aus. Das Fenster bleibt bedienbar; nach kurzer Zeit erscheint
|
||||
/// eine Fortschrittsanzeige mit Abbrechen-Knopf (bei schnellen Vorgängen gar nicht erst).
|
||||
/// </summary>
|
||||
private async Task<T> RunWithProgress<T>(string title, string detail, Func<ProgressSink, CancellationToken, Task<T>> work)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
_busyCts = cts;
|
||||
var sink = new ProgressSink { Detail = detail };
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
ProgressTitle.Text = title;
|
||||
ProgressDetail.Text = detail;
|
||||
ProgressStats.Text = "";
|
||||
ProgressFill.Width = 0;
|
||||
ProgressCancel.IsEnabled = true;
|
||||
ProgressCancel.Content = "Abbrechen";
|
||||
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
if (ProgressOverlay.Visibility != Visibility.Visible && sw.ElapsedMilliseconds > 500)
|
||||
ProgressOverlay.Visibility = Visibility.Visible;
|
||||
if (sink.Detail is { } d) ProgressDetail.Text = d;
|
||||
long done = sink.Done, total = sink.Total;
|
||||
if (total > 0)
|
||||
{
|
||||
double frac = Math.Min(1, done / (double)total);
|
||||
ProgressFill.Width = ProgressTrack.ActualWidth * frac;
|
||||
ProgressStats.Text = $"{Backup.FormatSize(done)} von {Backup.FormatSize(total)}{Remaining(done, total, sw.Elapsed)}";
|
||||
}
|
||||
else if (done > 0) ProgressStats.Text = Backup.FormatSize(done);
|
||||
};
|
||||
timer.Start();
|
||||
|
||||
var task = Task.Run(() => work(sink, cts.Token));
|
||||
_busyTask = task;
|
||||
try
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
timer.Stop();
|
||||
ProgressOverlay.Visibility = Visibility.Collapsed;
|
||||
_busyCts = null;
|
||||
_busyTask = null;
|
||||
cts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Remaining(long done, long total, TimeSpan elapsed)
|
||||
{
|
||||
if (done <= 0 || elapsed.TotalSeconds < 2) return "";
|
||||
var rest = TimeSpan.FromSeconds((total - done) / (done / elapsed.TotalSeconds));
|
||||
return rest.TotalSeconds < 5 ? " · gleich fertig"
|
||||
: rest.TotalMinutes < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalSeconds / 5) * 5:0} Sek."
|
||||
: rest.TotalHours < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalMinutes):0} Min."
|
||||
: $" · noch ca. {rest.TotalHours:0.#} Std.";
|
||||
}
|
||||
|
||||
private void ProgressCancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_busyCts?.Cancel();
|
||||
ProgressCancel.IsEnabled = false;
|
||||
ProgressCancel.Content = "Wird abgebrochen …";
|
||||
}
|
||||
|
||||
/// <summary>Beim Beenden: laufenden Vorgang abbrechen und kurz aufs Aufräumen warten.</summary>
|
||||
public void CancelBusyAndWait()
|
||||
{
|
||||
if (_busyCts is null) return;
|
||||
_busyCts.Cancel();
|
||||
try { _busyTask?.Wait(TimeSpan.FromSeconds(5)); } catch { }
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Fensterposition merken
|
||||
// =====================================================================
|
||||
|
||||
private void RestorePlacement()
|
||||
{
|
||||
if (S.WindowLeft is double l && S.WindowTop is double t && S.WindowWidth is double w && S.WindowHeight is double h)
|
||||
{
|
||||
var r = new Rect(l, t, Math.Max(MinWidth, w), Math.Max(MinHeight, h));
|
||||
// nur übernehmen, wenn das Fenster dort noch erreichbar ist (Monitor evtl. abgesteckt)
|
||||
if (Screens.IsReachable(r))
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
Left = r.Left;
|
||||
Top = r.Top;
|
||||
Width = r.Width;
|
||||
Height = r.Height;
|
||||
}
|
||||
}
|
||||
if (S.WindowMaximized) WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
public void SavePlacement()
|
||||
{
|
||||
if (!IsLoaded) return; // nie angezeigt (Start im Infobereich) → nichts zu merken
|
||||
var r = WindowState == WindowState.Normal ? new Rect(Left, Top, ActualWidth, ActualHeight) : RestoreBounds;
|
||||
if (r.IsEmpty || r.Width < 100) return;
|
||||
S.WindowLeft = r.Left;
|
||||
S.WindowTop = r.Top;
|
||||
S.WindowWidth = r.Width;
|
||||
S.WindowHeight = r.Height;
|
||||
S.WindowMaximized = WindowState == WindowState.Maximized;
|
||||
S.Save();
|
||||
}
|
||||
|
||||
/// <summary>Bildschirm abgesteckt/umgestellt: Fenster zurückholen, falls es nicht mehr erreichbar ist.</summary>
|
||||
public void EnsureOnScreen()
|
||||
{
|
||||
if (WindowState != WindowState.Normal) return;
|
||||
if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return;
|
||||
var wa = Screens.PrimaryWorkArea;
|
||||
Width = Math.Min(Width, wa.Width);
|
||||
Height = Math.Min(Height, wa.Height);
|
||||
Left = wa.Left + (wa.Width - Width) / 2;
|
||||
Top = wa.Top + (wa.Height - Height) / 2;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Updates
|
||||
// =====================================================================
|
||||
|
||||
private UpdateInfo? _update;
|
||||
|
||||
/// <summary>Beim Start: höchstens einmal am Tag nachsehen, leise im Hintergrund.</summary>
|
||||
public async void CheckForUpdatesOnStartup()
|
||||
{
|
||||
if (!S.AutoUpdateCheck) return;
|
||||
if (S.LastUpdateCheck is { } last && DateTime.Now - last < TimeSpan.FromHours(20)) return;
|
||||
await Task.Delay(TimeSpan.FromSeconds(8)); // Start nicht ausbremsen
|
||||
await CheckForUpdates(manual: false);
|
||||
}
|
||||
|
||||
private async Task CheckForUpdates(bool manual)
|
||||
{
|
||||
if (manual)
|
||||
{
|
||||
CheckUpdateButton.IsEnabled = false;
|
||||
UpdateStatusText.Text = "Suche nach Updates …";
|
||||
}
|
||||
try
|
||||
{
|
||||
var info = await UpdateChecker.CheckAsync();
|
||||
S.LastUpdateCheck = DateTime.Now;
|
||||
S.Save();
|
||||
if (info is null)
|
||||
{
|
||||
if (manual) UpdateStatusText.Text = $"Du hast die neueste Version ({UpdateChecker.Current}).";
|
||||
return;
|
||||
}
|
||||
if (!manual && S.SkippedVersion == info.Version.ToString()) return;
|
||||
|
||||
_update = info;
|
||||
UpdateText.Text = $"DeskCore {info.Version} ist verfügbar – du hast Version {UpdateChecker.Current}.";
|
||||
UpdateNowButton.Content = info.SetupUrl is not null ? "Jetzt aktualisieren" : "Zur Download-Seite";
|
||||
UpdateBar.Visibility = Visibility.Visible;
|
||||
if (manual) UpdateStatusText.Text = $"Version {info.Version} ist verfügbar – oben auf der Startseite aktualisieren.";
|
||||
else if (!IsVisible) App.TrayIcon?.Notify("Update verfügbar", $"DeskCore {info.Version} ist verfügbar. Zum Aktualisieren DeskCore öffnen.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (manual) UpdateStatusText.Text = $"Konnte nicht prüfen: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (manual) CheckUpdateButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void CheckUpdate_Click(object sender, RoutedEventArgs e) => await CheckForUpdates(manual: true);
|
||||
|
||||
private void AutoUpdateToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.AutoUpdateCheck = AutoUpdateToggle.IsChecked == true;
|
||||
S.Save();
|
||||
}
|
||||
|
||||
private void UpdateLater_Click(object sender, RoutedEventArgs e) => UpdateBar.Visibility = Visibility.Collapsed;
|
||||
|
||||
private void UpdateSkip_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_update is not null)
|
||||
{
|
||||
S.SkippedVersion = _update.Version.ToString();
|
||||
S.Save();
|
||||
}
|
||||
UpdateBar.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async void UpdateNow_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_update is not { } info || IsBusy) return;
|
||||
if (info.SetupUrl is null)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(info.PageUrl) { UseShellExecute = true });
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var setup = await RunWithProgress($"Update auf {info.Version}", "Neue Version wird heruntergeladen …",
|
||||
(sink, ct) => UpdateChecker.DownloadSetupAsync(info, (done, total) => { sink.SetTotal(total); sink.SetDone(done); }, ct));
|
||||
// Installer still ausführen und DeskCore danach automatisch neu starten
|
||||
Process.Start(new ProcessStartInfo(setup, "/S /launch") { UseShellExecute = true });
|
||||
App.Quit();
|
||||
}
|
||||
catch (OperationCanceledException) { ShowToast("Update abgebrochen", "", info: true); }
|
||||
catch (Exception ex) { Error("Update fehlgeschlagen", $"{ex.Message}\n\nDu kannst die neue Version auch selbst herunterladen:\n{info.PageUrl}"); }
|
||||
}
|
||||
}
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Fortschrittsanzeige für lange Vorgänge, Fensterposition merken und Update-Hinweis.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
// =====================================================================
|
||||
// Fortschritt mit Abbrechen
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Meldet Fortschritt aus dem Hintergrund (threadsicher).</summary>
|
||||
public sealed class ProgressSink
|
||||
{
|
||||
private long _done, _total;
|
||||
public long Done => Interlocked.Read(ref _done);
|
||||
public long Total => Interlocked.Read(ref _total);
|
||||
public void Add(long bytes) => Interlocked.Add(ref _done, bytes);
|
||||
public void SetTotal(long total) => Interlocked.Exchange(ref _total, total);
|
||||
public void SetDone(long done) => Interlocked.Exchange(ref _done, done);
|
||||
public volatile string? Detail;
|
||||
}
|
||||
|
||||
private CancellationTokenSource? _busyCts;
|
||||
private Task? _busyTask;
|
||||
|
||||
public bool IsBusy => _busyCts is not null;
|
||||
|
||||
/// <summary>
|
||||
/// Führt einen langen Vorgang im Hintergrund aus. Das Fenster bleibt bedienbar; nach kurzer Zeit erscheint
|
||||
/// eine Fortschrittsanzeige mit Abbrechen-Knopf (bei schnellen Vorgängen gar nicht erst).
|
||||
/// </summary>
|
||||
private async Task<T> RunWithProgress<T>(string title, string detail, Func<ProgressSink, CancellationToken, Task<T>> work)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
_busyCts = cts;
|
||||
var sink = new ProgressSink { Detail = detail };
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
ProgressTitle.Text = title;
|
||||
ProgressDetail.Text = detail;
|
||||
ProgressStats.Text = "";
|
||||
ProgressFill.Width = 0;
|
||||
ProgressCancel.IsEnabled = true;
|
||||
ProgressCancel.Content = "Abbrechen";
|
||||
|
||||
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(200) };
|
||||
timer.Tick += (_, _) =>
|
||||
{
|
||||
if (ProgressOverlay.Visibility != Visibility.Visible && sw.ElapsedMilliseconds > 500)
|
||||
ProgressOverlay.Visibility = Visibility.Visible;
|
||||
if (sink.Detail is { } d) ProgressDetail.Text = d;
|
||||
long done = sink.Done, total = sink.Total;
|
||||
if (total > 0)
|
||||
{
|
||||
double frac = Math.Min(1, done / (double)total);
|
||||
ProgressFill.Width = ProgressTrack.ActualWidth * frac;
|
||||
ProgressStats.Text = $"{Backup.FormatSize(done)} von {Backup.FormatSize(total)}{Remaining(done, total, sw.Elapsed)}";
|
||||
}
|
||||
else if (done > 0) ProgressStats.Text = Backup.FormatSize(done);
|
||||
};
|
||||
timer.Start();
|
||||
|
||||
var task = Task.Run(() => work(sink, cts.Token));
|
||||
_busyTask = task;
|
||||
try
|
||||
{
|
||||
return await task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
timer.Stop();
|
||||
ProgressOverlay.Visibility = Visibility.Collapsed;
|
||||
_busyCts = null;
|
||||
_busyTask = null;
|
||||
cts.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static string Remaining(long done, long total, TimeSpan elapsed)
|
||||
{
|
||||
if (done <= 0 || elapsed.TotalSeconds < 2) return "";
|
||||
var rest = TimeSpan.FromSeconds((total - done) / (done / elapsed.TotalSeconds));
|
||||
return rest.TotalSeconds < 5 ? " · gleich fertig"
|
||||
: rest.TotalMinutes < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalSeconds / 5) * 5:0} Sek."
|
||||
: rest.TotalHours < 1 ? $" · noch ca. {Math.Ceiling(rest.TotalMinutes):0} Min."
|
||||
: $" · noch ca. {rest.TotalHours:0.#} Std.";
|
||||
}
|
||||
|
||||
private void ProgressCancel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_busyCts?.Cancel();
|
||||
ProgressCancel.IsEnabled = false;
|
||||
ProgressCancel.Content = "Wird abgebrochen …";
|
||||
}
|
||||
|
||||
/// <summary>Beim Beenden: laufenden Vorgang abbrechen und kurz aufs Aufräumen warten.</summary>
|
||||
public void CancelBusyAndWait()
|
||||
{
|
||||
if (_busyCts is null) return;
|
||||
_busyCts.Cancel();
|
||||
try { _busyTask?.Wait(TimeSpan.FromSeconds(5)); } catch { }
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Fensterposition merken
|
||||
// =====================================================================
|
||||
|
||||
private void RestorePlacement()
|
||||
{
|
||||
if (S.WindowLeft is double l && S.WindowTop is double t && S.WindowWidth is double w && S.WindowHeight is double h)
|
||||
{
|
||||
var r = new Rect(l, t, Math.Max(MinWidth, w), Math.Max(MinHeight, h));
|
||||
// nur übernehmen, wenn das Fenster dort noch erreichbar ist (Monitor evtl. abgesteckt)
|
||||
if (Screens.IsReachable(r))
|
||||
{
|
||||
WindowStartupLocation = WindowStartupLocation.Manual;
|
||||
Left = r.Left;
|
||||
Top = r.Top;
|
||||
Width = r.Width;
|
||||
Height = r.Height;
|
||||
}
|
||||
}
|
||||
if (S.WindowMaximized) WindowState = WindowState.Maximized;
|
||||
}
|
||||
|
||||
public void SavePlacement()
|
||||
{
|
||||
if (!IsLoaded) return; // nie angezeigt (Start im Infobereich) → nichts zu merken
|
||||
var r = WindowState == WindowState.Normal ? new Rect(Left, Top, ActualWidth, ActualHeight) : RestoreBounds;
|
||||
if (r.IsEmpty || r.Width < 100) return;
|
||||
S.WindowLeft = r.Left;
|
||||
S.WindowTop = r.Top;
|
||||
S.WindowWidth = r.Width;
|
||||
S.WindowHeight = r.Height;
|
||||
S.WindowMaximized = WindowState == WindowState.Maximized;
|
||||
S.Save();
|
||||
}
|
||||
|
||||
/// <summary>Bildschirm abgesteckt/umgestellt: Fenster zurückholen, falls es nicht mehr erreichbar ist.</summary>
|
||||
public void EnsureOnScreen()
|
||||
{
|
||||
if (WindowState != WindowState.Normal) return;
|
||||
if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return;
|
||||
var wa = Screens.PrimaryWorkArea;
|
||||
Width = Math.Min(Width, wa.Width);
|
||||
Height = Math.Min(Height, wa.Height);
|
||||
Left = wa.Left + (wa.Width - Width) / 2;
|
||||
Top = wa.Top + (wa.Height - Height) / 2;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Updates
|
||||
// =====================================================================
|
||||
|
||||
private UpdateInfo? _update;
|
||||
|
||||
/// <summary>Beim Start: höchstens einmal am Tag nachsehen, leise im Hintergrund.</summary>
|
||||
public async void CheckForUpdatesOnStartup()
|
||||
{
|
||||
if (!S.AutoUpdateCheck) return;
|
||||
if (S.LastUpdateCheck is { } last && DateTime.Now - last < TimeSpan.FromHours(20)) return;
|
||||
await Task.Delay(TimeSpan.FromSeconds(8)); // Start nicht ausbremsen
|
||||
await CheckForUpdates(manual: false);
|
||||
}
|
||||
|
||||
private async Task CheckForUpdates(bool manual)
|
||||
{
|
||||
if (manual)
|
||||
{
|
||||
CheckUpdateButton.IsEnabled = false;
|
||||
UpdateStatusText.Text = "Suche nach Updates …";
|
||||
}
|
||||
try
|
||||
{
|
||||
var info = await UpdateChecker.CheckAsync();
|
||||
S.LastUpdateCheck = DateTime.Now;
|
||||
S.Save();
|
||||
if (info is null)
|
||||
{
|
||||
if (manual) UpdateStatusText.Text = $"Du hast die neueste Version ({UpdateChecker.Current}).";
|
||||
return;
|
||||
}
|
||||
if (!manual && S.SkippedVersion == info.Version.ToString()) return;
|
||||
|
||||
_update = info;
|
||||
UpdateText.Text = $"DeskCore {info.Version} ist verfügbar – du hast Version {UpdateChecker.Current}.";
|
||||
UpdateNowButton.Content = info.SetupUrl is not null ? "Jetzt aktualisieren" : "Zur Download-Seite";
|
||||
UpdateBar.Visibility = Visibility.Visible;
|
||||
if (manual) UpdateStatusText.Text = $"Version {info.Version} ist verfügbar – oben auf der Startseite aktualisieren.";
|
||||
else if (!IsVisible) App.TrayIcon?.Notify("Update verfügbar", $"DeskCore {info.Version} ist verfügbar. Zum Aktualisieren DeskCore öffnen.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (manual) UpdateStatusText.Text = $"Konnte nicht prüfen: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (manual) CheckUpdateButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void CheckUpdate_Click(object sender, RoutedEventArgs e) => await CheckForUpdates(manual: true);
|
||||
|
||||
private void AutoUpdateToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.AutoUpdateCheck = AutoUpdateToggle.IsChecked == true;
|
||||
S.Save();
|
||||
}
|
||||
|
||||
private void UpdateLater_Click(object sender, RoutedEventArgs e) => UpdateBar.Visibility = Visibility.Collapsed;
|
||||
|
||||
private void UpdateSkip_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_update is not null)
|
||||
{
|
||||
S.SkippedVersion = _update.Version.ToString();
|
||||
S.Save();
|
||||
}
|
||||
UpdateBar.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private async void UpdateNow_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_update is not { } info || IsBusy) return;
|
||||
if (info.SetupUrl is null)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(info.PageUrl) { UseShellExecute = true });
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
var setup = await RunWithProgress($"Update auf {info.Version}", "Neue Version wird heruntergeladen …",
|
||||
(sink, ct) => UpdateChecker.DownloadSetupAsync(info, (done, total) => { sink.SetTotal(total); sink.SetDone(done); }, ct));
|
||||
// Installer still ausführen und DeskCore danach automatisch neu starten.
|
||||
// Windows fragt dabei nach Administratorrechten (der Installer kümmert sich auch um die .NET-Laufzeit).
|
||||
Process.Start(new ProcessStartInfo(setup, "/S /launch") { UseShellExecute = true });
|
||||
App.Quit();
|
||||
}
|
||||
catch (OperationCanceledException) { ShowToast("Update abgebrochen", "", info: true); }
|
||||
catch (System.ComponentModel.Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||||
{
|
||||
ShowToast("Update abgebrochen – bitte die Windows-Abfrage mit „Ja“ bestätigen", "", info: true);
|
||||
}
|
||||
catch (Exception ex) { Error("Update fehlgeschlagen", $"{ex.Message}\n\nDu kannst die neue Version auch selbst herunterladen:\n{info.PageUrl}"); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Einstellungen der Desktop-Boxen: Portale, Schnell verstecken, Peek, Chamäleon und Snapshots.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
/// <summary>Box geschlossen o. Ä. → Kennzeichnung "hat Box" in der Seitenleiste aktualisieren.</summary>
|
||||
public void RefreshFromFences()
|
||||
{
|
||||
Refresh();
|
||||
if (InSettings) { LoadFenceSettings(); LoadDesktopSettings(); }
|
||||
}
|
||||
|
||||
private void LoadFenceSettings()
|
||||
{
|
||||
QuickHideToggle.IsChecked = S.QuickHide;
|
||||
PeekToggle.IsChecked = S.PeekEnabled;
|
||||
PeekHint.Text = $"{Hotkey.Describe(S.PeekModifiers, S.PeekKey)} holt alle Boxen vor die offenen Fenster. " +
|
||||
"Nochmal drücken, Esc oder woanders hinklicken schickt sie zurück.";
|
||||
ChameleonToggle.IsChecked = S.FenceChameleon;
|
||||
SnapshotList.ItemsSource = null;
|
||||
SnapshotList.ItemsSource = S.Snapshots.OrderByDescending(x => x.Created).ToList();
|
||||
}
|
||||
|
||||
private void AddPortal_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
int before = S.Fences.Count;
|
||||
FenceManager.AddPortal();
|
||||
if (S.Fences.Count > before) ShowToast("Ordner-Portal liegt jetzt auf dem Desktop", "");
|
||||
LoadSettingsView();
|
||||
}
|
||||
|
||||
private void QuickHideToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.QuickHide = QuickHideToggle.IsChecked == true;
|
||||
S.Save();
|
||||
DesktopToggle.Apply();
|
||||
}
|
||||
|
||||
private void PeekToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.PeekEnabled = PeekToggle.IsChecked == true;
|
||||
S.Save();
|
||||
if (!App.ApplyPeekHotkey())
|
||||
PeekHint.Text = "⚠ Diese Tastenkombination ist schon von einem anderen Programm belegt.";
|
||||
}
|
||||
|
||||
private void ChameleonToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.FenceChameleon = ChameleonToggle.IsChecked == true;
|
||||
S.Save();
|
||||
FenceManager.RefreshAll();
|
||||
}
|
||||
|
||||
// ---------- Snapshots ----------
|
||||
|
||||
private void SaveSnapshot_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (S.Fences.Count == 0)
|
||||
{
|
||||
ShowToast("Es gibt noch keine Boxen zum Speichern", "", info: true);
|
||||
return;
|
||||
}
|
||||
var name = Dialog.Prompt(this, "Anordnung speichern", "Name für diese Anordnung (z. B. „Arbeit“ oder „Zocken“):",
|
||||
$"Anordnung {S.Snapshots.Count + 1}", "Speichern",
|
||||
t => t.Trim().Length == 0 ? "Bitte einen Namen eingeben." : t.Contains('"') ? "Bitte ohne Anführungszeichen." : null);
|
||||
if (name is null) return;
|
||||
bool replace = S.Snapshots.Any(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
FenceManager.SaveSnapshot(name);
|
||||
LoadFenceSettings();
|
||||
ShowToast(replace ? $"Anordnung „{name}“ aktualisiert" : $"Anordnung „{name}“ gespeichert", "");
|
||||
}
|
||||
|
||||
private void RestoreSnapshot(FenceSnapshot snap)
|
||||
{
|
||||
var before = FenceManager.RestoreSnapshot(snap);
|
||||
var entry = new UndoEntry($"Anordnung „{snap.Name}“ laden");
|
||||
entry.Add(() => { FenceManager.ApplyFences(before); return null; });
|
||||
Undo.Push(entry);
|
||||
Refresh();
|
||||
ShowToast($"Anordnung „{snap.Name}“ wiederhergestellt", "", undoable: true);
|
||||
}
|
||||
|
||||
private void RestoreSnapshot_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is FenceSnapshot snap) RestoreSnapshot(snap);
|
||||
}
|
||||
|
||||
private void DeleteSnapshot_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is not FenceSnapshot snap) return;
|
||||
S.Snapshots.Remove(snap);
|
||||
S.Save();
|
||||
var entry = new UndoEntry($"Anordnung „{snap.Name}“ löschen");
|
||||
entry.Add(() => { S.Snapshots.Add(snap); S.Save(); return null; });
|
||||
Undo.Push(entry);
|
||||
LoadFenceSettings();
|
||||
ShowToast($"Anordnung „{snap.Name}“ gelöscht", "", undoable: true);
|
||||
}
|
||||
|
||||
/// <summary>Für die Befehlszeile: DeskCore.exe --snapshot "Name".</summary>
|
||||
public void RestoreSnapshotByName(string name)
|
||||
{
|
||||
var snap = S.Snapshots.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
if (snap is null) App.TrayIcon?.Notify("Anordnung nicht gefunden", $"Es gibt keine gespeicherte Anordnung „{name}“.");
|
||||
else RestoreSnapshot(snap);
|
||||
}
|
||||
|
||||
// ---------- Regeln: Name oder Dateityp ----------
|
||||
|
||||
private void RuleKind_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is not SortRule r) return;
|
||||
r.Kind = r.Kind == "Typ" ? "Name" : "Typ";
|
||||
S.Save();
|
||||
LoadSettingsView();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Start-Gruppen: ein Klick startet mehrere Programme nacheinander.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
private LaunchGroupItem? CurrentGroup => LaunchList.SelectedItem as LaunchGroupItem;
|
||||
|
||||
/// <summary>Kategorie-Dialog von außen (z. B. "Neue Box hier" auf dem Desktop).</summary>
|
||||
public string? CreateCategoryInteractive() => NewCategory();
|
||||
|
||||
private static string DelayText(LaunchGroup g) =>
|
||||
g.DelaySeconds <= 0 ? "gleichzeitig" : $"{g.DelaySeconds:0.#} s Abstand";
|
||||
|
||||
/// <summary>Wo liegt ein Element? (für Tooltip/Listenansicht)</summary>
|
||||
private static string LocationOf(string path)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(path) ?? "";
|
||||
if (Storage.IsDesktopDir(dir)) return DesktopName;
|
||||
return Storage.IsInStorage(path) ? Path.GetFileName(dir) : dir;
|
||||
}
|
||||
|
||||
private void LoadGroupIcons(LaunchGroup g)
|
||||
{
|
||||
var items = g.Items.Select(p => new IconItem { Path = p, Category = LocationOf(p) }).ToList();
|
||||
HeaderText.Text = g.Name;
|
||||
int missing = items.Count(i => !File.Exists(i.Path) && !Directory.Exists(i.Path));
|
||||
SubText.Text = $"{Plural(items.Count, "Programm", "Programme")} · {DelayText(g)}" +
|
||||
(missing > 0 ? $" · ⚠ {missing} nicht mehr vorhanden" : "");
|
||||
SetEmpty("", "Noch leer",
|
||||
"Ziehe Programme hierher oder auf die Gruppe links – oder Rechtsklick auf ein Icon → „Zur Start-Gruppe hinzufügen“.");
|
||||
|
||||
// Reihenfolge = Startreihenfolge, deshalb hier nicht sortieren
|
||||
var px = IconPixels;
|
||||
foreach (var item in items) IconLoader.Load(item, px);
|
||||
IconList.ItemsSource = items;
|
||||
|
||||
EmptyState.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
CategoryCommands.Visibility = Visibility.Collapsed;
|
||||
InfoBar.Visibility = Visibility.Collapsed;
|
||||
GroupCommands.Visibility = Visibility.Visible;
|
||||
}
|
||||
|
||||
private void SelectGroup(LaunchGroup g)
|
||||
{
|
||||
var item = LaunchList.Items.OfType<LaunchGroupItem>().FirstOrDefault(x => x.Group == g);
|
||||
if (item is not null) LaunchList.SelectedItem = item;
|
||||
}
|
||||
|
||||
// ---------- Starten ----------
|
||||
|
||||
/// <summary>Startet alle Programme der Gruppe nacheinander (mit eingestelltem Abstand).</summary>
|
||||
public async void LaunchGroupNow(LaunchGroup g)
|
||||
{
|
||||
if (g.Items.Count == 0)
|
||||
{
|
||||
ShowToast($"„{g.Name}“ ist noch leer", "", info: true);
|
||||
return;
|
||||
}
|
||||
Usage.Record(Usage.GroupPrefix + g.Name); // damit die Schnellstart-Suche häufige Gruppen weiter oben zeigt
|
||||
int started = 0;
|
||||
var problems = new List<string>();
|
||||
var items = g.Items.ToList();
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
if (i > 0 && g.DelaySeconds > 0) await Task.Delay(TimeSpan.FromSeconds(g.DelaySeconds));
|
||||
var p = items[i];
|
||||
if (!File.Exists(p) && !Directory.Exists(p)) { problems.Add($"„{Storage.DisplayName(p)}“ gibt es nicht mehr"); continue; }
|
||||
try
|
||||
{
|
||||
Usage.Start(p);
|
||||
started++;
|
||||
}
|
||||
catch (Exception ex) { problems.Add($"„{Storage.DisplayName(p)}“: {ex.Message}"); }
|
||||
}
|
||||
|
||||
var text = $"„{g.Name}“: {Plural(started, "Programm", "Programme")} gestartet" +
|
||||
(problems.Count > 0 ? $" · {problems.Count} nicht" : "");
|
||||
if (IsVisible && IsActive) ShowToast(text, problems.Count > 0 ? "" : "", info: problems.Count > 0);
|
||||
else App.TrayIcon?.Notify(g.Name, problems.Count > 0 ? text + "\n" + string.Join("\n", problems.Take(3)) : text);
|
||||
}
|
||||
|
||||
/// <summary>Für die Befehlszeile und Desktop-Verknüpfungen: DeskCore.exe --launch "Name".</summary>
|
||||
public bool LaunchGroupByName(string name)
|
||||
{
|
||||
var g = S.LaunchGroups.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
if (g is null)
|
||||
{
|
||||
App.TrayIcon?.Notify("Start-Gruppe nicht gefunden", $"Es gibt keine Start-Gruppe „{name}“.");
|
||||
return false;
|
||||
}
|
||||
LaunchGroupNow(g);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LaunchCurrentGroup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CurrentGroup is { } g) LaunchGroupNow(g.Group);
|
||||
}
|
||||
|
||||
private void LaunchGroupPlay_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is LaunchGroupItem g) LaunchGroupNow(g.Group);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
// ---------- Anlegen / Bearbeiten ----------
|
||||
|
||||
/// <summary>Dialog für Name, Startabstand und Farbe. Gibt null bei Abbruch zurück.</summary>
|
||||
private (string Name, double Delay, string? Color)? LaunchGroupDialog(string title, string primary, LaunchGroup? existing)
|
||||
{
|
||||
var res = Application.Current.Resources;
|
||||
var panel = new StackPanel();
|
||||
TextBlock Label(string t, double top = 16) => new()
|
||||
{
|
||||
Text = t, Margin = new Thickness(0, top, 0, 8), FontWeight = FontWeights.SemiBold, FontSize = 13,
|
||||
};
|
||||
|
||||
panel.Children.Add(Label("Name", 0));
|
||||
var nameBox = new TextBox { Text = existing?.Name ?? "" };
|
||||
panel.Children.Add(nameBox);
|
||||
|
||||
panel.Children.Add(Label("Abstand zwischen den Programmen"));
|
||||
var delayText = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Width = 110, Foreground = (Brush)res["TextSecondaryBrush"] };
|
||||
var slider = new Slider { Minimum = 0, Maximum = 10, TickFrequency = 0.5, IsSnapToTickEnabled = true, Value = existing?.DelaySeconds ?? 1 };
|
||||
void UpdateDelay() => delayText.Text = slider.Value <= 0 ? "gleichzeitig" : $"{slider.Value:0.#} Sekunden";
|
||||
slider.ValueChanged += (_, _) => UpdateDelay();
|
||||
UpdateDelay();
|
||||
var row = new DockPanel();
|
||||
DockPanel.SetDock(delayText, Dock.Right);
|
||||
row.Children.Add(delayText);
|
||||
row.Children.Add(slider);
|
||||
panel.Children.Add(row);
|
||||
panel.Children.Add(new TextBlock
|
||||
{
|
||||
Text = "Etwas Abstand hilft, wenn Programme beim Start viel Leistung brauchen.", FontSize = 12, Margin = new Thickness(0, 4, 0, 0),
|
||||
TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextTertiaryBrush"],
|
||||
});
|
||||
|
||||
panel.Children.Add(Label("Farbe"));
|
||||
string? color = existing?.Color;
|
||||
var swatches = new WrapPanel();
|
||||
var none = new RadioButton
|
||||
{
|
||||
Style = (Style)res["SwatchRadio"], GroupName = "lgcolor", ToolTip = "Akzentfarbe", Background = (Brush)res["AccentBrush"],
|
||||
IsChecked = color is null,
|
||||
};
|
||||
none.Checked += (_, _) => color = null;
|
||||
swatches.Children.Add(none);
|
||||
foreach (var (cname, hex) in CategoryColors.All)
|
||||
{
|
||||
var rb = new RadioButton
|
||||
{
|
||||
Style = (Style)res["SwatchRadio"], GroupName = "lgcolor", ToolTip = cname, Background = CategoryColors.Brush(hex),
|
||||
IsChecked = string.Equals(color, hex, StringComparison.OrdinalIgnoreCase),
|
||||
};
|
||||
rb.Checked += (_, _) => color = hex;
|
||||
swatches.Children.Add(rb);
|
||||
}
|
||||
panel.Children.Add(swatches);
|
||||
|
||||
string? Validate()
|
||||
{
|
||||
var n = nameBox.Text.Trim();
|
||||
if (n.Length == 0) return "Bitte einen Namen eingeben.";
|
||||
if (n.Contains('"')) return "Der Name darf keine Anführungszeichen enthalten.";
|
||||
bool taken = S.LaunchGroups.Any(x => x != existing && string.Equals(x.Name, n, StringComparison.OrdinalIgnoreCase));
|
||||
return taken ? "Eine Start-Gruppe mit diesem Namen gibt es schon." : null;
|
||||
}
|
||||
|
||||
if (!Dialog.Show(this, title, panel, primary, "Abbrechen", Validate, nameBox, width: 470)) return null;
|
||||
return (nameBox.Text.Trim(), slider.Value, color);
|
||||
}
|
||||
|
||||
private LaunchGroup? NewLaunchGroup(IEnumerable<string>? initialItems = null)
|
||||
{
|
||||
var form = LaunchGroupDialog("Neue Start-Gruppe", "Erstellen", null);
|
||||
if (form is null) return null;
|
||||
var g = new LaunchGroup { Name = form.Value.Name, DelaySeconds = form.Value.Delay, Color = form.Value.Color };
|
||||
S.LaunchGroups.Add(g);
|
||||
if (initialItems is not null) AddPathsToGroup(g, initialItems, toast: false);
|
||||
S.Save();
|
||||
|
||||
var entry = new UndoEntry($"Start-Gruppe „{g.Name}“ erstellen");
|
||||
entry.Add(() => { S.LaunchGroups.Remove(g); S.Save(); return null; });
|
||||
Undo.Push(entry);
|
||||
Refresh();
|
||||
return g;
|
||||
}
|
||||
|
||||
private void NewLaunchGroup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (NewLaunchGroup() is not { } g) return;
|
||||
ShowBrowse();
|
||||
SelectGroup(g);
|
||||
ShowToast($"Start-Gruppe „{g.Name}“ erstellt – jetzt Programme hineinziehen", "", undoable: true);
|
||||
}
|
||||
|
||||
private void EditLaunchGroup(LaunchGroup g)
|
||||
{
|
||||
var form = LaunchGroupDialog("Start-Gruppe bearbeiten", "Speichern", g);
|
||||
if (form is null) return;
|
||||
var before = (g.Name, g.DelaySeconds, g.Color);
|
||||
(g.Name, g.DelaySeconds, g.Color) = form.Value;
|
||||
Usage.Rename(Usage.GroupPrefix + before.Name, Usage.GroupPrefix + g.Name);
|
||||
S.Save();
|
||||
var entry = new UndoEntry($"Start-Gruppe „{before.Name}“ bearbeiten");
|
||||
entry.Add(() => { (g.Name, g.DelaySeconds, g.Color) = before; S.Save(); return null; });
|
||||
Undo.Push(entry);
|
||||
Refresh();
|
||||
if (before.Name != g.Name)
|
||||
ShowToast("Gespeichert – eine vorhandene Desktop-Verknüpfung bitte neu anlegen (sie nutzt noch den alten Namen)", "", info: true);
|
||||
else ShowToast("Start-Gruppe gespeichert", undoable: true);
|
||||
}
|
||||
|
||||
private void EditLaunchGroup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CurrentGroup is { } g) EditLaunchGroup(g.Group);
|
||||
}
|
||||
|
||||
private void DeleteLaunchGroup(LaunchGroup g)
|
||||
{
|
||||
if (!Dialog.Confirm(this, "Start-Gruppe löschen?",
|
||||
$"„{g.Name}“ wird entfernt. Die Programme selbst bleiben unverändert.", "Löschen"))
|
||||
return;
|
||||
int index = S.LaunchGroups.IndexOf(g);
|
||||
S.LaunchGroups.Remove(g);
|
||||
S.Save();
|
||||
var entry = new UndoEntry($"Start-Gruppe „{g.Name}“ löschen");
|
||||
entry.Add(() => { S.LaunchGroups.Insert(Math.Min(index, S.LaunchGroups.Count), g); S.Save(); return null; });
|
||||
Undo.Push(entry);
|
||||
Refresh();
|
||||
ShowToast($"Start-Gruppe „{g.Name}“ gelöscht", "", undoable: true);
|
||||
}
|
||||
|
||||
private void DeleteLaunchGroup_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CurrentGroup is { } g) DeleteLaunchGroup(g.Group);
|
||||
}
|
||||
|
||||
private void MoveLaunchGroup(LaunchGroup g, int delta)
|
||||
{
|
||||
int i = S.LaunchGroups.IndexOf(g), j = i + delta;
|
||||
if (i < 0 || j < 0 || j >= S.LaunchGroups.Count) return;
|
||||
(S.LaunchGroups[i], S.LaunchGroups[j]) = (S.LaunchGroups[j], S.LaunchGroups[i]);
|
||||
S.Save();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
// ---------- Inhalt ----------
|
||||
|
||||
/// <summary>
|
||||
/// Desktop-Icons, die schon in einer Kategorie gesichert sind, werden durch die gesicherte Kopie ersetzt –
|
||||
/// so funktioniert die Gruppe auch noch, wenn das Icon später vom Desktop gelöscht wird.
|
||||
/// </summary>
|
||||
private static string ResolveForGroup(string path)
|
||||
{
|
||||
if (Storage.IsInStorage(path)) return path;
|
||||
foreach (var c in Storage.Categories())
|
||||
if (Storage.Entries(Storage.CategoryPath(c)).FirstOrDefault(e => Storage.SameItem(e, path)) is { } saved)
|
||||
return saved;
|
||||
return path;
|
||||
}
|
||||
|
||||
private void AddPathsToGroup(LaunchGroup g, IEnumerable<string> paths, bool toast = true)
|
||||
{
|
||||
int added = 0, dup = 0;
|
||||
foreach (var raw in paths)
|
||||
{
|
||||
var p = ResolveForGroup(raw);
|
||||
if (g.Items.Contains(p, StringComparer.OrdinalIgnoreCase)) { dup++; continue; }
|
||||
g.Items.Add(p);
|
||||
added++;
|
||||
}
|
||||
S.Save();
|
||||
if (!toast) return;
|
||||
Refresh();
|
||||
if (added == 0) ShowToast(dup == 1 ? "Das ist schon in der Gruppe" : "Alles schon in der Gruppe", "", info: true);
|
||||
else ShowToast($"{Plural(added, "Programm", "Programme")} zu „{g.Name}“ hinzugefügt", "");
|
||||
}
|
||||
|
||||
private void RemoveFromGroup(LaunchGroup g, List<IconItem> sel)
|
||||
{
|
||||
var before = g.Items.ToList();
|
||||
foreach (var i in sel) g.Items.RemoveAll(p => string.Equals(p, i.Path, StringComparison.OrdinalIgnoreCase));
|
||||
S.Save();
|
||||
var entry = new UndoEntry($"Aus „{g.Name}“ entfernen");
|
||||
entry.Add(() => { g.Items = before; S.Save(); return null; });
|
||||
Undo.Push(entry);
|
||||
Refresh();
|
||||
ShowToast($"{Plural(sel.Count, "Programm", "Programme")} aus „{g.Name}“ entfernt – die Dateien bleiben erhalten", "", undoable: true);
|
||||
}
|
||||
|
||||
private void MoveInGroup(LaunchGroup g, IconItem item, int delta)
|
||||
{
|
||||
int i = g.Items.FindIndex(p => string.Equals(p, item.Path, StringComparison.OrdinalIgnoreCase)), j = i + delta;
|
||||
if (i < 0 || j < 0 || j >= g.Items.Count) return;
|
||||
(g.Items[i], g.Items[j]) = (g.Items[j], g.Items[i]);
|
||||
S.Save();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
/// <summary>Kontextmenü der Icons, wenn eine Start-Gruppe angezeigt wird.</summary>
|
||||
private void FillGroupItemMenu(LaunchGroup g, List<IconItem> sel)
|
||||
{
|
||||
IconMenu.Items.Add(MenuItem("Öffnen", "", () => sel.ForEach(i => Open(i.Path)), bold: true, gesture: "Enter"));
|
||||
if (sel.Count == 1)
|
||||
{
|
||||
IconMenu.Items.Add(MenuItem("Dateispeicherort öffnen", "", () =>
|
||||
Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\"")));
|
||||
IconMenu.Items.Add(new Separator());
|
||||
int idx = g.Items.FindIndex(p => string.Equals(p, sel[0].Path, StringComparison.OrdinalIgnoreCase));
|
||||
var up = MenuItem("Früher starten", "", () => MoveInGroup(g, sel[0], -1));
|
||||
up.IsEnabled = idx > 0;
|
||||
var down = MenuItem("Später starten", "", () => MoveInGroup(g, sel[0], +1));
|
||||
down.IsEnabled = idx >= 0 && idx < g.Items.Count - 1;
|
||||
IconMenu.Items.Add(up);
|
||||
IconMenu.Items.Add(down);
|
||||
}
|
||||
IconMenu.Items.Add(new Separator());
|
||||
IconMenu.Items.Add(MenuItem("Aus Start-Gruppe entfernen", "", () => RemoveFromGroup(g, sel), gesture: "Entf"));
|
||||
}
|
||||
|
||||
/// <summary>Untermenü "Zur Start-Gruppe hinzufügen" (Haupt-Kontextmenü und Desktop-Boxen).</summary>
|
||||
public MenuItem LaunchGroupSubmenu(List<string> paths)
|
||||
{
|
||||
var menu = MenuItem("Zur Start-Gruppe hinzufügen", "", null);
|
||||
foreach (var g in S.LaunchGroups)
|
||||
menu.Items.Add(MenuItem(g.Name, "", () => AddPathsToGroup(g, paths)));
|
||||
if (menu.Items.Count > 0) menu.Items.Add(new Separator());
|
||||
menu.Items.Add(MenuItem("Neue Start-Gruppe…", "", () =>
|
||||
{
|
||||
if (NewLaunchGroup(paths) is { } g)
|
||||
ShowToast($"Start-Gruppe „{g.Name}“ mit {Plural(g.Items.Count, "Programm", "Programmen")} erstellt", "", undoable: true);
|
||||
}));
|
||||
return menu;
|
||||
}
|
||||
|
||||
// ---------- Seitenleiste ----------
|
||||
|
||||
private void LaunchList_ContextMenuOpening(object sender, ContextMenuEventArgs e)
|
||||
{
|
||||
if (FindParent<ListBoxItem>(e.OriginalSource as DependencyObject)?.DataContext is not LaunchGroupItem item)
|
||||
{
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
var g = item.Group;
|
||||
int idx = S.LaunchGroups.IndexOf(g);
|
||||
LaunchMenu.Items.Clear();
|
||||
LaunchMenu.Items.Add(MenuItem("Alle starten", "", () => LaunchGroupNow(g), bold: true));
|
||||
LaunchMenu.Items.Add(MenuItem("Bearbeiten…", "", () => EditLaunchGroup(g)));
|
||||
LaunchMenu.Items.Add(MenuItem("Verknüpfung auf den Desktop", "", () => CreateGroupShortcut(g)));
|
||||
LaunchMenu.Items.Add(new Separator());
|
||||
var up = MenuItem("Nach oben", "", () => MoveLaunchGroup(g, -1));
|
||||
up.IsEnabled = idx > 0;
|
||||
var down = MenuItem("Nach unten", "", () => MoveLaunchGroup(g, +1));
|
||||
down.IsEnabled = idx < S.LaunchGroups.Count - 1;
|
||||
LaunchMenu.Items.Add(up);
|
||||
LaunchMenu.Items.Add(down);
|
||||
LaunchMenu.Items.Add(new Separator());
|
||||
LaunchMenu.Items.Add(MenuItem("Löschen", "", () => DeleteLaunchGroup(g)));
|
||||
}
|
||||
|
||||
private void LaunchList_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
var lbi = FindParent<ListBoxItem>(e.OriginalSource as DependencyObject);
|
||||
bool ok = lbi is not null && e.Data.GetDataPresent(DataFormats.FileDrop);
|
||||
SetDropHighlight(ok ? lbi : null);
|
||||
e.Effects = ok ? DragDropEffects.Link | (e.AllowedEffects & DragDropEffects.Copy) : DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void LaunchList_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
SetDropHighlight(null);
|
||||
if (FindParent<ListBoxItem>(e.OriginalSource as DependencyObject)?.DataContext is LaunchGroupItem item &&
|
||||
e.Data.GetData(DataFormats.FileDrop) is string[] files)
|
||||
AddPathsToGroup(item.Group, files);
|
||||
}
|
||||
|
||||
// ---------- Desktop-Verknüpfung ----------
|
||||
|
||||
private void LaunchGroupShortcut_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (CurrentGroup is { } g) CreateGroupShortcut(g.Group);
|
||||
}
|
||||
|
||||
/// <summary>Legt "<Name> starten.lnk" auf den Desktop, die DeskCore.exe --launch "<Name>" aufruft.</summary>
|
||||
private void CreateGroupShortcut(LaunchGroup g)
|
||||
{
|
||||
try
|
||||
{
|
||||
var exe = Environment.ProcessPath!;
|
||||
var safe = string.Concat(g.Name.Split(Path.GetInvalidFileNameChars()));
|
||||
var link = Storage.UniquePath(Storage.UserDesktop, $"{safe} starten.lnk");
|
||||
Storage.MarkCreated(link); // nicht von der Auto-Sortierung einsammeln lassen
|
||||
dynamic shell = Activator.CreateInstance(Type.GetTypeFromProgID("WScript.Shell")!)!;
|
||||
dynamic l = shell.CreateShortcut(link);
|
||||
l.TargetPath = exe;
|
||||
l.Arguments = $"--launch \"{g.Name}\"";
|
||||
l.WorkingDirectory = Path.GetDirectoryName(exe);
|
||||
l.IconLocation = $"{exe},0";
|
||||
l.Description = $"Startet die DeskCore-Start-Gruppe „{g.Name}“";
|
||||
l.Save();
|
||||
ShowToast($"„{Path.GetFileNameWithoutExtension(link)}“ liegt jetzt auf dem Desktop", "");
|
||||
}
|
||||
catch (Exception ex) { Error("Verknüpfung fehlgeschlagen", ex.Message); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Einstellungen für Desktop-Seiten, das Aufziehen neuer Boxen und die Tönung der Desktop-Icons.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
public record PageRow(int Index, string Name, string Info, Brush Dot);
|
||||
|
||||
private bool _loadingDesktopSettings;
|
||||
|
||||
private void LoadDesktopSettings()
|
||||
{
|
||||
_loadingDesktopSettings = true;
|
||||
DrawFencesToggle.IsChecked = S.DrawFences;
|
||||
PageWheelBox.IsChecked = S.PageWheel;
|
||||
PageWheelBox.IsEnabled = S.Pages.Count > 1;
|
||||
|
||||
var res = Application.Current.Resources;
|
||||
PageList.ItemsSource = S.Pages.Select((name, i) =>
|
||||
{
|
||||
int boxes = S.Fences.Count(f => f.Page == i);
|
||||
var info = (boxes == 1 ? "1 Box" : $"{boxes} Boxen") + (i == S.CurrentPage ? " · wird gerade angezeigt" : "");
|
||||
var o = S.PageOptions.GetValueOrDefault(name);
|
||||
if (o?.Scheduled == true) info += " · " + PageScheduler.Describe(o);
|
||||
if (string.Equals(S.PageFallback, name, StringComparison.OrdinalIgnoreCase)) info += " · sonst immer";
|
||||
if (o?.Wallpaper is not null) info += " · eigenes Hintergrundbild";
|
||||
return new PageRow(i, name, info, (Brush)res[i == S.CurrentPage ? "AccentBrush" : "ControlStrongStrokeBrush"]);
|
||||
}).ToList();
|
||||
|
||||
DesktopTintSwatches.Children.Clear();
|
||||
void AddSwatch(string? hex, Brush brush, string tip)
|
||||
{
|
||||
var rb = new RadioButton
|
||||
{
|
||||
Style = (Style)res["SwatchRadio"], GroupName = "desktoptint", Background = brush, ToolTip = tip,
|
||||
IsChecked = S.DesktopTint == hex,
|
||||
};
|
||||
rb.Checked += (_, _) =>
|
||||
{
|
||||
if (_loadingDesktopSettings) return;
|
||||
S.DesktopTint = hex;
|
||||
S.Save();
|
||||
DesktopTintStrength.IsEnabled = hex is not null;
|
||||
DesktopIcons.RefreshCache();
|
||||
DesktopTint.Refresh(force: true);
|
||||
};
|
||||
DesktopTintSwatches.Children.Add(rb);
|
||||
}
|
||||
AddSwatch(null, Brushes.Transparent, "Keine Tönung");
|
||||
foreach (var (name, hex) in CategoryColors.All.Concat([("Weiß", "#F5F5F5")]))
|
||||
AddSwatch(hex, CategoryColors.Brush(hex)!, name);
|
||||
DesktopTintStrength.Value = Math.Round(S.DesktopTintStrength * 100);
|
||||
DesktopTintStrength.IsEnabled = S.DesktopTint is not null;
|
||||
_loadingDesktopSettings = false;
|
||||
}
|
||||
|
||||
private void DrawFencesToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.DrawFences = DrawFencesToggle.IsChecked == true;
|
||||
S.Save();
|
||||
DesktopToggle.Apply();
|
||||
}
|
||||
|
||||
private void PageWheel_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.PageWheel = PageWheelBox.IsChecked == true;
|
||||
S.Save();
|
||||
DesktopToggle.Apply();
|
||||
}
|
||||
|
||||
private void DesktopTintStrength_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
|
||||
{
|
||||
if (_loadingDesktopSettings || !IsLoaded) return;
|
||||
S.DesktopTintStrength = DesktopTintStrength.Value / 100;
|
||||
S.Save();
|
||||
DesktopTint.Refresh(force: true);
|
||||
}
|
||||
|
||||
// ---------- Seiten ----------
|
||||
|
||||
private static string? ValidatePageName(string t, int except) =>
|
||||
t.Trim().Length == 0 ? "Bitte einen Namen eingeben."
|
||||
: S.Pages.Where((_, i) => i != except).Any(p => string.Equals(p, t.Trim(), StringComparison.OrdinalIgnoreCase))
|
||||
? "Diesen Namen gibt es schon." : null;
|
||||
|
||||
private void AddPage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var name = Dialog.Prompt(this, "Neue Desktop-Seite", "Name der Seite (z. B. „Arbeit“ oder „Freizeit“):",
|
||||
$"Seite {S.Pages.Count + 1}", "Hinzufügen", t => ValidatePageName(t, -1));
|
||||
if (name is null) return;
|
||||
S.Pages.Add(name.Trim());
|
||||
S.Save();
|
||||
DesktopToggle.Apply();
|
||||
LoadDesktopSettings();
|
||||
ShowToast($"Seite „{name.Trim()}“ hinzugefügt",
|
||||
"Mit „Anzeigen“ wechselst du hin – neue Boxen landen immer auf der gerade angezeigten Seite.");
|
||||
}
|
||||
|
||||
private void ShowPage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is int i) FenceManager.SwitchPage(i);
|
||||
LoadDesktopSettings();
|
||||
}
|
||||
|
||||
private void RenamePage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return;
|
||||
var name = Dialog.Prompt(this, "Seite umbenennen", "Neuer Name:", S.Pages[i], "Umbenennen", t => ValidatePageName(t, i));
|
||||
if (name is null) return;
|
||||
S.RenamePage(i, name.Trim());
|
||||
S.Save();
|
||||
LoadDesktopSettings();
|
||||
}
|
||||
|
||||
// ---------- Zeitplan & Hintergrundbild ----------
|
||||
|
||||
private void PageSchedule_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return;
|
||||
var page = S.Pages[i];
|
||||
var o = S.PageOption(page);
|
||||
var res = Application.Current.Resources;
|
||||
var panel = new StackPanel();
|
||||
TextBlock Label(string t, double top = 18) => new()
|
||||
{
|
||||
Text = t, Margin = new Thickness(0, top, 0, 8), FontWeight = FontWeights.SemiBold, FontSize = 13,
|
||||
};
|
||||
TextBlock Hint(string t) => new()
|
||||
{
|
||||
Text = t, FontSize = 12, TextWrapping = TextWrapping.Wrap, Foreground = (Brush)res["TextSecondaryBrush"],
|
||||
Margin = new Thickness(0, 4, 0, 0),
|
||||
};
|
||||
|
||||
// Zeitplan
|
||||
var scheduled = new CheckBox { Content = "Diese Seite automatisch anzeigen", IsChecked = o.Scheduled };
|
||||
panel.Children.Add(scheduled);
|
||||
var details = new StackPanel { Margin = new Thickness(28, 0, 0, 0) };
|
||||
details.Children.Add(Label("An diesen Tagen", 12));
|
||||
var days = new WrapPanel();
|
||||
string[] dayNames = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
var dayBoxes = new List<CheckBox>();
|
||||
for (int d = 0; d < 7; d++)
|
||||
{
|
||||
var cb = new CheckBox { Content = dayNames[d], IsChecked = (o.Days & (1 << d)) != 0, Margin = new Thickness(0, 0, 14, 4) };
|
||||
dayBoxes.Add(cb);
|
||||
days.Children.Add(cb);
|
||||
}
|
||||
details.Children.Add(days);
|
||||
details.Children.Add(Label("Von – bis"));
|
||||
var from = new TextBox { Text = o.From, Width = 76, HorizontalContentAlignment = HorizontalAlignment.Center };
|
||||
var to = new TextBox { Text = o.To, Width = 76, HorizontalContentAlignment = HorizontalAlignment.Center };
|
||||
details.Children.Add(new StackPanel
|
||||
{
|
||||
Orientation = Orientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
from,
|
||||
new TextBlock { Text = "bis", Margin = new Thickness(12, 0, 12, 0), VerticalAlignment = VerticalAlignment.Center },
|
||||
to,
|
||||
new TextBlock { Text = "Uhr", Margin = new Thickness(12, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center },
|
||||
},
|
||||
});
|
||||
details.Children.Add(Hint("Über Mitternacht geht auch, z. B. 22:00 bis 06:00."));
|
||||
panel.Children.Add(details);
|
||||
void UpdateEnabled() => details.IsEnabled = scheduled.IsChecked == true;
|
||||
scheduled.Click += (_, _) => UpdateEnabled();
|
||||
UpdateEnabled();
|
||||
|
||||
var fallback = new CheckBox
|
||||
{
|
||||
Content = "Zu allen anderen Zeiten diese Seite anzeigen", Margin = new Thickness(0, 18, 0, 0),
|
||||
IsChecked = string.Equals(S.PageFallback, page, StringComparison.OrdinalIgnoreCase),
|
||||
};
|
||||
panel.Children.Add(fallback);
|
||||
panel.Children.Add(Hint("Beispiel: „Arbeit“ Mo–Fr 08:00–17:00 und bei „Freizeit“ diesen Haken setzen."));
|
||||
|
||||
// Hintergrundbild
|
||||
panel.Children.Add(Label("Hintergrundbild", 22));
|
||||
string? wallpaper = o.Wallpaper;
|
||||
var wpText = new TextBlock { TextTrimming = TextTrimming.CharacterEllipsis, VerticalAlignment = VerticalAlignment.Center };
|
||||
var wpRemove = new Button { Content = "Entfernen", Margin = new Thickness(8, 0, 0, 0) };
|
||||
void UpdateWallpaper()
|
||||
{
|
||||
wpText.Text = wallpaper is null ? "Keins – der Hintergrund bleibt, wie er ist" : Path.GetFileName(wallpaper);
|
||||
wpText.ToolTip = wallpaper;
|
||||
wpRemove.IsEnabled = wallpaper is not null;
|
||||
}
|
||||
var wpPick = new Button { Content = "Bild wählen …" };
|
||||
wpPick.Click += (_, _) =>
|
||||
{
|
||||
var dlg = new Microsoft.Win32.OpenFileDialog
|
||||
{
|
||||
Title = $"Hintergrundbild für „{page}“", Filter = "Bilder|*.jpg;*.jpeg;*.png;*.bmp;*.gif;*.webp|Alle Dateien|*.*",
|
||||
};
|
||||
if (wallpaper is not null) dlg.InitialDirectory = Path.GetDirectoryName(wallpaper);
|
||||
if (dlg.ShowDialog() == true) { wallpaper = dlg.FileName; UpdateWallpaper(); }
|
||||
};
|
||||
wpRemove.Click += (_, _) => { wallpaper = null; UpdateWallpaper(); };
|
||||
UpdateWallpaper();
|
||||
var wpRow = new DockPanel();
|
||||
var wpButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(12, 0, 0, 0), Children = { wpPick, wpRemove } };
|
||||
DockPanel.SetDock(wpButtons, Dock.Right);
|
||||
wpRow.Children.Add(wpButtons);
|
||||
wpRow.Children.Add(wpText);
|
||||
panel.Children.Add(wpRow);
|
||||
panel.Children.Add(Hint("Wechselst du auf eine Seite ohne eigenes Bild, kommt dein ursprünglicher Hintergrund zurück."));
|
||||
|
||||
string? Validate()
|
||||
{
|
||||
if (scheduled.IsChecked != true) return null;
|
||||
if (!TimeSpan.TryParse(from.Text.Trim(), out var f) || f.TotalHours >= 24 ||
|
||||
!TimeSpan.TryParse(to.Text.Trim(), out var t) || t.TotalHours >= 24)
|
||||
return "Bitte die Uhrzeiten so eingeben: 08:00";
|
||||
if (f == t) return "Start und Ende dürfen nicht gleich sein.";
|
||||
if (!dayBoxes.Any(b => b.IsChecked == true)) return "Bitte mindestens einen Tag auswählen.";
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!Dialog.Show(this, $"Seite „{page}“", panel, "Speichern", "Abbrechen", Validate)) return;
|
||||
|
||||
o.Scheduled = scheduled.IsChecked == true;
|
||||
o.Days = dayBoxes.Select((b, d) => b.IsChecked == true ? 1 << d : 0).Sum();
|
||||
if (TimeSpan.TryParse(from.Text.Trim(), out var ff)) o.From = ff.ToString(@"hh\:mm");
|
||||
if (TimeSpan.TryParse(to.Text.Trim(), out var tt)) o.To = tt.ToString(@"hh\:mm");
|
||||
if (fallback.IsChecked == true) S.PageFallback = page;
|
||||
else if (string.Equals(S.PageFallback, page, StringComparison.OrdinalIgnoreCase)) S.PageFallback = null;
|
||||
bool wallpaperChanged = !string.Equals(o.Wallpaper, wallpaper, StringComparison.OrdinalIgnoreCase);
|
||||
o.Wallpaper = wallpaper;
|
||||
S.Save();
|
||||
|
||||
if (wallpaperChanged && i == S.CurrentPage) PageScheduler.ApplyWallpaper(i);
|
||||
PageScheduler.Reset();
|
||||
PageScheduler.Tick();
|
||||
LoadDesktopSettings();
|
||||
ShowToast($"Seite „{page}“ gespeichert", "");
|
||||
}
|
||||
|
||||
private void DeletePage_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.Tag is not int i || i >= S.Pages.Count) return;
|
||||
if (S.Pages.Count < 2)
|
||||
{
|
||||
ShowToast("Die letzte Seite kann nicht gelöscht werden", "", info: true);
|
||||
return;
|
||||
}
|
||||
var name = S.Pages[i];
|
||||
int boxes = S.Fences.Count(f => f.Page == i);
|
||||
if (boxes > 0 && !Dialog.Confirm(this, "Seite löschen",
|
||||
$"Auf „{name}“ liegen {boxes} Box(en). Sie wandern auf die Seite „{S.Pages[i == 0 ? 1 : i - 1]}“.",
|
||||
"Löschen"))
|
||||
return;
|
||||
FenceManager.DeletePage(i);
|
||||
S.ForgetPage(name);
|
||||
S.Save();
|
||||
DesktopToggle.Apply();
|
||||
Refresh();
|
||||
LoadDesktopSettings();
|
||||
ShowToast($"Seite „{name}“ gelöscht", "");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Einstellungen der Schnellstart-Suche.</summary>
|
||||
public partial class MainWindow
|
||||
{
|
||||
private void LoadQuickSearchSettings()
|
||||
{
|
||||
QuickSearchToggle.IsChecked = S.QuickSearchEnabled;
|
||||
QuickSearchHotkeyBox.Text = Hotkey.Describe(S.QuickSearchModifiers, S.QuickSearchKey);
|
||||
QuickSearchHotkeyBox.IsEnabled = S.QuickSearchEnabled;
|
||||
QuickSearchStartMenuBox.IsChecked = S.QuickSearchStartMenu;
|
||||
UpdateQuickSearchHint(App.QuickSearchRegistered);
|
||||
}
|
||||
|
||||
private void QuickSearchToggle_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.QuickSearchEnabled = QuickSearchToggle.IsChecked == true;
|
||||
S.Save();
|
||||
App.ApplyQuickSearchHotkey();
|
||||
LoadQuickSearchSettings();
|
||||
}
|
||||
|
||||
private void QuickSearchStartMenu_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
S.QuickSearchStartMenu = QuickSearchStartMenuBox.IsChecked == true;
|
||||
S.Save();
|
||||
}
|
||||
|
||||
private void QuickSearchHotkeyBox_PreviewKeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
var key = e.Key == Key.System ? e.SystemKey : e.Key;
|
||||
if (key is Key.Tab) return;
|
||||
e.Handled = true;
|
||||
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt or Key.LeftShift or Key.RightShift
|
||||
or Key.LWin or Key.RWin or Key.Escape) return;
|
||||
|
||||
int mods = (int)Keyboard.Modifiers; // Alt=1, Strg=2, Umschalt=4, Win=8 – gleiche Bits wie RegisterHotKey
|
||||
if ((mods & (Hotkey.ModCtrl | Hotkey.ModAlt | Hotkey.ModWin)) == 0)
|
||||
{
|
||||
QuickSearchHint.Text = "Bitte mit Strg, Alt oder Win kombinieren.";
|
||||
return;
|
||||
}
|
||||
S.QuickSearchModifiers = mods;
|
||||
S.QuickSearchKey = KeyInterop.VirtualKeyFromKey(key);
|
||||
S.Save();
|
||||
QuickSearchHotkeyBox.Text = Hotkey.Describe(S.QuickSearchModifiers, S.QuickSearchKey);
|
||||
UpdateQuickSearchHint(App.ApplyQuickSearchHotkey());
|
||||
}
|
||||
|
||||
private void UpdateQuickSearchHint(bool ok) =>
|
||||
QuickSearchHint.Text = !S.QuickSearchEnabled ? "Ausgeschaltet"
|
||||
: ok ? $"{Hotkey.Describe(S.QuickSearchModifiers, S.QuickSearchKey)} öffnet von überall eine Suchleiste: ein paar Buchstaben tippen, Enter – fertig. " +
|
||||
"Was du oft startest, steht weiter oben."
|
||||
: "⚠ Diese Kombination ist schon von einem anderen Programm belegt – bitte ins Feld klicken und eine andere drücken.";
|
||||
}
|
||||
+955
-676
File diff suppressed because it is too large
Load Diff
+1580
-1503
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ public static class Migration
|
||||
|
||||
public static void Run()
|
||||
{
|
||||
if (Environment.GetEnvironmentVariable("DESKCORE_DATA") is { Length: > 0 }) return; // Testmodus: nichts umziehen
|
||||
if (FindLegacy() is { } legacy)
|
||||
{
|
||||
foreach (var folder in Folders)
|
||||
@@ -67,6 +68,10 @@ public static class Migration
|
||||
/// <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)
|
||||
{
|
||||
// Testmodus: alle Daten in einem eigenen Ordner, damit Tests nie echte Einstellungen/Kategorien berühren
|
||||
if (Environment.GetEnvironmentVariable("DESKCORE_DATA") is { Length: > 0 } testRoot)
|
||||
return Path.Combine([testRoot, folder.ToString(), .. sub]);
|
||||
|
||||
var baseDir = Environment.GetFolderPath(folder);
|
||||
if (!Directory.Exists(Path.Combine(baseDir, NewName)) && FindLegacy() is { } legacy
|
||||
&& Directory.Exists(Path.Combine(baseDir, legacy)))
|
||||
|
||||
@@ -1,97 +1,109 @@
|
||||
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;
|
||||
}
|
||||
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 => !IsDesktop && AppSettings.Current.HasFence(Name);
|
||||
}
|
||||
|
||||
/// <summary>Eintrag einer Start-Gruppe in der Seitenleiste.</summary>
|
||||
public class LaunchGroupItem
|
||||
{
|
||||
public required LaunchGroup Group { get; init; }
|
||||
public string Name => Group.Name;
|
||||
public int Count => Group.Items.Count;
|
||||
public string Glyph => ""; // Abspielen
|
||||
public Brush? GlyphBrush => CategoryColors.Brush(Group.Color);
|
||||
public bool HasFence => false;
|
||||
public bool IsDesktop => true; // blendet den Umsortier-Griff der Kategorien aus
|
||||
}
|
||||
|
||||
public static class CategoryColors
|
||||
{
|
||||
public static readonly (string Name, string Hex)[] All =
|
||||
[
|
||||
("Blau", "#3B82F6"), ("Türkis", "#12A594"), ("Grün", "#22A06B"), ("Gelb", "#E2A600"),
|
||||
("Orange", "#F07A1A"), ("Rot", "#E5484D"), ("Pink", "#D6409F"), ("Lila", "#8E4EC6"), ("Grau", "#8B8D98"),
|
||||
];
|
||||
|
||||
public static Brush? Brush(string? hex)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hex)) return null;
|
||||
try
|
||||
{
|
||||
var b = new SolidColorBrush((Color)ColorConverter.ConvertFromString(hex));
|
||||
b.Freeze();
|
||||
return b;
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
|
||||
public static class CategoryGlyphs
|
||||
{
|
||||
public static readonly (string Glyph, string Name)[] All =
|
||||
[
|
||||
("", "Ordner"), ("", "Spiele"), ("", "Arbeit"), ("", "Code"),
|
||||
("", "Musik"), ("", "Video"), ("", "Bilder"), ("", "Internet"),
|
||||
("", "Mail"), ("", "Shopping"), ("", "System"), ("", "Werkzeuge"),
|
||||
("", "Dokumente"), ("", "Favoriten"), ("", "Herz"), ("", "Cloud"),
|
||||
("", "Downloads"), ("", "Personen"), ("", "Bibliothek"), ("", "Rechner"),
|
||||
("", "Ideen"), ("", "Zuhause"), ("", "Lernen"), ("", "Erweiterungen"),
|
||||
("", "Computer"), ("", "Sicherheit"), ("", "Ebenen"),
|
||||
];
|
||||
}
|
||||
|
||||
public class IconItem : INotifyPropertyChanged
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
public string? Category { get; init; }
|
||||
|
||||
private ImageSource? _icon;
|
||||
public ImageSource? Icon
|
||||
{
|
||||
get => _icon;
|
||||
set { _icon = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Icon))); }
|
||||
}
|
||||
|
||||
/// <summary>Nur für Desktop-Icons: Kategorie, in der bereits eine Kopie liegt.</summary>
|
||||
public string? SortedInto { get; init; }
|
||||
public bool IsSorted => SortedInto is not null;
|
||||
|
||||
public string Name => Storage.DisplayName(Path);
|
||||
|
||||
public string TypeText
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Directory.Exists(Path)) return "Ordner";
|
||||
var ext = System.IO.Path.GetExtension(Path).ToLowerInvariant();
|
||||
return ext switch
|
||||
{
|
||||
".lnk" => "Verknüpfung",
|
||||
".url" => "Internetverknüpfung",
|
||||
".exe" => "Programm",
|
||||
"" => "Datei",
|
||||
_ => ext.TrimStart('.').ToUpperInvariant() + "-Datei",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public string LocationText => SortedInto is not null ? $"✓ gesichert in {SortedInto}" : Category ?? "";
|
||||
|
||||
public string ToolTip =>
|
||||
SortedInto is not null ? $"{Name}\n✓ Bereits gesichert in „{SortedInto}“ – kann vom Desktop gelöscht werden." :
|
||||
Category is not null ? $"{Name}\nOrt: {Category}" : $"{Name}\n{TypeText}";
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>
|
||||
/// Wechselt die Desktop-Seite nach Zeitplan (z. B. Mo–Fr 8–17 Uhr „Arbeit“, sonst „Freizeit“)
|
||||
/// und setzt das Hintergrundbild einer Seite.
|
||||
/// </summary>
|
||||
public static class PageScheduler
|
||||
{
|
||||
/// <summary>Seite, die der Zeitplan zuletzt wollte. Gewechselt wird nur, wenn sich das ändert –
|
||||
/// von Hand gewählte Seiten bleiben also bis zum nächsten Zeitplan-Wechsel stehen.</summary>
|
||||
private static string? _lastWanted = "\0";
|
||||
|
||||
private static AppSettings S => AppSettings.Current;
|
||||
|
||||
/// <summary>Gibt es überhaupt einen Zeitplan?</summary>
|
||||
public static bool Active => S.Pages.Count > 1 && S.Pages.Any(p => S.PageOptions.GetValueOrDefault(p)?.Scheduled == true);
|
||||
|
||||
/// <summary>Welche Seite laut Zeitplan jetzt dran ist (null = keine Vorgabe).</summary>
|
||||
public static string? Wanted(DateTime now)
|
||||
{
|
||||
if (!Active) return null;
|
||||
foreach (var p in S.Pages)
|
||||
if (S.PageOptions.GetValueOrDefault(p)?.Matches(now) == true) return p;
|
||||
return S.PageFallback is { } f && S.Pages.Contains(f, StringComparer.OrdinalIgnoreCase) ? f : null;
|
||||
}
|
||||
|
||||
/// <summary>Regelmäßig aufrufen (alle paar Sekunden).</summary>
|
||||
public static void Tick()
|
||||
{
|
||||
var wanted = Wanted(DateTime.Now);
|
||||
if (string.Equals(wanted, _lastWanted, StringComparison.OrdinalIgnoreCase)) return;
|
||||
_lastWanted = wanted;
|
||||
if (wanted is null) return;
|
||||
int index = S.Pages.FindIndex(p => string.Equals(p, wanted, StringComparison.OrdinalIgnoreCase));
|
||||
if (index >= 0 && index != S.CurrentPage) FenceManager.SwitchPage(index);
|
||||
}
|
||||
|
||||
/// <summary>Zeitplan geändert → beim nächsten Tick sofort anwenden.</summary>
|
||||
public static void Reset() => _lastWanted = "\0";
|
||||
|
||||
/// <summary>Text für die Einstellungen, z. B. „Mo–Fr · 08:00–17:00“.</summary>
|
||||
public static string Describe(PageOption o)
|
||||
{
|
||||
string[] names = ["Mo", "Di", "Mi", "Do", "Fr", "Sa", "So"];
|
||||
var days = Enumerable.Range(0, 7).Where(i => (o.Days & (1 << i)) != 0).ToList();
|
||||
string d = days.Count == 7 ? "Täglich"
|
||||
: days.SequenceEqual([0, 1, 2, 3, 4]) ? "Mo–Fr"
|
||||
: days.SequenceEqual([5, 6]) ? "Wochenende"
|
||||
: days.Count > 2 && days.Last() - days.First() == days.Count - 1 ? $"{names[days.First()]}–{names[days.Last()]}"
|
||||
: string.Join(", ", days.Select(i => names[i]));
|
||||
return $"{d} · {o.From}–{o.To} Uhr";
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Hintergrundbild
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Beim Seitenwechsel: Bild der Seite setzen – oder das ursprüngliche zurückholen.</summary>
|
||||
public static void ApplyWallpaper(int pageIndex)
|
||||
{
|
||||
if (pageIndex < 0 || pageIndex >= S.Pages.Count) return;
|
||||
var wanted = S.PageOptions.GetValueOrDefault(S.Pages[pageIndex])?.Wallpaper;
|
||||
if (wanted is not null && !File.Exists(wanted)) wanted = null;
|
||||
|
||||
string? target;
|
||||
if (wanted is not null)
|
||||
{
|
||||
// Vor dem ersten Wechsel das bisherige Bild merken, um es später zurückzuholen
|
||||
S.SavedWallpaper ??= CurrentWallpaper() ?? "";
|
||||
target = wanted;
|
||||
}
|
||||
else if (S.SavedWallpaper is { } saved)
|
||||
{
|
||||
target = saved;
|
||||
S.SavedWallpaper = null;
|
||||
}
|
||||
else return; // Seiten ohne eigenes Bild lassen den Hintergrund in Ruhe
|
||||
S.Save();
|
||||
|
||||
if (string.Equals(CurrentWallpaper(), target, StringComparison.OrdinalIgnoreCase)) return;
|
||||
Task.Run(() => SystemParametersInfo(SPI_SETDESKWALLPAPER, 0, target, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE));
|
||||
}
|
||||
|
||||
public static string? CurrentWallpaper()
|
||||
{
|
||||
var sb = new StringBuilder(520);
|
||||
return SystemParametersInfo(SPI_GETDESKWALLPAPER, (uint)sb.Capacity, sb, 0) ? sb.ToString() : null;
|
||||
}
|
||||
|
||||
private const uint SPI_GETDESKWALLPAPER = 0x73, SPI_SETDESKWALLPAPER = 0x14, SPIF_UPDATEINIFILE = 0x1, SPIF_SENDCHANGE = 0x2;
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool SystemParametersInfo(uint action, uint param, string value, uint flags);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] private static extern bool SystemParametersInfo(uint action, uint param, StringBuilder value, uint flags);
|
||||
}
|
||||
+397
@@ -0,0 +1,397 @@
|
||||
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;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>
|
||||
/// Schnellstart-Suche: Tastenkürzel → Suchleiste mitten auf dem Bildschirm, tippen, Enter, läuft.
|
||||
/// Durchsucht Kategorien, Desktop, Startmenü, Start-Gruppen, Kategorien und Desktop-Seiten.
|
||||
/// Was du oft startest, steht weiter oben.
|
||||
/// </summary>
|
||||
public sealed class QuickSearch : Window
|
||||
{
|
||||
private const int MaxResults = 8;
|
||||
|
||||
private sealed record Entry(string Name, string Sub, string? Path, string Glyph, string Key, Action Run, Action? Locate = null)
|
||||
{
|
||||
/// <summary>Für die Ähnlichkeit: Name ohne Endungen wie „- Verknüpfung“.</summary>
|
||||
public string SearchName { get; } = Name.Replace(" - Verknüpfung", "", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static QuickSearch? _open;
|
||||
private static List<Entry>? _startMenu;
|
||||
private static DateTime _startMenuRead;
|
||||
|
||||
private readonly TextBox _box;
|
||||
private readonly ListBox _list;
|
||||
private readonly TextBlock _hint;
|
||||
private List<Entry> _all = [];
|
||||
private List<Entry> _shown = [];
|
||||
private bool _closing;
|
||||
|
||||
private static AppSettings S => AppSettings.Current;
|
||||
|
||||
/// <summary>Per Tastenkürzel: öffnen bzw. wieder schließen.</summary>
|
||||
public static void Toggle()
|
||||
{
|
||||
if (_open is not null) { _open.CloseNow(); return; }
|
||||
_open = new QuickSearch();
|
||||
_open.Show();
|
||||
}
|
||||
|
||||
private QuickSearch()
|
||||
{
|
||||
var res = Application.Current.Resources;
|
||||
WindowStyle = WindowStyle.None;
|
||||
AllowsTransparency = true;
|
||||
Background = Brushes.Transparent;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
ShowInTaskbar = false;
|
||||
Topmost = true;
|
||||
Width = 680;
|
||||
SizeToContent = SizeToContent.Height;
|
||||
Title = "DeskCore Suche";
|
||||
FontFamily = (FontFamily)res["UiFont"];
|
||||
Foreground = (Brush)res["TextBrush"];
|
||||
|
||||
// Suchzeile
|
||||
_box = new TextBox
|
||||
{
|
||||
FontSize = 20, BorderThickness = new Thickness(0), Background = Brushes.Transparent, Padding = new Thickness(0),
|
||||
VerticalContentAlignment = VerticalAlignment.Center, Foreground = (Brush)res["TextBrush"],
|
||||
CaretBrush = (Brush)res["TextBrush"],
|
||||
};
|
||||
var placeholder = new TextBlock
|
||||
{
|
||||
Text = "Programm, Datei, Start-Gruppe oder Kategorie suchen …", FontSize = 20, IsHitTestVisible = false,
|
||||
Foreground = (Brush)res["TextTertiaryBrush"], VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
_box.TextChanged += (_, _) =>
|
||||
{
|
||||
placeholder.Visibility = _box.Text.Length == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
Filter();
|
||||
};
|
||||
var searchRow = new DockPanel { Margin = new Thickness(18, 14, 18, 14) };
|
||||
var glass = new TextBlock
|
||||
{
|
||||
Text = "", FontFamily = (FontFamily)res["IconFont"], FontSize = 20, Margin = new Thickness(0, 0, 14, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center, Foreground = (Brush)res["AccentBrush"],
|
||||
};
|
||||
DockPanel.SetDock(glass, Dock.Left);
|
||||
searchRow.Children.Add(glass);
|
||||
searchRow.Children.Add(new Grid { Children = { placeholder, _box } });
|
||||
|
||||
_list = new ListBox
|
||||
{
|
||||
BorderThickness = new Thickness(0), Background = Brushes.Transparent, Margin = new Thickness(6, 0, 6, 6),
|
||||
Focusable = false, Visibility = Visibility.Collapsed,
|
||||
};
|
||||
ScrollViewer.SetHorizontalScrollBarVisibility(_list, ScrollBarVisibility.Disabled);
|
||||
_list.MouseDoubleClick += (_, _) => RunSelected(locate: false);
|
||||
_list.PreviewMouseLeftButtonUp += (_, e) =>
|
||||
{
|
||||
// Ein Klick genügt – wie im Startmenü
|
||||
if (MainWindow.FindParent<ListBoxItem>(e.OriginalSource as DependencyObject) is not null) RunSelected(locate: false);
|
||||
};
|
||||
|
||||
_hint = new TextBlock
|
||||
{
|
||||
FontSize = 12, Margin = new Thickness(18, 8, 18, 10), Foreground = (Brush)res["TextTertiaryBrush"],
|
||||
Text = "Enter Starten · Strg + Enter Speicherort öffnen · Esc Schließen",
|
||||
};
|
||||
|
||||
var divider = new Border { Height = 1, Background = (Brush)res["ControlStrokeBrush"] };
|
||||
var footer = new Border { Child = _hint, BorderBrush = (Brush)res["ControlStrokeBrush"], BorderThickness = new Thickness(0, 1, 0, 0) };
|
||||
var stack = new StackPanel { Children = { searchRow, divider, _list, footer } };
|
||||
|
||||
Content = new Border
|
||||
{
|
||||
Margin = new Thickness(24), CornerRadius = new CornerRadius(10),
|
||||
Background = (Brush)res["FlyoutBrush"], BorderBrush = (Brush)res["FlyoutStrokeBrush"], BorderThickness = new Thickness(1),
|
||||
Effect = (System.Windows.Media.Effects.Effect)res["DialogShadow"], Child = stack,
|
||||
};
|
||||
|
||||
PreviewKeyDown += OnKey;
|
||||
Deactivated += (_, _) => CloseNow();
|
||||
Closed += (_, _) => { if (_open == this) _open = null; };
|
||||
Loaded += (_, _) =>
|
||||
{
|
||||
Position();
|
||||
BringToFront();
|
||||
_box.Focus();
|
||||
BuildIndex();
|
||||
};
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Position & Fokus
|
||||
// =====================================================================
|
||||
|
||||
private void Position()
|
||||
{
|
||||
var mouse = GetCursorPos(out var p) ? new Point(p.X / Screens.Scale, p.Y / Screens.Scale) : new Point(0, 0);
|
||||
var wa = Screens.WorkAreaAt(mouse);
|
||||
Left = wa.Left + (wa.Width - Width) / 2;
|
||||
Top = wa.Top + wa.Height * 0.22;
|
||||
}
|
||||
|
||||
private void BringToFront()
|
||||
{
|
||||
// Das Tastenkürzel erlaubt DeskCore, nach vorne zu kommen
|
||||
var hwnd = new WindowInteropHelper(this).Handle;
|
||||
SetForegroundWindow(hwnd);
|
||||
Activate();
|
||||
}
|
||||
|
||||
private void CloseNow()
|
||||
{
|
||||
if (_closing) return;
|
||||
_closing = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Was durchsucht wird
|
||||
// =====================================================================
|
||||
|
||||
private async void BuildIndex()
|
||||
{
|
||||
var list = new List<Entry>();
|
||||
|
||||
// Start-Gruppen
|
||||
foreach (var g in S.LaunchGroups)
|
||||
{
|
||||
var group = g;
|
||||
list.Add(new Entry(g.Name, $"Start-Gruppe · {g.Items.Count} {(g.Items.Count == 1 ? "Programm" : "Programme")}", null, "",
|
||||
Usage.GroupPrefix + g.Name, () => App.Win?.LaunchGroupNow(group)));
|
||||
}
|
||||
// Kategorien selbst (öffnet DeskCore dort) und Desktop-Seiten
|
||||
foreach (var c in Storage.Categories())
|
||||
{
|
||||
var cat = c;
|
||||
list.Add(new Entry(c, "Kategorie in DeskCore öffnen", null, S.Categories.GetValueOrDefault(c)?.Glyph ?? "",
|
||||
"kategorie:" + c, () => { App.Win?.ShowFromBackground(); App.Win?.ShowCategory(cat); }));
|
||||
}
|
||||
if (S.Pages.Count > 1)
|
||||
for (int i = 0; i < S.Pages.Count; i++)
|
||||
{
|
||||
int page = i;
|
||||
list.Add(new Entry(S.Pages[i], "Desktop-Seite anzeigen", null, "", "seite:" + S.Pages[i], () => FenceManager.SwitchPage(page)));
|
||||
}
|
||||
|
||||
_all = list;
|
||||
Filter();
|
||||
|
||||
// Dateien aus Kategorien, Desktop und Startmenü im Hintergrund sammeln (Startmenü 10 Minuten zwischengespeichert)
|
||||
bool withStartMenu = S.QuickSearchStartMenu;
|
||||
var categories = Storage.Categories();
|
||||
var files = await Task.Run(() =>
|
||||
{
|
||||
var l = new List<Entry>();
|
||||
foreach (var c in categories)
|
||||
foreach (var p in Storage.Entries(Storage.CategoryPath(c)))
|
||||
l.Add(FileEntry(p, c));
|
||||
foreach (var p in Storage.DesktopEntries())
|
||||
l.Add(FileEntry(p, "Desktop"));
|
||||
if (withStartMenu)
|
||||
{
|
||||
if (_startMenu is null || DateTime.Now - _startMenuRead > TimeSpan.FromMinutes(10))
|
||||
{
|
||||
_startMenu = ReadStartMenu();
|
||||
_startMenuRead = DateTime.Now;
|
||||
}
|
||||
l.AddRange(_startMenu);
|
||||
}
|
||||
return Dedupe(l);
|
||||
});
|
||||
if (_closing) return;
|
||||
_all = list.Concat(files).ToList();
|
||||
Filter();
|
||||
}
|
||||
|
||||
private static Entry FileEntry(string path, string where) =>
|
||||
new(Storage.DisplayName(path), where, path, "", path, () => Usage.Start(path),
|
||||
() => Process.Start("explorer.exe", $"/select,\"{path}\""));
|
||||
|
||||
private static List<Entry> ReadStartMenu()
|
||||
{
|
||||
var result = new List<Entry>();
|
||||
foreach (var root in new[]
|
||||
{
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.StartMenu),
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu),
|
||||
})
|
||||
{
|
||||
if (!Directory.Exists(root)) continue;
|
||||
IEnumerable<string> files;
|
||||
try { files = Directory.EnumerateFiles(root, "*.*", SearchOption.AllDirectories).ToList(); }
|
||||
catch { continue; }
|
||||
foreach (var f in files)
|
||||
{
|
||||
var ext = Path.GetExtension(f).ToLowerInvariant();
|
||||
if (ext is not (".lnk" or ".url" or ".appref-ms")) continue;
|
||||
var name = Path.GetFileNameWithoutExtension(f);
|
||||
// Deinstallations- und Hilfe-Verknüpfungen will niemand per Suche starten
|
||||
if (name.Contains("uninstall", StringComparison.OrdinalIgnoreCase) || name.Contains("deinstall", StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
result.Add(new Entry(name, "Startmenü", f, "", f, () => Usage.Start(f),
|
||||
() => Process.Start("explorer.exe", $"/select,\"{f}\"")));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gleiches Programm aus Kategorie, Desktop und Startmenü nur einmal zeigen – die Reihenfolge der Quellen
|
||||
/// entscheidet (Kategorie vor Desktop vor Startmenü). Gleicher Name gilt als gleiches Programm.
|
||||
/// </summary>
|
||||
private static List<Entry> Dedupe(IEnumerable<Entry> entries)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
return entries.Where(e => e.Path is null || seen.Add(e.SearchName.Trim())).ToList();
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Suchen & Anzeigen
|
||||
// =====================================================================
|
||||
|
||||
/// <summary>Wie gut passt der Name zur Eingabe? -1 = gar nicht.</summary>
|
||||
private static int Match(string name, string q)
|
||||
{
|
||||
if (q.Length == 0) return 0;
|
||||
var n = name.ToLowerInvariant();
|
||||
if (n == q) return 1000;
|
||||
if (n.StartsWith(q)) return 800;
|
||||
var words = n.Split([' ', '-', '_', '.', '(', ')', '&'], StringSplitOptions.RemoveEmptyEntries);
|
||||
if (words.Any(w => w.StartsWith(q))) return 650;
|
||||
if (q.Length >= 2 && string.Concat(words.Select(w => w[0])).StartsWith(q)) return 600; // "vsc" → Visual Studio Code
|
||||
if (n.Contains(q)) return 450;
|
||||
// Buchstaben in der richtigen Reihenfolge ("fzla" → FileZilla) – aber nur ab einem Wortanfang,
|
||||
// sonst passt fast alles irgendwie ("ets" → Malwarebytes)
|
||||
if (q.Length < 3) return -1;
|
||||
for (int start = 0; start < n.Length; start++)
|
||||
{
|
||||
if (n[start] != q[0] || (start > 0 && char.IsLetterOrDigit(n[start - 1]))) continue;
|
||||
int k = 0;
|
||||
for (int j = start; j < n.Length && k < q.Length; j++) if (n[j] == q[k]) k++;
|
||||
if (k == q.Length) return 200;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void Filter()
|
||||
{
|
||||
var q = _box.Text.Trim().ToLowerInvariant();
|
||||
if (q.Length == 0)
|
||||
{
|
||||
// Ohne Eingabe: zuletzt/oft benutzt
|
||||
_shown = _all.Where(e => Usage.Score(e.Key) > 0)
|
||||
.OrderByDescending(e => Usage.Score(e.Key)).Take(MaxResults).ToList();
|
||||
}
|
||||
else
|
||||
{
|
||||
_shown = _all.Select(e => (e, m: Match(e.SearchName, q)))
|
||||
.Where(x => x.m >= 0)
|
||||
.OrderByDescending(x => x.m + Math.Min(300, Usage.Score(x.e.Key) * 40))
|
||||
.ThenBy(x => x.e.Name.Length)
|
||||
.Take(MaxResults).Select(x => x.e).ToList();
|
||||
}
|
||||
ShowResults(q.Length > 0);
|
||||
}
|
||||
|
||||
private void ShowResults(bool searching)
|
||||
{
|
||||
var res = Application.Current.Resources;
|
||||
_list.Items.Clear();
|
||||
int px = (int)Math.Round(32 * VisualTreeHelper.GetDpi(this).DpiScaleX);
|
||||
foreach (var e in _shown)
|
||||
{
|
||||
FrameworkElement icon;
|
||||
if (e.Path is not null)
|
||||
{
|
||||
var img = new Image { Width = 32, Height = 32 };
|
||||
var item = new IconItem { Path = e.Path };
|
||||
item.PropertyChanged += (_, _) => img.Source = item.Icon;
|
||||
IconLoader.Load(item, px);
|
||||
img.Source = item.Icon;
|
||||
icon = img;
|
||||
}
|
||||
else
|
||||
{
|
||||
var meta = e.Key.StartsWith("kategorie:") ? S.Categories.GetValueOrDefault(e.Name) : null;
|
||||
icon = new TextBlock
|
||||
{
|
||||
Text = e.Glyph, FontFamily = (FontFamily)res["IconFont"], FontSize = 22, Width = 32, TextAlignment = TextAlignment.Center,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)res["AccentBrush"],
|
||||
};
|
||||
}
|
||||
icon.Margin = new Thickness(0, 0, 14, 0);
|
||||
DockPanel.SetDock(icon, Dock.Left);
|
||||
var texts = new StackPanel { VerticalAlignment = VerticalAlignment.Center };
|
||||
texts.Children.Add(new TextBlock { Text = e.Name, FontSize = 15, TextTrimming = TextTrimming.CharacterEllipsis });
|
||||
texts.Children.Add(new TextBlock { Text = e.Sub, FontSize = 12, Foreground = (Brush)res["TextSecondaryBrush"] });
|
||||
_list.Items.Add(new DockPanel { Margin = new Thickness(8, 6, 8, 6), Children = { icon, texts } });
|
||||
}
|
||||
if (_shown.Count == 0 && searching)
|
||||
_list.Items.Add(new TextBlock
|
||||
{
|
||||
Text = "Nichts gefunden", Margin = new Thickness(12, 10, 12, 10), Foreground = (Brush)res["TextSecondaryBrush"],
|
||||
IsHitTestVisible = false,
|
||||
});
|
||||
_list.Visibility = _list.Items.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
if (_shown.Count > 0) _list.SelectedIndex = 0;
|
||||
}
|
||||
|
||||
private void OnKey(object sender, KeyEventArgs e)
|
||||
{
|
||||
switch (e.Key)
|
||||
{
|
||||
case Key.Escape:
|
||||
CloseNow();
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Down or Key.Up when _shown.Count > 0:
|
||||
int i = _list.SelectedIndex + (e.Key == Key.Down ? 1 : -1);
|
||||
_list.SelectedIndex = (i + _shown.Count) % _shown.Count;
|
||||
_list.ScrollIntoView(_list.SelectedItem);
|
||||
e.Handled = true;
|
||||
break;
|
||||
case Key.Enter:
|
||||
RunSelected(locate: Keyboard.Modifiers.HasFlag(ModifierKeys.Control));
|
||||
e.Handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void RunSelected(bool locate)
|
||||
{
|
||||
if (_list.SelectedIndex < 0 || _list.SelectedIndex >= _shown.Count) return;
|
||||
var entry = _shown[_list.SelectedIndex];
|
||||
CloseNow(); // zuerst schließen, damit das gestartete Programm nach vorne kommt
|
||||
try
|
||||
{
|
||||
if (locate && entry.Locate is not null) entry.Locate();
|
||||
else
|
||||
{
|
||||
entry.Run();
|
||||
// Kategorien/Seiten mitzählen (Dateien zählt Usage.Start, Gruppen LaunchGroupNow selbst)
|
||||
if (entry.Path is null && !entry.Key.StartsWith(Usage.GroupPrefix)) Usage.Record(entry.Key);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
App.TrayIcon?.Notify("Starten fehlgeschlagen", $"„{entry.Name}“ konnte nicht geöffnet werden.\n{ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)] private struct POINT { public int X, Y; }
|
||||
[DllImport("user32.dll")] private static extern bool GetCursorPos(out POINT p);
|
||||
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
|
||||
}
|
||||
@@ -1,155 +1,487 @@
|
||||
# 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 (64 Bit)
|
||||
- Das war's. Falls DeskCore noch etwas von Microsoft braucht („.NET“), erledigt das der Installer automatisch.
|
||||
|
||||
---
|
||||
|
||||
## 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**.
|
||||
Nur wenn „.NET“ fehlt und installiert wird, fragt Windows einmal nach. Einfach mit **Ja** bestätigen.
|
||||
|
||||
> **Windows warnt beim Start des Installers?**
|
||||
> Bei neuen Programmen zeigt Windows manchmal „Der Computer wurde durch Windows geschützt“.
|
||||
> Klick dann auf **„Weitere Informationen“** und danach auf **„Trotzdem ausführen“**.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
|
||||
### Icons in den Desktop-Boxen
|
||||
Auch in einer Box auf dem Desktop kannst du per **Rechtsklick** umbenennen, löschen, in eine andere Kategorie verschieben
|
||||
oder zurück auf den Desktop legen. Icons lassen sich auch einfach aus der Box herausziehen.
|
||||
|
||||
### Updates
|
||||
DeskCore schaut einmal am Tag nach, ob es eine neue Version gibt.
|
||||
Wenn ja, erscheint oben ein Hinweis. Ein Klick auf **„Jetzt aktualisieren“** genügt, und DeskCore startet danach von selbst neu.
|
||||
|
||||
---
|
||||
|
||||
## 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.
|
||||
Beim Kopieren siehst du einen Fortschrittsbalken und kannst jederzeit **abbrechen**. Dann bleibt alles wie vorher.
|
||||
|
||||
**Ich habe einen Bildschirm abgesteckt und das Fenster ist weg.**
|
||||
Keine Sorge: DeskCore und die Desktop-Boxen holen sich automatisch auf den verbliebenen Bildschirm zurück.
|
||||
|
||||
**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)
|
||||
<p align="center">
|
||||
<img src="app.png" width="110" alt="DeskCore-Logo">
|
||||
</p>
|
||||
|
||||
<h1 align="center">DeskCore</h1>
|
||||
|
||||
<p align="center">
|
||||
<b>Endlich ein aufgeräumter Desktop.</b><br>
|
||||
Sortiere deine Desktop-Icons in eigene Kategorien – übersichtlich, schnell und im Windows-11-Look.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://git.viper.ipv64.net/M_Viper/DeskCore/releases/latest"><b>⬇️ DeskCore herunterladen</b></a>
|
||||
· Windows 10 & 11 · kostenlos für private Nutzung
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/desktop.png" width="820" alt="DeskCore – Übersicht deines Desktops">
|
||||
<br><sub>Alle Icons deines Desktops auf einen Blick. Der grüne Haken ✓ zeigt, was schon in einer Kategorie gesichert ist.</sub>
|
||||
</p>
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Inhalt
|
||||
|
||||
- [Auf einen Blick](#auf-einen-blick)
|
||||
- [Was du brauchst](#was-du-brauchst)
|
||||
- [Installieren](#installieren)
|
||||
- [So räumst du deinen Desktop auf](#so-räumst-du-deinen-desktop-auf)
|
||||
- [Programme starten](#programme-starten)
|
||||
- [Schnellstart-Suche](#schnellstart-suche)
|
||||
- [Start-Gruppen – mehrere Programme mit einem Klick](#start-gruppen--mehrere-programme-mit-einem-klick)
|
||||
- [Boxen auf dem Desktop](#boxen-auf-dem-desktop)
|
||||
- [Desktop-Seiten](#desktop-seiten)
|
||||
- [Den Desktop schick machen](#den-desktop-schick-machen)
|
||||
- [Automatisch sortieren](#automatisch-sortieren)
|
||||
- [Weitere Extras](#weitere-extras)
|
||||
- [Alle Tastenkürzel](#alle-tastenkürzel)
|
||||
- [Hoppla, falsch geklickt?](#hoppla-falsch-geklickt)
|
||||
- [Häufige Fragen](#häufige-fragen)
|
||||
- [Deinstallieren](#deinstallieren)
|
||||
- [Lizenz](#lizenz)
|
||||
|
||||
---
|
||||
|
||||
## Auf einen Blick
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| 🗂️ **Eigene Kategorien** | mit Symbol und Farbe, Reihenfolge frei wählbar |
|
||||
| ✅ **Sicher aufräumen** | Icons werden gesichert, bevor du sie vom Desktop löschst |
|
||||
| 🚀 **Schnellstart-Suche** | Alt + Umschalt + Leertaste, ein paar Buchstaben, Enter – läuft |
|
||||
| ▶️ **Start-Gruppen** | ein Klick startet mehrere Programme auf einmal |
|
||||
| 🧱 **Desktop-Boxen** | Kategorien oder Ordner als Kästen direkt auf dem Desktop, mit Tabs und eigenem Aussehen |
|
||||
| 📑 **Desktop-Seiten** | mehrere Desktops zum Umschalten, z. B. „Arbeit“ und „Freizeit“ – auch automatisch nach Uhrzeit |
|
||||
| ⭐ **Meistgenutzt zuerst** | DeskCore merkt sich, was du oft startest, und sortiert danach |
|
||||
| 🎨 **Icons einfärben** | alle Desktop-Icons in einem einheitlichen Farbton |
|
||||
| 👁️ **Schnell verstecken** | Doppelklick auf den Desktop – alles weg, noch ein Doppelklick – alles wieder da |
|
||||
| ⚡ **Automatisch sortieren** | neue Icons landen per Regel von selbst in der richtigen Kategorie |
|
||||
| 🧹 **Räumt selbst auf** | deinstallierte Programme verschwinden auch aus DeskCore |
|
||||
| 🔍 **Suche** | jedes Programm in Sekunden finden |
|
||||
| ↩️ **Rückgängig** | Strg + Z macht jeden Schritt rückgängig |
|
||||
| 🌙 **Hell & Dunkel** | passt sich Windows an oder nach deinem Geschmack |
|
||||
| 💾 **Sicherung** | alles als ZIP sichern, z. B. für einen neuen PC |
|
||||
| 🔄 **Updates** | DeskCore sagt Bescheid, wenn es eine neue Version gibt |
|
||||
|
||||
---
|
||||
|
||||
## Was du brauchst
|
||||
|
||||
- Windows 10 oder Windows 11 (64 Bit)
|
||||
- Das war's. Falls DeskCore noch etwas von Microsoft braucht („.NET“), erledigt das der Installer automatisch.
|
||||
|
||||
---
|
||||
|
||||
## Installieren
|
||||
|
||||
1. Doppelklick auf **`DeskCore-Setup.exe`**
|
||||
2. Windows fragt: **„Möchten Sie zulassen, dass durch diese App Änderungen an Ihrem Gerät vorgenommen werden?“** → **Ja**
|
||||
3. **Weiter** klicken
|
||||
4. Die Lizenz lesen, den Haken bei **„Ich akzeptiere …“** setzen, dann **Weiter**
|
||||
5. Bei Bedarf noch auswählen:
|
||||
- Verknüpfung auf dem Desktop (empfohlen)
|
||||
- Mit Windows starten
|
||||
6. **Installieren** klicken, und fertig.
|
||||
|
||||
Der Installer braucht **Administratorrechte**, weil er bei Bedarf die von DeskCore benötigte Microsoft-Laufzeit („.NET“) gleich mitinstalliert.
|
||||
DeskCore selbst läuft danach ganz normal **ohne** Administratorrechte.
|
||||
|
||||
> **Windows warnt beim Start des Installers?**
|
||||
> Bei neuen Programmen zeigt Windows manchmal „Der Computer wurde durch Windows geschützt“.
|
||||
> Klick dann auf **„Weitere Informationen“** und danach auf **„Trotzdem ausführen“**.
|
||||
|
||||
**Update auf eine neue Version:** Einfach den neuen Installer starten – oder in DeskCore auf **„Jetzt aktualisieren“** klicken und die Windows-Abfrage mit **Ja** bestätigen. Deine Kategorien und Einstellungen bleiben erhalten.
|
||||
|
||||
---
|
||||
|
||||
## So räumst du deinen Desktop auf
|
||||
|
||||
### 1. Kategorie 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.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/kategorie.png" width="820" alt="Eine Kategorie in DeskCore">
|
||||
<br><sub>Eine fertige Kategorie: Alle Programme ordentlich beisammen, starten per Doppelklick.</sub>
|
||||
</p>
|
||||
|
||||
### 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.
|
||||
|
||||
### Kategorien verwalten
|
||||
- **Umbenennen, Symbol oder Farbe ändern:** Kategorie anklicken → oben **„Bearbeiten“**
|
||||
- **Reihenfolge ändern:** Kategorie links mit der Maus **nach oben oder unten ziehen** (oder Rechtsklick → **Nach oben / Nach unten**)
|
||||
- **Ordner im Explorer ansehen:** oben **„Im Explorer öffnen“**
|
||||
- **Löschen:** oben **„Kategorie löschen“** – die Icons landen im Papierkorb, Strg + Z holt alles zurück
|
||||
|
||||
---
|
||||
|
||||
## Programme starten
|
||||
|
||||
- Kategorie links anklicken
|
||||
- **Doppelklick** auf das Icon
|
||||
|
||||
Oder schneller: **Suchfeld** oben benutzen (Strg + F), den Namen eintippen und Doppelklick.
|
||||
Die Suche findet Programme in allen Kategorien und auf dem Desktop.
|
||||
|
||||
Auch über das **DeskCore-Symbol neben der Uhr** kommst du per Rechtsklick direkt an deine Kategorien.
|
||||
|
||||
**Meistgenutzt zuerst:** DeskCore merkt sich, was du wie oft startest. Über den **Ansicht-Knopf** oben rechts →
|
||||
**„Meistgenutzt zuerst“** stehen deine Lieblingsprogramme in jeder Kategorie ganz vorne.
|
||||
Was du lange nicht mehr benutzt hast, rutscht mit der Zeit von selbst wieder nach hinten.
|
||||
|
||||
---
|
||||
|
||||
## Schnellstart-Suche
|
||||
|
||||
Programme starten, ohne DeskCore zu öffnen – ähnlich wie die Spotlight-Suche am Mac:
|
||||
|
||||
1. **Alt + Umschalt + Leertaste** drücken – egal, was gerade offen ist
|
||||
2. Ein paar Buchstaben tippen
|
||||
3. **Enter** – das Programm startet
|
||||
|
||||
Gefunden wird alles aus deinen **Kategorien**, vom **Desktop** und aus dem **Windows-Startmenü**,
|
||||
dazu deine **Start-Gruppen**, **Kategorien** (öffnet DeskCore dort) und **Desktop-Seiten**.
|
||||
Was du oft startest, steht weiter oben. Ohne Eingabe siehst du direkt deine meistgenutzten Programme.
|
||||
|
||||
Die Suche ist großzügig: **„vsc“** findet **V**isual **S**tudio **C**ode, **„fzla“** findet **F**ile**Z**i**l**l**a**.
|
||||
|
||||
| Taste | Was passiert |
|
||||
|---|---|
|
||||
| **↑ / ↓** | Treffer auswählen |
|
||||
| **Enter** | Starten |
|
||||
| **Strg + Enter** | Speicherort im Explorer öffnen |
|
||||
| **Esc** | Schließen (oder einfach woanders hinklicken) |
|
||||
|
||||
Das Tastenkürzel änderst du unter **Einstellungen → Schnellstart-Suche** (ins Feld klicken, neue Kombination drücken).
|
||||
Dort kannst du auch einstellen, ob das Windows-Startmenü mit durchsucht wird.
|
||||
|
||||
---
|
||||
|
||||
## Start-Gruppen – mehrere Programme mit einem Klick
|
||||
|
||||
Du startest jeden Tag dieselben Programme, z. B. Browser, Mail und Teams? Pack sie in eine **Start-Gruppe**.
|
||||
Ein Klick, und alle gehen nacheinander auf.
|
||||
|
||||
**Gruppe anlegen**
|
||||
1. Links unten bei **„Start-Gruppen“** auf das **+** klicken
|
||||
2. Namen eingeben, z. B. **Feierabend** oder **Arbeit**
|
||||
3. Einstellen, wie viele Sekunden zwischen den Programmen liegen sollen (oder „gleichzeitig“)
|
||||
|
||||
**Programme hinzufügen**
|
||||
- Ein Icon mit der Maus **auf die Start-Gruppe ziehen**, oder
|
||||
- Rechtsklick auf ein Icon → **„Zur Start-Gruppe hinzufügen“**
|
||||
|
||||
**Starten**
|
||||
- Auf das kleine **▶** neben der Gruppe klicken, oder
|
||||
- Gruppe anklicken → **„Alle starten“**, oder
|
||||
- Rechtsklick auf das DeskCore-Symbol neben der Uhr → **„… starten“**
|
||||
|
||||
**Noch bequemer:** Gruppe anklicken → **„Verknüpfung auf den Desktop“**.
|
||||
Dann liegt ein Symbol auf dem Desktop, das die ganze Gruppe startet.
|
||||
|
||||
Die Reihenfolge änderst du per Rechtsklick auf ein Programm → **„Früher starten“ / „Später starten“**.
|
||||
Ein Programm aus der Gruppe nehmen: Rechtsklick → **„Aus Start-Gruppe entfernen“**. Das Programm selbst bleibt dabei unangetastet.
|
||||
|
||||
---
|
||||
|
||||
## Boxen auf dem Desktop
|
||||
|
||||
Eine **Box** ist ein Kasten direkt auf deinem Desktop, in dem die Icons einer Kategorie liegen.
|
||||
So hast du deine Programme auf dem Desktop – aber ordentlich.
|
||||
|
||||
### Box anlegen
|
||||
Es gibt drei Wege:
|
||||
- Kategorie in DeskCore anklicken → oben **„Als Box auf den Desktop“**
|
||||
- Mit **gedrückter rechter Maustaste** auf einer freien Stelle des Desktops **einen Rahmen ziehen**. Danach wählst du, welche Kategorie hinein soll – oder legst gleich eine neue an.
|
||||
(Ein ganz normaler Rechtsklick öffnet weiterhin das gewohnte Windows-Menü.)
|
||||
- **Einstellungen → Desktop-Boxen → Boxen auf dem Desktop anzeigen** schaltet alle Boxen auf einmal ein oder aus
|
||||
|
||||
### Mit Boxen arbeiten
|
||||
- **Verschieben:** an der Titelleiste ziehen
|
||||
- **Größe ändern:** am Rand ziehen
|
||||
- **Einklappen:** Doppelklick auf den Titel – noch ein Doppelklick klappt sie wieder auf. Fährst du mit der Maus über eine eingeklappte Box, geht sie kurz auf.
|
||||
- **Programm starten:** Doppelklick auf das Icon
|
||||
- **Rechtsklick auf ein Icon:** Öffnen, Umbenennen, Löschen, in eine andere Kategorie verschieben oder **„Zurück auf den Desktop legen“**
|
||||
- Icons lassen sich auch einfach in eine Box hinein- oder aus ihr herausziehen
|
||||
- **Eigene Reihenfolge:** Zieh ein Icon innerhalb der Box an die gewünschte Stelle – ein blauer Strich zeigt, wo es landet.
|
||||
Die Box merkt sich die Reihenfolge (Sortierung „Eigene“).
|
||||
|
||||
Das **Menü einer Box** (die drei Punkte ⋯ oben rechts) bietet:
|
||||
|
||||
| Menüpunkt | Was passiert |
|
||||
|---|---|
|
||||
| **Tab hinzufügen** | Eine weitere Kategorie, ein Ordner-Portal oder **„Zuletzt benutzt“** in dieselbe Box packen. Oben erscheinen dann Reiter zum Umschalten. |
|
||||
| **Ordner-Portal …** | Einen Ordner als Reiter hinzufügen (siehe unten) |
|
||||
| **Aussehen …** | Farbe, Deckkraft, Milchglas, Icon-Größe, Icon-Tönung und Sortierung der Box: nach Name, Typ, Datum, **Meistgenutzt** oder **Eigene** Reihenfolge |
|
||||
| **Sperren** | Die Box lässt sich nicht mehr versehentlich verschieben oder in der Größe ändern |
|
||||
| **Auf Seite verschieben** | Die Box auf eine andere [Desktop-Seite](#desktop-seiten) schicken |
|
||||
| **Auf Seite kopieren** | Die gleiche Box zusätzlich auf eine andere Seite legen – z. B. „Internet“ auf „Arbeit“ **und** „Freizeit“ |
|
||||
| **Box schließen** | Die Box verschwindet vom Desktop. Die Kategorie in DeskCore bleibt natürlich erhalten. |
|
||||
|
||||
Rechtsklick auf einen **Reiter**: **„Als eigene Box lösen“** macht daraus wieder eine eigene Box, **„Tab entfernen“** nimmt ihn heraus.
|
||||
|
||||
### Box „Zuletzt benutzt“
|
||||
Eine Box, die sich von selbst füllt: Sie zeigt, was du zuletzt über DeskCore gestartet hast – neueste zuerst.
|
||||
Anlegen: beim **Aufziehen einer Box** „Zuletzt benutzt“ wählen, oder im Box-Menü ⋯ → **Tab hinzufügen → Zuletzt benutzt**.
|
||||
Mitgezählt wird alles, was du über DeskCore startest: aus dem Fenster, aus Boxen, über die Schnellstart-Suche und über Start-Gruppen.
|
||||
|
||||
### Ordner-Portal
|
||||
Ein Portal ist eine Box, die **live einen beliebigen Ordner** zeigt, z. B. **Downloads** oder einen Projektordner.
|
||||
Neue Dateien erscheinen sofort darin, und Unterordner öffnest du direkt in der Box.
|
||||
Anlegen: **Einstellungen → Desktop-Boxen → Ordner-Portal → „Ordner wählen …“** oder beim Aufziehen einer Box **„Ordner-Portal …“** wählen.
|
||||
|
||||
### Peek – Boxen nach vorne holen
|
||||
Alle Fenster sind offen und verdecken den Desktop? Drück **Win + Umschalt + B**.
|
||||
Die Boxen kommen nach vorne, über alle Fenster. Nochmal drücken, **Esc** oder woanders hinklicken schickt sie zurück.
|
||||
Ein- und ausschalten unter **Einstellungen → Peek**.
|
||||
|
||||
### Anordnungen speichern (Snapshots)
|
||||
Du hast deine Boxen perfekt angeordnet? Speicher dir das:
|
||||
**Einstellungen → Snapshots → „Aktuelle Anordnung speichern“**.
|
||||
Mit **„Wiederherstellen“** ist später alles wieder genau so – auch über das DeskCore-Symbol neben der Uhr.
|
||||
|
||||
Arbeitest du mal am Laptop allein und mal mit Monitor? DeskCore merkt sich die Positionen der Boxen **für jede Bildschirm-Anordnung automatisch**.
|
||||
|
||||
---
|
||||
|
||||
## Desktop-Seiten
|
||||
|
||||
Du kannst **mehrere Desktop-Seiten** anlegen, z. B. **Arbeit** und **Freizeit** – jede Seite hat ihre eigenen Boxen.
|
||||
So siehst du nach Feierabend nur deine Spiele und tagsüber nur deine Arbeitsprogramme.
|
||||
|
||||
**Seiten anlegen:** **Einstellungen → Desktop-Seiten → „Seite hinzufügen“**.
|
||||
Dort kannst du Seiten auch umbenennen (Stift) oder löschen (Mülleimer). Die Boxen einer gelöschten Seite wandern auf die Seite davor, es geht also nichts verloren.
|
||||
|
||||
**Zwischen Seiten wechseln:**
|
||||
- **Win + Alt + ←** oder **→**
|
||||
- **Mausrad** drehen, während die Maus über einer freien Stelle des Desktops ist
|
||||
- Rechtsklick auf das DeskCore-Symbol neben der Uhr → **„Desktop-Seite“**
|
||||
- in den Einstellungen bei der Seite auf **„Anzeigen“**
|
||||
|
||||
Beim Wechseln zeigt DeskCore kurz unten am Bildschirm, auf welcher Seite du bist.
|
||||
Neue Boxen landen immer auf der Seite, die gerade angezeigt wird. Eine Box auf eine andere Seite schicken: Box-Menü ⋯ → **„Auf Seite verschieben“**.
|
||||
|
||||
**Eine Kategorie auf mehreren Seiten:** Jede Seite darf ihre eigene Box mit derselben Kategorie haben – z. B. „Internet“ auf „Arbeit“ und auf „Freizeit“.
|
||||
Am schnellsten geht das über das Box-Menü ⋯ → **„Auf Seite kopieren“**. Es sind dieselben Icons: Was du in der einen Box änderst, siehst du auch in der anderen.
|
||||
Nur **auf derselben Seite** steht eine Kategorie in höchstens einer Box.
|
||||
|
||||
> Die Icons, die direkt auf dem Desktop liegen (nicht in einer Box), siehst du auf jeder Seite.
|
||||
|
||||
### Seiten automatisch wechseln (Zeitplan)
|
||||
Klick in den Einstellungen bei einer Seite auf die **Uhr** 🕒:
|
||||
|
||||
- **„Diese Seite automatisch anzeigen“** – Tage und Uhrzeit wählen, z. B. **Mo–Fr, 08:00 bis 17:00**
|
||||
- **„Zu allen anderen Zeiten diese Seite anzeigen“** – für die Seite, die sonst gelten soll
|
||||
|
||||
Beispiel: „Arbeit“ bekommt Mo–Fr 08:00–17:00, bei „Freizeit“ setzt du den Haken für „alle anderen Zeiten“.
|
||||
Morgens um acht wechselt DeskCore dann von selbst auf „Arbeit“, um fünf zurück auf „Freizeit“.
|
||||
Wechselst du zwischendurch von Hand, bleibt deine Wahl bis zum nächsten geplanten Wechsel stehen.
|
||||
Zeiträume über Mitternacht (z. B. 22:00 bis 06:00) gehen auch.
|
||||
|
||||
### Eigenes Hintergrundbild je Seite
|
||||
Im selben Fenster (**Uhr** 🕒) unter **„Hintergrundbild“** → **„Bild wählen …“**.
|
||||
Beim Wechsel auf die Seite stellt DeskCore das Bild ein. Wechselst du auf eine Seite ohne eigenes Bild,
|
||||
kommt dein ursprünglicher Hintergrund zurück.
|
||||
|
||||
---
|
||||
|
||||
## Den Desktop schick machen
|
||||
|
||||
### Desktop-Icons einfärben
|
||||
**Einstellungen → Desktop-Icons tönen:** eine Farbe anklicken, und alle Icons auf dem Desktop bekommen denselben Farbton.
|
||||
Mit dem Regler **„Stärke“** stellst du ein, wie kräftig. Der erste, leere Kreis schaltet die Tönung wieder aus.
|
||||
Die Icons in den Boxen färbst du getrennt über das Box-Menü ⋯ → **„Aussehen …“**.
|
||||
|
||||
### Schnell verstecken
|
||||
**Doppelklick auf eine freie Stelle des Desktops** blendet alle Icons und Boxen aus – perfekt für ein sauberes Hintergrundbild oder eine Bildschirmfreigabe.
|
||||
Noch ein Doppelklick holt alles zurück. Einschalten unter **Einstellungen → Schnell verstecken**.
|
||||
|
||||
### Chamäleon
|
||||
Die Boxen werden fast unsichtbar und erscheinen erst, wenn du mit der Maus darüberfährst.
|
||||
Einschalten unter **Einstellungen → Chamäleon**.
|
||||
|
||||
### Hell oder Dunkel
|
||||
Oben rechts auf die **Sonne bzw. den Mond** klicken und auswählen – oder **„Wie Windows“**, dann passt sich DeskCore automatisch an.
|
||||
|
||||
### Ansicht
|
||||
Oben rechts gibt es den **Ansicht-Knopf**: kleine, mittlere oder große Symbole oder eine Liste.
|
||||
|
||||
---
|
||||
|
||||
## Automatisch sortieren
|
||||
|
||||
DeskCore kann neue Icons selbst einsortieren. Sobald z. B. ein Installer eine Verknüpfung auf den Desktop legt, wandert sie in die richtige Kategorie.
|
||||
|
||||
So geht's: **Einstellungen → Automatisch sortieren → „Regel hinzufügen“**. Es gibt zwei Arten von Regeln
|
||||
(zum Umschalten auf die Art der Regel klicken):
|
||||
|
||||
| Regel | Beispiel |
|
||||
|---|---|
|
||||
| **Name enthält** | alles mit „Steam“ im Namen → **Spiele** |
|
||||
| **Dateityp ist** | Bilder, Videos, Musik, Dokumente, Archive, Programme, Ordner – oder Endungen wie „pdf, docx“ → **Dokumente** |
|
||||
|
||||
Die erste passende Regel gewinnt. Groß- und Kleinschreibung ist egal.
|
||||
Mit **„Danach vom Desktop entfernen“** verschwindet das Original gleich vom Desktop.
|
||||
**„Jetzt auf den Desktop anwenden“** sortiert alles ein, was schon auf dem Desktop liegt.
|
||||
|
||||
---
|
||||
|
||||
## Weitere Extras
|
||||
|
||||
### DeskCore schnell öffnen
|
||||
- **Win + Umschalt + D** drücken, von überall aus (änderbar unter **Einstellungen → Tastenkürzel**)
|
||||
- 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 – deine Boxen bleiben also da.
|
||||
Richtig beenden: **Rechtsklick auf das Symbol neben der Uhr → Beenden**
|
||||
|
||||
### Das Symbol neben der Uhr
|
||||
Ein Rechtsklick darauf bietet dir alles Wichtige, ohne das Fenster zu öffnen:
|
||||
deine Kategorien, Start-Gruppen, gespeicherte Anordnungen, Desktop-Seiten und **„Desktop ausblenden“**.
|
||||
|
||||
### Mit Windows starten
|
||||
**Einstellungen → Mit Windows starten.** DeskCore startet dann unauffällig neben der Uhr, und Boxen sowie Auto-Sortierung sind sofort da.
|
||||
|
||||
### Deinstallierte Programme verschwinden von selbst
|
||||
Deinstallierst du ein Programm, entfernt DeskCore dessen Icon nach etwa einer Minute auch aus deinen Kategorien – auch bei **Steam- und Epic-Spielen**.
|
||||
Strg + Z holt es zurück. Ausschalten unter **Einstellungen → Deinstallierte Programme entfernen**.
|
||||
|
||||
### Sicherung
|
||||
**Einstellungen → Sicherung → „Sichern …“** speichert alle Kategorien, Boxen, Start-Gruppen und Einstellungen in eine ZIP-Datei.
|
||||
Mit **„Wiederherstellen …“** holst du alles zurück, z. B. auf einem neuen PC.
|
||||
|
||||
### Updates
|
||||
DeskCore schaut einmal am Tag nach, ob es eine neue Version gibt.
|
||||
Wenn ja, erscheint oben ein Hinweis. Ein Klick auf **„Jetzt aktualisieren“** genügt, und DeskCore startet danach von selbst neu.
|
||||
Prüfen von Hand: **Einstellungen → Updates → „Jetzt prüfen“**.
|
||||
|
||||
---
|
||||
|
||||
## Alle Tastenkürzel
|
||||
|
||||
**Überall in Windows**
|
||||
|
||||
| Taste | Was passiert |
|
||||
|---|---|
|
||||
| **Alt + Umschalt + Leertaste** | Schnellstart-Suche |
|
||||
| **Win + Umschalt + D** | DeskCore öffnen / verstecken |
|
||||
| **Win + Umschalt + B** | Peek: Boxen nach vorne holen |
|
||||
| **Win + Alt + ← / →** | Desktop-Seite wechseln |
|
||||
|
||||
**Im DeskCore-Fenster**
|
||||
|
||||
| Taste | Was passiert |
|
||||
|---|---|
|
||||
| **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 nach oben / unten verschieben |
|
||||
| **Strg + 1 … 9** | Zur Kategorie 1 … 9 springen |
|
||||
| **F5** | Aktualisieren |
|
||||
|
||||
---
|
||||
|
||||
## Hoppla, falsch geklickt?
|
||||
|
||||
**Strg + Z** macht den letzten Schritt rückgängig – auch mehrmals hintereinander.
|
||||
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.
|
||||
|
||||
**Ich habe ein Programm deinstalliert – verschwindet es auch aus DeskCore?**
|
||||
Ja, automatisch nach etwa einer Minute – auch bei **Steam- und Epic-Spielen**. Hast du es dir anders überlegt: **Strg + Z** holt es zurück.
|
||||
Programme auf einem gerade abgesteckten USB- oder Netzlaufwerk bleiben natürlich erhalten.
|
||||
|
||||
**Mein Rechtsklick auf den Desktop fühlt sich anders an.**
|
||||
Damit du Boxen aufziehen kannst, schaut DeskCore kurz, ob du ziehst oder nur klickst. Das Windows-Menü erscheint deshalb erst beim **Loslassen** der Taste.
|
||||
Stört dich das: **Einstellungen → Box auf dem Desktop aufziehen** ausschalten.
|
||||
|
||||
**Das Mausrad wechselt ungewollt die Desktop-Seite.**
|
||||
Unter **Einstellungen → Desktop-Seiten** den Haken bei **„Mit dem Mausrad … wechseln“** entfernen.
|
||||
Mit gedrückter **Strg**-Taste ändert das Mausrad wie gewohnt die Icon-Größe des Desktops.
|
||||
|
||||
**Die Farbe der Icon-Tönung sitzt nicht genau auf den Icons.**
|
||||
Kurz warten – DeskCore liest die Positionen alle zwei Sekunden neu. Hast du gerade die Bildschirmskalierung geändert, hilft ein Neustart von DeskCore.
|
||||
|
||||
**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.
|
||||
Beim Kopieren siehst du einen Fortschrittsbalken und kannst jederzeit **abbrechen**. Dann bleibt alles wie vorher.
|
||||
|
||||
**Ich habe einen Bildschirm abgesteckt und das Fenster ist weg.**
|
||||
Keine Sorge: DeskCore und die Desktop-Boxen holen sich automatisch auf den verbliebenen Bildschirm zurück.
|
||||
Steckst du den Bildschirm wieder an, kommen die Boxen an ihre alten Plätze.
|
||||
|
||||
**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 …“**.
|
||||
|
||||
**Merkt sich DeskCore, welche Programme ich starte? Geht das irgendwohin?**
|
||||
Nur auf deinem PC: in der Datei `usage.json` im DeskCore-Einstellungsordner. Gezählt wird nur, was du **über DeskCore** startest.
|
||||
Nichts davon verlässt deinen Rechner.
|
||||
|
||||
**Ein Tastenkürzel funktioniert nicht.**
|
||||
Ein anderes Programm benutzt es vielleicht schon. Unter **Einstellungen → Tastenkürzel** ins Feld klicken und eine andere Kombination drücken.
|
||||
|
||||
**Kann ich DeskCore auch per Verknüpfung oder Skript steuern?**
|
||||
Ja, für Fortgeschrittene:
|
||||
|
||||
| Befehl | Was passiert |
|
||||
|---|---|
|
||||
| `DeskCore.exe --launch "Feierabend"` | Start-Gruppe „Feierabend“ starten |
|
||||
| `DeskCore.exe --snapshot "Arbeit"` | Gespeicherte Anordnung „Arbeit“ wiederherstellen |
|
||||
| `DeskCore.exe --page "Freizeit"` | Zur Desktop-Seite „Freizeit“ wechseln (oder `--page 2`) |
|
||||
| `DeskCore.exe --toggle-desktop` | Desktop aus- / einblenden |
|
||||
| `DeskCore.exe --peek` | Boxen nach vorne holen |
|
||||
| `DeskCore.exe --search` | Schnellstart-Suche öffnen |
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
+48
-39
@@ -1,39 +1,48 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Bildschirm-Hilfen, damit Fenster nie außerhalb des sichtbaren Bereichs landen (z. B. nach Abstecken eines Monitors).</summary>
|
||||
public static class Screens
|
||||
{
|
||||
/// <summary>Umrechnung Pixel → WPF-Einheiten (System-DPI).</summary>
|
||||
private static double Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
using var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero);
|
||||
return g.DpiX / 96.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Arbeitsbereiche (ohne Taskleiste) aller angeschlossenen Bildschirme in WPF-Einheiten.</summary>
|
||||
public static List<Rect> WorkAreas()
|
||||
{
|
||||
var s = Scale;
|
||||
return System.Windows.Forms.Screen.AllScreens
|
||||
.Select(m => new Rect(m.WorkingArea.X / s, m.WorkingArea.Y / s, m.WorkingArea.Width / s, m.WorkingArea.Height / s))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Rect PrimaryWorkArea => SystemParameters.WorkArea;
|
||||
|
||||
/// <summary>Ist genug vom Fenster (Titelleiste) auf irgendeinem Bildschirm sichtbar, um es greifen zu können?</summary>
|
||||
public static bool IsReachable(Rect window)
|
||||
{
|
||||
var title = new Rect(window.X, window.Y, window.Width, Math.Min(40, window.Height));
|
||||
return WorkAreas().Any(a =>
|
||||
{
|
||||
var i = Rect.Intersect(a, title);
|
||||
return !i.IsEmpty && i.Width >= Math.Min(120, window.Width / 2) && i.Height >= 20;
|
||||
});
|
||||
}
|
||||
}
|
||||
using System.Windows;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
/// <summary>Bildschirm-Hilfen, damit Fenster nie außerhalb des sichtbaren Bereichs landen (z. B. nach Abstecken eines Monitors).</summary>
|
||||
public static class Screens
|
||||
{
|
||||
/// <summary>Umrechnung Pixel → WPF-Einheiten (System-DPI).</summary>
|
||||
public static double Scale
|
||||
{
|
||||
get
|
||||
{
|
||||
using var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero);
|
||||
return g.DpiX / 96.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Arbeitsbereiche (ohne Taskleiste) aller angeschlossenen Bildschirme in WPF-Einheiten.</summary>
|
||||
public static List<Rect> WorkAreas()
|
||||
{
|
||||
var s = Scale;
|
||||
return System.Windows.Forms.Screen.AllScreens
|
||||
.Select(m => new Rect(m.WorkingArea.X / s, m.WorkingArea.Y / s, m.WorkingArea.Width / s, m.WorkingArea.Height / s))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public static Rect PrimaryWorkArea => SystemParameters.WorkArea;
|
||||
|
||||
/// <summary>Arbeitsbereich des Bildschirms, auf dem der Punkt liegt (sonst der nächstgelegene).</summary>
|
||||
public static Rect WorkAreaAt(Point p)
|
||||
{
|
||||
var areas = WorkAreas();
|
||||
return areas.FirstOrDefault(a => a.Contains(p), areas.OrderBy(a =>
|
||||
Math.Pow(Math.Clamp(p.X, a.Left, a.Right) - p.X, 2) + Math.Pow(Math.Clamp(p.Y, a.Top, a.Bottom) - p.Y, 2))
|
||||
.FirstOrDefault(PrimaryWorkArea));
|
||||
}
|
||||
|
||||
/// <summary>Ist genug vom Fenster (Titelleiste) auf irgendeinem Bildschirm sichtbar, um es greifen zu können?</summary>
|
||||
public static bool IsReachable(Rect window)
|
||||
{
|
||||
var title = new Rect(window.X, window.Y, window.Width, Math.Min(40, window.Height));
|
||||
return WorkAreas().Any(a =>
|
||||
{
|
||||
var i = Rect.Intersect(a, title);
|
||||
return !i.IsEmpty && i.Width >= Math.Min(120, window.Width / 2) && i.Height >= 20;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+384
-141
@@ -1,141 +1,384 @@
|
||||
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;
|
||||
|
||||
// Fenster merken (WPF-Einheiten)
|
||||
public double? WindowLeft { get; set; }
|
||||
public double? WindowTop { get; set; }
|
||||
public double? WindowWidth { get; set; }
|
||||
public double? WindowHeight { get; set; }
|
||||
public bool WindowMaximized { get; set; }
|
||||
|
||||
// Updates
|
||||
public bool AutoUpdateCheck { get; set; } = true;
|
||||
public DateTime? LastUpdateCheck { get; set; }
|
||||
public string? SkippedVersion { get; set; }
|
||||
|
||||
public List<string> CategoryOrder { get; set; } = [];
|
||||
public 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));
|
||||
}
|
||||
}
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
public enum DesignMode { System, Light, Dark }
|
||||
public enum ViewMode { Small, Medium, Large, List }
|
||||
|
||||
public class CategoryMeta
|
||||
{
|
||||
public string? Glyph { get; set; }
|
||||
public string? Color { get; set; }
|
||||
// Die folgenden Box-Felder stammen aus Version 1.0.x und werden nur noch beim Umstieg auf FenceDef gelesen
|
||||
public bool Fence { get; set; }
|
||||
public double? FenceX { get; set; }
|
||||
public double? FenceY { get; set; }
|
||||
public double FenceW { get; set; } = 340;
|
||||
public double FenceH { get; set; } = 260;
|
||||
public bool FenceCollapsed { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Eine Start-Gruppe: ein Klick startet mehrere Programme nacheinander.</summary>
|
||||
public class LaunchGroup
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
/// <summary>Pfade in Startreihenfolge (meist Verknüpfungen aus den Kategorien).</summary>
|
||||
public List<string> Items { get; set; } = [];
|
||||
/// <summary>Wartezeit zwischen zwei Programmen in Sekunden.</summary>
|
||||
public double DelaySeconds { get; set; } = 1;
|
||||
public string? Color { get; set; }
|
||||
}
|
||||
|
||||
public class SortRule
|
||||
{
|
||||
/// <summary>"Name" = Name enthält …, "Typ" = Dateityp ist … (z. B. "Bilder" oder "pdf, docx")</summary>
|
||||
public string Kind { get; set; } = "Name";
|
||||
public string Contains { get; set; } = "";
|
||||
public string Category { get; set; } = "";
|
||||
[JsonIgnore] public string KindText => Kind == "Typ" ? "Dateityp ist" : "Name enthält";
|
||||
}
|
||||
|
||||
/// <summary>Ein Tab einer Desktop-Box: eine Kategorie, ein beliebiger Ordner ("Ordner-Portal") oder "Zuletzt benutzt".</summary>
|
||||
public class FenceTab
|
||||
{
|
||||
public const string CategoryKind = "Kategorie", FolderKind = "Ordner", RecentKind = "Zuletzt";
|
||||
public string Kind { get; set; } = CategoryKind;
|
||||
/// <summary>Kategoriename bzw. Ordnerpfad (bei "Zuletzt benutzt" leer).</summary>
|
||||
public string Value { get; set; } = "";
|
||||
/// <summary>Eigene Reihenfolge (Dateinamen), wenn die Box nach "Eigene" sortiert ist.</summary>
|
||||
public List<string> Order { get; set; } = [];
|
||||
[JsonIgnore] public bool IsFolder => Kind == FolderKind;
|
||||
[JsonIgnore] public bool IsRecent => Kind == RecentKind;
|
||||
[JsonIgnore] public bool IsCategory => Kind == CategoryKind;
|
||||
[JsonIgnore] public string Title => IsRecent ? "Zuletzt benutzt"
|
||||
: IsFolder ? Path.GetFileName(Value.TrimEnd('\\')) is { Length: > 0 } n ? n : Value : Value;
|
||||
}
|
||||
|
||||
/// <summary>Zusatzeinstellungen einer Desktop-Seite: Zeitplan und Hintergrundbild.</summary>
|
||||
public class PageOption
|
||||
{
|
||||
/// <summary>Seite automatisch zu bestimmten Zeiten anzeigen.</summary>
|
||||
public bool Scheduled { get; set; }
|
||||
/// <summary>Wochentage als Bits: Montag = 1, Dienstag = 2 … Sonntag = 64.</summary>
|
||||
public int Days { get; set; } = 0b0011111; // Mo–Fr
|
||||
public string From { get; set; } = "08:00";
|
||||
public string To { get; set; } = "17:00";
|
||||
/// <summary>Eigenes Hintergrundbild für diese Seite (null = nicht ändern).</summary>
|
||||
public string? Wallpaper { get; set; }
|
||||
|
||||
public static int DayBit(DayOfWeek d) => 1 << (((int)d + 6) % 7); // Montag = Bit 0
|
||||
|
||||
/// <summary>Gilt der Zeitplan zu diesem Zeitpunkt? Zeiträume über Mitternacht (z. B. 22:00–06:00) gehen auch.</summary>
|
||||
public bool Matches(DateTime now)
|
||||
{
|
||||
if (!Scheduled || !TimeSpan.TryParse(From, out var from) || !TimeSpan.TryParse(To, out var to)) return false;
|
||||
var t = now.TimeOfDay;
|
||||
if (from <= to) return (Days & DayBit(now.DayOfWeek)) != 0 && t >= from && t < to;
|
||||
// über Mitternacht: der Tag zählt, an dem der Zeitraum beginnt
|
||||
return t >= from ? (Days & DayBit(now.DayOfWeek)) != 0 : t < to && (Days & DayBit(now.AddDays(-1).DayOfWeek)) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Eine Desktop-Box mit Position, Tabs und Aussehen.</summary>
|
||||
public class FenceDef
|
||||
{
|
||||
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
||||
public List<FenceTab> Tabs { get; set; } = [];
|
||||
public int ActiveTab { get; set; }
|
||||
public double? X { get; set; }
|
||||
public double? Y { get; set; }
|
||||
public double W { get; set; } = 340;
|
||||
public double H { get; set; } = 260;
|
||||
public bool Collapsed { get; set; }
|
||||
/// <summary>Gesperrt: nicht verschieb- oder vergrößerbar.</summary>
|
||||
public bool Locked { get; set; }
|
||||
/// <summary>Hintergrundfarbe (#RRGGBB) oder null = passend zum Design.</summary>
|
||||
public string? Background { get; set; }
|
||||
/// <summary>Deckkraft des Hintergrunds 0.2–1.</summary>
|
||||
public double Opacity { get; set; } = 0.92;
|
||||
/// <summary>Milchglas-Effekt (Hintergrund verschwommen).</summary>
|
||||
public bool Blur { get; set; }
|
||||
public ViewMode IconSize { get; set; } = ViewMode.Medium;
|
||||
/// <summary>"Name", "Typ", "Datum", "Häufig" (meistgenutzt zuerst) oder "Eigene" (per Ziehen angeordnet)</summary>
|
||||
public string Sort { get; set; } = "Name";
|
||||
/// <summary>Icon-Tönung (#RRGGBB) oder null.</summary>
|
||||
public string? Tint { get; set; }
|
||||
|
||||
/// <summary>Desktop-Seite, auf der die Box liegt (0 = erste Seite).</summary>
|
||||
public int Page { get; set; }
|
||||
|
||||
[JsonIgnore] public FenceTab? Active => Tabs.Count == 0 ? null : Tabs[Math.Clamp(ActiveTab, 0, Tabs.Count - 1)];
|
||||
}
|
||||
|
||||
/// <summary>Gespeicherte Anordnung aller Desktop-Boxen.</summary>
|
||||
public class FenceSnapshot
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public DateTime Created { get; set; } = DateTime.Now;
|
||||
public List<FenceDef> Fences { get; set; } = [];
|
||||
[JsonIgnore] public string Info => $"{Fences.Count} {(Fences.Count == 1 ? "Box" : "Boxen")} · gespeichert am {Created:dd.MM.yyyy HH:mm}";
|
||||
}
|
||||
|
||||
/// <summary>Alle Einstellungen, gespeichert in %APPDATA%\DeskCore\settings.json.</summary>
|
||||
public class AppSettings
|
||||
{
|
||||
public DesignMode Design { get; set; } = DesignMode.System;
|
||||
public ViewMode View { get; set; } = ViewMode.Medium;
|
||||
|
||||
public bool TrayEnabled { get; set; } = true;
|
||||
public bool CloseToTray { get; set; } = true;
|
||||
public bool TrayHintShown { get; set; }
|
||||
|
||||
public bool HotkeyEnabled { get; set; } = true;
|
||||
public int HotkeyModifiers { get; set; } = Hotkey.ModWin | Hotkey.ModShift;
|
||||
public int HotkeyKey { get; set; } = 0x44; // D
|
||||
|
||||
public bool AutoSort { get; set; }
|
||||
public bool AutoSortRemoveFromDesktop { get; set; }
|
||||
public List<SortRule> Rules { get; set; } = [];
|
||||
|
||||
public bool FencesEnabled { get; set; } = true;
|
||||
public List<FenceDef> Fences { get; set; } = [];
|
||||
public List<FenceSnapshot> Snapshots { get; set; } = [];
|
||||
/// <summary>Box-Positionen je Bildschirm-Anordnung (Signatur → Box-Id → x, y, w, h).</summary>
|
||||
public Dictionary<string, Dictionary<string, double[]>> ScreenLayouts { get; set; } = [];
|
||||
/// <summary>Chamäleon: Boxen blenden sich aus, bis die Maus darüberfährt.</summary>
|
||||
public bool FenceChameleon { get; set; }
|
||||
/// <summary>Doppelklick auf eine freie Desktop-Stelle blendet Icons und Boxen aus/ein.</summary>
|
||||
public bool QuickHide { get; set; } = true;
|
||||
/// <summary>Peek: Tastenkürzel holt alle Boxen vor die Fenster.</summary>
|
||||
public bool PeekEnabled { get; set; } = true;
|
||||
public int PeekModifiers { get; set; } = Hotkey.ModWin | Hotkey.ModShift;
|
||||
public int PeekKey { get; set; } = 0x42; // B wie "Boxen"
|
||||
|
||||
/// <summary>Desktop-Seiten: jede Seite hat ihre eigenen Boxen.</summary>
|
||||
public List<string> Pages { get; set; } = ["Seite 1"];
|
||||
public int CurrentPage { get; set; }
|
||||
/// <summary>Mausrad über einer freien Desktop-Stelle wechselt die Seite.</summary>
|
||||
public bool PageWheel { get; set; } = true;
|
||||
/// <summary>Zeitplan und Hintergrundbild je Seite (Schlüssel = Seitenname).</summary>
|
||||
public Dictionary<string, PageOption> PageOptions { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
/// <summary>Seite, die außerhalb aller Zeitpläne angezeigt wird (null = keine).</summary>
|
||||
public string? PageFallback { get; set; }
|
||||
/// <summary>Hintergrundbild, das vor dem ersten Seiten-Hintergrund eingestellt war (zum Zurückholen).</summary>
|
||||
public string? SavedWallpaper { get; set; }
|
||||
|
||||
/// <summary>Schnellstart-Suche per Tastenkürzel.</summary>
|
||||
public bool QuickSearchEnabled { get; set; } = true;
|
||||
public int QuickSearchModifiers { get; set; } = Hotkey.ModAlt | Hotkey.ModShift;
|
||||
public int QuickSearchKey { get; set; } = 0x20; // Leertaste
|
||||
/// <summary>Programme aus dem Windows-Startmenü mit durchsuchen.</summary>
|
||||
public bool QuickSearchStartMenu { get; set; } = true;
|
||||
|
||||
/// <summary>Sortierung im Hauptfenster: "Name" oder "Häufig".</summary>
|
||||
public string MainSort { get; set; } = "Name";
|
||||
|
||||
/// <summary>Rechte Maustaste auf freier Desktop-Fläche ziehen → neue Box.</summary>
|
||||
public bool DrawFences { get; set; } = true;
|
||||
|
||||
/// <summary>Tönung der echten Desktop-Icons (#RRGGBB) oder null = aus.</summary>
|
||||
public string? DesktopTint { get; set; }
|
||||
public double DesktopTintStrength { get; set; } = 0.55;
|
||||
|
||||
/// <summary>Verknüpfungen zu deinstallierten Programmen automatisch entfernen.</summary>
|
||||
public bool RemoveDeadShortcuts { get; set; } = true;
|
||||
/// <summary>Tote Verknüpfungen, die der Nutzer per Rückgängig behalten wollte.</summary>
|
||||
public List<string> KeptShortcuts { get; set; } = [];
|
||||
|
||||
// Fenster merken (WPF-Einheiten)
|
||||
public double? WindowLeft { get; set; }
|
||||
public double? WindowTop { get; set; }
|
||||
public double? WindowWidth { get; set; }
|
||||
public double? WindowHeight { get; set; }
|
||||
public bool WindowMaximized { get; set; }
|
||||
|
||||
// Updates
|
||||
public bool AutoUpdateCheck { get; set; } = true;
|
||||
public DateTime? LastUpdateCheck { get; set; }
|
||||
public string? SkippedVersion { get; set; }
|
||||
|
||||
public List<string> CategoryOrder { get; set; } = [];
|
||||
public List<LaunchGroup> LaunchGroups { get; set; } = [];
|
||||
public Dictionary<string, CategoryMeta> Categories { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// ---------- Laden/Speichern ----------
|
||||
|
||||
public static readonly string Folder = Migration.Pick(Environment.SpecialFolder.ApplicationData);
|
||||
private static readonly string FilePath = Path.Combine(Folder, "settings.json");
|
||||
|
||||
public static readonly JsonSerializerOptions Json = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
public static AppSettings Current { get; private set; } = Load();
|
||||
|
||||
public static event Action? Saved;
|
||||
|
||||
private static AppSettings Load()
|
||||
{
|
||||
AppSettings s;
|
||||
try
|
||||
{
|
||||
s = File.Exists(FilePath)
|
||||
? JsonSerializer.Deserialize<AppSettings>(File.ReadAllText(FilePath), Json) ?? new()
|
||||
: new();
|
||||
}
|
||||
catch { s = new(); }
|
||||
|
||||
s.Categories = new(s.Categories ?? [], StringComparer.OrdinalIgnoreCase);
|
||||
s.Rules ??= [];
|
||||
s.CategoryOrder ??= [];
|
||||
s.KeptShortcuts ??= [];
|
||||
s.LaunchGroups ??= [];
|
||||
s.Fences ??= [];
|
||||
s.Snapshots ??= [];
|
||||
s.ScreenLayouts ??= [];
|
||||
s.PageOptions = new(s.PageOptions ?? [], StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var t in s.Fences.SelectMany(f => f.Tabs)) t.Order ??= [];
|
||||
if (s.Pages is null || s.Pages.Count == 0) s.Pages = ["Seite 1"];
|
||||
s.CurrentPage = Math.Clamp(s.CurrentPage, 0, s.Pages.Count - 1);
|
||||
|
||||
// Umstieg von 1.0.x: Box gehörte fest zu einer Kategorie → eigenständige Box mit einem Tab
|
||||
foreach (var (name, meta) in s.Categories)
|
||||
{
|
||||
if (!meta.Fence) continue;
|
||||
s.Fences.Add(new FenceDef
|
||||
{
|
||||
Tabs = [new FenceTab { Value = name }],
|
||||
X = meta.FenceX, Y = meta.FenceY, W = meta.FenceW, H = meta.FenceH, Collapsed = meta.FenceCollapsed,
|
||||
});
|
||||
meta.Fence = false;
|
||||
}
|
||||
|
||||
// Übernahme der alten Einstellung aus Version 2
|
||||
var old = Path.Combine(Folder, "design.txt");
|
||||
if (File.Exists(old))
|
||||
{
|
||||
try
|
||||
{
|
||||
var v = File.ReadAllText(old).Trim();
|
||||
s.Design = v == "Light" ? DesignMode.Light : v == "Dark" ? DesignMode.Dark : DesignMode.System;
|
||||
File.Delete(old);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(Folder);
|
||||
File.WriteAllText(FilePath, JsonSerializer.Serialize(this, Json));
|
||||
}
|
||||
catch { }
|
||||
Saved?.Invoke();
|
||||
}
|
||||
|
||||
public CategoryMeta Meta(string category)
|
||||
{
|
||||
if (!Categories.TryGetValue(category, out var m))
|
||||
Categories[category] = m = new CategoryMeta();
|
||||
return m;
|
||||
}
|
||||
|
||||
public void RenameCategory(string oldName, string newName)
|
||||
{
|
||||
if (Categories.Remove(oldName, out var m)) Categories[newName] = m;
|
||||
for (int i = 0; i < CategoryOrder.Count; i++)
|
||||
if (string.Equals(CategoryOrder[i], oldName, StringComparison.OrdinalIgnoreCase)) CategoryOrder[i] = newName;
|
||||
foreach (var r in Rules)
|
||||
if (string.Equals(r.Category, oldName, StringComparison.OrdinalIgnoreCase)) r.Category = newName;
|
||||
foreach (var t in Fences.SelectMany(f => f.Tabs))
|
||||
if (!t.IsFolder && string.Equals(t.Value, oldName, StringComparison.OrdinalIgnoreCase)) t.Value = newName;
|
||||
// Start-Gruppen zeigen auf Dateien in der Kategorie → Pfade mit umbenennen
|
||||
var oldDir = Storage.CategoryPath(oldName).TrimEnd('\\') + "\\";
|
||||
var newDir = Storage.CategoryPath(newName).TrimEnd('\\') + "\\";
|
||||
foreach (var g in LaunchGroups)
|
||||
for (int i = 0; i < g.Items.Count; i++)
|
||||
if (g.Items[i].StartsWith(oldDir, StringComparison.OrdinalIgnoreCase))
|
||||
g.Items[i] = newDir + g.Items[i][oldDir.Length..];
|
||||
}
|
||||
|
||||
/// <summary>Liegt diese Kategorie auf der gerade angezeigten Desktop-Seite in einer Box?</summary>
|
||||
public bool HasFence(string category) =>
|
||||
Fences.Any(f => f.Page == CurrentPage &&
|
||||
f.Tabs.Any(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
/// <summary>Kategorie auf der aktuellen Seite als eigene Box anzeigen bzw. von dieser Seite entfernen (andere Seiten bleiben).</summary>
|
||||
public void SetFence(string category, bool on)
|
||||
{
|
||||
if (on)
|
||||
{
|
||||
if (!HasFence(category)) Fences.Add(new FenceDef { Page = CurrentPage, Tabs = [new FenceTab { Value = category }] });
|
||||
FencesEnabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var f in Fences.Where(f => f.Page == CurrentPage))
|
||||
f.Tabs.RemoveAll(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
|
||||
Fences.RemoveAll(f => f.Tabs.Count == 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Tiefe Kopie (für Snapshots).</summary>
|
||||
public static List<FenceDef> CloneFences(List<FenceDef> fences) =>
|
||||
JsonSerializer.Deserialize<List<FenceDef>>(JsonSerializer.Serialize(fences, Json), Json) ?? [];
|
||||
|
||||
/// <summary>Datei wurde umbenannt/verschoben → Start-Gruppen zeigen auf den neuen Pfad.</summary>
|
||||
public void ReplaceLaunchPath(string oldPath, string newPath)
|
||||
{
|
||||
bool changed = false;
|
||||
foreach (var g in LaunchGroups)
|
||||
for (int i = 0; i < g.Items.Count; i++)
|
||||
if (string.Equals(g.Items[i], oldPath, StringComparison.OrdinalIgnoreCase)) { g.Items[i] = newPath; changed = true; }
|
||||
// Umbenannt im selben Ordner → Platz in der eigenen Box-Reihenfolge behalten
|
||||
if (Storage.SamePath(Path.GetDirectoryName(oldPath), Path.GetDirectoryName(newPath)))
|
||||
{
|
||||
string oldName = Path.GetFileName(oldPath), newName = Path.GetFileName(newPath);
|
||||
foreach (var t in Fences.SelectMany(f => f.Tabs))
|
||||
for (int i = 0; i < t.Order.Count; i++)
|
||||
if (string.Equals(t.Order[i], oldName, StringComparison.OrdinalIgnoreCase)) { t.Order[i] = newName; changed = true; }
|
||||
}
|
||||
Usage.Rename(oldPath, newPath);
|
||||
if (changed) Save();
|
||||
}
|
||||
|
||||
// ---------- Desktop-Seiten ----------
|
||||
|
||||
public PageOption PageOption(string page)
|
||||
{
|
||||
if (!PageOptions.TryGetValue(page, out var o)) PageOptions[page] = o = new PageOption();
|
||||
return o;
|
||||
}
|
||||
|
||||
public void RenamePage(int index, string newName)
|
||||
{
|
||||
var old = Pages[index];
|
||||
Pages[index] = newName;
|
||||
if (PageOptions.Remove(old, out var o)) PageOptions[newName] = o;
|
||||
if (string.Equals(PageFallback, old, StringComparison.OrdinalIgnoreCase)) PageFallback = newName;
|
||||
}
|
||||
|
||||
/// <summary>Aufräumen nach dem Löschen einer Seite (Boxen verschiebt der FenceManager).</summary>
|
||||
public void ForgetPage(string name)
|
||||
{
|
||||
PageOptions.Remove(name);
|
||||
if (string.Equals(PageFallback, name, StringComparison.OrdinalIgnoreCase)) PageFallback = null;
|
||||
}
|
||||
|
||||
public void RemoveCategory(string name)
|
||||
{
|
||||
foreach (var f in Fences)
|
||||
f.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, name, StringComparison.OrdinalIgnoreCase));
|
||||
Fences.RemoveAll(f => f.Tabs.Count == 0);
|
||||
Categories.Remove(name);
|
||||
CategoryOrder.RemoveAll(n => string.Equals(n, name, StringComparison.OrdinalIgnoreCase));
|
||||
Rules.RemoveAll(r => string.Equals(r.Category, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
+77
-77
@@ -1,77 +1,77 @@
|
||||
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));
|
||||
bool launch = e.Args.Any(a => a.Equals("/launch", StringComparison.OrdinalIgnoreCase));
|
||||
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(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
|
||||
{
|
||||
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
|
||||
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
|
||||
autostart: Installer.AutostartEnabled, progress, installRuntime: true);
|
||||
// nach einem Update aus DeskCore heraus: gleich wieder starten
|
||||
if (launch) System.Diagnostics.Process.Start(System.IO.Path.Combine(dir, Installer.ExeName));
|
||||
}
|
||||
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));
|
||||
}
|
||||
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));
|
||||
bool launch = e.Args.Any(a => a.Equals("/launch", StringComparison.OrdinalIgnoreCase));
|
||||
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(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
|
||||
{
|
||||
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
|
||||
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
|
||||
autostart: Installer.AutostartEnabled, progress, installRuntime: true);
|
||||
// nach einem Update aus DeskCore heraus: gleich wieder starten
|
||||
if (launch) Installer.StartUnelevated(System.IO.Path.Combine(dir, Installer.ExeName));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
<AssemblyName>DeskCore-Setup</AssemblyName>
|
||||
<RootNamespace>DeskCoreSetup</RootNamespace>
|
||||
<ApplicationIcon>..\app.ico</ApplicationIcon>
|
||||
<!-- Installer fragt beim Start einmal nach Administratorrechten (für die .NET-Laufzeit von Microsoft) -->
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<Product>DeskCore Setup</Product>
|
||||
<AssemblyTitle>DeskCore Setup</AssemblyTitle>
|
||||
<Description>Installiert DeskCore</Description>
|
||||
|
||||
+401
-388
@@ -1,388 +1,401 @@
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pfad dieses Setups (auch als Deinstallationsprogramm).</summary>
|
||||
public static string SelfPath => System.Reflection.Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
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,
|
||||
bool installRuntime = false)
|
||||
{
|
||||
if (!Runtime.Is64BitWindows)
|
||||
throw new InvalidOperationException("DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
|
||||
if (installRuntime && !Runtime.IsInstalled)
|
||||
{
|
||||
// 0–60 % für die Laufzeit, Rest für DeskCore
|
||||
Runtime.Install((v, text) => progress.Report((v * 0.6, text)));
|
||||
var inner = progress;
|
||||
progress = new Scaled(inner, 0.6, 0.4);
|
||||
}
|
||||
|
||||
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 = SelfPath;
|
||||
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();
|
||||
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.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
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 = SelfPath;
|
||||
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>Rechnet Fortschritt 0–1 in einen Teilbereich um.</summary>
|
||||
private sealed class Scaled(IProgress<(double, string)> inner, double offset, double factor) : IProgress<(double, string)>
|
||||
{
|
||||
public void Report((double, string) value) => inner.Report((offset + value.Item1 * factor, value.Item2));
|
||||
}
|
||||
|
||||
/// <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();
|
||||
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",
|
||||
};
|
||||
}
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace DeskCoreSetup;
|
||||
|
||||
/// <summary>
|
||||
/// Installation/Deinstallation für den aktuellen Benutzer. Der Installer selbst läuft mit Administratorrechten
|
||||
/// (wegen der .NET-Laufzeit von Microsoft), DeskCore wird aber immer ohne gestartet.
|
||||
/// </summary>
|
||||
public static class Installer
|
||||
{
|
||||
/// <summary>
|
||||
/// DeskCore ohne Administratorrechte starten – über den Explorer, der als normaler Benutzer läuft.
|
||||
/// Wichtig: Ein Programm mit Adminrechten darf von Windows aus keine Icons per Ziehen vom Desktop annehmen.
|
||||
/// </summary>
|
||||
public static void StartUnelevated(string exe)
|
||||
{
|
||||
try { Process.Start(new ProcessStartInfo("explorer.exe", $"\"{exe}\"") { UseShellExecute = false }); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Pfad dieses Setups (auch als Deinstallationsprogramm).</summary>
|
||||
public static string SelfPath => System.Reflection.Assembly.GetExecutingAssembly().Location;
|
||||
|
||||
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,
|
||||
bool installRuntime = false)
|
||||
{
|
||||
if (!Runtime.Is64BitWindows)
|
||||
throw new InvalidOperationException("DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
|
||||
if (installRuntime && !Runtime.IsInstalled)
|
||||
{
|
||||
// 0–60 % für die Laufzeit, Rest für DeskCore
|
||||
Runtime.Install((v, text) => progress.Report((v * 0.6, text)));
|
||||
var inner = progress;
|
||||
progress = new Scaled(inner, 0.6, 0.4);
|
||||
}
|
||||
|
||||
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 = SelfPath;
|
||||
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();
|
||||
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.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
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 = SelfPath;
|
||||
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>Rechnet Fortschritt 0–1 in einen Teilbereich um.</summary>
|
||||
private sealed class Scaled(IProgress<(double, string)> inner, double offset, double factor) : IProgress<(double, string)>
|
||||
{
|
||||
public void Report((double, string) value) => inner.Report((offset + value.Item1 * factor, value.Item2));
|
||||
}
|
||||
|
||||
/// <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();
|
||||
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",
|
||||
};
|
||||
}
|
||||
|
||||
+183
-183
@@ -1,183 +1,183 @@
|
||||
<Window x:Class="DeskCoreSetup.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="DeskCore Setup" Icon="app.ico" Width="720" Height="500" ResizeMode="CanMinimize"
|
||||
Background="{DynamicResource BaseBrush}" Foreground="{DynamicResource TextBrush}"
|
||||
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
|
||||
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
<Window.Resources>
|
||||
<Style x:Key="StepText" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="0,0,0,14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextTertiaryBrush}"/>
|
||||
</Style>
|
||||
<Style x:Key="PageTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
|
||||
<Setter Property="FontSize" Value="24"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
</Style>
|
||||
<Style x:Key="Body" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="LineHeight" Value="21"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="36"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="220"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Titelleiste -->
|
||||
<Grid Grid.ColumnSpan="2">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock x:Name="WindowTitle" Text="DeskCore Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="" Click="Cancel_Click"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Seitenleiste mit Schritten -->
|
||||
<StackPanel Grid.Row="1" Grid.RowSpan="2" Margin="28,18,20,20">
|
||||
<Image Source="app.png" Width="72" Height="72" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock Text="DeskCore" FontFamily="{DynamicResource DisplayFont}" FontSize="22" FontWeight="SemiBold" Margin="0,14,0,0"/>
|
||||
<TextBlock x:Name="VersionText" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Margin="0,2,0,28"/>
|
||||
<StackPanel x:Name="Steps">
|
||||
<TextBlock x:Name="Step0" Text="Willkommen" Style="{StaticResource StepText}"/>
|
||||
<TextBlock x:Name="Step1" Text="Lizenz" Style="{StaticResource StepText}"/>
|
||||
<TextBlock x:Name="Step2" Text="Optionen" Style="{StaticResource StepText}"/>
|
||||
<TextBlock x:Name="Step3" Text="Installation" Style="{StaticResource StepText}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource LayerStrokeBrush}"
|
||||
BorderThickness="1,1,0,1" CornerRadius="8,0,0,8" Padding="32,28,32,20">
|
||||
<Grid>
|
||||
<!-- 0: Willkommen -->
|
||||
<StackPanel x:Name="PageWelcome">
|
||||
<TextBlock Text="Willkommen bei DeskCore" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Style="{StaticResource Body}"
|
||||
Text="DeskCore sortiert deine Desktop-Icons in eigene Kategorien – mit Suche, Desktop-Boxen, automatischem Einsortieren und Rückgängig-Funktion."/>
|
||||
<TextBlock Style="{StaticResource Body}" Margin="0,14,0,0"
|
||||
Text="Der Assistent installiert DeskCore nur für dein Benutzerkonto. Administratorrechte sind nicht nötig."/>
|
||||
<Border x:Name="RuntimeHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||
<TextBlock x:Name="RuntimeHintText" TextWrapping="Wrap" LineHeight="20"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border x:Name="UpdateHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 1: Lizenz -->
|
||||
<DockPanel x:Name="PageLicense" Visibility="Collapsed">
|
||||
<TextBlock DockPanel.Dock="Top" Text="Lizenz- und Nutzungsbedingungen" Style="{StaticResource PageTitle}"/>
|
||||
<CheckBox x:Name="AcceptLicense" DockPanel.Dock="Bottom" Margin="0,14,0,0" Click="AcceptLicense_Click"
|
||||
Content="Ich akzeptiere die Lizenz- und Nutzungsbedingungen"/>
|
||||
<Border Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}"
|
||||
BorderThickness="1" CornerRadius="6">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock x:Name="LicenseText" TextWrapping="Wrap" FontSize="12.5" LineHeight="18" Margin="14,10"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 2: Optionen -->
|
||||
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
|
||||
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,4,0,8"/>
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
|
||||
<TextBox x:Name="DirBox"/>
|
||||
</DockPanel>
|
||||
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" Margin="0,24,0,0" IsChecked="True"/>
|
||||
<CheckBox x:Name="StartMenuBox" Content="Eintrag im Startmenü" Margin="0,12,0,0" IsChecked="True" IsEnabled="False"/>
|
||||
<CheckBox x:Name="AutostartBox" Content="Mit Windows starten (unauffällig im Infobereich)" Margin="0,12,0,0"/>
|
||||
<CheckBox x:Name="RuntimeBox" Margin="0,12,0,0" IsChecked="True" Visibility="Collapsed"
|
||||
Content="Microsoft .NET 9 Desktop Runtime automatisch installieren (benötigt, ca. 55 MB)"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 3: Fortschritt / Fertig -->
|
||||
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
|
||||
<TextBlock x:Name="ProgressTitle" Text="DeskCore wird installiert …" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Text="Einen Moment bitte."/>
|
||||
<Border x:Name="ProgressTrack" Height="4" CornerRadius="2" Margin="0,22,0,0" Background="{DynamicResource SubtleSelectedBrush}">
|
||||
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="2" Background="{DynamicResource AccentBrush}"/>
|
||||
</Border>
|
||||
<StackPanel x:Name="DonePanel" Visibility="Collapsed" Margin="0,26,0,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border Width="28" Height="28" CornerRadius="14" Background="{DynamicResource SuccessBrush}">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="13" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="DoneText" VerticalAlignment="Center" Margin="12,0,0,0" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="RuntimeMissing" Margin="0,18,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||
<StackPanel>
|
||||
<TextBlock TextWrapping="Wrap" LineHeight="20"
|
||||
Text="Damit DeskCore startet, wird noch die kostenlose „Microsoft .NET 9 Desktop Runtime“ benötigt."/>
|
||||
<Button Content="Download-Seite von Microsoft öffnen" HorizontalAlignment="Left" Margin="0,10,0,0" Click="OpenRuntimePage_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<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>
|
||||
<Window x:Class="DeskCoreSetup.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="DeskCore Setup" Icon="app.ico" Width="720" Height="500" ResizeMode="CanMinimize"
|
||||
Background="{DynamicResource BaseBrush}" Foreground="{DynamicResource TextBrush}"
|
||||
FontFamily="{DynamicResource UiFont}" FontSize="14"
|
||||
TextOptions.TextFormattingMode="Display" UseLayoutRounding="True"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
|
||||
<WindowChrome.WindowChrome>
|
||||
<WindowChrome CaptionHeight="36" ResizeBorderThickness="0" GlassFrameThickness="0,0,0,1"
|
||||
CornerRadius="0" UseAeroCaptionButtons="False"/>
|
||||
</WindowChrome.WindowChrome>
|
||||
|
||||
<Window.Resources>
|
||||
<Style x:Key="StepText" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="0,0,0,14"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextTertiaryBrush}"/>
|
||||
</Style>
|
||||
<Style x:Key="PageTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource DisplayFont}"/>
|
||||
<Setter Property="FontSize" Value="24"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Margin" Value="0,0,0,12"/>
|
||||
</Style>
|
||||
<Style x:Key="Body" TargetType="TextBlock">
|
||||
<Setter Property="TextWrapping" Value="Wrap"/>
|
||||
<Setter Property="LineHeight" Value="21"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource TextSecondaryBrush}"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="36"/>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="220"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Titelleiste -->
|
||||
<Grid Grid.ColumnSpan="2">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="14,0,0,0">
|
||||
<Image Source="app.ico" Width="16" Height="16" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock x:Name="WindowTitle" Text="DeskCore Setup" FontSize="12" Margin="10,0,0,0" VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Top">
|
||||
<Button Style="{StaticResource CaptionButton}" Content="" Click="Minimize_Click"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||
<Button x:Name="CloseButton" Style="{StaticResource CloseCaptionButton}" Content="" Click="Cancel_Click"
|
||||
WindowChrome.IsHitTestVisibleInChrome="True"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Seitenleiste mit Schritten -->
|
||||
<StackPanel Grid.Row="1" Grid.RowSpan="2" Margin="28,18,20,20">
|
||||
<Image Source="app.png" Width="72" Height="72" HorizontalAlignment="Left" RenderOptions.BitmapScalingMode="HighQuality"/>
|
||||
<TextBlock Text="DeskCore" FontFamily="{DynamicResource DisplayFont}" FontSize="22" FontWeight="SemiBold" Margin="0,14,0,0"/>
|
||||
<TextBlock x:Name="VersionText" Foreground="{DynamicResource TextSecondaryBrush}" FontSize="12" Margin="0,2,0,28"/>
|
||||
<StackPanel x:Name="Steps">
|
||||
<TextBlock x:Name="Step0" Text="Willkommen" Style="{StaticResource StepText}"/>
|
||||
<TextBlock x:Name="Step1" Text="Lizenz" Style="{StaticResource StepText}"/>
|
||||
<TextBlock x:Name="Step2" Text="Optionen" Style="{StaticResource StepText}"/>
|
||||
<TextBlock x:Name="Step3" Text="Installation" Style="{StaticResource StepText}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Inhalt -->
|
||||
<Border Grid.Row="1" Grid.Column="1" Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource LayerStrokeBrush}"
|
||||
BorderThickness="1,1,0,1" CornerRadius="8,0,0,8" Padding="32,28,32,20">
|
||||
<Grid>
|
||||
<!-- 0: Willkommen -->
|
||||
<StackPanel x:Name="PageWelcome">
|
||||
<TextBlock Text="Willkommen bei DeskCore" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Style="{StaticResource Body}"
|
||||
Text="DeskCore sortiert deine Desktop-Icons in eigene Kategorien – mit Suche, Desktop-Boxen, automatischem Einsortieren und Rückgängig-Funktion."/>
|
||||
<TextBlock Style="{StaticResource Body}" Margin="0,14,0,0"
|
||||
Text="Der Assistent installiert DeskCore für dein Benutzerkonto und – falls nötig – die benötigte Laufzeit von Microsoft. DeskCore selbst läuft danach ganz normal ohne Administratorrechte."/>
|
||||
<Border x:Name="RuntimeHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||
<TextBlock x:Name="RuntimeHintText" TextWrapping="Wrap" LineHeight="20"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
<Border x:Name="UpdateHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
|
||||
<TextBlock x:Name="UpdateHintText" TextWrapping="Wrap" LineHeight="20"/>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 1: Lizenz -->
|
||||
<DockPanel x:Name="PageLicense" Visibility="Collapsed">
|
||||
<TextBlock DockPanel.Dock="Top" Text="Lizenz- und Nutzungsbedingungen" Style="{StaticResource PageTitle}"/>
|
||||
<CheckBox x:Name="AcceptLicense" DockPanel.Dock="Bottom" Margin="0,14,0,0" Click="AcceptLicense_Click"
|
||||
Content="Ich akzeptiere die Lizenz- und Nutzungsbedingungen"/>
|
||||
<Border Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}"
|
||||
BorderThickness="1" CornerRadius="6">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock x:Name="LicenseText" TextWrapping="Wrap" FontSize="12.5" LineHeight="18" Margin="14,10"/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</DockPanel>
|
||||
|
||||
<!-- 2: Optionen -->
|
||||
<StackPanel x:Name="PageOptions" Visibility="Collapsed">
|
||||
<TextBlock Text="Optionen" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock Text="Installationsordner" FontWeight="SemiBold" FontSize="13" Margin="0,4,0,8"/>
|
||||
<DockPanel>
|
||||
<Button DockPanel.Dock="Right" Content="Ändern …" Margin="8,0,0,0" Click="ChangeDir_Click"/>
|
||||
<TextBox x:Name="DirBox"/>
|
||||
</DockPanel>
|
||||
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" Margin="0,24,0,0" IsChecked="True"/>
|
||||
<CheckBox x:Name="StartMenuBox" Content="Eintrag im Startmenü" Margin="0,12,0,0" IsChecked="True" IsEnabled="False"/>
|
||||
<CheckBox x:Name="AutostartBox" Content="Mit Windows starten (unauffällig im Infobereich)" Margin="0,12,0,0"/>
|
||||
<CheckBox x:Name="RuntimeBox" Margin="0,12,0,0" IsChecked="True" Visibility="Collapsed"
|
||||
Content="Microsoft .NET 9 Desktop Runtime automatisch installieren (benötigt, ca. 55 MB)"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 3: Fortschritt / Fertig -->
|
||||
<StackPanel x:Name="PageProgress" Visibility="Collapsed">
|
||||
<TextBlock x:Name="ProgressTitle" Text="DeskCore wird installiert …" Style="{StaticResource PageTitle}"/>
|
||||
<TextBlock x:Name="ProgressText" Style="{StaticResource Body}" Text="Einen Moment bitte."/>
|
||||
<Border x:Name="ProgressTrack" Height="4" CornerRadius="2" Margin="0,22,0,0" Background="{DynamicResource SubtleSelectedBrush}">
|
||||
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="2" Background="{DynamicResource AccentBrush}"/>
|
||||
</Border>
|
||||
<StackPanel x:Name="DonePanel" Visibility="Collapsed" Margin="0,26,0,0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Border Width="28" Height="28" CornerRadius="14" Background="{DynamicResource SuccessBrush}">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="13" Foreground="White"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="DoneText" VerticalAlignment="Center" Margin="12,0,0,0" FontWeight="SemiBold"/>
|
||||
</StackPanel>
|
||||
<Border x:Name="RuntimeMissing" Margin="0,18,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
|
||||
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
|
||||
<StackPanel>
|
||||
<TextBlock TextWrapping="Wrap" LineHeight="20"
|
||||
Text="Damit DeskCore startet, wird noch die kostenlose „Microsoft .NET 9 Desktop Runtime“ benötigt."/>
|
||||
<Button Content="Download-Seite von Microsoft öffnen" HorizontalAlignment="Left" Margin="0,10,0,0" Click="OpenRuntimePage_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<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>
|
||||
|
||||
+279
-279
@@ -1,279 +1,279 @@
|
||||
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 (!Runtime.IsInstalled)
|
||||
{
|
||||
RuntimeHint.Visibility = Visibility.Visible;
|
||||
RuntimeBox.Visibility = Visibility.Visible;
|
||||
RuntimeHintText.Text = Runtime.Is64BitWindows
|
||||
? "DeskCore benötigt die kostenlose „Microsoft .NET 9 Desktop Runtime“. Sie fehlt auf diesem PC und wird bei der " +
|
||||
"Installation automatisch von Microsoft geladen (ca. 55 MB). Windows fragt dabei einmal nach Administratorrechten."
|
||||
: "DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11) und kann auf diesem PC leider nicht installiert werden.";
|
||||
}
|
||||
|
||||
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 (!IsFullPath(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)
|
||||
{
|
||||
using var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = "Installationsordner wählen",
|
||||
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
ShowNewFolderButton = true,
|
||||
};
|
||||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||
var chosen = dlg.SelectedPath;
|
||||
// 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;
|
||||
bool runtime = RuntimeBox.Visibility == Visibility.Visible && RuntimeBox.IsChecked == true;
|
||||
try
|
||||
{
|
||||
await Task.Run(() => Installer.Install(dir, desktop, autostart, progress, runtime));
|
||||
_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;
|
||||
if (!Runtime.IsInstalled)
|
||||
{
|
||||
// Laufzeit fehlt weiterhin (abgewählt) → Hinweis statt Startversuch
|
||||
DoneText.Text = "DeskCore ist installiert";
|
||||
RuntimeMissing.Visibility = Visibility.Visible;
|
||||
LaunchBox.IsChecked = false;
|
||||
LaunchBox.IsEnabled = false;
|
||||
}
|
||||
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 static bool IsFullPath(string p) =>
|
||||
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
|
||||
|
||||
private void OpenRuntimePage_Click(object sender, RoutedEventArgs e) =>
|
||||
Process.Start(new ProcessStartInfo(Runtime.DownloadPage) { UseShellExecute = true });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 (!Runtime.IsInstalled)
|
||||
{
|
||||
RuntimeHint.Visibility = Visibility.Visible;
|
||||
RuntimeBox.Visibility = Visibility.Visible;
|
||||
RuntimeHintText.Text = Runtime.Is64BitWindows
|
||||
? "DeskCore benötigt die kostenlose „Microsoft .NET 9 Desktop Runtime“. Sie fehlt auf diesem PC und wird bei der " +
|
||||
"Installation automatisch von Microsoft geladen (ca. 55 MB) und mitinstalliert."
|
||||
: "DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11) und kann auf diesem PC leider nicht installiert werden.";
|
||||
}
|
||||
|
||||
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 (!IsFullPath(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)
|
||||
Installer.StartUnelevated(_installedExe); // ohne Adminrechte, sonst klappt Ziehen vom Desktop nicht
|
||||
Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ChangeDir_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
using var dlg = new System.Windows.Forms.FolderBrowserDialog
|
||||
{
|
||||
Description = "Installationsordner wählen",
|
||||
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
ShowNewFolderButton = true,
|
||||
};
|
||||
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
||||
var chosen = dlg.SelectedPath;
|
||||
// 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;
|
||||
bool runtime = RuntimeBox.Visibility == Visibility.Visible && RuntimeBox.IsChecked == true;
|
||||
try
|
||||
{
|
||||
await Task.Run(() => Installer.Install(dir, desktop, autostart, progress, runtime));
|
||||
_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;
|
||||
if (!Runtime.IsInstalled)
|
||||
{
|
||||
// Laufzeit fehlt weiterhin (abgewählt) → Hinweis statt Startversuch
|
||||
DoneText.Text = "DeskCore ist installiert";
|
||||
RuntimeMissing.Visibility = Visibility.Visible;
|
||||
LaunchBox.IsChecked = false;
|
||||
LaunchBox.IsEnabled = false;
|
||||
}
|
||||
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 static bool IsFullPath(string p) =>
|
||||
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
|
||||
|
||||
private void OpenRuntimePage_Click(object sender, RoutedEventArgs e) =>
|
||||
Process.Start(new ProcessStartInfo(Runtime.DownloadPage) { UseShellExecute = true });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+20
-3
@@ -20,6 +20,19 @@ public static class Runtime
|
||||
|
||||
public static bool Is64BitWindows => Environment.Is64BitOperatingSystem;
|
||||
|
||||
public static bool IsElevated
|
||||
{
|
||||
get
|
||||
{
|
||||
try
|
||||
{
|
||||
using var id = System.Security.Principal.WindowsIdentity.GetCurrent();
|
||||
return new System.Security.Principal.WindowsPrincipal(id).IsInRole(System.Security.Principal.WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsInstalled
|
||||
{
|
||||
get
|
||||
@@ -43,7 +56,7 @@ public static class Runtime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Lädt die Laufzeit von Microsoft und installiert sie (Windows fragt einmal nach Administratorrechten).</summary>
|
||||
/// <summary>Lädt die Laufzeit von Microsoft und installiert sie (der Installer läuft bereits mit Administratorrechten).</summary>
|
||||
public static void Install(Action<double, string> progress)
|
||||
{
|
||||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||||
@@ -77,11 +90,15 @@ public static class Runtime
|
||||
throw new InvalidOperationException("Die heruntergeladene Datei ist nicht gültig von Microsoft signiert und wurde nicht ausgeführt.");
|
||||
}
|
||||
|
||||
progress(1, $"{DisplayName} wird installiert – bitte die Windows-Abfrage bestätigen …");
|
||||
progress(1, $"{DisplayName} wird installiert …");
|
||||
Process p;
|
||||
try
|
||||
{
|
||||
p = Process.Start(new ProcessStartInfo(file, "/install /quiet /norestart") { UseShellExecute = true, Verb = "runas" })!;
|
||||
// Der Installer hat schon Adminrechte – "runas" nur als Absicherung, falls er doch ohne gestartet wurde
|
||||
p = Process.Start(new ProcessStartInfo(file, "/install /quiet /norestart")
|
||||
{
|
||||
UseShellExecute = true, Verb = IsElevated ? "" : "runas", WindowStyle = ProcessWindowStyle.Hidden,
|
||||
})!;
|
||||
}
|
||||
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<assemblyIdentity version="1.0.0.0" name="DeskCore.Setup"/>
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<!-- Der Installer läuft mit Administratorrechten: so kann er die von Microsoft benötigte
|
||||
.NET-Laufzeit ohne zweite Nachfrage mitinstallieren. DeskCore selbst startet danach ohne Adminrechte. -->
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- Windows 10 und 11 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v9.0/win-x64",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v9.0": {},
|
||||
".NETCoreApp,Version=v9.0/win-x64": {
|
||||
"DeskCore-Setup/1.0.0": {
|
||||
"runtime": {
|
||||
"DeskCore-Setup.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DeskCore-Setup/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"runtimeOptions": {
|
||||
"tfm": "net9.0",
|
||||
"frameworks": [
|
||||
{
|
||||
"name": "Microsoft.NETCore.App",
|
||||
"version": "9.0.0"
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.WindowsDesktop.App",
|
||||
"version": "9.0.0"
|
||||
}
|
||||
],
|
||||
"configProperties": {
|
||||
"System.Reflection.Metadata.MetadataUpdater.IsSupported": false,
|
||||
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false,
|
||||
"CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"format": 1,
|
||||
"restore": {
|
||||
"C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj": {}
|
||||
},
|
||||
"projects": {
|
||||
"C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj": {
|
||||
"version": "1.0.4",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj",
|
||||
"projectName": "DeskCore-Setup",
|
||||
"projectPath": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj",
|
||||
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net48"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.100"
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"suppressParent": "All",
|
||||
"target": "Package",
|
||||
"version": "[1.0.3, )",
|
||||
"autoReferenced": true
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.112\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
|
||||
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
|
||||
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
|
||||
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">$(UserProfile)\.nuget\packages\</NuGetPackageRoot>
|
||||
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">C:\Users\Viper\.nuget\packages\</NuGetPackageFolders>
|
||||
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
|
||||
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<SourceRoot Include="C:\Users\Viper\.nuget\packages\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
|
||||
<Import Project="$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.netframework.referenceassemblies.net48\1.0.3\build\Microsoft.NETFramework.ReferenceAssemblies.net48.targets')" />
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
|
||||
Binary file not shown.
@@ -0,0 +1,77 @@
|
||||
#pragma checksum "..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "2F55484CCDC4F93150C83267DEA1BE82F9151116"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms.Integration;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace DeskCoreSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// App
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application {
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/DeskCore-Setup;component/app.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\App.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application Entry Point.
|
||||
/// </summary>
|
||||
[System.STAThreadAttribute()]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public static void Main() {
|
||||
DeskCoreSetup.App app = new DeskCoreSetup.App();
|
||||
app.InitializeComponent();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
</configuration>
|
||||
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
DeskCore-Setup
|
||||
|
||||
|
||||
winexe
|
||||
C#
|
||||
.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\
|
||||
DeskCoreSetup
|
||||
none
|
||||
false
|
||||
TRACE;RELEASE;NETFRAMEWORK;NET48;NET30_OR_GREATER;NET35_OR_GREATER;NET40_OR_GREATER;NET45_OR_GREATER;NET451_OR_GREATER;NET452_OR_GREATER;NET46_OR_GREATER;NET461_OR_GREATER;NET462_OR_GREATER;NET47_OR_GREATER;NET471_OR_GREATER;NET472_OR_GREATER;NET48_OR_GREATER
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\App.xaml
|
||||
4-1182040411
|
||||
|
||||
5-706554335
|
||||
241796289289
|
||||
MainWindow.xaml;..\Themes\Colors.Light.xaml;..\Themes\Colors.Dark.xaml;..\Themes\Controls.xaml;
|
||||
|
||||
False
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert DeskCore")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.4.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.4")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("DeskCore Setup")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("DeskCore Setup")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.4.0")]
|
||||
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||
|
||||
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
d2e033b6ce1399c33b5b36da4d426df28d32a528502994c6fd13241554523b24
|
||||
@@ -0,0 +1,14 @@
|
||||
is_global = true
|
||||
build_property.ApplicationManifest = app.manifest
|
||||
build_property.StartupObject =
|
||||
build_property.ApplicationDefaultFont =
|
||||
build_property.ApplicationHighDpiMode =
|
||||
build_property.ApplicationUseCompatibleTextRendering =
|
||||
build_property.ApplicationVisualStyles =
|
||||
build_property.RootNamespace = DeskCoreSetup
|
||||
build_property.ProjectDir = C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle =
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
@@ -0,0 +1,6 @@
|
||||
// <auto-generated/>
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
054b30b1baba2269cbcd4852a291a982fdd2874935a858bccfe0f353f3966d8b
|
||||
@@ -0,0 +1,17 @@
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\artifacts\setup\DeskCore-Setup.exe.config
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\artifacts\setup\DeskCore-Setup.exe
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCoreSetup.csproj.AssemblyReference.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\MainWindow.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\Colors.Light.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\Colors.Dark.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\Controls.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\App.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\MainWindow.g.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\App.g.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCore-Setup_MarkupCompile.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCore-Setup.g.resources
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCoreSetup.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCoreSetup.AssemblyInfoInputs.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCoreSetup.AssemblyInfo.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCoreSetup.csproj.CoreCompileInputs.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net48\DeskCore-Setup.exe
|
||||
Binary file not shown.
@@ -0,0 +1,556 @@
|
||||
#pragma checksum "..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "1A3CEC98E280296CC3F91E26F8B8AC24476C46EF"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Forms.Integration;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace DeskCoreSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MainWindow
|
||||
/// </summary>
|
||||
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||
|
||||
|
||||
#line 48 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 53 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CloseButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 62 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock VersionText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 63 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel Steps;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 64 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 65 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 66 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 67 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 76 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 82 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border RuntimeHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 87 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock RuntimeHintText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 90 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border UpdateHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 95 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 101 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.DockPanel PageLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 103 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox AcceptLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 108 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock LicenseText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 114 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageOptions;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 119 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBox DirBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 121 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 122 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox StartMenuBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 123 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox AutostartBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 124 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox RuntimeBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 129 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageProgress;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 130 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 131 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 132 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressTrack;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 133 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressFill;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 135 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel DonePanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 141 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DoneText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 143 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border RuntimeMissing;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 151 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 156 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 164 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DataInfo;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 165 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 167 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 178 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BackButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 179 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button NextButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 180 "..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CancelButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/DeskCore-Setup;component/mainwindow.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\MainWindow.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||
switch (connectionId)
|
||||
{
|
||||
case 1:
|
||||
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 2:
|
||||
|
||||
#line 51 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 3:
|
||||
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 53 "..\..\..\MainWindow.xaml"
|
||||
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 4:
|
||||
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 5:
|
||||
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 6:
|
||||
this.Step0 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 7:
|
||||
this.Step1 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 8:
|
||||
this.Step2 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 9:
|
||||
this.Step3 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 10:
|
||||
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 11:
|
||||
this.RuntimeHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 12:
|
||||
this.RuntimeHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 13:
|
||||
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 14:
|
||||
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 15:
|
||||
this.PageLicense = ((System.Windows.Controls.DockPanel)(target));
|
||||
return;
|
||||
case 16:
|
||||
this.AcceptLicense = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 103 "..\..\..\MainWindow.xaml"
|
||||
this.AcceptLicense.Click += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 17:
|
||||
this.LicenseText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 18:
|
||||
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 19:
|
||||
|
||||
#line 118 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 20:
|
||||
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||
return;
|
||||
case 21:
|
||||
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 22:
|
||||
this.StartMenuBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 23:
|
||||
this.AutostartBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 24:
|
||||
this.RuntimeBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 25:
|
||||
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 26:
|
||||
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 27:
|
||||
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 28:
|
||||
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 29:
|
||||
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 30:
|
||||
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 31:
|
||||
this.DoneText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 32:
|
||||
this.RuntimeMissing = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 33:
|
||||
|
||||
#line 148 "..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.OpenRuntimePage_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 34:
|
||||
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 35:
|
||||
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 36:
|
||||
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 37:
|
||||
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 166 "..\..\..\MainWindow.xaml"
|
||||
this.RemoveDataBox.Click += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 38:
|
||||
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 39:
|
||||
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 178 "..\..\..\MainWindow.xaml"
|
||||
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 40:
|
||||
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 179 "..\..\..\MainWindow.xaml"
|
||||
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 41:
|
||||
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 180 "..\..\..\MainWindow.xaml"
|
||||
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
}
|
||||
this._contentLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
// <autogenerated />
|
||||
using System;
|
||||
using System.Reflection;
|
||||
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v9.0", FrameworkDisplayName = ".NET 9.0")]
|
||||
Binary file not shown.
@@ -0,0 +1,76 @@
|
||||
#pragma checksum "..\..\..\..\App.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "2F55484CCDC4F93150C83267DEA1BE82F9151116"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace DeskCoreSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// App
|
||||
/// </summary>
|
||||
public partial class App : System.Windows.Application {
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/DeskCore-Setup;component/app.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\..\App.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Application Entry Point.
|
||||
/// </summary>
|
||||
[System.STAThreadAttribute()]
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public static void Main() {
|
||||
DeskCoreSetup.App app = new DeskCoreSetup.App();
|
||||
app.InitializeComponent();
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"runtimeTarget": {
|
||||
"name": ".NETCoreApp,Version=v9.0/win-x64",
|
||||
"signature": ""
|
||||
},
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v9.0": {},
|
||||
".NETCoreApp,Version=v9.0/win-x64": {
|
||||
"DeskCore-Setup/1.0.0": {
|
||||
"runtime": {
|
||||
"DeskCore-Setup.dll": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DeskCore-Setup/1.0.0": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
DeskCore-Setup
|
||||
|
||||
|
||||
winexe
|
||||
C#
|
||||
.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\
|
||||
DeskCoreSetup
|
||||
none
|
||||
false
|
||||
TRACE;RELEASE;NET;NET9_0;NETCOREAPP;WINDOWS;WINDOWS7_0;NET5_0_OR_GREATER;NET6_0_OR_GREATER;NET7_0_OR_GREATER;NET8_0_OR_GREATER;NET9_0_OR_GREATER;NETCOREAPP1_0_OR_GREATER;NETCOREAPP1_1_OR_GREATER;NETCOREAPP2_0_OR_GREATER;NETCOREAPP2_1_OR_GREATER;NETCOREAPP2_2_OR_GREATER;NETCOREAPP3_0_OR_GREATER;NETCOREAPP3_1_OR_GREATER;WINDOWS7_0_OR_GREATER
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\App.xaml
|
||||
4-1182040411
|
||||
|
||||
4647195960
|
||||
200-1670248436
|
||||
MainWindow.xaml;..\Themes\Colors.Light.xaml;..\Themes\Colors.Dark.xaml;..\Themes\Controls.xaml;
|
||||
|
||||
False
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("M_Viper")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyCopyrightAttribute("Copyright (c) M_Viper. Alle Rechte vorbehalten.")]
|
||||
[assembly: System.Reflection.AssemblyDescriptionAttribute("Installiert DeskCore")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("DeskCore Setup")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("DeskCore Setup")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]
|
||||
[assembly: System.Resources.NeutralResourcesLanguageAttribute("de-DE")]
|
||||
[assembly: System.Runtime.Versioning.TargetPlatformAttribute("Windows7.0")]
|
||||
[assembly: System.Runtime.Versioning.SupportedOSPlatformAttribute("Windows7.0")]
|
||||
|
||||
// Von der MSBuild WriteCodeFragment-Klasse generiert.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
aa9cb945fc0e24f2fe5d76e4d4c638a76152e98407d3ea2108dbe178b58b4abf
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
is_global = true
|
||||
build_property.EnableAotAnalyzer =
|
||||
build_property.EnableSingleFileAnalyzer = true
|
||||
build_property.EnableTrimAnalyzer =
|
||||
build_property.IncludeAllContentForSelfExtract =
|
||||
build_property.TargetFramework = net9.0-windows
|
||||
build_property.TargetFrameworkIdentifier = .NETCoreApp
|
||||
build_property.TargetFrameworkVersion = v9.0
|
||||
build_property.TargetPlatformMinVersion = 7.0
|
||||
build_property.UsingMicrosoftNETSdkWeb =
|
||||
build_property.ProjectTypeGuids =
|
||||
build_property.InvariantGlobalization =
|
||||
build_property.PlatformNeutralAssembly =
|
||||
build_property.EnforceExtendedAnalyzerRules =
|
||||
build_property._SupportedPlatformList = Linux,macOS,Windows
|
||||
build_property.RootNamespace = DeskCoreSetup
|
||||
build_property.ProjectDir = C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\
|
||||
build_property.EnableComHosting =
|
||||
build_property.EnableGeneratedComInterfaceComImportInterop =
|
||||
build_property.CsWinRTUseWindowsUIXamlProjections = false
|
||||
build_property.EffectiveAnalysisLevelStyle = 9.0
|
||||
build_property.EnableCodeStyleSeverity =
|
||||
@@ -0,0 +1,6 @@
|
||||
// <auto-generated/>
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Linq;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
2b200cd0f14b4e4eb4b4885f0e8b3c56c346fd55b4f74f792b3cdc502d3e8cbb
|
||||
@@ -0,0 +1,21 @@
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\bin\Release\net9.0-windows\win-x64\DeskCore-Setup.exe
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\bin\Release\net9.0-windows\win-x64\DeskCore-Setup.deps.json
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\bin\Release\net9.0-windows\win-x64\DeskCore-Setup.runtimeconfig.json
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\bin\Release\net9.0-windows\win-x64\DeskCore-Setup.dll
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\MainWindow.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\Colors.Light.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\Colors.Dark.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\Controls.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\App.baml
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\MainWindow.g.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\App.g.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCore-Setup_MarkupCompile.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCore-Setup.g.resources
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCoreSetup.GeneratedMSBuildEditorConfig.editorconfig
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCoreSetup.AssemblyInfoInputs.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCoreSetup.AssemblyInfo.cs
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCoreSetup.csproj.CoreCompileInputs.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCore-Setup.dll
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\refint\DeskCore-Setup.dll
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\DeskCoreSetup.genruntimeconfig.cache
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Source\Setup\obj\Release\net9.0-windows\win-x64\ref\DeskCore-Setup.dll
|
||||
@@ -0,0 +1 @@
|
||||
914b6a259056b8c0f6c3227ea7cdf35b8381c900fbf78428cfcb5446fdb60624
|
||||
@@ -0,0 +1 @@
|
||||
c41825f0c5957fff8272ec718c138cd0a31afc83a6d3eab90859164c0e911ccf
|
||||
@@ -0,0 +1 @@
|
||||
94e9cde7f02f6a1624bb951dc6d8d1cb1351d8ca9035e4dbef6cd3f5efea400c
|
||||
Binary file not shown.
@@ -0,0 +1,503 @@
|
||||
#pragma checksum "..\..\..\..\MainWindow.xaml" "{ff1816ec-aa5e-4d10-87f7-6f4963833460}" "6F090F12111B2DA7E86EB9B4D3E7CE46DAE44931"
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// This code was generated by a tool.
|
||||
//
|
||||
// Changes to this file may cause incorrect behavior and will be lost if
|
||||
// the code is regenerated.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Windows;
|
||||
using System.Windows.Automation;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Controls.Ribbon;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Ink;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Markup;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Animation;
|
||||
using System.Windows.Media.Effects;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Media.Media3D;
|
||||
using System.Windows.Media.TextFormatting;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using System.Windows.Shell;
|
||||
|
||||
|
||||
namespace DeskCoreSetup {
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// MainWindow
|
||||
/// </summary>
|
||||
public partial class MainWindow : System.Windows.Window, System.Windows.Markup.IComponentConnector {
|
||||
|
||||
|
||||
#line 48 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock WindowTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 53 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CloseButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 62 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock VersionText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 63 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel Steps;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 64 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step0;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 65 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step1;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 66 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step2;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 67 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock Step3;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 76 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageWelcome;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 82 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border UpdateHint;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 87 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock UpdateHintText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 93 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.DockPanel PageLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 95 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox AcceptLicense;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 100 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock LicenseText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 106 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageOptions;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 111 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBox DirBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 113 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox DesktopLinkBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 114 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox StartMenuBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 115 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox AutostartBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 119 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageProgress;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 120 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressTitle;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 121 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock ProgressText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 122 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressTrack;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 123 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Border ProgressFill;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 125 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel DonePanel;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 131 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DoneText;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 133 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox LaunchBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 138 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.StackPanel PageUninstall;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 146 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock DataInfo;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 147 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.CheckBox RemoveDataBox;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 149 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.TextBlock RemoveDataWarning;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 160 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button BackButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 161 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button NextButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
|
||||
#line 162 "..\..\..\..\MainWindow.xaml"
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]
|
||||
internal System.Windows.Controls.Button CancelButton;
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
|
||||
private bool _contentLoaded;
|
||||
|
||||
/// <summary>
|
||||
/// InitializeComponent
|
||||
/// </summary>
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
public void InitializeComponent() {
|
||||
if (_contentLoaded) {
|
||||
return;
|
||||
}
|
||||
_contentLoaded = true;
|
||||
System.Uri resourceLocater = new System.Uri("/DeskCore-Setup;component/mainwindow.xaml", System.UriKind.Relative);
|
||||
|
||||
#line 1 "..\..\..\..\MainWindow.xaml"
|
||||
System.Windows.Application.LoadComponent(this, resourceLocater);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
|
||||
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "10.0.12.0")]
|
||||
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
|
||||
void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {
|
||||
switch (connectionId)
|
||||
{
|
||||
case 1:
|
||||
this.WindowTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 2:
|
||||
|
||||
#line 51 "..\..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.Minimize_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 3:
|
||||
this.CloseButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 53 "..\..\..\..\MainWindow.xaml"
|
||||
this.CloseButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 4:
|
||||
this.VersionText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 5:
|
||||
this.Steps = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 6:
|
||||
this.Step0 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 7:
|
||||
this.Step1 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 8:
|
||||
this.Step2 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 9:
|
||||
this.Step3 = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 10:
|
||||
this.PageWelcome = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 11:
|
||||
this.UpdateHint = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 12:
|
||||
this.UpdateHintText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 13:
|
||||
this.PageLicense = ((System.Windows.Controls.DockPanel)(target));
|
||||
return;
|
||||
case 14:
|
||||
this.AcceptLicense = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 95 "..\..\..\..\MainWindow.xaml"
|
||||
this.AcceptLicense.Click += new System.Windows.RoutedEventHandler(this.AcceptLicense_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 15:
|
||||
this.LicenseText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 16:
|
||||
this.PageOptions = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 17:
|
||||
|
||||
#line 110 "..\..\..\..\MainWindow.xaml"
|
||||
((System.Windows.Controls.Button)(target)).Click += new System.Windows.RoutedEventHandler(this.ChangeDir_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 18:
|
||||
this.DirBox = ((System.Windows.Controls.TextBox)(target));
|
||||
return;
|
||||
case 19:
|
||||
this.DesktopLinkBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 20:
|
||||
this.StartMenuBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 21:
|
||||
this.AutostartBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 22:
|
||||
this.PageProgress = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 23:
|
||||
this.ProgressTitle = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 24:
|
||||
this.ProgressText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 25:
|
||||
this.ProgressTrack = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 26:
|
||||
this.ProgressFill = ((System.Windows.Controls.Border)(target));
|
||||
return;
|
||||
case 27:
|
||||
this.DonePanel = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 28:
|
||||
this.DoneText = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 29:
|
||||
this.LaunchBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
return;
|
||||
case 30:
|
||||
this.PageUninstall = ((System.Windows.Controls.StackPanel)(target));
|
||||
return;
|
||||
case 31:
|
||||
this.DataInfo = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 32:
|
||||
this.RemoveDataBox = ((System.Windows.Controls.CheckBox)(target));
|
||||
|
||||
#line 148 "..\..\..\..\MainWindow.xaml"
|
||||
this.RemoveDataBox.Click += new System.Windows.RoutedEventHandler(this.RemoveDataBox_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 33:
|
||||
this.RemoveDataWarning = ((System.Windows.Controls.TextBlock)(target));
|
||||
return;
|
||||
case 34:
|
||||
this.BackButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 160 "..\..\..\..\MainWindow.xaml"
|
||||
this.BackButton.Click += new System.Windows.RoutedEventHandler(this.Back_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 35:
|
||||
this.NextButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 161 "..\..\..\..\MainWindow.xaml"
|
||||
this.NextButton.Click += new System.Windows.RoutedEventHandler(this.Next_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
case 36:
|
||||
this.CancelButton = ((System.Windows.Controls.Button)(target));
|
||||
|
||||
#line 162 "..\..\..\..\MainWindow.xaml"
|
||||
this.CancelButton.Click += new System.Windows.RoutedEventHandler(this.Cancel_Click);
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
return;
|
||||
}
|
||||
this._contentLoaded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
C:\Users\Viper\Desktop\Programme\DeskCore-Installer\DeskCore-Setup.exe
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,471 @@
|
||||
{
|
||||
"version": 3,
|
||||
"targets": {
|
||||
".NETFramework,Version=v4.8": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||
"type": "package",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48": "1.0.3"
|
||||
}
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||
"type": "package",
|
||||
"build": {
|
||||
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies/1.0.3": {
|
||||
"sha512": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
|
||||
"type": "package",
|
||||
"path": "microsoft.netframework.referenceassemblies/1.0.3",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||
"microsoft.netframework.referenceassemblies.nuspec"
|
||||
]
|
||||
},
|
||||
"Microsoft.NETFramework.ReferenceAssemblies.net48/1.0.3": {
|
||||
"sha512": "zMk4D+9zyiEWByyQ7oPImPN/Jhpj166Ky0Nlla4eXlNL8hI/BtSJsgR8Inldd4NNpIAH3oh8yym0W2DrhXdSLQ==",
|
||||
"type": "package",
|
||||
"path": "microsoft.netframework.referenceassemblies.net48/1.0.3",
|
||||
"files": [
|
||||
".nupkg.metadata",
|
||||
".signature.p7s",
|
||||
"build/.NETFramework/v4.8/Accessibility.dll",
|
||||
"build/.NETFramework/v4.8/Accessibility.xml",
|
||||
"build/.NETFramework/v4.8/CustomMarshalers.dll",
|
||||
"build/.NETFramework/v4.8/CustomMarshalers.xml",
|
||||
"build/.NETFramework/v4.8/Facades/Microsoft.Win32.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.AppContext.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.Concurrent.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.NonGeneric.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.Specialized.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Collections.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Annotations.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.EventBasedAsync.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.TypeConverter.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ComponentModel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Console.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Data.Common.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Contracts.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Debug.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.FileVersionInfo.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Process.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.StackTrace.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TextWriterTraceListener.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.Tools.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Diagnostics.TraceSource.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Drawing.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Dynamic.Runtime.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Globalization.Calendars.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Globalization.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Globalization.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.Compression.ZipFile.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.DriveInfo.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.Watcher.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.FileSystem.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.IsolatedStorage.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.MemoryMappedFiles.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.Pipes.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.UnmanagedMemoryStream.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.IO.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.Expressions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.Parallel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.Queryable.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Linq.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Http.Rtc.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.NameResolution.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.NetworkInformation.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Ping.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Requests.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Security.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.Sockets.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.WebHeaderCollection.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.Client.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Net.WebSockets.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ObjectModel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.ILGeneration.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.Lightweight.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Emit.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Reflection.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Resources.Reader.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Resources.ResourceManager.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Resources.Writer.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.CompilerServices.VisualC.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Handles.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.RuntimeInformation.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.WindowsRuntime.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.InteropServices.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Numerics.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Formatters.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Json.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.Serialization.Xml.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Runtime.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Claims.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Algorithms.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Csp.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Encoding.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Cryptography.X509Certificates.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.Principal.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Security.SecureString.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Duplex.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Http.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.NetTcp.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Primitives.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ServiceModel.Security.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Text.Encoding.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Text.RegularExpressions.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Overlapped.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.Parallel.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Tasks.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Thread.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.ThreadPool.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.Timer.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Threading.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.ValueTuple.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.ReaderWriter.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XDocument.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.XDocument.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XPath.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XmlDocument.dll",
|
||||
"build/.NETFramework/v4.8/Facades/System.Xml.XmlSerializer.dll",
|
||||
"build/.NETFramework/v4.8/Facades/netstandard.dll",
|
||||
"build/.NETFramework/v4.8/ISymWrapper.dll",
|
||||
"build/.NETFramework/v4.8/ISymWrapper.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Activities.Build.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Activities.Build.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Conversion.v4.0.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Engine.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Engine.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Framework.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Framework.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Tasks.v4.0.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.Utilities.v4.0.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.Build.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.CSharp.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.CSharp.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.JScript.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.JScript.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.Data.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.Compatibility.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualBasic.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.STLCLR.xml",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.dll",
|
||||
"build/.NETFramework/v4.8/Microsoft.VisualC.xml",
|
||||
"build/.NETFramework/v4.8/PermissionSets/FullTrust.xml",
|
||||
"build/.NETFramework/v4.8/PermissionSets/Internet.xml",
|
||||
"build/.NETFramework/v4.8/PermissionSets/LocalIntranet.xml",
|
||||
"build/.NETFramework/v4.8/PresentationBuildTasks.dll",
|
||||
"build/.NETFramework/v4.8/PresentationBuildTasks.xml",
|
||||
"build/.NETFramework/v4.8/PresentationCore.dll",
|
||||
"build/.NETFramework/v4.8/PresentationCore.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero2.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Aero2.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.AeroLite.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Classic.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Classic.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Luna.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Luna.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Royale.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.Royale.xml",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.dll",
|
||||
"build/.NETFramework/v4.8/PresentationFramework.xml",
|
||||
"build/.NETFramework/v4.8/ReachFramework.dll",
|
||||
"build/.NETFramework/v4.8/ReachFramework.xml",
|
||||
"build/.NETFramework/v4.8/RedistList/FrameworkList.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.Core.Presentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.DurableInstancing.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.Presentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.Presentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Activities.dll",
|
||||
"build/.NETFramework/v4.8/System.Activities.xml",
|
||||
"build/.NETFramework/v4.8/System.AddIn.Contract.dll",
|
||||
"build/.NETFramework/v4.8/System.AddIn.Contract.xml",
|
||||
"build/.NETFramework/v4.8/System.AddIn.dll",
|
||||
"build/.NETFramework/v4.8/System.AddIn.xml",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.dll",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.Registration.xml",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.dll",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.Composition.xml",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.dll",
|
||||
"build/.NETFramework/v4.8/System.ComponentModel.DataAnnotations.xml",
|
||||
"build/.NETFramework/v4.8/System.Configuration.Install.dll",
|
||||
"build/.NETFramework/v4.8/System.Configuration.Install.xml",
|
||||
"build/.NETFramework/v4.8/System.Configuration.dll",
|
||||
"build/.NETFramework/v4.8/System.Configuration.xml",
|
||||
"build/.NETFramework/v4.8/System.Core.dll",
|
||||
"build/.NETFramework/v4.8/System.Core.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.DataSetExtensions.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Entity.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Linq.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Linq.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.OracleClient.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.OracleClient.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Client.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Client.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.Services.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.SqlXml.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.SqlXml.xml",
|
||||
"build/.NETFramework/v4.8/System.Data.dll",
|
||||
"build/.NETFramework/v4.8/System.Data.xml",
|
||||
"build/.NETFramework/v4.8/System.Deployment.dll",
|
||||
"build/.NETFramework/v4.8/System.Deployment.xml",
|
||||
"build/.NETFramework/v4.8/System.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Device.dll",
|
||||
"build/.NETFramework/v4.8/System.Device.xml",
|
||||
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.dll",
|
||||
"build/.NETFramework/v4.8/System.Diagnostics.Tracing.xml",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.dll",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.AccountManagement.xml",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.dll",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.Protocols.xml",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.dll",
|
||||
"build/.NETFramework/v4.8/System.DirectoryServices.xml",
|
||||
"build/.NETFramework/v4.8/System.Drawing.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Drawing.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Drawing.dll",
|
||||
"build/.NETFramework/v4.8/System.Drawing.xml",
|
||||
"build/.NETFramework/v4.8/System.Dynamic.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.Thunk.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.Wrapper.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.dll",
|
||||
"build/.NETFramework/v4.8/System.EnterpriseServices.xml",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.dll",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.FileSystem.xml",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.dll",
|
||||
"build/.NETFramework/v4.8/System.IO.Compression.xml",
|
||||
"build/.NETFramework/v4.8/System.IO.Log.dll",
|
||||
"build/.NETFramework/v4.8/System.IO.Log.xml",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.dll",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Selectors.xml",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Services.dll",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.Services.xml",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.dll",
|
||||
"build/.NETFramework/v4.8/System.IdentityModel.xml",
|
||||
"build/.NETFramework/v4.8/System.Linq.xml",
|
||||
"build/.NETFramework/v4.8/System.Management.Instrumentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Management.Instrumentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Management.dll",
|
||||
"build/.NETFramework/v4.8/System.Management.xml",
|
||||
"build/.NETFramework/v4.8/System.Messaging.dll",
|
||||
"build/.NETFramework/v4.8/System.Messaging.xml",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.dll",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.WebRequest.xml",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.dll",
|
||||
"build/.NETFramework/v4.8/System.Net.Http.xml",
|
||||
"build/.NETFramework/v4.8/System.Net.dll",
|
||||
"build/.NETFramework/v4.8/System.Net.xml",
|
||||
"build/.NETFramework/v4.8/System.Numerics.dll",
|
||||
"build/.NETFramework/v4.8/System.Numerics.xml",
|
||||
"build/.NETFramework/v4.8/System.Printing.dll",
|
||||
"build/.NETFramework/v4.8/System.Printing.xml",
|
||||
"build/.NETFramework/v4.8/System.Reflection.Context.dll",
|
||||
"build/.NETFramework/v4.8/System.Reflection.Context.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Caching.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Caching.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.DurableInstancing.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Remoting.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Remoting.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.Formatters.Soap.xml",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.dll",
|
||||
"build/.NETFramework/v4.8/System.Runtime.Serialization.xml",
|
||||
"build/.NETFramework/v4.8/System.Security.dll",
|
||||
"build/.NETFramework/v4.8/System.Security.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activation.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activation.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activities.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Activities.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Channels.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Channels.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Discovery.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Routing.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Routing.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Web.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.Web.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceModel.xml",
|
||||
"build/.NETFramework/v4.8/System.ServiceProcess.dll",
|
||||
"build/.NETFramework/v4.8/System.ServiceProcess.xml",
|
||||
"build/.NETFramework/v4.8/System.Speech.dll",
|
||||
"build/.NETFramework/v4.8/System.Speech.xml",
|
||||
"build/.NETFramework/v4.8/System.Threading.Tasks.Dataflow.xml",
|
||||
"build/.NETFramework/v4.8/System.Transactions.dll",
|
||||
"build/.NETFramework/v4.8/System.Transactions.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Abstractions.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.ApplicationServices.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.ApplicationServices.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.DataVisualization.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DataVisualization.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DataVisualization.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.DynamicData.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Entity.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.Design.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Extensions.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Mobile.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Mobile.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.RegularExpressions.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.RegularExpressions.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.Routing.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Services.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.Services.xml",
|
||||
"build/.NETFramework/v4.8/System.Web.dll",
|
||||
"build/.NETFramework/v4.8/System.Web.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Controls.Ribbon.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.Design.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.DataVisualization.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Forms.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Input.Manipulations.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.Presentation.dll",
|
||||
"build/.NETFramework/v4.8/System.Windows.Presentation.xml",
|
||||
"build/.NETFramework/v4.8/System.Windows.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Activities.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Activities.xml",
|
||||
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.ComponentModel.xml",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Runtime.dll",
|
||||
"build/.NETFramework/v4.8/System.Workflow.Runtime.xml",
|
||||
"build/.NETFramework/v4.8/System.WorkflowServices.dll",
|
||||
"build/.NETFramework/v4.8/System.WorkflowServices.xml",
|
||||
"build/.NETFramework/v4.8/System.Xaml.dll",
|
||||
"build/.NETFramework/v4.8/System.Xaml.xml",
|
||||
"build/.NETFramework/v4.8/System.Xml.Linq.dll",
|
||||
"build/.NETFramework/v4.8/System.Xml.Linq.xml",
|
||||
"build/.NETFramework/v4.8/System.Xml.Serialization.dll",
|
||||
"build/.NETFramework/v4.8/System.Xml.dll",
|
||||
"build/.NETFramework/v4.8/System.Xml.xml",
|
||||
"build/.NETFramework/v4.8/System.dll",
|
||||
"build/.NETFramework/v4.8/System.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationClient.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationClient.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationClientsideProviders.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationProvider.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationProvider.xml",
|
||||
"build/.NETFramework/v4.8/UIAutomationTypes.dll",
|
||||
"build/.NETFramework/v4.8/UIAutomationTypes.xml",
|
||||
"build/.NETFramework/v4.8/WindowsBase.dll",
|
||||
"build/.NETFramework/v4.8/WindowsBase.xml",
|
||||
"build/.NETFramework/v4.8/WindowsFormsIntegration.dll",
|
||||
"build/.NETFramework/v4.8/WindowsFormsIntegration.xml",
|
||||
"build/.NETFramework/v4.8/XamlBuildTask.dll",
|
||||
"build/.NETFramework/v4.8/XamlBuildTask.xml",
|
||||
"build/.NETFramework/v4.8/mscorlib.dll",
|
||||
"build/.NETFramework/v4.8/mscorlib.xml",
|
||||
"build/.NETFramework/v4.8/namespaces.xml",
|
||||
"build/.NETFramework/v4.8/sysglobl.dll",
|
||||
"build/.NETFramework/v4.8/sysglobl.xml",
|
||||
"build/Microsoft.NETFramework.ReferenceAssemblies.net48.targets",
|
||||
"microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512",
|
||||
"microsoft.netframework.referenceassemblies.net48.nuspec"
|
||||
]
|
||||
}
|
||||
},
|
||||
"projectFileDependencyGroups": {
|
||||
".NETFramework,Version=v4.8": [
|
||||
"Microsoft.NETFramework.ReferenceAssemblies >= 1.0.3"
|
||||
]
|
||||
},
|
||||
"packageFolders": {
|
||||
"C:\\Users\\Viper\\.nuget\\packages\\": {}
|
||||
},
|
||||
"project": {
|
||||
"version": "1.0.4",
|
||||
"restore": {
|
||||
"projectUniqueName": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj",
|
||||
"projectName": "DeskCore-Setup",
|
||||
"projectPath": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj",
|
||||
"packagesPath": "C:\\Users\\Viper\\.nuget\\packages\\",
|
||||
"outputPath": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\obj\\",
|
||||
"projectStyle": "PackageReference",
|
||||
"configFilePaths": [
|
||||
"C:\\Users\\Viper\\AppData\\Roaming\\NuGet\\NuGet.Config"
|
||||
],
|
||||
"originalTargetFrameworks": [
|
||||
"net48"
|
||||
],
|
||||
"sources": {
|
||||
"https://api.nuget.org/v3/index.json": {}
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"projectReferences": {}
|
||||
}
|
||||
},
|
||||
"warningProperties": {
|
||||
"warnAsError": [
|
||||
"NU1605"
|
||||
]
|
||||
},
|
||||
"restoreAuditProperties": {
|
||||
"enableAudit": "true",
|
||||
"auditLevel": "low",
|
||||
"auditMode": "direct"
|
||||
},
|
||||
"SdkAnalysisLevel": "10.0.100"
|
||||
},
|
||||
"frameworks": {
|
||||
"net48": {
|
||||
"targetAlias": "net48",
|
||||
"dependencies": {
|
||||
"Microsoft.NETFramework.ReferenceAssemblies": {
|
||||
"suppressParent": "All",
|
||||
"target": "Package",
|
||||
"version": "[1.0.3, )",
|
||||
"autoReferenced": true
|
||||
}
|
||||
},
|
||||
"runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.112\\RuntimeIdentifierGraph.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dgSpecHash": "1d5+c4eGnFw=",
|
||||
"success": true,
|
||||
"projectFilePath": "C:\\Users\\Viper\\Desktop\\Programme\\DeskCore-Source\\Setup\\DeskCoreSetup.csproj",
|
||||
"expectedPackageFiles": [
|
||||
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies\\1.0.3\\microsoft.netframework.referenceassemblies.1.0.3.nupkg.sha512",
|
||||
"C:\\Users\\Viper\\.nuget\\packages\\microsoft.netframework.referenceassemblies.net48\\1.0.3\\microsoft.netframework.referenceassemblies.net48.1.0.3.nupkg.sha512"
|
||||
],
|
||||
"logs": []
|
||||
}
|
||||
+476
-392
@@ -1,392 +1,476 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
public enum TransferStatus { Ok, Skipped, Error, Cancelled }
|
||||
|
||||
/// <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,
|
||||
Action<long>? onBytes = null, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested) return new(TransferStatus.Cancelled, source);
|
||||
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);
|
||||
try
|
||||
{
|
||||
if (copy) CopyAny(source, dest, onBytes, ct);
|
||||
else MoveAny(source, dest, onBytes, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// halb kopiertes Ziel wieder entfernen – die Quelle ist unverändert
|
||||
// (beim Verschieben zwischen Laufwerken wird die Quelle erst nach vollständiger Kopie gelöscht)
|
||||
DeletePartial(dest);
|
||||
return new(TransferStatus.Cancelled, source);
|
||||
}
|
||||
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, Action<long>? onBytes = null, CancellationToken ct = default)
|
||||
{
|
||||
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, onBytes, ct);
|
||||
Directory.Delete(source, recursive: true);
|
||||
}
|
||||
|
||||
public static void CopyAny(string source, string dest, Action<long>? onBytes = null, CancellationToken ct = default)
|
||||
{
|
||||
NoteCreated(dest);
|
||||
if (Directory.Exists(source)) CopyDirectory(source, dest, onBytes, ct);
|
||||
else CopyFile(source, dest, onBytes, ct);
|
||||
}
|
||||
|
||||
/// <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, Action<long>? onBytes, CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(dest);
|
||||
foreach (var f in Directory.GetFiles(source))
|
||||
CopyFile(f, Path.Combine(dest, Path.GetFileName(f)), onBytes, ct);
|
||||
foreach (var d in Directory.GetDirectories(source))
|
||||
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)), onBytes, ct);
|
||||
}
|
||||
|
||||
/// <summary>Kopiert in 1-MB-Stücken, damit Fortschritt angezeigt und abgebrochen werden kann.</summary>
|
||||
private static void CopyFile(string source, string dest, Action<long>? onBytes, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
const int Chunk = 1 << 20;
|
||||
using (var input = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, Chunk, FileOptions.SequentialScan))
|
||||
using (var output = new FileStream(dest, FileMode.CreateNew, FileAccess.Write, FileShare.None, Chunk))
|
||||
{
|
||||
var buffer = new byte[Chunk];
|
||||
int read;
|
||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
output.Write(buffer, 0, read);
|
||||
onBytes?.Invoke(read);
|
||||
}
|
||||
}
|
||||
// Zeitstempel und Attribute wie beim normalen Kopieren übernehmen
|
||||
try
|
||||
{
|
||||
File.SetCreationTimeUtc(dest, File.GetCreationTimeUtc(source));
|
||||
File.SetLastWriteTimeUtc(dest, File.GetLastWriteTimeUtc(source));
|
||||
File.SetAttributes(dest, File.GetAttributes(source) & ~FileAttributes.ReadOnly);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void DeletePartial(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
|
||||
else if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Wie viele Bytes müssen für diese Übertragung tatsächlich kopiert werden? (0 = nur umbenennen)</summary>
|
||||
public static long BytesToCopy(string source, string targetDir, bool forceMove)
|
||||
{
|
||||
bool copy = !forceMove && !IsInStorage(source) && IsInStorage(targetDir);
|
||||
bool crossDrive = !string.Equals(Path.GetPathRoot(Path.GetFullPath(source)), Path.GetPathRoot(Path.GetFullPath(targetDir)),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
if (!copy && !crossDrive) return 0;
|
||||
try { return Directory.Exists(source) ? DirectorySize(source) : new FileInfo(source).Length; }
|
||||
catch { return 0; }
|
||||
}
|
||||
}
|
||||
using System.Collections.Concurrent;
|
||||
using System.IO;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
public enum TransferStatus { Ok, Skipped, Error, Cancelled }
|
||||
|
||||
/// <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;
|
||||
}
|
||||
|
||||
// ---------- Verknüpfungen zu deinstallierten Programmen ----------
|
||||
|
||||
private static readonly ConcurrentDictionary<string, (DateTime Stamp, string? Target)> _targetPathCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>Reiner Zielpfad einer .lnk-Verknüpfung (ohne Argumente), sonst null.</summary>
|
||||
public static string? LinkTargetPath(string path)
|
||||
{
|
||||
if (!string.Equals(Path.GetExtension(path), ".lnk", StringComparison.OrdinalIgnoreCase)) return null;
|
||||
DateTime stamp;
|
||||
try { stamp = File.GetLastWriteTimeUtc(path); } catch { return null; }
|
||||
if (_targetPathCache.TryGetValue(path, out var c) && c.Stamp == stamp) return c.Target;
|
||||
string? target = null;
|
||||
try
|
||||
{
|
||||
// Direkt über IShellLink lesen (Rohpfad). WScript.Shell "repariert" Ziele, die es nicht findet,
|
||||
// z. B. \\server\… zu C:\server\… – darauf darf eine Lösch-Entscheidung nicht beruhen.
|
||||
var link = (IShellLinkW)new ShellLinkCoClass();
|
||||
try
|
||||
{
|
||||
((System.Runtime.InteropServices.ComTypes.IPersistFile)link).Load(path, 0); // STGM_READ
|
||||
var sb = new System.Text.StringBuilder(32768);
|
||||
link.GetPath(sb, sb.Capacity, IntPtr.Zero, 0x4); // SLGP_RAWPATH
|
||||
var raw = sb.ToString().Trim();
|
||||
target = raw.Length == 0 ? null : Environment.ExpandEnvironmentVariables(raw);
|
||||
}
|
||||
finally { System.Runtime.InteropServices.Marshal.ReleaseComObject(link); }
|
||||
}
|
||||
catch { }
|
||||
_targetPathCache[path] = (stamp, target);
|
||||
return target;
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.ComImport, System.Runtime.InteropServices.Guid("00021401-0000-0000-C000-000000000046")]
|
||||
private class ShellLinkCoClass { }
|
||||
|
||||
[System.Runtime.InteropServices.ComImport, System.Runtime.InteropServices.Guid("000214F9-0000-0000-C000-000000000046"),
|
||||
System.Runtime.InteropServices.InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsIUnknown)]
|
||||
private interface IShellLinkW
|
||||
{
|
||||
void GetPath([System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.LPWStr)] System.Text.StringBuilder pszFile,
|
||||
int cch, IntPtr pfd, uint fFlags);
|
||||
// weitere Methoden werden nicht gebraucht (Reihenfolge der vtable ab hier egal)
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zeigt die Verknüpfung auf ein Programm, das es nicht mehr gibt (z. B. deinstalliert)?
|
||||
/// Bewusst vorsichtig – im Zweifel "nein":
|
||||
/// .lnk nur mit festem Ziel auf einer eingebauten, erreichbaren Festplatte; .url nur bei Steam/Epic, wenn deren
|
||||
/// Installationsliste das Spiel nicht mehr kennt. Netzwerk-/USB-Laufwerke, Web-Links, Store-Apps und
|
||||
/// Windows-Installer-Verknüpfungen werden nie als tot gewertet.
|
||||
/// </summary>
|
||||
public static bool IsDeadShortcut(string path)
|
||||
{
|
||||
// Steam-/Epic-Links: deren eigene Installationslisten fragen
|
||||
if (string.Equals(Path.GetExtension(path), ".url", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var url = ShortcutTarget(path);
|
||||
return url is not null && GameLibraries.IsInstalled(url) == false;
|
||||
}
|
||||
|
||||
var target = LinkTargetPath(path);
|
||||
if (target is null) return false;
|
||||
try
|
||||
{
|
||||
if (!Path.IsPathFullyQualified(target) || target.StartsWith(@"\\")) return false; // relativ oder Netzwerk
|
||||
var drive = new DriveInfo(Path.GetPathRoot(target)!);
|
||||
if (!drive.IsReady || drive.DriveType != DriveType.Fixed) return false; // abgesteckt/USB/Netz
|
||||
var installer = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Installer");
|
||||
if (IsUnder(target, installer)) return false; // MSI-"Werbe"-Verknüpfung
|
||||
return !File.Exists(target) && !Directory.Exists(target);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
/// <summary>Alle toten Verknüpfungen in den Kategorien (ohne die, die der Nutzer behalten will).</summary>
|
||||
public static List<string> FindDeadShortcuts(ICollection<string> keep) =>
|
||||
Categories()
|
||||
.SelectMany(c => Entries(CategoryPath(c)))
|
||||
.Where(p => !keep.Contains(p, StringComparer.OrdinalIgnoreCase) && IsDeadShortcut(p))
|
||||
.ToList();
|
||||
|
||||
/// <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,
|
||||
Action<long>? onBytes = null, CancellationToken ct = default)
|
||||
{
|
||||
if (ct.IsCancellationRequested) return new(TransferStatus.Cancelled, source);
|
||||
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);
|
||||
try
|
||||
{
|
||||
if (copy) CopyAny(source, dest, onBytes, ct);
|
||||
else MoveAny(source, dest, onBytes, ct);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// halb kopiertes Ziel wieder entfernen – die Quelle ist unverändert
|
||||
// (beim Verschieben zwischen Laufwerken wird die Quelle erst nach vollständiger Kopie gelöscht)
|
||||
DeletePartial(dest);
|
||||
return new(TransferStatus.Cancelled, source);
|
||||
}
|
||||
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, Action<long>? onBytes = null, CancellationToken ct = default)
|
||||
{
|
||||
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, onBytes, ct);
|
||||
Directory.Delete(source, recursive: true);
|
||||
}
|
||||
|
||||
public static void CopyAny(string source, string dest, Action<long>? onBytes = null, CancellationToken ct = default)
|
||||
{
|
||||
NoteCreated(dest);
|
||||
if (Directory.Exists(source)) CopyDirectory(source, dest, onBytes, ct);
|
||||
else CopyFile(source, dest, onBytes, ct);
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
/// <summary>Von der App selbst angelegte Datei merken (z. B. Desktop-Verknüpfung einer Start-Gruppe).</summary>
|
||||
public static void MarkCreated(string path) => NoteCreated(path);
|
||||
|
||||
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, Action<long>? onBytes, CancellationToken ct)
|
||||
{
|
||||
Directory.CreateDirectory(dest);
|
||||
foreach (var f in Directory.GetFiles(source))
|
||||
CopyFile(f, Path.Combine(dest, Path.GetFileName(f)), onBytes, ct);
|
||||
foreach (var d in Directory.GetDirectories(source))
|
||||
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)), onBytes, ct);
|
||||
}
|
||||
|
||||
/// <summary>Kopiert in 1-MB-Stücken, damit Fortschritt angezeigt und abgebrochen werden kann.</summary>
|
||||
private static void CopyFile(string source, string dest, Action<long>? onBytes, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
const int Chunk = 1 << 20;
|
||||
using (var input = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, Chunk, FileOptions.SequentialScan))
|
||||
using (var output = new FileStream(dest, FileMode.CreateNew, FileAccess.Write, FileShare.None, Chunk))
|
||||
{
|
||||
var buffer = new byte[Chunk];
|
||||
int read;
|
||||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
output.Write(buffer, 0, read);
|
||||
onBytes?.Invoke(read);
|
||||
}
|
||||
}
|
||||
// Zeitstempel und Attribute wie beim normalen Kopieren übernehmen
|
||||
try
|
||||
{
|
||||
File.SetCreationTimeUtc(dest, File.GetCreationTimeUtc(source));
|
||||
File.SetLastWriteTimeUtc(dest, File.GetLastWriteTimeUtc(source));
|
||||
File.SetAttributes(dest, File.GetAttributes(source) & ~FileAttributes.ReadOnly);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static void DeletePartial(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
|
||||
else if (File.Exists(path)) File.Delete(path);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>Wie viele Bytes müssen für diese Übertragung tatsächlich kopiert werden? (0 = nur umbenennen)</summary>
|
||||
public static long BytesToCopy(string source, string targetDir, bool forceMove)
|
||||
{
|
||||
bool copy = !forceMove && !IsInStorage(source) && IsInStorage(targetDir);
|
||||
bool crossDrive = !string.Equals(Path.GetPathRoot(Path.GetFullPath(source)), Path.GetPathRoot(Path.GetFullPath(targetDir)),
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
if (!copy && !crossDrive) return 0;
|
||||
try { return Directory.Exists(source) ? DirectorySize(source) : new FileInfo(source).Length; }
|
||||
catch { return 0; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,74 +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;
|
||||
}
|
||||
}
|
||||
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
-27
@@ -1,27 +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>
|
||||
<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
-27
@@ -1,27 +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>
|
||||
<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
-651
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace DeskCore;
|
||||
|
||||
public class UsageEntry
|
||||
{
|
||||
public int Count { get; set; }
|
||||
public DateTime Last { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Merkt sich, was wie oft und wann zuletzt über DeskCore gestartet wurde (für „Meistgenutzt“, „Zuletzt benutzt“
|
||||
/// und die Schnellstart-Suche). Liegt getrennt von den Einstellungen in usage.json, weil es sich ständig ändert.
|
||||
/// </summary>
|
||||
public static class Usage
|
||||
{
|
||||
/// <summary>Schlüssel-Präfix für Start-Gruppen (die keinen Dateipfad haben).</summary>
|
||||
public const string GroupPrefix = "gruppe:";
|
||||
|
||||
private static readonly string FilePath = Path.Combine(AppSettings.Folder, "usage.json");
|
||||
private static readonly Dictionary<string, UsageEntry> _data = Load();
|
||||
private static DispatcherTimer? _saveTimer;
|
||||
|
||||
/// <summary>Etwas wurde gestartet (z. B. damit „Zuletzt benutzt“-Boxen sich aktualisieren).</summary>
|
||||
public static event Action? Changed;
|
||||
|
||||
private static Dictionary<string, UsageEntry> Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(FilePath) &&
|
||||
JsonSerializer.Deserialize<Dictionary<string, UsageEntry>>(File.ReadAllText(FilePath)) is { } d)
|
||||
return new(d, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
catch { }
|
||||
return new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>Programm/Datei starten und den Start mitzählen. Wirft bei Fehlern wie Process.Start.</summary>
|
||||
public static void Start(string path)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });
|
||||
Record(path);
|
||||
}
|
||||
|
||||
public static void Record(string key)
|
||||
{
|
||||
if (!_data.TryGetValue(key, out var e)) _data[key] = e = new UsageEntry();
|
||||
e.Count++;
|
||||
e.Last = DateTime.Now;
|
||||
ScheduleSave();
|
||||
Changed?.Invoke();
|
||||
}
|
||||
|
||||
public static int Count(string key) => _data.TryGetValue(key, out var e) ? e.Count : 0;
|
||||
|
||||
/// <summary>
|
||||
/// Wie „beliebt“ etwas ist: Anzahl der Starts, mit der Zeit abnehmend gewichtet –
|
||||
/// was du früher oft, aber seit Wochen nicht mehr benutzt hast, rutscht langsam nach hinten.
|
||||
/// </summary>
|
||||
public static double Score(string key)
|
||||
{
|
||||
if (!_data.TryGetValue(key, out var e)) return 0;
|
||||
double days = Math.Max(0, (DateTime.Now - e.Last).TotalDays);
|
||||
return e.Count / (1 + days / 30);
|
||||
}
|
||||
|
||||
/// <summary>Zuletzt gestartete Dateien, die es noch gibt (neueste zuerst).</summary>
|
||||
public static List<string> Recent(int max) => _data
|
||||
.Where(kv => !kv.Key.StartsWith(GroupPrefix) && (File.Exists(kv.Key) || Directory.Exists(kv.Key)))
|
||||
.OrderByDescending(kv => kv.Value.Last)
|
||||
.Take(max)
|
||||
.Select(kv => kv.Key)
|
||||
.ToList();
|
||||
|
||||
/// <summary>Datei wurde umbenannt/verschoben → Zählung mitnehmen.</summary>
|
||||
public static void Rename(string oldKey, string newKey)
|
||||
{
|
||||
if (!_data.Remove(oldKey, out var e)) return;
|
||||
if (_data.TryGetValue(newKey, out var existing))
|
||||
{
|
||||
existing.Count += e.Count;
|
||||
if (e.Last > existing.Last) existing.Last = e.Last;
|
||||
}
|
||||
else _data[newKey] = e;
|
||||
ScheduleSave();
|
||||
}
|
||||
|
||||
public static void Forget(string key)
|
||||
{
|
||||
if (_data.Remove(key)) { ScheduleSave(); Changed?.Invoke(); }
|
||||
}
|
||||
|
||||
private static void ScheduleSave()
|
||||
{
|
||||
_saveTimer ??= new DispatcherTimer(TimeSpan.FromSeconds(2), DispatcherPriority.Background, (_, _) => Flush(),
|
||||
Dispatcher.CurrentDispatcher);
|
||||
_saveTimer.Stop();
|
||||
_saveTimer.Start();
|
||||
}
|
||||
|
||||
/// <summary>Sofort speichern (z. B. beim Beenden).</summary>
|
||||
public static void Flush()
|
||||
{
|
||||
_saveTimer?.Stop();
|
||||
try
|
||||
{
|
||||
// Nicht mehr vorhandene Einträge, die lange nicht benutzt wurden, aufräumen
|
||||
foreach (var key in _data.Where(kv => kv.Value.Last < DateTime.Now.AddDays(-180) && !kv.Key.StartsWith(GroupPrefix)
|
||||
&& !File.Exists(kv.Key) && !Directory.Exists(kv.Key)).Select(kv => kv.Key).ToList())
|
||||
_data.Remove(key);
|
||||
Directory.CreateDirectory(AppSettings.Folder);
|
||||
File.WriteAllText(FilePath, JsonSerializer.Serialize(_data));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
+140
-111
@@ -1,111 +1,140 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
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>Dateityp-Gruppen für Regeln vom Typ "Dateityp ist".</summary>
|
||||
private static readonly Dictionary<string, string[]> Groups = new(StringComparer.CurrentCultureIgnoreCase)
|
||||
{
|
||||
["Bilder"] = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "heic", "tif", "tiff", "svg", "ico", "raw", "psd"],
|
||||
["Videos"] = ["mp4", "mkv", "avi", "mov", "wmv", "webm", "m4v", "flv", "mpg", "mpeg"],
|
||||
["Musik"] = ["mp3", "wav", "flac", "aac", "ogg", "m4a", "wma", "opus"],
|
||||
["Dokumente"] = ["pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "ods", "odp", "txt", "rtf", "md", "csv"],
|
||||
["Archive"] = ["zip", "rar", "7z", "tar", "gz", "bz2", "xz", "iso"],
|
||||
["Programme"] = ["exe", "msi", "lnk", "url", "appref-ms", "bat", "cmd"],
|
||||
["Verknüpfungen"] = ["lnk", "url", "appref-ms"],
|
||||
};
|
||||
|
||||
/// <summary>Passt eine Regel auf das Element?</summary>
|
||||
public static bool Matches(SortRule r, string path)
|
||||
{
|
||||
var value = r.Contains.Trim();
|
||||
if (value.Length == 0) return false;
|
||||
if (r.Kind != "Typ") return Storage.DisplayName(path).Contains(value, StringComparison.CurrentCultureIgnoreCase);
|
||||
|
||||
bool isFolder = Directory.Exists(path);
|
||||
var ext = Path.GetExtension(path).TrimStart('.').ToLowerInvariant();
|
||||
foreach (var token in value.Split([',', ';', ' '], StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
var t = token.Trim().TrimStart('*', '.').ToLowerInvariant();
|
||||
if (t is "ordner") { if (isFolder) return true; continue; }
|
||||
if (Groups.TryGetValue(t, out var exts)) { if (!isFolder && exts.Contains(ext)) return true; continue; }
|
||||
if (!isFolder && t == ext) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Erste passende Regel → Kategoriename (nur existierende Kategorien).</summary>
|
||||
public static string? Match(string path)
|
||||
{
|
||||
var cats = Storage.Categories();
|
||||
foreach (var r in AppSettings.Current.Rules)
|
||||
{
|
||||
if (!Matches(r, path)) continue;
|
||||
var cat = cats.FirstOrDefault(c => string.Equals(c, r.Category, StringComparison.OrdinalIgnoreCase));
|
||||
if (cat is not null) return cat;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user