Upload via GUI (186 Dateien)
This commit is contained in:
+19
-1
@@ -14,7 +14,7 @@ public partial class App : Application
|
||||
|
||||
private static Hotkey? _hotkey;
|
||||
private static Hotkey? _peekHotkey;
|
||||
private static Hotkey? _pagePrev, _pageNext;
|
||||
private static Hotkey? _pagePrev, _pageNext, _quickSearch;
|
||||
private static System.Windows.Threading.DispatcherTimer? _desktopTimer;
|
||||
private static Watcher? _watcher;
|
||||
private Mutex? _mutex;
|
||||
@@ -65,6 +65,9 @@ public partial class App : Application
|
||||
_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();
|
||||
@@ -110,6 +113,7 @@ public partial class App : Application
|
||||
{
|
||||
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. Ä. */ }
|
||||
@@ -124,6 +128,7 @@ public partial class App : Application
|
||||
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();
|
||||
}
|
||||
@@ -138,6 +143,17 @@ public partial class App : Application
|
||||
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()
|
||||
{
|
||||
@@ -172,6 +188,8 @@ public partial class App : Application
|
||||
DesktopToggle.RestoreOnExit();
|
||||
_peekHotkey?.Dispose();
|
||||
_pagePrev?.Dispose();
|
||||
_quickSearch?.Dispose();
|
||||
Usage.Flush();
|
||||
_pageNext?.Dispose();
|
||||
Win?.Undo.FlushAll();
|
||||
FenceManager.CloseAll();
|
||||
|
||||
+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);
|
||||
}
|
||||
+3
-2
@@ -81,10 +81,11 @@ public static class DesktopIcons
|
||||
return changed;
|
||||
}
|
||||
|
||||
public static bool HitsIcon(int x, int y) => Cached.Any(i =>
|
||||
/// <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 && x < l + w && y >= t && y < t + h;
|
||||
return x >= l - margin && x < l + w + margin && y >= t - margin && y < t + h + margin;
|
||||
});
|
||||
|
||||
// ---------- Desktop-Ansicht finden ----------
|
||||
|
||||
+263
-261
@@ -1,261 +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)
|
||||
{
|
||||
_w?.Hide();
|
||||
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) return; // zu klein – war wohl nur ein Versehen
|
||||
FenceManager.ShowCreateMenu(dip);
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+12
-3
@@ -68,7 +68,10 @@ public static class DesktopToggle
|
||||
{
|
||||
_lastDownTick = 0;
|
||||
var pt = info.pt;
|
||||
dispatcher?.BeginInvoke(() => OnDoubleClick(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
|
||||
{
|
||||
@@ -144,9 +147,15 @@ public static class DesktopToggle
|
||||
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
|
||||
}
|
||||
|
||||
private static void OnDoubleClick(POINT pt)
|
||||
private static void OnDoubleClick(POINT pt, POINT first)
|
||||
{
|
||||
if (!IsEmptyDesktopSpot(pt)) return;
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project>
|
||||
<!-- Eine Versionsnummer für App und Installer – nur hier ändern -->
|
||||
<PropertyGroup>
|
||||
<Version>1.0.2</Version>
|
||||
<Version>1.0.4</Version>
|
||||
<!-- keine Git-Kennung an die Produktversion anhängen (sonst "1.0.1+a1b2c3…") -->
|
||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
+50
-41
@@ -73,14 +73,30 @@ public static class FenceManager
|
||||
App.Win?.RefreshFromFences();
|
||||
}
|
||||
|
||||
/// <summary>Kategorie aus allen Boxen (außer except) entfernen – eine Kategorie soll nur in einer Box stehen.</summary>
|
||||
public static void RemoveCategoryTab(string category, FenceDef? except)
|
||||
/// <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.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
|
||||
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)
|
||||
{
|
||||
@@ -117,18 +133,12 @@ public static class FenceManager
|
||||
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: Menü "Neue Box hier" an der Mausposition.</summary>
|
||||
/// <summary>Nach dem Aufziehen: Auswahl "Neue Box hier" an der Mausposition.</summary>
|
||||
public static void ShowCreateMenu(Rect area)
|
||||
{
|
||||
var menu = new ContextMenu { Placement = System.Windows.Controls.Primitives.PlacementMode.MousePoint };
|
||||
var header = MainWindow.MenuItem("Neue Box hier für …", "\uE8A1", null, bold: true);
|
||||
header.IsEnabled = false;
|
||||
menu.Items.Add(header);
|
||||
menu.Items.Add(new Separator());
|
||||
|
||||
void Create(FenceTab tab)
|
||||
BoxPicker.Show(area, tab =>
|
||||
{
|
||||
if (!tab.IsFolder) RemoveCategoryTab(tab.Value, except: null);
|
||||
if (tab.IsCategory) RemoveCategoryTab(tab.Value, except: null, S.CurrentPage);
|
||||
S.Fences.Add(new FenceDef
|
||||
{
|
||||
Page = S.CurrentPage, Tabs = [tab],
|
||||
@@ -138,35 +148,9 @@ public static class FenceManager
|
||||
S.Save();
|
||||
Sync();
|
||||
App.Win?.RefreshFromFences();
|
||||
}
|
||||
|
||||
foreach (var c in Storage.Categories())
|
||||
{
|
||||
var label = S.HasFence(c) ? $"{c} (aus anderer Box hierher holen)" : c;
|
||||
menu.Items.Add(MainWindow.MenuItem(label, S.Categories.GetValueOrDefault(c)?.Glyph ?? "\uF12B",
|
||||
() => Create(new FenceTab { Value = c })));
|
||||
}
|
||||
if (Storage.Categories().Count > 0) menu.Items.Add(new Separator());
|
||||
menu.Items.Add(MainWindow.MenuItem("Neue Kategorie …", "\uE710", () =>
|
||||
{
|
||||
if (App.Win?.CreateCategoryInteractive() is { } name) Create(new FenceTab { Value = name });
|
||||
}));
|
||||
menu.Items.Add(MainWindow.MenuItem("Ordner-Portal …", "\uF12B", () =>
|
||||
{
|
||||
if (PickFolder() is { } folder) Create(new FenceTab { Kind = FenceTab.FolderKind, Value = folder });
|
||||
}));
|
||||
menu.Items.Add(new Separator());
|
||||
menu.Items.Add(MainWindow.MenuItem("Abbrechen", "\uE711", () => { }));
|
||||
// Damit das Menü bei Klick daneben zugeht, muss es im Vordergrund sein
|
||||
menu.Opened += (_, _) =>
|
||||
{
|
||||
if (PresentationSource.FromVisual(menu) is System.Windows.Interop.HwndSource src) SetForegroundWindow(src.Handle);
|
||||
};
|
||||
menu.IsOpen = true;
|
||||
});
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hwnd);
|
||||
|
||||
// =====================================================================
|
||||
// Desktop-Seiten
|
||||
// =====================================================================
|
||||
@@ -186,6 +170,7 @@ public static class FenceManager
|
||||
S.CurrentPage = index;
|
||||
S.Save();
|
||||
if (changed) Sync();
|
||||
PageScheduler.ApplyWallpaper(index);
|
||||
PageIndicator.Show(S.Pages[index], index, S.Pages.Count);
|
||||
App.Win?.RefreshFromFences();
|
||||
}
|
||||
@@ -193,8 +178,24 @@ public static class FenceManager
|
||||
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>
|
||||
@@ -205,6 +206,8 @@ public static class FenceManager
|
||||
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();
|
||||
@@ -267,10 +270,16 @@ public static class FenceManager
|
||||
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")], def.Sort, v => def.Sort = v),
|
||||
Child = Segments("sort", [("Name", "Name"), ("Typ", "Typ"), ("Datum", "Datum"), ("Meistgenutzt", "Häufig"), ("Eigene", "Eigene")],
|
||||
def.Sort, v => def.Sort = v),
|
||||
});
|
||||
|
||||
// Icon-Tönung
|
||||
|
||||
+6
-2
@@ -53,7 +53,7 @@
|
||||
|
||||
<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">
|
||||
@@ -84,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>
|
||||
|
||||
+119
-21
@@ -55,7 +55,8 @@ public partial class FenceWindow : Window
|
||||
SourceInitialized += (_, _) => PinToDesktop();
|
||||
MouseEnter += (_, _) => OnHover(true);
|
||||
MouseLeave += (_, _) => OnHover(false);
|
||||
Closed += (_, _) => _portalWatcher?.Dispose();
|
||||
Usage.Changed += OnUsageChanged;
|
||||
Closed += (_, _) => { _portalWatcher?.Dispose(); Usage.Changed -= OnUsageChanged; };
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@@ -215,29 +216,52 @@ public partial class FenceWindow : Window
|
||||
WatchPortal(portal ? folder : null);
|
||||
|
||||
List<IconItem> items = [];
|
||||
if (folder is not null && Directory.Exists(folder))
|
||||
if (tab?.IsRecent == true)
|
||||
items = Usage.Recent(RecentCount).Select(p => new IconItem { Path = p }).ToList();
|
||||
else if (folder is not null && Directory.Exists(folder))
|
||||
items = (portal ? PortalEntries(folder) : Storage.Entries(folder)).Select(p => new IconItem { Path = p }).ToList();
|
||||
items = Sort(items);
|
||||
items = Sort(items, tab);
|
||||
|
||||
int px = (int)Math.Round((double)Resources["FIcon"] * VisualTreeHelper.GetDpi(this).DpiScaleX);
|
||||
foreach (var i in items) IconLoader.Load(i, px);
|
||||
Items.ItemsSource = items;
|
||||
CountText.Text = items.Count.ToString();
|
||||
EmptyText.Text = folder is not null && !Directory.Exists(folder) ? "Ordner nicht gefunden" : "Leer – Icons hierher ziehen";
|
||||
EmptyText.Text = tab?.IsRecent == true ? "Noch nichts gestartet – was du über DeskCore öffnest, erscheint hier"
|
||||
: folder is not null && !Directory.Exists(folder) ? "Ordner nicht gefunden" : "Leer – Icons hierher ziehen";
|
||||
EmptyText.Visibility = items.Count == 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
/// <summary>Ordner-Portal zeigt alles außer versteckten/System-Dateien.</summary>
|
||||
private static IEnumerable<string> PortalEntries(string dir) => Storage.Entries(dir);
|
||||
|
||||
private List<IconItem> Sort(List<IconItem> items)
|
||||
/// <summary>Wie viele Einträge eine „Zuletzt benutzt“-Box zeigt.</summary>
|
||||
private const int RecentCount = 24;
|
||||
|
||||
private void OnUsageChanged()
|
||||
{
|
||||
// Zuletzt-Tab oder Sortierung nach Häufigkeit → neu ordnen (kurz verzögert, falls mehrere Starts kommen)
|
||||
if (Tab?.IsRecent == true || Def.Sort == "Häufig") Dispatcher.BeginInvoke(Refresh, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private List<IconItem> Sort(List<IconItem> items, FenceTab? tab)
|
||||
{
|
||||
// Zuletzt benutzt: neueste zuerst – außer es ist ausdrücklich "Häufig" gewählt
|
||||
if (tab?.IsRecent == true)
|
||||
return Def.Sort == "Häufig" ? items.OrderByDescending(i => Usage.Score(i.Path)).ToList() : items;
|
||||
// Eigene Reihenfolge: wie gezogen; Neues hinten dran (alphabetisch)
|
||||
if (Def.Sort == "Eigene" && tab is not null)
|
||||
{
|
||||
var order = tab.Order;
|
||||
return items.OrderBy(i => order.FindIndex(n => string.Equals(n, Path.GetFileName(i.Path), StringComparison.OrdinalIgnoreCase)) is var k and >= 0 ? k : int.MaxValue)
|
||||
.ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
}
|
||||
var folders = items.Where(i => Directory.Exists(i.Path));
|
||||
var files = items.Where(i => !Directory.Exists(i.Path));
|
||||
IEnumerable<IconItem> Order(IEnumerable<IconItem> x) => Def.Sort switch
|
||||
{
|
||||
"Typ" => x.OrderBy(i => Path.GetExtension(i.Path), StringComparer.OrdinalIgnoreCase).ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
|
||||
"Datum" => x.OrderByDescending(i => { try { return File.GetLastWriteTime(i.Path); } catch { return DateTime.MinValue; } }),
|
||||
"Häufig" => x.OrderByDescending(i => Usage.Score(i.Path)).ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
|
||||
_ => x.OrderBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase),
|
||||
};
|
||||
return Order(folders).Concat(Order(files)).ToList();
|
||||
@@ -250,13 +274,13 @@ public partial class FenceWindow : Window
|
||||
{
|
||||
var t = Def.Tabs[i];
|
||||
bool active = i == Math.Clamp(Def.ActiveTab, 0, Def.Tabs.Count - 1);
|
||||
var meta = t.IsFolder ? null : S.Categories.GetValueOrDefault(t.Value);
|
||||
var meta = t.IsCategory ? S.Categories.GetValueOrDefault(t.Value) : null;
|
||||
var glyph = new TextBlock
|
||||
{
|
||||
Text = t.IsFolder ? "" : meta?.Glyph ?? "",
|
||||
Text = t.IsRecent ? "" : t.IsFolder ? "" : meta?.Glyph ?? "",
|
||||
FontFamily = (FontFamily)FindResource("IconFont"), FontSize = 13, VerticalAlignment = VerticalAlignment.Center,
|
||||
Margin = new Thickness(0, 0, 7, 0),
|
||||
Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)FindResource(t.IsFolder ? "AccentBrush" : "TextBrush"),
|
||||
Foreground = CategoryColors.Brush(meta?.Color) ?? (Brush)FindResource(t.IsCategory ? "TextBrush" : "AccentBrush"),
|
||||
};
|
||||
var label = new TextBlock
|
||||
{
|
||||
@@ -275,7 +299,7 @@ public partial class FenceWindow : Window
|
||||
{
|
||||
Style = (Style)FindResource("SubtleButton"), Padding = new Thickness(6, 4, 6, 4), MinHeight = 0, Tag = i,
|
||||
Content = new Grid { Children = { content, underline } }, Margin = new Thickness(0, 0, 2, 0),
|
||||
ToolTip = t.IsFolder ? t.Value : $"Kategorie „{t.Value}“",
|
||||
ToolTip = t.IsRecent ? "Was du zuletzt über DeskCore gestartet hast" : t.IsFolder ? t.Value : $"Kategorie „{t.Value}“",
|
||||
};
|
||||
WindowChrome.SetIsHitTestVisibleInChrome(btn, Def.Tabs.Count > 1); // bei nur einem Tab: Titel = Ziehfläche
|
||||
btn.Click += (_, _) => SwitchTab((int)btn.Tag);
|
||||
@@ -344,7 +368,7 @@ public partial class FenceWindow : Window
|
||||
Refresh();
|
||||
return;
|
||||
}
|
||||
try { Process.Start(new ProcessStartInfo(i.Path) { UseShellExecute = true }); }
|
||||
try { Usage.Start(i.Path); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
@@ -383,7 +407,7 @@ public partial class FenceWindow : Window
|
||||
|
||||
bool inStorage = sel.All(i => Storage.IsInStorage(i.Path));
|
||||
var moveTo = MainWindow.MenuItem(inStorage ? "Verschieben nach" : "Sichern in", inStorage ? "" : "", null);
|
||||
var ownCategory = Tab is { IsFolder: false } t ? t.Value : null;
|
||||
var ownCategory = Tab is { IsCategory: true } t ? t.Value : null;
|
||||
foreach (var c in Storage.Categories().Where(c => !string.Equals(c, ownCategory, StringComparison.OrdinalIgnoreCase)))
|
||||
moveTo.Items.Add(MainWindow.MenuItem(c, S.Categories.GetValueOrDefault(c)?.Glyph ?? "",
|
||||
() => win.TransferAll(sel.Select(i => i.Path), Storage.CategoryPath(c))));
|
||||
@@ -423,18 +447,85 @@ public partial class FenceWindow : Window
|
||||
|
||||
private void Items_DragOver(object sender, DragEventArgs e)
|
||||
{
|
||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && !_dragging && CurrentFolder is not null
|
||||
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
|
||||
: DragDropEffects.None;
|
||||
if (_dragging)
|
||||
{
|
||||
// Innerhalb der eigenen Box: neu anordnen (nicht bei „Zuletzt benutzt“ – die ordnet sich selbst)
|
||||
e.Effects = Tab is { IsRecent: false } ? DragDropEffects.Copy : DragDropEffects.None;
|
||||
ShowDropMarker(e.Effects == DragDropEffects.None ? null : e.GetPosition(Items));
|
||||
}
|
||||
else
|
||||
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && CurrentFolder is not null
|
||||
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
|
||||
: DragDropEffects.None;
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void Items_DragLeave(object sender, DragEventArgs e) => ShowDropMarker(null);
|
||||
|
||||
private void Items_Drop(object sender, DragEventArgs e)
|
||||
{
|
||||
if (!_dragging && CurrentFolder is { } folder && e.Data.GetData(DataFormats.FileDrop) is string[] files)
|
||||
ShowDropMarker(null);
|
||||
if (_dragging)
|
||||
{
|
||||
if (Tab is { IsRecent: false } t) Reorder(t, Selected, e.GetPosition(Items));
|
||||
return;
|
||||
}
|
||||
if (CurrentFolder is { } folder && e.Data.GetData(DataFormats.FileDrop) is string[] files)
|
||||
App.Win?.TransferAll(files, folder);
|
||||
}
|
||||
|
||||
// ---------- Eigene Reihenfolge ----------
|
||||
|
||||
/// <summary>Einfügestelle an der Mausposition: vor welchem Eintrag landet das Gezogene? (Count = ans Ende)</summary>
|
||||
private (int Index, Rect? Marker) InsertionAt(Point p)
|
||||
{
|
||||
var list = Items.ItemsSource as List<IconItem> ?? [];
|
||||
for (int k = 0; k < list.Count; k++)
|
||||
{
|
||||
if (Items.ItemContainerGenerator.ContainerFromIndex(k) is not ListBoxItem c || !c.IsVisible) continue;
|
||||
var r = new Rect(c.TranslatePoint(new Point(0, 0), Items), c.RenderSize);
|
||||
if (p.Y < r.Top) return (k, new Rect(r.Left - 2, r.Top, 3, r.Height)); // Zeile darüber, ganz links
|
||||
if (p.Y <= r.Bottom && p.X < r.Left + r.Width / 2) return (k, new Rect(r.Left - 2, r.Top, 3, r.Height));
|
||||
}
|
||||
if (list.Count > 0 && Items.ItemContainerGenerator.ContainerFromIndex(list.Count - 1) is ListBoxItem last)
|
||||
{
|
||||
var r = new Rect(last.TranslatePoint(new Point(0, 0), Items), last.RenderSize);
|
||||
return (list.Count, new Rect(r.Right - 1, r.Top, 3, r.Height));
|
||||
}
|
||||
return (list.Count, null);
|
||||
}
|
||||
|
||||
private void ShowDropMarker(Point? p)
|
||||
{
|
||||
if (p is { } pos && InsertionAt(pos).Marker is { } m)
|
||||
{
|
||||
DropMarker.Margin = new Thickness(m.Left, m.Top, 0, 0);
|
||||
DropMarker.Height = m.Height;
|
||||
DropMarker.Visibility = Visibility.Visible;
|
||||
}
|
||||
else DropMarker.Visibility = Visibility.Collapsed;
|
||||
}
|
||||
|
||||
private void Reorder(FenceTab tab, List<IconItem> moved, Point drop)
|
||||
{
|
||||
var list = Items.ItemsSource as List<IconItem> ?? [];
|
||||
if (moved.Count == 0) return;
|
||||
int index = InsertionAt(drop).Index;
|
||||
// Einfügestelle in der Liste ohne die gezogenen Einträge
|
||||
int before = list.Take(index).Count(i => moved.Contains(i));
|
||||
var rest = list.Where(i => !moved.Contains(i)).ToList();
|
||||
rest.InsertRange(Math.Clamp(index - before, 0, rest.Count), moved);
|
||||
|
||||
tab.Order = rest.Select(i => Path.GetFileName(i.Path)).ToList();
|
||||
bool switched = Def.Sort != "Eigene";
|
||||
Def.Sort = "Eigene";
|
||||
S.Save();
|
||||
Refresh();
|
||||
foreach (var i in (Items.ItemsSource as List<IconItem> ?? []).Where(i => moved.Any(m => m.Path == i.Path)))
|
||||
Items.SelectedItems.Add(i);
|
||||
if (switched) App.TrayIcon?.Notify("Eigene Reihenfolge", "Die Box ist jetzt so sortiert, wie du die Icons ziehst. Ändern unter „Aussehen …“.");
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Menüs
|
||||
// =====================================================================
|
||||
@@ -448,15 +539,17 @@ public partial class FenceWindow : Window
|
||||
private void MenuButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
FenceMenu.Items.Clear();
|
||||
if (Tab is { IsFolder: false } t)
|
||||
if (Tab is { IsCategory: true } t)
|
||||
FenceMenu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
|
||||
else if (Tab is { IsFolder: true } pt)
|
||||
FenceMenu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{CurrentFolder ?? pt.Value}\"")));
|
||||
|
||||
// Tabs
|
||||
var add = MainWindow.MenuItem("Tab hinzufügen", "", null);
|
||||
foreach (var c in Storage.Categories().Where(c => !Def.Tabs.Any(x => !x.IsFolder && string.Equals(x.Value, c, StringComparison.OrdinalIgnoreCase))))
|
||||
foreach (var c in Storage.Categories().Where(c => !Def.Tabs.Any(x => x.IsCategory && string.Equals(x.Value, c, StringComparison.OrdinalIgnoreCase))))
|
||||
add.Items.Add(MainWindow.MenuItem(c, S.Categories.GetValueOrDefault(c)?.Glyph ?? "", () => AddTab(new FenceTab { Value = c })));
|
||||
if (!Def.Tabs.Any(x => x.IsRecent))
|
||||
add.Items.Add(MainWindow.MenuItem("Zuletzt benutzt", "", () => AddTab(new FenceTab { Kind = FenceTab.RecentKind })));
|
||||
if (add.Items.Count > 0) add.Items.Add(new Separator());
|
||||
add.Items.Add(MainWindow.MenuItem("Ordner-Portal…", "", () =>
|
||||
{
|
||||
@@ -475,14 +568,19 @@ public partial class FenceWindow : Window
|
||||
if (S.Pages.Count > 1)
|
||||
{
|
||||
var move = MainWindow.MenuItem("Auf Seite verschieben", "\uE8AB", null);
|
||||
var copy = MainWindow.MenuItem("Auf Seite kopieren", "\uE8C8", null);
|
||||
for (int p = 0; p < S.Pages.Count; p++)
|
||||
{
|
||||
int page = p;
|
||||
var mi = MainWindow.MenuItem(S.Pages[p], "\uE8A1", () => FenceManager.MoveToPage(Def, page));
|
||||
mi.IsEnabled = page != Def.Page;
|
||||
move.Items.Add(mi);
|
||||
var ci = MainWindow.MenuItem(S.Pages[p], "\uE8A1", () => FenceManager.CopyToPage(Def, page));
|
||||
ci.IsEnabled = page != Def.Page;
|
||||
copy.Items.Add(ci);
|
||||
}
|
||||
FenceMenu.Items.Add(move);
|
||||
FenceMenu.Items.Add(copy);
|
||||
}
|
||||
FenceMenu.Items.Add(new Separator());
|
||||
FenceMenu.Items.Add(MainWindow.MenuItem("Box schließen", "", () => FenceManager.Remove(Def)));
|
||||
@@ -495,8 +593,8 @@ public partial class FenceWindow : Window
|
||||
if (index < 0 || index >= Def.Tabs.Count) return;
|
||||
var t = Def.Tabs[index];
|
||||
var menu = new ContextMenu();
|
||||
if (!t.IsFolder) menu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
|
||||
else menu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{t.Value}\"")));
|
||||
if (t.IsCategory) menu.Items.Add(MainWindow.MenuItem("In DeskCore öffnen", "", () => App.Win?.ShowCategory(t.Value)));
|
||||
else if (t.IsFolder) menu.Items.Add(MainWindow.MenuItem("Im Explorer öffnen", "", () => Process.Start("explorer.exe", $"\"{t.Value}\"")));
|
||||
if (Def.Tabs.Count > 1)
|
||||
{
|
||||
menu.Items.Add(MainWindow.MenuItem("Als eigene Box lösen", "", () => FenceManager.DetachTab(Def, index)));
|
||||
@@ -507,8 +605,8 @@ public partial class FenceWindow : Window
|
||||
|
||||
private void AddTab(FenceTab tab)
|
||||
{
|
||||
// Kategorie steckt schon in einer anderen Box? Dann dorthin verschieben statt doppelt anzeigen
|
||||
if (!tab.IsFolder) FenceManager.RemoveCategoryTab(tab.Value, except: Def);
|
||||
// Kategorie steckt auf dieser Seite schon in einer anderen Box? Dann hierher holen statt doppelt anzeigen
|
||||
if (tab.IsCategory) FenceManager.RemoveCategoryTab(tab.Value, except: Def, Def.Page);
|
||||
Def.Tabs.Add(tab);
|
||||
Def.ActiveTab = Def.Tabs.Count - 1;
|
||||
_portalSub = null;
|
||||
|
||||
+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}"); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ public partial class MainWindow
|
||||
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();
|
||||
@@ -72,7 +73,7 @@ public partial class MainWindow
|
||||
if (!File.Exists(p) && !Directory.Exists(p)) { problems.Add($"„{Storage.DisplayName(p)}“ gibt es nicht mehr"); continue; }
|
||||
try
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(p) { UseShellExecute = true });
|
||||
Usage.Start(p);
|
||||
started++;
|
||||
}
|
||||
catch (Exception ex) { problems.Add($"„{Storage.DisplayName(p)}“: {ex.Message}"); }
|
||||
@@ -206,6 +207,7 @@ public partial class MainWindow
|
||||
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; });
|
||||
|
||||
+132
-2
@@ -1,3 +1,4 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
@@ -23,6 +24,10 @@ public partial class MainWindow
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -46,7 +51,7 @@ public partial class MainWindow
|
||||
DesktopTintSwatches.Children.Add(rb);
|
||||
}
|
||||
AddSwatch(null, Brushes.Transparent, "Keine Tönung");
|
||||
foreach (var (name, hex) in CategoryColors.All.Concat([("Grau", "#8A8A8A"), ("Weiß", "#F5F5F5")]))
|
||||
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;
|
||||
@@ -106,11 +111,134 @@ public partial class MainWindow
|
||||
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.Pages[i] = name.Trim();
|
||||
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;
|
||||
@@ -126,6 +254,8 @@ public partial class MainWindow
|
||||
"Löschen"))
|
||||
return;
|
||||
FenceManager.DeletePage(i);
|
||||
S.ForgetPage(name);
|
||||
S.Save();
|
||||
DesktopToggle.Apply();
|
||||
Refresh();
|
||||
LoadDesktopSettings();
|
||||
|
||||
@@ -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.";
|
||||
}
|
||||
+24
-3
@@ -27,7 +27,8 @@
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<!-- Anzahl bzw. Zieh-Griff teilen sich diese Spalte – feste Mindestbreite, damit der Griff nicht ins Box-Symbol ragt -->
|
||||
<ColumnDefinition Width="Auto" MinWidth="30"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Text="{Binding Glyph}" FontFamily="{DynamicResource IconFont}" FontSize="16"
|
||||
VerticalAlignment="Center" Margin="0,0,14,0">
|
||||
@@ -74,7 +75,7 @@
|
||||
</TextBlock>
|
||||
<!-- Griff zum Umsortieren (nur beim Überfahren) -->
|
||||
<TextBlock Grid.Column="3" Text="" FontFamily="{DynamicResource IconFont}" FontSize="14" Cursor="SizeNS"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}" VerticalAlignment="Center" HorizontalAlignment="Right"
|
||||
Foreground="{DynamicResource TextSecondaryBrush}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="10,0,0,0"
|
||||
ToolTip="Ziehen zum Umsortieren (oder Alt+↑/↓)">
|
||||
<TextBlock.Style>
|
||||
<Style TargetType="TextBlock">
|
||||
@@ -648,6 +649,23 @@
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Schnellstart-Suche" Style="{StaticResource SectionTitle}"/>
|
||||
<Border Style="{StaticResource Card}">
|
||||
<StackPanel>
|
||||
<DockPanel>
|
||||
<CheckBox x:Name="QuickSearchToggle" DockPanel.Dock="Right" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="QuickSearchToggle_Click"/>
|
||||
<TextBox x:Name="QuickSearchHotkeyBox" DockPanel.Dock="Right" Width="200" IsReadOnly="True" Margin="0,0,16,0" IsReadOnlyCaretVisible="False"
|
||||
VerticalAlignment="Center" PreviewKeyDown="QuickSearchHotkeyBox_PreviewKeyDown" ToolTip="Hier klicken und neue Tastenkombination drücken"/>
|
||||
<TextBlock DockPanel.Dock="Left" Text="" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
|
||||
<StackPanel VerticalAlignment="Center" Margin="0,0,16,0">
|
||||
<TextBlock Text="Programme blitzschnell starten"/>
|
||||
<TextBlock x:Name="QuickSearchHint" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<CheckBox x:Name="QuickSearchStartMenuBox" Content="Auch Programme aus dem Windows-Startmenü finden" Margin="34,12,0,0" Click="QuickSearchStartMenu_Click"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="Desktop-Boxen" Style="{StaticResource SectionTitle}"/>
|
||||
<Border Style="{StaticResource Card}">
|
||||
<DockPanel>
|
||||
@@ -700,7 +718,7 @@
|
||||
<StackPanel VerticalAlignment="Center" Margin="0,0,16,0">
|
||||
<TextBlock Text="Desktop-Seiten"/>
|
||||
<TextBlock x:Name="PagesHint" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"
|
||||
Text="Jede Seite hat ihre eigenen Boxen. Wechseln mit Win + Alt + ← / →, mit dem Mausrad über dem freien Desktop oder über das Symbol neben der Uhr."/>
|
||||
Text="Jede Seite hat ihre eigenen Boxen. Wechseln mit Win + Alt + ← / →, mit dem Mausrad über dem freien Desktop oder über das Symbol neben der Uhr. Über die Uhr bei einer Seite stellst du einen Zeitplan und ein eigenes Hintergrundbild ein."/>
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
<ItemsControl x:Name="PageList" Margin="34,10,0,0">
|
||||
@@ -709,6 +727,9 @@
|
||||
<DockPanel Margin="0,4,0,0">
|
||||
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal">
|
||||
<Button Content="Anzeigen" Click="ShowPage_Click" Tag="{Binding Index}" Margin="0,0,6,0"/>
|
||||
<Button Style="{StaticResource SubtleButton}" Click="PageSchedule_Click" Tag="{Binding Index}" ToolTip="Zeitplan und Hintergrundbild">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14"/>
|
||||
</Button>
|
||||
<Button Style="{StaticResource SubtleButton}" Click="RenamePage_Click" Tag="{Binding Index}" ToolTip="Umbenennen">
|
||||
<TextBlock Text="" FontFamily="{DynamicResource IconFont}" FontSize="14"/>
|
||||
</Button>
|
||||
|
||||
+12
-2
@@ -314,7 +314,9 @@ public partial class MainWindow : Window
|
||||
SetEmpty(cat.Glyph, "Noch leer", $"Ziehe Icons vom Desktop hierher oder auf „{cat.Name}“ in der Seitenleiste.");
|
||||
}
|
||||
|
||||
items = items.OrderBy(i => !Directory.Exists(i.Path))
|
||||
items = (S.MainSort == "Häufig"
|
||||
? items.OrderBy(i => !Directory.Exists(i.Path)).ThenByDescending(i => Usage.Score(i.Path))
|
||||
: items.OrderBy(i => !Directory.Exists(i.Path)))
|
||||
.ThenBy(i => i.Name, StringComparer.CurrentCultureIgnoreCase).ToList();
|
||||
var px = IconPixels;
|
||||
foreach (var item in items) IconLoader.Load(item, px);
|
||||
@@ -456,6 +458,13 @@ public partial class MainWindow : Window
|
||||
if (S.View == mode) mi.InputGestureText = "✓";
|
||||
ViewMenu.Items.Add(mi);
|
||||
}
|
||||
ViewMenu.Items.Add(new Separator());
|
||||
foreach (var (sort, label) in new[] { ("Name", "Nach Name sortieren"), ("Häufig", "Meistgenutzt zuerst") })
|
||||
{
|
||||
var mi = MenuItem(label, "", () => { S.MainSort = sort; S.Save(); Refresh(); }, bold: S.MainSort == sort);
|
||||
if (S.MainSort == sort) mi.InputGestureText = "✓";
|
||||
ViewMenu.Items.Add(mi);
|
||||
}
|
||||
ViewMenu.PlacementTarget = ViewButton;
|
||||
ViewMenu.IsOpen = true;
|
||||
}
|
||||
@@ -803,7 +812,7 @@ public partial class MainWindow : Window
|
||||
|
||||
private void Open(string path)
|
||||
{
|
||||
try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
|
||||
try { Usage.Start(path); }
|
||||
catch (Exception ex) { Error("Öffnen fehlgeschlagen", $"„{Path.GetFileName(path)}“ konnte nicht geöffnet werden.\n\n{ex.Message}"); }
|
||||
}
|
||||
|
||||
@@ -1208,6 +1217,7 @@ public partial class MainWindow : Window
|
||||
DeadShortcutsToggle.IsChecked = S.RemoveDeadShortcuts;
|
||||
LoadFenceSettings();
|
||||
LoadDesktopSettings();
|
||||
LoadQuickSearchSettings();
|
||||
AutoRemoveToggle.IsChecked = S.AutoSortRemoveFromDesktop;
|
||||
RulesList.ItemsSource = null;
|
||||
RulesList.ItemsSource = S.Rules;
|
||||
|
||||
@@ -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,419 +1,487 @@
|
||||
<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)
|
||||
- [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 |
|
||||
| ▶️ **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“ |
|
||||
| 🎨 **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. **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“**.
|
||||
|
||||
**Update auf eine neue Version:** Einfach den neuen Installer starten. 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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
Das **Menü einer Box** (die drei Punkte ⋯ oben rechts) bietet:
|
||||
|
||||
| Menüpunkt | Was passiert |
|
||||
|---|---|
|
||||
| **Tab hinzufügen** | Eine weitere Kategorie 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, Sortierung und Icon-Tönung der Box |
|
||||
| **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 |
|
||||
| **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.
|
||||
|
||||
### 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“**.
|
||||
|
||||
> Die Icons, die direkt auf dem Desktop liegen (nicht in einer Box), siehst du auf jeder Seite.
|
||||
|
||||
---
|
||||
|
||||
## 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 |
|
||||
|---|---|
|
||||
| **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 …“**.
|
||||
|
||||
**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 |
|
||||
|
||||
---
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -26,6 +26,15 @@ public static class Screens
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
+91
-10
@@ -40,15 +40,45 @@ public class SortRule
|
||||
[JsonIgnore] public string KindText => Kind == "Typ" ? "Dateityp ist" : "Name enthält";
|
||||
}
|
||||
|
||||
/// <summary>Ein Tab einer Desktop-Box: eine Kategorie oder ein beliebiger Ordner ("Ordner-Portal").</summary>
|
||||
/// <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";
|
||||
public const string CategoryKind = "Kategorie", FolderKind = "Ordner", RecentKind = "Zuletzt";
|
||||
public string Kind { get; set; } = CategoryKind;
|
||||
/// <summary>Kategoriename bzw. Ordnerpfad.</summary>
|
||||
/// <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 string Title => IsFolder ? Path.GetFileName(Value.TrimEnd('\\')) is { Length: > 0 } n ? n : Value : Value;
|
||||
[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>
|
||||
@@ -71,7 +101,7 @@ public class FenceDef
|
||||
/// <summary>Milchglas-Effekt (Hintergrund verschwommen).</summary>
|
||||
public bool Blur { get; set; }
|
||||
public ViewMode IconSize { get; set; } = ViewMode.Medium;
|
||||
/// <summary>"Name", "Typ" oder "Datum"</summary>
|
||||
/// <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; }
|
||||
@@ -128,6 +158,22 @@ public class AppSettings
|
||||
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;
|
||||
@@ -191,6 +237,8 @@ public class AppSettings
|
||||
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);
|
||||
|
||||
@@ -257,11 +305,12 @@ public class AppSettings
|
||||
g.Items[i] = newDir + g.Items[i][oldDir.Length..];
|
||||
}
|
||||
|
||||
/// <summary>Wird diese Kategorie in irgendeiner Box angezeigt?</summary>
|
||||
/// <summary>Liegt diese Kategorie auf der gerade angezeigten Desktop-Seite in einer Box?</summary>
|
||||
public bool HasFence(string category) =>
|
||||
Fences.Any(f => f.Tabs.Any(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
|
||||
Fences.Any(f => f.Page == CurrentPage &&
|
||||
f.Tabs.Any(t => t.IsCategory && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
/// <summary>Kategorie als eigene Box anzeigen bzw. aus allen Boxen entfernen.</summary>
|
||||
/// <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)
|
||||
@@ -271,8 +320,8 @@ public class AppSettings
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var f in Fences)
|
||||
f.Tabs.RemoveAll(t => !t.IsFolder && string.Equals(t.Value, category, StringComparison.OrdinalIgnoreCase));
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -288,9 +337,41 @@ public class AppSettings
|
||||
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)
|
||||
|
||||
@@ -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 { }
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
"compilationOptions": {},
|
||||
"targets": {
|
||||
".NETCoreApp,Version=v9.0": {
|
||||
"DeskCore/1.1.0": {
|
||||
"DeskCore/1.0.3": {
|
||||
"runtime": {
|
||||
"DeskCore.dll": {}
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
}
|
||||
},
|
||||
"libraries": {
|
||||
"DeskCore/1.1.0": {
|
||||
"DeskCore/1.0.3": {
|
||||
"type": "project",
|
||||
"serviceable": false,
|
||||
"sha512": ""
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user