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