Fortschritt, .NET-Prüfung, Updates, Fenster merken, Box-Funktionen

- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen;
  abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert
- Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien
- Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime
  und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft)
- Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart
- Fenstergröße und -position werden gemerkt
- Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen
- Fenster und Boxen kehren auf sichtbare Bildschirme zurück
- README ergänzt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 20:46:48 +02:00
co-authored by Claude Opus 5.5
parent 77bb3e6b9d
commit 1ae6e1abc2
19 changed files with 1098 additions and 95 deletions
+11
View File
@@ -79,6 +79,15 @@ public partial class App : Application
bool minimized = e.Args.Contains("--minimized", StringComparer.OrdinalIgnoreCase); bool minimized = e.Args.Contains("--minimized", StringComparer.OrdinalIgnoreCase);
if (!minimized || !AppSettings.Current.TrayEnabled) Win.Show(); if (!minimized || !AppSettings.Current.TrayEnabled) Win.Show();
FenceManager.Sync(); FenceManager.Sync();
// Bildschirm abgesteckt/Auflösung geändert → Fenster und Boxen wieder in den sichtbaren Bereich holen
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += (_, _) => Dispatcher.BeginInvoke(() =>
{
Win.EnsureOnScreen();
FenceManager.EnsureOnScreen();
});
Win.CheckForUpdatesOnStartup();
} }
/// <summary>Tastenkürzel neu registrieren. false = von anderem Programm belegt.</summary> /// <summary>Tastenkürzel neu registrieren. false = von anderem Programm belegt.</summary>
@@ -99,6 +108,8 @@ public partial class App : Application
{ {
if (Quitting) return; if (Quitting) return;
Quitting = true; Quitting = true;
Win?.CancelBusyAndWait(); // z. B. große Kopie abbrechen, halbe Dateien aufräumen
Win?.SavePlacement();
Win?.Undo.FlushAll(); Win?.Undo.FlushAll();
FenceManager.CloseAll(); FenceManager.CloseAll();
_watcher?.Dispose(); _watcher?.Dispose();
+102 -35
View File
@@ -4,17 +4,20 @@ using System.Text.Json;
namespace DeskCore; namespace DeskCore;
/// <summary>Export/Import aller Kategorien samt Einstellungen als ZIP.</summary> /// <summary>Export/Import aller Kategorien samt Einstellungen als ZIP – mit Fortschritt und Abbrechen.</summary>
public static class Backup public static class Backup
{ {
private const string CategoriesPrefix = "Kategorien/"; private const string CategoriesPrefix = "Kategorien/";
private const string SettingsEntry = "einstellungen.json"; private const string SettingsEntry = "einstellungen.json";
private const int Chunk = 1 << 20;
public static long StorageSize() public static long StorageSize() => StorageSize(Storage.Root);
public static long StorageSize(string root)
{ {
try try
{ {
return Directory.EnumerateFiles(Storage.Root, "*", SearchOption.AllDirectories) return Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories)
.Sum(f => { try { return new FileInfo(f).Length; } catch { return 0L; } }); .Sum(f => { try { return new FileInfo(f).Length; } catch { return 0L; } });
} }
catch { return 0; } catch { return 0; }
@@ -27,41 +30,93 @@ public static class Backup
_ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB", _ => $"{bytes / 1024.0 / 1024 / 1024:0.##} GB",
}; };
/// <summary>Gibt die Anzahl gesicherter Dateien zurück.</summary> /// <summary>Sichert alle Kategorien. Gibt die Anzahl gesicherter Dateien zurück.</summary>
public static int Export(string zipPath) public static int Export(string zipPath, Action<long>? onBytes = null, CancellationToken ct = default) =>
Export(zipPath, Storage.Root, Storage.Categories(), JsonSerializer.Serialize(AppSettings.Current, AppSettings.Json), onBytes, ct);
public static int Export(string zipPath, string root, IEnumerable<string> categories, string settingsJson,
Action<long>? onBytes = null, CancellationToken ct = default)
{ {
if (File.Exists(zipPath)) File.Delete(zipPath); var tmp = zipPath + ".teil";
int count = 0; int count = 0;
using var zip = ZipFile.Open(zipPath, ZipArchiveMode.Create); try
foreach (var cat in Storage.Categories())
{ {
var dir = Storage.CategoryPath(cat); if (File.Exists(tmp)) File.Delete(tmp);
zip.CreateEntry(CategoriesPrefix + cat + "/"); // auch leere Kategorien sichern // große Sicherungen bestehen meist aus Videos/Bildern → stärkere Kompression bringt kaum etwas
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories)) var level = StorageSize(root) > 1L << 30 ? CompressionLevel.Fastest : CompressionLevel.Optimal;
using (var zip = ZipFile.Open(tmp, ZipArchiveMode.Create))
{ {
var rel = Path.GetRelativePath(Storage.Root, f).Replace('\\', '/'); foreach (var cat in categories)
zip.CreateEntryFromFile(f, CategoriesPrefix + rel, CompressionLevel.Optimal); {
count++; var dir = Path.Combine(root, cat);
zip.CreateEntry(CategoriesPrefix + cat + "/"); // auch leere Kategorien sichern
foreach (var f in Directory.EnumerateFiles(dir, "*", SearchOption.AllDirectories))
{
ct.ThrowIfCancellationRequested();
var rel = Path.GetRelativePath(root, f).Replace('\\', '/');
var entry = zip.CreateEntry(CategoriesPrefix + rel, level);
entry.LastWriteTime = File.GetLastWriteTime(f);
using (var input = new FileStream(f, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, Chunk, FileOptions.SequentialScan))
using (var output = entry.Open())
CopyWithProgress(input, output, onBytes, ct);
count++;
}
foreach (var d in Directory.EnumerateDirectories(dir, "*", SearchOption.AllDirectories))
if (!Directory.EnumerateFileSystemEntries(d).Any())
zip.CreateEntry(CategoriesPrefix + Path.GetRelativePath(root, d).Replace('\\', '/') + "/");
}
var se = zip.CreateEntry(SettingsEntry);
using (var w = new StreamWriter(se.Open()))
w.Write(settingsJson);
} }
foreach (var d in Directory.EnumerateDirectories(dir, "*", SearchOption.AllDirectories)) // erst am Ende an den richtigen Namen – so bleibt nie eine halbe Sicherung mit gültigem Namen liegen
if (!Directory.EnumerateFileSystemEntries(d).Any()) if (File.Exists(zipPath)) File.Delete(zipPath);
zip.CreateEntry(CategoriesPrefix + Path.GetRelativePath(Storage.Root, d).Replace('\\', '/') + "/"); File.Move(tmp, zipPath);
return count;
} }
var entry = zip.CreateEntry(SettingsEntry); catch
using (var w = new StreamWriter(entry.Open())) {
w.Write(JsonSerializer.Serialize(AppSettings.Current, AppSettings.Json)); try { if (File.Exists(tmp)) File.Delete(tmp); } catch { }
return count; throw;
}
}
/// <summary>Summe der unkomprimierten Größen (für die Fortschrittsanzeige beim Wiederherstellen).</summary>
public static long ArchiveSize(string zipPath)
{
using var zip = ZipFile.OpenRead(zipPath);
return zip.Entries.Sum(e => e.Length);
} }
/// <summary>Führt eine Sicherung mit dem Bestand zusammen. Vorhandenes wird nicht überschrieben.</summary> /// <summary>Führt eine Sicherung mit dem Bestand zusammen. Vorhandenes wird nicht überschrieben.</summary>
public static (int Added, int Skipped) Import(string zipPath) public static (int Added, int Skipped) Import(string zipPath, Action<long>? onBytes = null, CancellationToken ct = default)
{
var (added, skipped, settings) = Import(zipPath, Storage.Root, onBytes, ct);
if (settings is not null) Merge(settings);
return (added, skipped);
}
public static (int Added, int Skipped, AppSettings? Settings) Import(string zipPath, string root,
Action<long>? onBytes = null, CancellationToken ct = default)
{ {
int added = 0, skipped = 0; int added = 0, skipped = 0;
AppSettings? settings = null;
using var zip = ZipFile.OpenRead(zipPath); using var zip = ZipFile.OpenRead(zipPath);
var root = Path.GetFullPath(Storage.Root); root = Path.GetFullPath(root);
foreach (var e in zip.Entries) foreach (var e in zip.Entries)
{ {
ct.ThrowIfCancellationRequested();
if (e.FullName.Equals(SettingsEntry, StringComparison.OrdinalIgnoreCase))
{
try
{
using var r = new StreamReader(e.Open());
settings = JsonSerializer.Deserialize<AppSettings>(r.ReadToEnd(), AppSettings.Json);
}
catch { }
continue;
}
if (!e.FullName.StartsWith(CategoriesPrefix, StringComparison.OrdinalIgnoreCase)) continue; if (!e.FullName.StartsWith(CategoriesPrefix, StringComparison.OrdinalIgnoreCase)) continue;
var rel = e.FullName[CategoriesPrefix.Length..]; var rel = e.FullName[CategoriesPrefix.Length..];
if (rel.Length == 0) continue; if (rel.Length == 0) continue;
@@ -73,23 +128,35 @@ public static class Backup
Directory.CreateDirectory(target); Directory.CreateDirectory(target);
continue; continue;
} }
if (File.Exists(target)) { skipped++; continue; } if (File.Exists(target)) { skipped++; onBytes?.Invoke(e.Length); continue; }
Directory.CreateDirectory(Path.GetDirectoryName(target)!); Directory.CreateDirectory(Path.GetDirectoryName(target)!);
e.ExtractToFile(target);
added++;
}
if (zip.GetEntry(SettingsEntry) is { } se)
{
try try
{ {
using var r = new StreamReader(se.Open()); using (var input = e.Open())
var imported = JsonSerializer.Deserialize<AppSettings>(r.ReadToEnd(), AppSettings.Json); using (var output = new FileStream(target, FileMode.CreateNew, FileAccess.Write, FileShare.None, Chunk))
if (imported is not null) Merge(imported); CopyWithProgress(input, output, onBytes, ct);
File.SetLastWriteTime(target, e.LastWriteTime.DateTime);
} }
catch { } catch (OperationCanceledException)
{
try { File.Delete(target); } catch { } // halbe Datei nicht liegen lassen
throw;
}
added++;
}
return (added, skipped, settings);
}
private static void CopyWithProgress(Stream input, Stream output, Action<long>? onBytes, CancellationToken ct)
{
var buffer = new byte[Chunk];
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ct.ThrowIfCancellationRequested();
output.Write(buffer, 0, read);
onBytes?.Invoke(read);
} }
return (added, skipped);
} }
private static void Merge(AppSettings from) private static void Merge(AppSettings from)
+1
View File
@@ -38,6 +38,7 @@
<ListBox x:Name="Items" Padding="6,0,6,8" SelectionMode="Extended" ItemContainerStyle="{StaticResource TileItem}" <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" Drop="Items_Drop"
MouseDoubleClick="Items_MouseDoubleClick" KeyDown="Items_KeyDown" MouseDoubleClick="Items_MouseDoubleClick" KeyDown="Items_KeyDown"
PreviewMouseLeftButtonDown="Items_PreviewMouseLeftButtonDown" PreviewMouseMove="Items_PreviewMouseMove"
ContextMenuOpening="Items_ContextMenuOpening"> ContextMenuOpening="Items_ContextMenuOpening">
<ListBox.ContextMenu> <ListBox.ContextMenu>
<ContextMenu x:Name="ItemMenu"/> <ContextMenu x:Name="ItemMenu"/>
+78 -10
View File
@@ -51,15 +51,34 @@ public partial class FenceWindow : Window
{ {
_applying = true; _applying = true;
var m = Meta; var m = Meta;
var wa = SystemParameters.WorkArea;
Width = Math.Max(MinWidth, m.FenceW); Width = Math.Max(MinWidth, m.FenceW);
Height = m.FenceCollapsed ? CollapsedHeight : Math.Max(120, m.FenceH); Height = m.FenceCollapsed ? CollapsedHeight : Math.Max(120, m.FenceH);
Left = Math.Clamp(m.FenceX ?? wa.Left + 24, wa.Left - Width + 80, wa.Right - 80); var wa = Screens.PrimaryWorkArea;
Top = Math.Clamp(m.FenceY ?? wa.Top + 24, wa.Top, wa.Bottom - 40); double left = m.FenceX ?? wa.Right - Width - 24, top = m.FenceY ?? wa.Top + 24;
// Bildschirm abgesteckt? → zurück auf den Hauptbildschirm (oben rechts)
if (!Screens.IsReachable(new Rect(left, top, Width, Height)))
{
left = wa.Right - Width - 24;
top = wa.Top + 24;
m.FenceX = left;
m.FenceY = top;
}
Left = left;
Top = top;
Body.Visibility = m.FenceCollapsed ? Visibility.Collapsed : Visibility.Visible; Body.Visibility = m.FenceCollapsed ? Visibility.Collapsed : Visibility.Visible;
_applying = false; _applying = false;
} }
/// <summary>Nach Bildschirm-Änderungen: Box wieder sichtbar machen.</summary>
public void EnsureOnScreen()
{
if (Screens.IsReachable(new Rect(Left, Top, ActualWidth, ActualHeight))) return;
Meta.FenceX = null;
Meta.FenceY = null;
ApplyBounds();
AppSettings.Current.Save();
}
private void RememberBounds() private void RememberBounds()
{ {
if (_applying || !IsLoaded) return; if (_applying || !IsLoaded) return;
@@ -114,20 +133,40 @@ public partial class FenceWindow : Window
private void Items_KeyDown(object sender, KeyEventArgs e) private void Items_KeyDown(object sender, KeyEventArgs e)
{ {
if (e.Key == Key.Enter) Selected.ForEach(i => Open(i.Path)); var sel = Selected;
if (sel.Count == 0 || App.Win is not { } win) return;
switch (e.Key)
{
case Key.Enter: sel.ForEach(i => Open(i.Path)); e.Handled = true; break;
case Key.Delete: win.DeleteIcons(sel); e.Handled = true; break;
case Key.F2 when sel.Count == 1: win.RenameIcon(sel[0]); e.Handled = true; break;
}
} }
private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e) private void Items_ContextMenuOpening(object sender, ContextMenuEventArgs e)
{ {
var sel = Selected; var sel = Selected;
if (sel.Count == 0) { e.Handled = true; return; } if (sel.Count == 0 || App.Win is not { } win) { e.Handled = true; return; }
ItemMenu.Items.Clear(); ItemMenu.Items.Clear();
ItemMenu.Items.Add(MainWindow.MenuItem("Öffnen", "", () => sel.ForEach(i => Open(i.Path)), bold: true)); ItemMenu.Items.Add(MainWindow.MenuItem("Öffnen", "\uE8A7", () => sel.ForEach(i => Open(i.Path)), bold: true, gesture: "Enter"));
if (sel.Count == 1) if (sel.Count == 1)
ItemMenu.Items.Add(MainWindow.MenuItem("Dateispeicherort öffnen", "", () => {
ItemMenu.Items.Add(MainWindow.MenuItem("Dateispeicherort öffnen", "\uED25", () =>
Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\""))); Process.Start("explorer.exe", $"/select,\"{sel[0].Path}\"")));
ItemMenu.Items.Add(MainWindow.MenuItem("Umbenennen", "\uE70F", () => win.RenameIcon(sel[0]), gesture: "F2"));
}
ItemMenu.Items.Add(new Separator()); ItemMenu.Items.Add(new Separator());
ItemMenu.Items.Add(MainWindow.MenuItem("In DeskCore zeigen", "", ShowInApp));
var moveTo = MainWindow.MenuItem("Verschieben nach", "\uE8DE", null);
foreach (var c in Storage.Categories().Where(c => !string.Equals(c, Category, StringComparison.OrdinalIgnoreCase)))
moveTo.Items.Add(MainWindow.MenuItem(c, AppSettings.Current.Categories.GetValueOrDefault(c)?.Glyph ?? "\uF12B",
() => win.TransferAll(sel.Select(i => i.Path), Storage.CategoryPath(c))));
if (moveTo.Items.Count > 0) ItemMenu.Items.Add(moveTo);
ItemMenu.Items.Add(MainWindow.MenuItem("Zurück auf den Desktop legen", "\uE7F4",
() => win.TransferAll(sel.Select(i => i.Path), Storage.UserDesktop)));
ItemMenu.Items.Add(MainWindow.MenuItem("Löschen", "\uE74D", () => win.DeleteIcons(sel), gesture: "Entf"));
ItemMenu.Items.Add(new Separator());
ItemMenu.Items.Add(MainWindow.MenuItem("In DeskCore zeigen", "\uE8A7", ShowInApp));
} }
private void MenuButton_Click(object sender, RoutedEventArgs e) private void MenuButton_Click(object sender, RoutedEventArgs e)
@@ -149,9 +188,33 @@ public partial class FenceWindow : Window
private void ShowInApp() => App.Win?.ShowCategory(Category); private void ShowInApp() => App.Win?.ShowCategory(Category);
private Point _dragStart;
private bool _dragging;
private void Items_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e) => _dragStart = e.GetPosition(null);
private void Items_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || _dragging) return;
var d = e.GetPosition(null) - _dragStart;
if (Math.Abs(d.X) < SystemParameters.MinimumHorizontalDragDistance &&
Math.Abs(d.Y) < SystemParameters.MinimumVerticalDragDistance) return;
if (MainWindow.FindParent<ListBoxItem>(e.OriginalSource as DependencyObject) is null) return;
var paths = Selected.Select(i => i.Path).ToArray();
if (paths.Length == 0) return;
_dragging = true;
try
{
// nur "Kopieren" erlauben: der Explorer soll die Datei nicht aus der Kategorie entfernen.
// In eine andere Box/Kategorie gezogen, verschiebt DeskCore selbst.
DragDrop.DoDragDrop(Items, new DataObject(DataFormats.FileDrop, paths), DragDropEffects.Copy);
}
finally { _dragging = false; }
}
private void Items_DragOver(object sender, DragEventArgs e) private void Items_DragOver(object sender, DragEventArgs e)
{ {
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) && !_dragging
? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move) ? (e.AllowedEffects.HasFlag(DragDropEffects.Copy) ? DragDropEffects.Copy : DragDropEffects.Move)
: DragDropEffects.None; : DragDropEffects.None;
e.Handled = true; e.Handled = true;
@@ -159,7 +222,7 @@ public partial class FenceWindow : Window
private void Items_Drop(object sender, DragEventArgs e) private void Items_Drop(object sender, DragEventArgs e)
{ {
if (e.Data.GetData(DataFormats.FileDrop) is string[] files) if (!_dragging && e.Data.GetData(DataFormats.FileDrop) is string[] files)
App.Win?.TransferAll(files, Storage.CategoryPath(Category)); App.Win?.TransferAll(files, Storage.CategoryPath(Category));
} }
@@ -265,6 +328,11 @@ public static class FenceManager
foreach (var f in _open) f.Refresh(); foreach (var f in _open) f.Refresh();
} }
public static void EnsureOnScreen()
{
foreach (var f in _open) f.EnsureOnScreen();
}
public static void Renamed(string oldName, string newName) public static void Renamed(string oldName, string newName)
{ {
_open.FirstOrDefault(x => string.Equals(x.Category, oldName, StringComparison.OrdinalIgnoreCase))?.Rename(newName); _open.FirstOrDefault(x => string.Equals(x.Category, oldName, StringComparison.OrdinalIgnoreCase))?.Rename(newName);
+244
View File
@@ -0,0 +1,244 @@
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}"); }
}
}
+51
View File
@@ -343,6 +343,22 @@
</Button> </Button>
</StackPanel> </StackPanel>
<!-- Update verfügbar -->
<Border x:Name="UpdateBar" DockPanel.Dock="Top" Margin="36,20,36,0" Padding="16,10" CornerRadius="6" Visibility="Collapsed"
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentBrush}" BorderThickness="1">
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="&#xE896;" FontFamily="{DynamicResource IconFont}" FontSize="18"
Foreground="{DynamicResource AccentBrush}" VerticalAlignment="Center" Margin="0,0,14,0"/>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="UpdateNowButton" Content="Jetzt aktualisieren" Style="{StaticResource AccentButton}" Click="UpdateNow_Click" Margin="12,0,6,0"/>
<Button Content="Später" Style="{StaticResource SubtleButton}" Click="UpdateLater_Click" Margin="0,0,2,0"/>
<Button Content="Überspringen" Style="{StaticResource SubtleButton}" Click="UpdateSkip_Click"
ToolTip="Diese Version nicht mehr anzeigen"/>
</StackPanel>
<TextBlock x:Name="UpdateText" VerticalAlignment="Center" TextWrapping="Wrap"/>
</DockPanel>
</Border>
<!-- Hinweis (InfoBar) --> <!-- Hinweis (InfoBar) -->
<Border x:Name="InfoBar" DockPanel.Dock="Top" Margin="36,20,36,8" Padding="16,12" CornerRadius="6" <Border x:Name="InfoBar" DockPanel.Dock="Top" Margin="36,20,36,8" Padding="16,12" CornerRadius="6"
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1"> Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
@@ -567,6 +583,22 @@
</DockPanel> </DockPanel>
</Border> </Border>
<TextBlock Text="Updates" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource Card}">
<DockPanel>
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" VerticalAlignment="Center">
<Button x:Name="CheckUpdateButton" Content="Jetzt prüfen" Click="CheckUpdate_Click" Margin="0,0,16,0"/>
<CheckBox x:Name="AutoUpdateToggle" Style="{StaticResource ToggleSwitch}" VerticalAlignment="Center" Click="AutoUpdateToggle_Click"/>
</StackPanel>
<TextBlock DockPanel.Dock="Left" Text="&#xE896;" FontFamily="{DynamicResource IconFont}" FontSize="18" VerticalAlignment="Center" Margin="0,0,16,0"/>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Automatisch nach Updates suchen"/>
<TextBlock x:Name="UpdateStatusText" FontSize="12" TextWrapping="Wrap" Foreground="{DynamicResource TextSecondaryBrush}"
Text="Einmal täglich beim Start – du wirst gefragt, bevor etwas installiert wird"/>
</StackPanel>
</DockPanel>
</Border>
<TextBlock Text="Info" Style="{StaticResource SectionTitle}"/> <TextBlock Text="Info" Style="{StaticResource SectionTitle}"/>
<Border Style="{StaticResource Card}"> <Border Style="{StaticResource Card}">
<DockPanel> <DockPanel>
@@ -621,5 +653,24 @@
<!-- Abdunkeln bei offenem Dialog --> <!-- Abdunkeln bei offenem Dialog -->
<Border x:Name="DimOverlay" Grid.RowSpan="2" Grid.ColumnSpan="2" Background="{DynamicResource OverlayBrush}" Visibility="Collapsed"/> <Border x:Name="DimOverlay" Grid.RowSpan="2" Grid.ColumnSpan="2" Background="{DynamicResource OverlayBrush}" Visibility="Collapsed"/>
<!-- Fortschritt bei langen Vorgängen (große Ordner, Sicherung, Update) -->
<Grid x:Name="ProgressOverlay" Grid.Row="1" Grid.ColumnSpan="2" Background="{DynamicResource OverlayBrush}" Visibility="Collapsed">
<Border Width="460" HorizontalAlignment="Center" VerticalAlignment="Center" Padding="24,22" CornerRadius="8"
Background="{DynamicResource LayerBrush}" BorderBrush="{DynamicResource FlyoutStrokeBrush}" BorderThickness="1"
Effect="{StaticResource DialogShadow}">
<StackPanel>
<TextBlock x:Name="ProgressTitle" FontFamily="{DynamicResource DisplayFont}" FontSize="20" FontWeight="SemiBold"/>
<TextBlock x:Name="ProgressDetail" Margin="0,8,0,0" Foreground="{DynamicResource TextSecondaryBrush}" TextTrimming="CharacterEllipsis"/>
<Border x:Name="ProgressTrack" Height="4" CornerRadius="2" Margin="0,18,0,0" Background="{DynamicResource SubtleSelectedBrush}">
<Border x:Name="ProgressFill" HorizontalAlignment="Left" Width="0" CornerRadius="2" Background="{DynamicResource AccentBrush}"/>
</Border>
<DockPanel Margin="0,12,0,0">
<Button x:Name="ProgressCancel" DockPanel.Dock="Right" Content="Abbrechen" Click="ProgressCancel_Click" Width="110"/>
<TextBlock x:Name="ProgressStats" VerticalAlignment="Center" FontSize="12" Foreground="{DynamicResource TextSecondaryBrush}"/>
</DockPanel>
</StackPanel>
</Border>
</Grid>
</Grid> </Grid>
</Window> </Window>
+84 -15
View File
@@ -32,6 +32,7 @@ public partial class MainWindow : Window
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
RestorePlacement();
StateChanged += (_, _) => UpdateWindowState(); StateChanged += (_, _) => UpdateWindowState();
Closing += OnClosing; Closing += OnClosing;
PreviewKeyDown += OnPreviewKeyDown; PreviewKeyDown += OnPreviewKeyDown;
@@ -73,6 +74,7 @@ public partial class MainWindow : Window
private void OnClosing(object? sender, CancelEventArgs e) private void OnClosing(object? sender, CancelEventArgs e)
{ {
if (App.Quitting) return; if (App.Quitting) return;
SavePlacement();
if (S.TrayEnabled && S.CloseToTray) if (S.TrayEnabled && S.CloseToTray)
{ {
e.Cancel = true; e.Cancel = true;
@@ -189,7 +191,7 @@ public partial class MainWindow : Window
/// <summary>Vom Watcher: Dateien haben sich geändert.</summary> /// <summary>Vom Watcher: Dateien haben sich geändert.</summary>
public void OnStorageChanged() public void OnStorageChanged()
{ {
if (!_dragging) Refresh(); if (!_dragging && !IsBusy) Refresh(); // während großer Kopien nicht ständig neu laden
} }
private void Refresh() private void Refresh()
@@ -792,7 +794,7 @@ public partial class MainWindow : Window
IconMenu.Items.Add(MenuItem(fromApp ? "Aus der App löschen" : "Vom Desktop löschen", "", () => DeleteIcons(sel), gesture: "Entf")); IconMenu.Items.Add(MenuItem(fromApp ? "Aus der App löschen" : "Vom Desktop löschen", "", () => DeleteIcons(sel), gesture: "Entf"));
} }
private void RenameIcon(IconItem item) public void RenameIcon(IconItem item)
{ {
var ext = Storage.HiddenExtension(item.Path); var ext = Storage.HiddenExtension(item.Path);
var newName = Dialog.Prompt(this, "Umbenennen", "Neuer Name:", item.Name, "Umbenennen", text => var newName = Dialog.Prompt(this, "Umbenennen", "Neuer Name:", item.Name, "Umbenennen", text =>
@@ -816,7 +818,7 @@ public partial class MainWindow : Window
catch (Exception ex) { Error("Umbenennen fehlgeschlagen", ex.Message); } catch (Exception ex) { Error("Umbenennen fehlgeschlagen", ex.Message); }
} }
private void DeleteIcons(List<IconItem> sel) public void DeleteIcons(List<IconItem> sel)
{ {
// Ungesicherte Desktop-Icons: nachfragen. Alles andere geht direkt (Strg+Z holt es zurück). // Ungesicherte Desktop-Icons: nachfragen. Alles andere geht direkt (Strg+Z holt es zurück).
var unsorted = sel.Count(i => !Storage.IsInStorage(i.Path) && !i.IsSorted); var unsorted = sel.Count(i => !Storage.IsInStorage(i.Path) && !i.IsSorted);
@@ -842,19 +844,31 @@ public partial class MainWindow : Window
else ShowToast($"{Plural(sel.Count, "Element", "Elemente")} gelöscht", "", undoable: true); else ShowToast($"{Plural(sel.Count, "Element", "Elemente")} gelöscht", "", undoable: true);
} }
/// <summary>Kopiert/verschiebt Dateien in einen Ordner – mit Rückgängig und Rückmeldung.</summary> /// <summary>
public void TransferAll(IEnumerable<string> paths, string targetDir) /// Kopiert/verschiebt Dateien in einen Ordner – mit Rückgängig und Rückmeldung.
/// Läuft im Hintergrund; bei großen Datenmengen erscheint ein Fortschritt mit Abbrechen.
/// </summary>
public async void TransferAll(IEnumerable<string> paths, string targetDir)
{ {
if (IsBusy) { ShowToast("Bitte warten, bis der laufende Vorgang fertig ist", "\uE946", info: true); return; }
var list = paths.ToList(); var list = paths.ToList();
if (list.Count == 0) return;
var target = Storage.IsDesktopDir(targetDir) ? DesktopName : Path.GetFileName(targetDir.TrimEnd('\\')); var target = Storage.IsDesktopDir(targetDir) ? DesktopName : Path.GetFileName(targetDir.TrimEnd('\\'));
// Große Ordner würden beim Kopieren doppelt Platz belegen → lieber verschieben? // Große Ordner würden beim Kopieren doppelt Platz belegen → lieber verschieben?
var bigFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase); var bigFolders = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (Storage.IsInStorage(targetDir)) if (Storage.IsInStorage(targetDir))
{ {
const long Limit = 200L * 1024 * 1024; const long Limit = 200L * 1024 * 1024;
var big = list.Where(p => Directory.Exists(p) && !Storage.IsInStorage(p)) Mouse.OverrideCursor = Cursors.Wait;
.Select(p => (Path: p, Size: Storage.DirectorySize(p, Limit * 50))) List<(string Path, long Size)> big;
.Where(x => x.Size > Limit).ToList(); try
{
big = await Task.Run(() => list.Where(p => Directory.Exists(p) && !Storage.IsInStorage(p))
.Select(p => (Path: p, Size: Storage.DirectorySize(p, Limit * 50)))
.Where(x => x.Size > Limit).ToList());
}
finally { Mouse.OverrideCursor = null; }
if (big.Count > 0) if (big.Count > 0)
{ {
var what = big.Count == 1 var what = big.Count == 1
@@ -867,8 +881,31 @@ public partial class MainWindow : Window
} }
} }
bool anyCopy = list.Any(p => !bigFolders.Contains(p) && !Storage.IsInStorage(p) && Storage.IsInStorage(targetDir));
var detail = list.Count == 1 ? $"„{Storage.DisplayName(list[0])}“ → {target}" : $"{list.Count} Elemente → {target}";
List<TransferResult> results;
try
{
results = await RunWithProgress(anyCopy ? "Wird kopiert …" : "Wird verschoben …", detail, (sink, ct) =>
{
sink.SetTotal(list.Sum(p => Storage.BytesToCopy(p, targetDir, bigFolders.Contains(p))));
var r = new List<TransferResult>();
foreach (var p in list)
{
if (list.Count > 1) sink.Detail = $"„{Storage.DisplayName(p)}“ → {target}";
r.Add(Storage.Transfer(p, targetDir, bigFolders.Contains(p), sink.Add, ct));
}
return Task.FromResult(r);
});
}
catch (Exception ex)
{
Refresh();
Error("Übertragen fehlgeschlagen", ex.Message);
return;
}
var entry = new UndoEntry($"{Plural(list.Count, "Element", "Elemente")} nach „{target}“"); var entry = new UndoEntry($"{Plural(list.Count, "Element", "Elemente")} nach „{target}“");
var results = list.Select(p => Storage.Transfer(p, targetDir, forceMove: bigFolders.Contains(p))).ToList();
foreach (var r in results.Where(r => r.Status == TransferStatus.Ok)) foreach (var r in results.Where(r => r.Status == TransferStatus.Ok))
{ {
if (r.Copied) entry.AddCopy(r.Source, r.Dest!); if (r.Copied) entry.AddCopy(r.Source, r.Dest!);
@@ -876,25 +913,33 @@ public partial class MainWindow : Window
} }
Undo.Push(entry); Undo.Push(entry);
Refresh(); Refresh();
FenceManager.RefreshAll();
var ok = results.Where(r => r.Status == TransferStatus.Ok).ToList(); var ok = results.Where(r => r.Status == TransferStatus.Ok).ToList();
var skipped = results.Where(r => r.Status == TransferStatus.Skipped).ToList(); var skipped = results.Where(r => r.Status == TransferStatus.Skipped).ToList();
var cancelled = results.Count(r => r.Status == TransferStatus.Cancelled);
var errors = results.Where(r => r.Status == TransferStatus.Error).Select(r => r.Message!).ToList(); var errors = results.Where(r => r.Status == TransferStatus.Error).Select(r => r.Message!).ToList();
if (errors.Count > 0) { ShowErrors(errors); return; } if (errors.Count > 0) { ShowErrors(errors); return; }
if (cancelled > 0)
{
ShowToast(ok.Count > 0 ? $"Abgebrochen – {Plural(ok.Count, "Element", "Elemente")} wurden noch übertragen" : "Abgebrochen – es wurde nichts verändert",
"\uE711", undoable: ok.Count > 0, info: true);
return;
}
if (ok.Count == 0 && skipped.Count > 0) if (ok.Count == 0 && skipped.Count > 0)
{ {
ShowToast(skipped.Count == 1 ? skipped[0].Message! : $"Alle {skipped.Count} Elemente sind schon in „{target}“", "", info: true); ShowToast(skipped.Count == 1 ? skipped[0].Message! : $"Alle {skipped.Count} Elemente sind schon in „{target}“", "\uE946", info: true);
return; return;
} }
var n = Plural(ok.Count, "Element", "Elemente"); var n = Plural(ok.Count, "Element", "Elemente");
var extra = skipped.Count > 0 ? $" · {skipped.Count} waren schon da" : ""; var extra = skipped.Count > 0 ? $" · {skipped.Count} waren schon da" : "";
if (Storage.IsDesktopDir(targetDir)) if (Storage.IsDesktopDir(targetDir))
ShowToast($"{n} zurück auf den Desktop gelegt{extra}", "", undoable: true); ShowToast($"{n} zurück auf den Desktop gelegt{extra}", "\uE7F4", undoable: true);
else if (ok.All(r => r.Copied)) else if (ok.All(r => r.Copied))
ShowToast($"{n} in „{target}“ gesichert{extra}", undoable: true); ShowToast($"{n} in „{target}“ gesichert{extra}", undoable: true);
else else
ShowToast($"{n} nach „{target}“ verschoben{extra}", "", undoable: true); ShowToast($"{n} nach „{target}“ verschoben{extra}", "\uE8DE", undoable: true);
} }
// ---------- Rückgängig ---------- // ---------- Rückgängig ----------
@@ -1095,6 +1140,7 @@ public partial class MainWindow : Window
HotkeyBox.IsEnabled = S.HotkeyEnabled; HotkeyBox.IsEnabled = S.HotkeyEnabled;
AutoSortToggle.IsChecked = S.AutoSort; AutoSortToggle.IsChecked = S.AutoSort;
AutoUpdateToggle.IsChecked = S.AutoUpdateCheck;
AutoRemoveToggle.IsChecked = S.AutoSortRemoveFromDesktop; AutoRemoveToggle.IsChecked = S.AutoSortRemoveFromDesktop;
RulesList.ItemsSource = null; RulesList.ItemsSource = null;
RulesList.ItemsSource = S.Rules; RulesList.ItemsSource = S.Rules;
@@ -1258,18 +1304,30 @@ public partial class MainWindow : Window
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
}; };
if (dlg.ShowDialog(this) != true) return; if (dlg.ShowDialog(this) != true) return;
if (Storage.IsUnder(dlg.FileName, Storage.Root))
{
Error("Anderen Ort wählen", "Die Sicherung kann nicht im DeskCore-Ordner selbst gespeichert werden – sie würde sich sonst mitsichern. " +
"Am besten z. B. unter Dokumente oder auf einem USB-Stick speichern.");
return;
}
var size = await Task.Run(Backup.StorageSize); var size = await Task.Run(Backup.StorageSize);
if (size > 2L * 1024 * 1024 * 1024 && if (size > 2L * 1024 * 1024 * 1024 &&
!Dialog.Confirm(this, "Große Sicherung", !Dialog.Confirm(this, "Große Sicherung",
$"Deine Kategorien sind zusammen {Backup.FormatSize(size)} groß (vor allem durch Ordner). Das Sichern kann eine Weile dauern " + $"Deine Kategorien sind zusammen {Backup.FormatSize(size)} groß (vor allem durch Ordner). Das Sichern kann eine Weile dauern " +
"und braucht entsprechend Platz.", "Trotzdem sichern")) "und braucht entsprechend Platz.", "Trotzdem sichern"))
return; return;
ShowToast("Sicherung läuft …", "", info: true); if (IsBusy) return;
try try
{ {
var count = await Task.Run(() => Backup.Export(dlg.FileName)); var file = dlg.FileName;
var count = await RunWithProgress("Sicherung wird erstellt …", Path.GetFileName(file), (sink, ct) =>
{
sink.SetTotal(size);
return Task.FromResult(Backup.Export(file, sink.Add, ct));
});
ShowToast($"{Plural(count, "Datei", "Dateien")} gesichert in {Path.GetFileName(dlg.FileName)}", ""); ShowToast($"{Plural(count, "Datei", "Dateien")} gesichert in {Path.GetFileName(dlg.FileName)}", "");
} }
catch (OperationCanceledException) { ShowToast("Sicherung abgebrochen – es wurde keine Datei angelegt", "\uE711", info: true); }
catch (Exception ex) { Error("Sichern fehlgeschlagen", ex.Message); } catch (Exception ex) { Error("Sichern fehlgeschlagen", ex.Message); }
} }
@@ -1286,14 +1344,25 @@ public partial class MainWindow : Window
"Die Kategorien aus der Sicherung werden zu deinen vorhandenen hinzugefügt. Vorhandene Dateien werden nicht überschrieben.", "Die Kategorien aus der Sicherung werden zu deinen vorhandenen hinzugefügt. Vorhandene Dateien werden nicht überschrieben.",
"Wiederherstellen")) "Wiederherstellen"))
return; return;
if (IsBusy) return;
try try
{ {
var (added, skipped) = await Task.Run(() => Backup.Import(dlg.FileName)); var file = dlg.FileName;
var (added, skipped) = await RunWithProgress("Sicherung wird wiederhergestellt …", Path.GetFileName(file), (sink, ct) =>
{
sink.SetTotal(Backup.ArchiveSize(file));
return Task.FromResult(Backup.Import(file, sink.Add, ct));
});
Refresh(); Refresh();
FenceManager.Sync(); FenceManager.Sync();
LoadSettingsView(); LoadSettingsView();
ShowToast($"{Plural(added, "Datei", "Dateien")} wiederhergestellt" + (skipped > 0 ? $" · {skipped} waren schon da" : ""), ""); ShowToast($"{Plural(added, "Datei", "Dateien")} wiederhergestellt" + (skipped > 0 ? $" · {skipped} waren schon da" : ""), "");
} }
catch (OperationCanceledException)
{
Refresh();
ShowToast("Wiederherstellen abgebrochen – bereits übernommene Dateien bleiben erhalten", "\uE711", info: true);
}
catch (Exception ex) { Error("Wiederherstellen fehlgeschlagen", $"Die Datei ist keine gültige Sicherung.\n\n{ex.Message}"); } catch (Exception ex) { Error("Wiederherstellen fehlgeschlagen", $"Die Datei ist keine gültige Sicherung.\n\n{ex.Message}"); }
} }
+19 -2
View File
@@ -9,8 +9,8 @@ Danach kannst du die Icons vom Desktop löschen. In DeskCore bleiben sie erhalte
## Was du brauchst ## Was du brauchst
- Windows 10 oder Windows 11 - Windows 10 oder Windows 11 (64 Bit)
- Fehlt beim ersten Start etwas („.NET“), zeigt Windows automatisch einen Hinweis mit einem Download-Link. Einfach herunterladen, installieren und DeskCore nochmal starten. - Das war's. Falls DeskCore noch etwas von Microsoft braucht („.NET“), erledigt das der Installer automatisch.
--- ---
@@ -25,6 +25,11 @@ Danach kannst du die Icons vom Desktop löschen. In DeskCore bleiben sie erhalte
5. **Installieren** klicken, und fertig. 5. **Installieren** klicken, und fertig.
Du brauchst dafür **keine Administratorrechte**. 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“**.
--- ---
@@ -86,6 +91,14 @@ Oder per Rechtsklick → **Nach oben / Nach unten**.
### Ansicht ### Ansicht
Oben rechts gibt es den **Ansicht-Knopf**: kleine, mittlere oder große Symbole oder eine Liste. Oben rechts gibt es den **Ansicht-Knopf**: kleine, mittlere oder große Symbole oder eine Liste.
### Icons in den Desktop-Boxen
Auch in einer Box auf dem Desktop kannst du per **Rechtsklick** umbenennen, löschen, in eine andere Kategorie verschieben
oder zurück auf den Desktop legen. Icons lassen sich auch einfach aus der Box herausziehen.
### Updates
DeskCore schaut einmal am Tag nach, ob es eine neue Version gibt.
Wenn ja, erscheint oben ein Hinweis. Ein Klick auf **„Jetzt aktualisieren“** genügt, und DeskCore startet danach von selbst neu.
--- ---
## Hoppla, falsch geklickt? ## Hoppla, falsch geklickt?
@@ -110,6 +123,10 @@ Manche Icons gehören allen Benutzern des PCs. Dafür DeskCore einmal mit
**Ich habe einen großen Ordner hineingezogen. Ist der jetzt doppelt da?** **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. DeskCore fragt bei großen Ordnern nach. Wählst du **„Verschieben“**, liegt er nur einmal da und belegt keinen doppelten Platz.
Beim Kopieren siehst du einen Fortschrittsbalken und kannst jederzeit **abbrechen**. Dann bleibt alles wie vorher.
**Ich habe einen Bildschirm abgesteckt und das Fenster ist weg.**
Keine Sorge: DeskCore und die Desktop-Boxen holen sich automatisch auf den verbliebenen Bildschirm zurück.
**Ich bekomme einen neuen PC. Was jetzt?** **Ich bekomme einen neuen PC. Was jetzt?**
**Einstellungen → Sicherung → „Sichern …“** speichert alles in eine ZIP-Datei. **Einstellungen → Sicherung → „Sichern …“** speichert alles in eine ZIP-Datei.
+39
View File
@@ -0,0 +1,39 @@
using System.Windows;
namespace DeskCore;
/// <summary>Bildschirm-Hilfen, damit Fenster nie außerhalb des sichtbaren Bereichs landen (z. B. nach Abstecken eines Monitors).</summary>
public static class Screens
{
/// <summary>Umrechnung Pixel → WPF-Einheiten (System-DPI).</summary>
private static double Scale
{
get
{
using var g = System.Drawing.Graphics.FromHwnd(IntPtr.Zero);
return g.DpiX / 96.0;
}
}
/// <summary>Arbeitsbereiche (ohne Taskleiste) aller angeschlossenen Bildschirme in WPF-Einheiten.</summary>
public static List<Rect> WorkAreas()
{
var s = Scale;
return System.Windows.Forms.Screen.AllScreens
.Select(m => new Rect(m.WorkingArea.X / s, m.WorkingArea.Y / s, m.WorkingArea.Width / s, m.WorkingArea.Height / s))
.ToList();
}
public static Rect PrimaryWorkArea => SystemParameters.WorkArea;
/// <summary>Ist genug vom Fenster (Titelleiste) auf irgendeinem Bildschirm sichtbar, um es greifen zu können?</summary>
public static bool IsReachable(Rect window)
{
var title = new Rect(window.X, window.Y, window.Width, Math.Min(40, window.Height));
return WorkAreas().Any(a =>
{
var i = Rect.Intersect(a, title);
return !i.IsEmpty && i.Width >= Math.Min(120, window.Width / 2) && i.Height >= 20;
});
}
}
+12
View File
@@ -45,6 +45,18 @@ public class AppSettings
public bool FencesEnabled { get; set; } = true; public bool FencesEnabled { get; set; } = true;
// Fenster merken (WPF-Einheiten)
public double? WindowLeft { get; set; }
public double? WindowTop { get; set; }
public double? WindowWidth { get; set; }
public double? WindowHeight { get; set; }
public bool WindowMaximized { get; set; }
// Updates
public bool AutoUpdateCheck { get; set; } = true;
public DateTime? LastUpdateCheck { get; set; }
public string? SkippedVersion { get; set; }
public List<string> CategoryOrder { get; set; } = []; public List<string> CategoryOrder { get; set; } = [];
public Dictionary<string, CategoryMeta> Categories { get; set; } = new(StringComparer.OrdinalIgnoreCase); public Dictionary<string, CategoryMeta> Categories { get; set; } = new(StringComparer.OrdinalIgnoreCase);
+10 -3
View File
@@ -11,7 +11,8 @@ public partial class App : Application
base.OnStartup(e); base.OnStartup(e);
bool uninstall = e.Args.Any(a => a.Equals("--uninstall", StringComparison.OrdinalIgnoreCase)); bool uninstall = e.Args.Any(a => a.Equals("--uninstall", StringComparison.OrdinalIgnoreCase));
bool silent = e.Args.Any(a => a.Equals("/S", StringComparison.OrdinalIgnoreCase)); bool silent = e.Args.Any(a => a.Equals("/S", StringComparison.OrdinalIgnoreCase));
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?[3..]; bool launch = e.Args.Any(a => a.Equals("/launch", StringComparison.OrdinalIgnoreCase));
var dirArg = e.Args.FirstOrDefault(a => a.StartsWith("/D=", StringComparison.OrdinalIgnoreCase))?.Substring(3);
// Stille Installation/Deinstallation für Skripte: DeskCore-Setup.exe /S [/D=Ordner] bzw. --uninstall /S // Stille Installation/Deinstallation für Skripte: DeskCore-Setup.exe /S [/D=Ordner] bzw. --uninstall /S
if (silent) if (silent)
@@ -20,8 +21,14 @@ public partial class App : Application
try try
{ {
if (uninstall) Installer.Uninstall(removeData: false, progress); if (uninstall) Installer.Uninstall(removeData: false, progress);
else Installer.Install(dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir, else
desktopLink: true, autostart: Installer.AutostartEnabled, progress); {
var dir = dirArg ?? Installer.InstalledDir ?? Installer.DefaultDir;
Installer.Install(dir, desktopLink: Installer.InstalledDir is null || Installer.DesktopLinkExists,
autostart: Installer.AutostartEnabled, progress, installRuntime: true);
// nach einem Update aus DeskCore heraus: gleich wieder starten
if (launch) System.Diagnostics.Process.Start(System.IO.Path.Combine(dir, Installer.ExeName));
}
Shutdown(0); Shutdown(0);
} }
catch (Exception ex) catch (Exception ex)
+17 -2
View File
@@ -1,10 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net9.0-windows</TargetFramework> <!-- .NET Framework 4.8 ist in jedem Windows 10/11 enthalten → der Installer läuft immer
und kann die für DeskCore nötige .NET-9-Laufzeit selbst nachinstallieren -->
<TargetFramework>net48</TargetFramework>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>AnyCPU</PlatformTarget>
<Prefer32Bit>false</Prefer32Bit>
<GenerateDocumentationFile>false</GenerateDocumentationFile>
<AssemblyName>DeskCore-Setup</AssemblyName> <AssemblyName>DeskCore-Setup</AssemblyName>
<RootNamespace>DeskCoreSetup</RootNamespace> <RootNamespace>DeskCoreSetup</RootNamespace>
<ApplicationIcon>..\app.ico</ApplicationIcon> <ApplicationIcon>..\app.ico</ApplicationIcon>
@@ -15,9 +22,17 @@
<Authors>M_Viper</Authors> <Authors>M_Viper</Authors>
<Copyright>Copyright (c) M_Viper. Alle Rechte vorbehalten.</Copyright> <Copyright>Copyright (c) M_Viper. Alle Rechte vorbehalten.</Copyright>
<NeutralLanguage>de-DE</NeutralLanguage> <NeutralLanguage>de-DE</NeutralLanguage>
<!-- fertig veröffentlichte DeskCore.exe, die eingepackt wird (build.ps1 legt sie hier ab) --> <!-- fertig veröffentlichte DeskCore.exe, die eingepackt wird (build.ps1 legt sie hier ab) -->
<AppPayload Condition="'$(AppPayload)' == ''">..\artifacts\app\DeskCore.exe</AppPayload> <AppPayload Condition="'$(AppPayload)' == ''">..\artifacts\app\DeskCore.exe</AppPayload>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<!-- WinForms nur für den Ordner-Dialog; nicht global, sonst Namenskonflikte mit WPF -->
<Using Remove="System.Windows.Forms" />
<Using Remove="System.Drawing" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.VisualBasic" />
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<!-- gleiches Design wie die App --> <!-- gleiches Design wie die App -->
<Page Include="..\Themes\Colors.Light.xaml" Link="Themes\Colors.Light.xaml" /> <Page Include="..\Themes\Colors.Light.xaml" Link="Themes\Colors.Light.xaml" />
+26 -6
View File
@@ -23,6 +23,9 @@ public static class Installer
} }
} }
/// <summary>Pfad dieses Setups (auch als Deinstallationsprogramm).</summary>
public static string SelfPath => System.Reflection.Assembly.GetExecutingAssembly().Location;
public static string DefaultDir => public static string DefaultDir =>
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName); Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Programs", AppName);
@@ -66,8 +69,19 @@ public static class Installer
// ---------- Installieren ---------- // ---------- Installieren ----------
public static void Install(string dir, bool desktopLink, bool autostart, IProgress<(double, string)> progress) public static void Install(string dir, bool desktopLink, bool autostart, IProgress<(double, string)> progress,
bool installRuntime = false)
{ {
if (!Runtime.Is64BitWindows)
throw new InvalidOperationException("DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11).");
if (installRuntime && !Runtime.IsInstalled)
{
// 0–60 % für die Laufzeit, Rest für DeskCore
Runtime.Install((v, text) => progress.Report((v * 0.6, text)));
var inner = progress;
progress = new Scaled(inner, 0.6, 0.4);
}
progress.Report((0.05, "DeskCore wird beendet …")); progress.Report((0.05, "DeskCore wird beendet …"));
CloseRunningApp(); CloseRunningApp();
RemoveLegacyInstall(); RemoveLegacyInstall();
@@ -79,7 +93,7 @@ public static class Installer
WriteResource("payload.LICENSE.txt", Path.Combine(dir, "LICENSE.txt")); WriteResource("payload.LICENSE.txt", Path.Combine(dir, "LICENSE.txt"));
// Setup selbst als Deinstallationsprogramm ablegen // Setup selbst als Deinstallationsprogramm ablegen
var self = Environment.ProcessPath!; var self = SelfPath;
var uninstaller = Path.Combine(dir, UninstallerName); var uninstaller = Path.Combine(dir, UninstallerName);
if (!string.Equals(Path.GetFullPath(self), Path.GetFullPath(uninstaller), StringComparison.OrdinalIgnoreCase)) if (!string.Equals(Path.GetFullPath(self), Path.GetFullPath(uninstaller), StringComparison.OrdinalIgnoreCase))
{ {
@@ -149,7 +163,7 @@ public static class Installer
{ {
if (p.MainModule?.FileName is { } f && Storage_IsUnder(f, dir)) if (p.MainModule?.FileName is { } f && Storage_IsUnder(f, dir))
{ {
p.Kill(entireProcessTree: true); p.Kill();
p.WaitForExit(10000); p.WaitForExit(10000);
} }
} }
@@ -160,7 +174,7 @@ public static class Installer
DeleteIfPointsTo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), LegacyName + ".lnk"), exe); DeleteIfPointsTo(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), LegacyName + ".lnk"), exe);
using (var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true)) using (var run = Registry.CurrentUser.OpenSubKey(RunKey, writable: true))
{ {
if (run?.GetValue(LegacyName) is string cmd && cmd.Contains(exe, StringComparison.OrdinalIgnoreCase)) if (run?.GetValue(LegacyName) is string cmd && cmd.IndexOf(exe, StringComparison.OrdinalIgnoreCase) >= 0)
run.DeleteValue(LegacyName, throwOnMissingValue: false); run.DeleteValue(LegacyName, throwOnMissingValue: false);
} }
@@ -221,7 +235,7 @@ public static class Installer
// Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd. // Das laufende Deinstallationsprogramm kann sich nicht selbst löschen → kurz nach dem Beenden per cmd.
// rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist. // rmdir ohne /s entfernt den Ordner nur, wenn er danach leer ist.
var self = Environment.ProcessPath!; var self = SelfPath;
if (Storage_IsUnder(self, dir)) if (Storage_IsUnder(self, dir))
{ {
Process.Start(new ProcessStartInfo("cmd.exe", Process.Start(new ProcessStartInfo("cmd.exe",
@@ -240,6 +254,12 @@ public static class Installer
// ---------- Helfer ---------- // ---------- Helfer ----------
/// <summary>Rechnet Fortschritt 0–1 in einen Teilbereich um.</summary>
private sealed class Scaled(IProgress<(double, string)> inner, double offset, double factor) : IProgress<(double, string)>
{
public void Report((double, string) value) => inner.Report((offset + value.Item1 * factor, value.Item2));
}
/// <summary>Laufendes DeskCore beenden (das X versteckt es nur im Infobereich).</summary> /// <summary>Laufendes DeskCore beenden (das X versteckt es nur im Infobereich).</summary>
private static void CloseRunningApp() private static void CloseRunningApp()
{ {
@@ -247,7 +267,7 @@ public static class Installer
{ {
try try
{ {
p.Kill(entireProcessTree: true); p.Kill();
p.WaitForExit(10000); p.WaitForExit(10000);
} }
catch { } catch { }
+18
View File
@@ -79,6 +79,14 @@
Text="DeskCore sortiert deine Desktop-Icons in eigene Kategorien – mit Suche, Desktop-Boxen, automatischem Einsortieren und Rückgängig-Funktion."/> Text="DeskCore sortiert deine Desktop-Icons in eigene Kategorien – mit Suche, Desktop-Boxen, automatischem Einsortieren und Rückgängig-Funktion."/>
<TextBlock Style="{StaticResource Body}" Margin="0,14,0,0" <TextBlock Style="{StaticResource Body}" Margin="0,14,0,0"
Text="Der Assistent installiert DeskCore nur für dein Benutzerkonto. Administratorrechte sind nicht nötig."/> Text="Der Assistent installiert DeskCore nur für dein Benutzerkonto. Administratorrechte sind nicht nötig."/>
<Border x:Name="RuntimeHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
Background="{DynamicResource ControlFillBrush}" BorderBrush="{DynamicResource ControlStrokeBrush}" BorderThickness="1">
<DockPanel>
<TextBlock DockPanel.Dock="Left" Text="&#xE7BA;" FontFamily="{DynamicResource IconFont}" FontSize="16"
Foreground="{DynamicResource AccentBrush}" Margin="0,2,12,0" VerticalAlignment="Top"/>
<TextBlock x:Name="RuntimeHintText" TextWrapping="Wrap" LineHeight="20"/>
</DockPanel>
</Border>
<Border x:Name="UpdateHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed" <Border x:Name="UpdateHint" Margin="0,22,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1"> Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
<DockPanel> <DockPanel>
@@ -113,6 +121,8 @@
<CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" Margin="0,24,0,0" IsChecked="True"/> <CheckBox x:Name="DesktopLinkBox" Content="Verknüpfung auf dem Desktop" Margin="0,24,0,0" IsChecked="True"/>
<CheckBox x:Name="StartMenuBox" Content="Eintrag im Startmenü" Margin="0,12,0,0" IsChecked="True" IsEnabled="False"/> <CheckBox x:Name="StartMenuBox" Content="Eintrag im Startmenü" Margin="0,12,0,0" IsChecked="True" IsEnabled="False"/>
<CheckBox x:Name="AutostartBox" Content="Mit Windows starten (unauffällig im Infobereich)" Margin="0,12,0,0"/> <CheckBox x:Name="AutostartBox" Content="Mit Windows starten (unauffällig im Infobereich)" Margin="0,12,0,0"/>
<CheckBox x:Name="RuntimeBox" Margin="0,12,0,0" IsChecked="True" Visibility="Collapsed"
Content="Microsoft .NET 9 Desktop Runtime automatisch installieren (benötigt, ca. 55 MB)"/>
</StackPanel> </StackPanel>
<!-- 3: Fortschritt / Fertig --> <!-- 3: Fortschritt / Fertig -->
@@ -130,6 +140,14 @@
</Border> </Border>
<TextBlock x:Name="DoneText" VerticalAlignment="Center" Margin="12,0,0,0" FontWeight="SemiBold"/> <TextBlock x:Name="DoneText" VerticalAlignment="Center" Margin="12,0,0,0" FontWeight="SemiBold"/>
</StackPanel> </StackPanel>
<Border x:Name="RuntimeMissing" Margin="0,18,0,0" Padding="14,10" CornerRadius="6" Visibility="Collapsed"
Background="{DynamicResource AccentSubtleBrush}" BorderBrush="{DynamicResource AccentSubtleStrokeBrush}" BorderThickness="1">
<StackPanel>
<TextBlock TextWrapping="Wrap" LineHeight="20"
Text="Damit DeskCore startet, wird noch die kostenlose „Microsoft .NET 9 Desktop Runtime“ benötigt."/>
<Button Content="Download-Seite von Microsoft öffnen" HorizontalAlignment="Left" Margin="0,10,0,0" Click="OpenRuntimePage_Click"/>
</StackPanel>
</Border>
<CheckBox x:Name="LaunchBox" Content="DeskCore jetzt starten" IsChecked="True" Margin="0,22,0,0"/> <CheckBox x:Name="LaunchBox" Content="DeskCore jetzt starten" IsChecked="True" Margin="0,22,0,0"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
+35 -5
View File
@@ -26,6 +26,16 @@ public partial class MainWindow : Window
DesktopLinkBox.IsChecked = Installer.InstalledDir is null || Installer.DesktopLinkExists; DesktopLinkBox.IsChecked = Installer.InstalledDir is null || Installer.DesktopLinkExists;
AutostartBox.IsChecked = Installer.AutostartEnabled; AutostartBox.IsChecked = Installer.AutostartEnabled;
if (!Runtime.IsInstalled)
{
RuntimeHint.Visibility = Visibility.Visible;
RuntimeBox.Visibility = Visibility.Visible;
RuntimeHintText.Text = Runtime.Is64BitWindows
? "DeskCore benötigt die kostenlose „Microsoft .NET 9 Desktop Runtime“. Sie fehlt auf diesem PC und wird bei der " +
"Installation automatisch von Microsoft geladen (ca. 55 MB). Windows fragt dabei einmal nach Administratorrechten."
: "DeskCore benötigt ein 64-Bit-Windows (Windows 10 oder 11) und kann auf diesem PC leider nicht installiert werden.";
}
if (Installer.InstalledDir is null && Installer.LegacyInstallDir is not null) if (Installer.InstalledDir is null && Installer.LegacyInstallDir is not null)
{ {
UpdateHint.Visibility = Visibility.Visible; UpdateHint.Visibility = Visibility.Visible;
@@ -102,7 +112,7 @@ public partial class MainWindow : Window
case 1 when AcceptLicense.IsChecked == true: ShowPage(2); break; case 1 when AcceptLicense.IsChecked == true: ShowPage(2); break;
case 2: case 2:
var dir = DirBox.Text.Trim(); var dir = DirBox.Text.Trim();
if (!Path.IsPathFullyQualified(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0) if (!IsFullPath(dir) || dir.IndexOfAny(Path.GetInvalidPathChars()) >= 0)
{ {
MessageBox.Show(this, "Bitte einen gültigen Ordner angeben, z. B. C:\\Programme\\DeskCore.", "DeskCore Setup"); MessageBox.Show(this, "Bitte einen gültigen Ordner angeben, z. B. C:\\Programme\\DeskCore.", "DeskCore Setup");
return; return;
@@ -119,9 +129,14 @@ public partial class MainWindow : Window
private void ChangeDir_Click(object sender, RoutedEventArgs e) private void ChangeDir_Click(object sender, RoutedEventArgs e)
{ {
var dlg = new Microsoft.Win32.OpenFolderDialog { Title = "Installationsordner wählen" }; using var dlg = new System.Windows.Forms.FolderBrowserDialog
if (dlg.ShowDialog(this) != true) return; {
var chosen = dlg.FolderName; Description = "Installationsordner wählen",
SelectedPath = Directory.Exists(DirBox.Text) ? DirBox.Text : Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
ShowNewFolderButton = true,
};
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
var chosen = dlg.SelectedPath;
// wie bei üblichen Installern: Unterordner "DeskCore" anhängen // wie bei üblichen Installern: Unterordner "DeskCore" anhängen
DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals("DeskCore", StringComparison.OrdinalIgnoreCase) DirBox.Text = Path.GetFileName(chosen.TrimEnd('\\')).Equals("DeskCore", StringComparison.OrdinalIgnoreCase)
? chosen : Path.Combine(chosen, "DeskCore"); ? chosen : Path.Combine(chosen, "DeskCore");
@@ -136,15 +151,24 @@ public partial class MainWindow : Window
SetButtons(false); SetButtons(false);
var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text)); var progress = new Progress<(double Value, string Text)>(p => SetProgress(p.Value, p.Text));
bool desktop = DesktopLinkBox.IsChecked == true, autostart = AutostartBox.IsChecked == true; bool desktop = DesktopLinkBox.IsChecked == true, autostart = AutostartBox.IsChecked == true;
bool runtime = RuntimeBox.Visibility == Visibility.Visible && RuntimeBox.IsChecked == true;
try try
{ {
await Task.Run(() => Installer.Install(dir, desktop, autostart, progress)); await Task.Run(() => Installer.Install(dir, desktop, autostart, progress, runtime));
_installedExe = Path.Combine(dir, Installer.ExeName); _installedExe = Path.Combine(dir, Installer.ExeName);
_done = true; _done = true;
ProgressTitle.Text = "Installation abgeschlossen"; ProgressTitle.Text = "Installation abgeschlossen";
ProgressText.Text = $"DeskCore wurde in {dir} installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : "."); ProgressText.Text = $"DeskCore wurde in {dir} installiert. Du findest es im Startmenü" + (desktop ? " und auf dem Desktop." : ".");
DoneText.Text = "Alles bereit!"; DoneText.Text = "Alles bereit!";
DonePanel.Visibility = Visibility.Visible; DonePanel.Visibility = Visibility.Visible;
if (!Runtime.IsInstalled)
{
// Laufzeit fehlt weiterhin (abgewählt) → Hinweis statt Startversuch
DoneText.Text = "DeskCore ist installiert";
RuntimeMissing.Visibility = Visibility.Visible;
LaunchBox.IsChecked = false;
LaunchBox.IsEnabled = false;
}
ShowPage(3); ShowPage(3);
} }
catch (Exception ex) catch (Exception ex)
@@ -230,6 +254,12 @@ public partial class MainWindow : Window
new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() }); new DoubleAnimation(target, TimeSpan.FromMilliseconds(250)) { EasingFunction = new CubicEase() });
} }
private static bool IsFullPath(string p) =>
p.Length >= 3 && char.IsLetter(p[0]) && p[1] == ':' && (p[2] == '\\' || p[2] == '/');
private void OpenRuntimePage_Click(object sender, RoutedEventArgs e) =>
Process.Start(new ProcessStartInfo(Runtime.DownloadPage) { UseShellExecute = true });
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized; private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private void Cancel_Click(object sender, RoutedEventArgs e) private void Cancel_Click(object sender, RoutedEventArgs e)
+169
View File
@@ -0,0 +1,169 @@
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using Microsoft.Win32;
namespace DeskCoreSetup;
/// <summary>Prüft und installiert die von DeskCore benötigte "Microsoft .NET 9 Desktop Runtime".</summary>
public static class Runtime
{
public const string DisplayName = "Microsoft .NET 9 Desktop Runtime";
public const int Major = 9;
public const string DownloadPage = "https://dotnet.microsoft.com/de-de/download/dotnet/9.0";
// offizieller Microsoft-Kurzlink auf die jeweils aktuelle 9.0-Version
private const string InstallerUrl = "https://aka.ms/dotnet/9.0/windowsdesktop-runtime-win-x64.exe";
public static bool Is64BitWindows => Environment.Is64BitOperatingSystem;
public static bool IsInstalled
{
get
{
try
{
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"dotnet", "shared", "Microsoft.WindowsDesktop.App");
if (Directory.Exists(dir) && Directory.GetDirectories(dir).Any(d => Path.GetFileName(d).StartsWith($"{Major}.")))
return true;
}
catch { }
try
{
using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
using var k = hklm.OpenSubKey(@"SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App");
if (k?.GetValueNames().Any(n => n.StartsWith($"{Major}.")) == true) return true;
}
catch { }
return false;
}
}
/// <summary>Lädt die Laufzeit von Microsoft und installiert sie (Windows fragt einmal nach Administratorrechten).</summary>
public static void Install(Action<double, string> progress)
{
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
var file = Path.Combine(Path.GetTempPath(), $"windowsdesktop-runtime-{Major}-win-x64.exe");
using (var http = new HttpClient { Timeout = TimeSpan.FromMinutes(20) })
{
http.DefaultRequestHeaders.UserAgent.ParseAdd("DeskCore-Setup");
using var resp = http.GetAsync(InstallerUrl, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult();
resp.EnsureSuccessStatusCode();
long total = resp.Content.Headers.ContentLength ?? 0;
using var input = resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
using var output = File.Create(file);
var buffer = new byte[81920];
long done = 0;
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, read);
done += read;
progress(total > 0 ? (double)done / total : 0,
total > 0 ? $"{DisplayName} wird heruntergeladen … {done / 1048576} von {total / 1048576} MB"
: $"{DisplayName} wird heruntergeladen … {done / 1048576} MB");
}
}
// Sicherheit: nur ausführen, wenn die Datei gültig von Microsoft signiert ist
if (!IsSignedByMicrosoft(file))
{
try { File.Delete(file); } catch { }
throw new InvalidOperationException("Die heruntergeladene Datei ist nicht gültig von Microsoft signiert und wurde nicht ausgeführt.");
}
progress(1, $"{DisplayName} wird installiert – bitte die Windows-Abfrage bestätigen …");
Process p;
try
{
p = Process.Start(new ProcessStartInfo(file, "/install /quiet /norestart") { UseShellExecute = true, Verb = "runas" })!;
}
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
{
throw new InvalidOperationException($"Die Installation von {DisplayName} wurde abgebrochen.");
}
p.WaitForExit();
try { File.Delete(file); } catch { }
// 0 = ok, 3010/1641 = ok, Neustart empfohlen
if (p.ExitCode is not (0 or 3010 or 1641) || !IsInstalled)
throw new InvalidOperationException($"{DisplayName} konnte nicht installiert werden (Code {p.ExitCode}).");
}
// ---------- Signaturprüfung (WinVerifyTrust) ----------
private static bool IsSignedByMicrosoft(string file)
{
try
{
var subject = X509Certificate.CreateFromSignedFile(file).Subject;
if (subject.IndexOf("O=Microsoft Corporation", StringComparison.OrdinalIgnoreCase) < 0) return false;
}
catch { return false; }
return VerifyTrust(file);
}
private static bool VerifyTrust(string file)
{
var fileInfo = new WINTRUST_FILE_INFO
{
cbStruct = (uint)Marshal.SizeOf<WINTRUST_FILE_INFO>(),
pcwszFilePath = file,
};
var pFile = Marshal.AllocHGlobal(Marshal.SizeOf<WINTRUST_FILE_INFO>());
try
{
Marshal.StructureToPtr(fileInfo, pFile, false);
var data = new WINTRUST_DATA
{
cbStruct = (uint)Marshal.SizeOf<WINTRUST_DATA>(),
dwUIChoice = 2, // WTD_UI_NONE
fdwRevocationChecks = 0, // WTD_REVOKE_NONE
dwUnionChoice = 1, // WTD_CHOICE_FILE
pFile = pFile,
dwStateAction = 0,
dwProvFlags = 0x40, // WTD_SAFER_FLAG
};
var action = new Guid("00AAC56B-CD44-11d0-8CC2-00C04FC295EE"); // WINTRUST_ACTION_GENERIC_VERIFY_V2
return WinVerifyTrust(IntPtr.Zero, ref action, ref data) == 0;
}
finally
{
Marshal.FreeHGlobal(pFile);
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WINTRUST_FILE_INFO
{
public uint cbStruct;
public string pcwszFilePath;
public IntPtr hFile;
public IntPtr pgKnownSubject;
}
[StructLayout(LayoutKind.Sequential)]
private struct WINTRUST_DATA
{
public uint cbStruct;
public IntPtr pPolicyCallbackData;
public IntPtr pSIPClientData;
public uint dwUIChoice;
public uint fdwRevocationChecks;
public uint dwUnionChoice;
public IntPtr pFile;
public uint dwStateAction;
public IntPtr hWVTStateData;
public IntPtr pwszURLReference;
public uint dwProvFlags;
public uint dwUIContext;
public IntPtr pSignatureSettings;
}
[DllImport("wintrust.dll", ExactSpelling = true, SetLastError = false, CharSet = CharSet.Unicode)]
private static extern int WinVerifyTrust(IntPtr hwnd, ref Guid pgActionID, ref WINTRUST_DATA pWVTData);
}
+72 -12
View File
@@ -3,7 +3,7 @@ using System.IO;
namespace DeskCore; namespace DeskCore;
public enum TransferStatus { Ok, Skipped, Error } public enum TransferStatus { Ok, Skipped, Error, Cancelled }
/// <param name="Copied">true = kopiert (Original bleibt), false = verschoben</param> /// <param name="Copied">true = kopiert (Original bleibt), false = verschoben</param>
public record TransferResult(TransferStatus Status, string Source, string? Dest = null, bool Copied = false, string? Message = null); public record TransferResult(TransferStatus Status, string Source, string? Dest = null, bool Copied = false, string? Message = null);
@@ -140,8 +140,10 @@ public static class Storage
/// wird kopiert – das Original kann danach gelöscht werden. Innerhalb der App und zurück auf den /// wird kopiert – das Original kann danach gelöscht werden. Innerhalb der App und zurück auf den
/// Desktop wird verschoben. Gleiche Elemente werden nicht doppelt angelegt. /// Desktop wird verschoben. Gleiche Elemente werden nicht doppelt angelegt.
/// </summary> /// </summary>
public static TransferResult Transfer(string source, string targetDir, bool forceMove = false) public static TransferResult Transfer(string source, string targetDir, bool forceMove = false,
Action<long>? onBytes = null, CancellationToken ct = default)
{ {
if (ct.IsCancellationRequested) return new(TransferStatus.Cancelled, source);
var name = Path.GetFileName(source.TrimEnd('\\')); var name = Path.GetFileName(source.TrimEnd('\\'));
try try
{ {
@@ -160,8 +162,18 @@ public static class Storage
Directory.CreateDirectory(targetDir); Directory.CreateDirectory(targetDir);
var dest = UniquePath(targetDir, name); var dest = UniquePath(targetDir, name);
if (copy) CopyAny(source, dest); try
else MoveAny(source, dest); {
if (copy) CopyAny(source, dest, onBytes, ct);
else MoveAny(source, dest, onBytes, ct);
}
catch (OperationCanceledException)
{
// halb kopiertes Ziel wieder entfernen – die Quelle ist unverändert
// (beim Verschieben zwischen Laufwerken wird die Quelle erst nach vollständiger Kopie gelöscht)
DeletePartial(dest);
return new(TransferStatus.Cancelled, source);
}
return new(TransferStatus.Ok, source, dest, copy); return new(TransferStatus.Ok, source, dest, copy);
} }
catch (UnauthorizedAccessException) catch (UnauthorizedAccessException)
@@ -176,7 +188,7 @@ public static class Storage
} }
/// <summary>Verschiebt Datei oder Ordner – auch zwischen Laufwerken.</summary> /// <summary>Verschiebt Datei oder Ordner – auch zwischen Laufwerken.</summary>
public static void MoveAny(string source, string dest) public static void MoveAny(string source, string dest, Action<long>? onBytes = null, CancellationToken ct = default)
{ {
NoteCreated(dest); NoteCreated(dest);
if (File.Exists(source)) if (File.Exists(source))
@@ -189,15 +201,15 @@ public static class Storage
Directory.Move(source, dest); Directory.Move(source, dest);
return; return;
} }
CopyDirectory(source, dest); CopyDirectory(source, dest, onBytes, ct);
Directory.Delete(source, recursive: true); Directory.Delete(source, recursive: true);
} }
public static void CopyAny(string source, string dest) public static void CopyAny(string source, string dest, Action<long>? onBytes = null, CancellationToken ct = default)
{ {
NoteCreated(dest); NoteCreated(dest);
if (Directory.Exists(source)) CopyDirectory(source, dest); if (Directory.Exists(source)) CopyDirectory(source, dest, onBytes, ct);
else File.Copy(source, dest); else CopyFile(source, dest, onBytes, ct);
} }
/// <summary>Benennt ein Element um. Gibt den neuen Pfad zurück.</summary> /// <summary>Benennt ein Element um. Gibt den neuen Pfad zurück.</summary>
@@ -321,12 +333,60 @@ public static class Storage
} }
} }
private static void CopyDirectory(string source, string dest) private static void CopyDirectory(string source, string dest, Action<long>? onBytes, CancellationToken ct)
{ {
Directory.CreateDirectory(dest); Directory.CreateDirectory(dest);
foreach (var f in Directory.GetFiles(source)) foreach (var f in Directory.GetFiles(source))
File.Copy(f, Path.Combine(dest, Path.GetFileName(f))); CopyFile(f, Path.Combine(dest, Path.GetFileName(f)), onBytes, ct);
foreach (var d in Directory.GetDirectories(source)) foreach (var d in Directory.GetDirectories(source))
CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d))); CopyDirectory(d, Path.Combine(dest, Path.GetFileName(d)), onBytes, ct);
}
/// <summary>Kopiert in 1-MB-Stücken, damit Fortschritt angezeigt und abgebrochen werden kann.</summary>
private static void CopyFile(string source, string dest, Action<long>? onBytes, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
const int Chunk = 1 << 20;
using (var input = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, Chunk, FileOptions.SequentialScan))
using (var output = new FileStream(dest, FileMode.CreateNew, FileAccess.Write, FileShare.None, Chunk))
{
var buffer = new byte[Chunk];
int read;
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
{
ct.ThrowIfCancellationRequested();
output.Write(buffer, 0, read);
onBytes?.Invoke(read);
}
}
// Zeitstempel und Attribute wie beim normalen Kopieren übernehmen
try
{
File.SetCreationTimeUtc(dest, File.GetCreationTimeUtc(source));
File.SetLastWriteTimeUtc(dest, File.GetLastWriteTimeUtc(source));
File.SetAttributes(dest, File.GetAttributes(source) & ~FileAttributes.ReadOnly);
}
catch { }
}
private static void DeletePartial(string path)
{
try
{
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
else if (File.Exists(path)) File.Delete(path);
}
catch { }
}
/// <summary>Wie viele Bytes müssen für diese Übertragung tatsächlich kopiert werden? (0 = nur umbenennen)</summary>
public static long BytesToCopy(string source, string targetDir, bool forceMove)
{
bool copy = !forceMove && !IsInStorage(source) && IsInStorage(targetDir);
bool crossDrive = !string.Equals(Path.GetPathRoot(Path.GetFullPath(source)), Path.GetPathRoot(Path.GetFullPath(targetDir)),
StringComparison.OrdinalIgnoreCase);
if (!copy && !crossDrive) return 0;
try { return Directory.Exists(source) ? DirectorySize(source) : new FileInfo(source).Length; }
catch { return 0; }
} }
} }
+102
View File
@@ -0,0 +1,102 @@
using System.IO;
using System.Net.Http;
using System.Text.Json;
namespace DeskCore;
public record UpdateInfo(Version Version, string Tag, string PageUrl, string? SetupUrl, string? Notes);
/// <summary>Fragt das neueste Release auf Gitea ab (https://git.viper.ipv64.net/M_Viper/DeskCore/releases).</summary>
public static class UpdateChecker
{
private const string Host = "git.viper.ipv64.net";
private const string LatestApi = $"https://{Host}/api/v1/repos/M_Viper/DeskCore/releases/latest";
public const string ReleasesPage = $"https://{Host}/M_Viper/DeskCore/releases";
private const string SetupAsset = "DeskCore-Setup.exe";
public static Version Current
{
get
{
var v = typeof(UpdateChecker).Assembly.GetName().Version ?? new Version(1, 0, 0);
return new Version(v.Major, v.Minor, Math.Max(0, v.Build));
}
}
private static HttpClient NewClient(TimeSpan timeout)
{
var http = new HttpClient { Timeout = timeout };
http.DefaultRequestHeaders.UserAgent.ParseAdd($"DeskCore/{Current}");
return http;
}
/// <summary>Neuere Version oder null. Wirft bei Netzwerk-/Serverfehlern.</summary>
public static async Task<UpdateInfo?> CheckAsync(CancellationToken ct = default)
{
using var http = NewClient(TimeSpan.FromSeconds(15));
using var resp = await http.GetAsync(LatestApi, ct);
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
throw new InvalidOperationException("Es ist noch kein Release veröffentlicht (oder das Repository ist nicht öffentlich).");
resp.EnsureSuccessStatusCode();
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync(ct));
var root = doc.RootElement;
var tag = root.GetProperty("tag_name").GetString() ?? "";
if (!TryParseVersion(tag, out var version) || version <= Current) return null;
string? setupUrl = null;
if (root.TryGetProperty("assets", out var assets))
foreach (var a in assets.EnumerateArray())
if (string.Equals(a.GetProperty("name").GetString(), SetupAsset, StringComparison.OrdinalIgnoreCase))
setupUrl = a.GetProperty("browser_download_url").GetString();
// Nur Downloads vom eigenen Server zulassen
if (setupUrl is not null && !IsTrusted(setupUrl)) setupUrl = null;
var page = root.TryGetProperty("html_url", out var h) ? h.GetString() : null;
return new UpdateInfo(version, tag, page is not null && IsTrusted(page) ? page : ReleasesPage, setupUrl,
root.TryGetProperty("body", out var b) ? b.GetString() : null);
}
/// <summary>Lädt den Installer in den Temp-Ordner und gibt den Pfad zurück.</summary>
public static async Task<string> DownloadSetupAsync(UpdateInfo info, Action<long, long>? progress, CancellationToken ct)
{
if (info.SetupUrl is null) throw new InvalidOperationException("Im Release fehlt die Datei DeskCore-Setup.exe.");
using var http = NewClient(TimeSpan.FromMinutes(10));
using var resp = await http.GetAsync(info.SetupUrl, HttpCompletionOption.ResponseHeadersRead, ct);
resp.EnsureSuccessStatusCode();
long total = resp.Content.Headers.ContentLength ?? 0;
var path = Path.Combine(Path.GetTempPath(), $"DeskCore-Setup-{info.Version}.exe");
await using (var input = await resp.Content.ReadAsStreamAsync(ct))
await using (var output = File.Create(path))
{
var buffer = new byte[81920];
long done = 0;
int read;
while ((read = await input.ReadAsync(buffer, ct)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), ct);
done += read;
progress?.Invoke(done, total);
}
}
return path;
}
private static bool IsTrusted(string url) =>
Uri.TryCreate(url, UriKind.Absolute, out var u) && u.Scheme == Uri.UriSchemeHttps &&
string.Equals(u.Host, Host, StringComparison.OrdinalIgnoreCase);
public static bool TryParseVersion(string tag, out Version version)
{
var t = tag.Trim().TrimStart('v', 'V');
if (Version.TryParse(t, out var v))
{
version = new Version(v.Major, v.Minor, Math.Max(0, v.Build));
return true;
}
version = new Version(0, 0, 0);
return false;
}
}
+8 -5
View File
@@ -1,19 +1,22 @@
# Baut DeskCore und den Installer. # Baut DeskCore und den Installer.
# DeskCore.exe -> ..\DeskCore\ (direkt startbar) # DeskCore.exe -> ..\DeskCore\ (direkt startbar)
# DeskCore-Setup.exe -> ..\DeskCore-Installer\ (zum Installieren/Weitergeben) # DeskCore-Setup.exe -> ..\DeskCore-Installer\ (zum Installieren/Weitergeben, mit README)
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = $PSScriptRoot $root = $PSScriptRoot
$common = @("-c", "Release", "-r", "win-x64", "-p:SelfContained=false", "-p:PublishSingleFile=true", "-p:DebugType=none", "--nologo", "-v", "q")
Write-Host "1/2 DeskCore wird gebaut ..." -ForegroundColor Cyan Write-Host "1/2 DeskCore wird gebaut ..." -ForegroundColor Cyan
dotnet publish "$root\DeskCore.csproj" @common -o "$root\artifacts\app" dotnet publish "$root\DeskCore.csproj" -c Release -r win-x64 -p:SelfContained=false -p:PublishSingleFile=true -p:DebugType=none --nologo -v q -o "$root\artifacts\app"
if ($LASTEXITCODE -ne 0) { throw "Build der App fehlgeschlagen" } if ($LASTEXITCODE -ne 0) { throw "Build der App fehlgeschlagen" }
New-Item -ItemType Directory -Force "$root\..\DeskCore" | Out-Null New-Item -ItemType Directory -Force "$root\..\DeskCore" | Out-Null
Copy-Item "$root\artifacts\app\*" "$root\..\DeskCore\" -Force Copy-Item "$root\artifacts\app\*" "$root\..\DeskCore\" -Force
Write-Host "2/2 Installer wird gebaut ..." -ForegroundColor Cyan Write-Host "2/2 Installer wird gebaut ..." -ForegroundColor Cyan
dotnet publish "$root\Setup\DeskCoreSetup.csproj" @common -o "$root\..\DeskCore-Installer" # Der Installer läuft auf .NET Framework 4.8 (in Windows enthalten) und ist eine einzelne .exe
dotnet build "$root\Setup\DeskCoreSetup.csproj" -c Release -p:DebugType=none --nologo -v q -o "$root\artifacts\setup"
if ($LASTEXITCODE -ne 0) { throw "Build des Installers fehlgeschlagen" } if ($LASTEXITCODE -ne 0) { throw "Build des Installers fehlgeschlagen" }
New-Item -ItemType Directory -Force "$root\..\DeskCore-Installer" | Out-Null
Copy-Item "$root\artifacts\setup\DeskCore-Setup.exe" "$root\..\DeskCore-Installer\" -Force
Copy-Item "$root\README.md" "$root\..\DeskCore-Installer\" -Force
Get-Item "$root\..\DeskCore-Installer\DeskCore-Setup.exe" | Select-Object Name, @{n="Größe";e={"{0:N0} KB" -f ($_.Length/1KB)}} Get-Item "$root\..\DeskCore-Installer\DeskCore-Setup.exe" | Select-Object Name, @{n="Groesse";e={"{0:N0} KB" -f ($_.Length/1KB)}}
Write-Host "Fertig." -ForegroundColor Green Write-Host "Fertig." -ForegroundColor Green