mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
changed.velopack,试试rust
This commit is contained in:
@@ -20,4 +20,7 @@ namespace LanMountainDesktop.Launcher;
|
||||
[JsonSerializable(typeof(GitHubRelease))]
|
||||
[JsonSerializable(typeof(GitHubAsset))]
|
||||
[JsonSerializable(typeof(List<GitHubRelease>))]
|
||||
[JsonSerializable(typeof(VelopackReleaseFeed))]
|
||||
[JsonSerializable(typeof(VelopackReleaseAsset))]
|
||||
[JsonSerializable(typeof(List<VelopackReleaseAsset>))]
|
||||
internal sealed partial class AppJsonContext : JsonSerializerContext;
|
||||
|
||||
@@ -11,6 +11,8 @@ public sealed class ReleaseInfo
|
||||
public required DateTime PublishedAt { get; init; }
|
||||
public required List<ReleaseAsset> Assets { get; init; }
|
||||
public string? Body { get; init; }
|
||||
public string? VelopackFeedUrl { get; init; }
|
||||
public string? VelopackLegacyReleasesUrl { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
23
LanMountainDesktop.Launcher/Models/VelopackModels.cs
Normal file
23
LanMountainDesktop.Launcher/Models/VelopackModels.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace LanMountainDesktop.Launcher.Models;
|
||||
|
||||
internal sealed class VelopackReleaseFeed
|
||||
{
|
||||
public List<VelopackReleaseAsset> Assets { get; set; } = [];
|
||||
}
|
||||
|
||||
internal sealed class VelopackReleaseAsset
|
||||
{
|
||||
public string PackageId { get; set; } = string.Empty;
|
||||
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
public string? SHA1 { get; set; }
|
||||
|
||||
public string? SHA256 { get; set; }
|
||||
|
||||
public long Size { get; set; }
|
||||
}
|
||||
@@ -91,11 +91,7 @@ internal static class Commands
|
||||
"check" => updateEngine.CheckPendingUpdate(),
|
||||
"apply" => await updateEngine.ApplyPendingUpdateAsync().ConfigureAwait(false),
|
||||
"rollback" => updateEngine.RollbackLatest(),
|
||||
"download" => await updateEngine.DownloadAsync(
|
||||
context.GetOption("manifest-url") ?? throw new InvalidOperationException("Missing --manifest-url."),
|
||||
context.GetOption("signature-url") ?? throw new InvalidOperationException("Missing --signature-url."),
|
||||
context.GetOption("archive-url") ?? throw new InvalidOperationException("Missing --archive-url."),
|
||||
CancellationToken.None).ConfigureAwait(false),
|
||||
"download" => await DownloadUpdatePayloadAsync(context, updateEngine).ConfigureAwait(false),
|
||||
_ => new LauncherResult
|
||||
{
|
||||
Success = false,
|
||||
@@ -106,6 +102,35 @@ internal static class Commands
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<LauncherResult> DownloadUpdatePayloadAsync(CommandContext context, UpdateEngineService updateEngine)
|
||||
{
|
||||
var releasesUrl = context.GetOption("releases-url");
|
||||
if (!string.IsNullOrWhiteSpace(releasesUrl))
|
||||
{
|
||||
var packageUrls = new List<string>();
|
||||
var packageUrl = context.GetOption("package-url");
|
||||
if (!string.IsNullOrWhiteSpace(packageUrl))
|
||||
{
|
||||
packageUrls.Add(packageUrl);
|
||||
}
|
||||
|
||||
var packageUrlsCsv = context.GetOption("package-urls");
|
||||
if (!string.IsNullOrWhiteSpace(packageUrlsCsv))
|
||||
{
|
||||
packageUrls.AddRange(packageUrlsCsv
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
|
||||
}
|
||||
|
||||
return await updateEngine.DownloadVelopackAsync(releasesUrl, packageUrls, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return await updateEngine.DownloadAsync(
|
||||
context.GetOption("manifest-url") ?? throw new InvalidOperationException("Missing --manifest-url."),
|
||||
context.GetOption("signature-url") ?? throw new InvalidOperationException("Missing --signature-url."),
|
||||
context.GetOption("archive-url") ?? throw new InvalidOperationException("Missing --archive-url."),
|
||||
CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static LauncherResult ExecutePluginCommand(
|
||||
CommandContext context,
|
||||
PluginInstallerService pluginInstaller,
|
||||
|
||||
@@ -104,7 +104,11 @@ internal sealed class UpdateCheckService
|
||||
Name = a.Name ?? "",
|
||||
BrowserDownloadUrl = a.BrowserDownloadUrl ?? "",
|
||||
Size = a.Size
|
||||
}).ToList() ?? []
|
||||
}).ToList() ?? [],
|
||||
VelopackFeedUrl = r.Assets?.FirstOrDefault(a =>
|
||||
string.Equals(a.Name, "releases.win.json", StringComparison.OrdinalIgnoreCase))?.BrowserDownloadUrl,
|
||||
VelopackLegacyReleasesUrl = r.Assets?.FirstOrDefault(a =>
|
||||
string.Equals(a.Name, "RELEASES", StringComparison.OrdinalIgnoreCase))?.BrowserDownloadUrl
|
||||
}).ToList() ?? [];
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ internal sealed class UpdateEngineService
|
||||
private const string SignedFileMapName = "files.json";
|
||||
private const string SignatureFileName = "files.json.sig";
|
||||
private const string ArchiveFileName = "update.zip";
|
||||
private const string VelopackReleasesFileName = "releases.win.json";
|
||||
private const string PublicKeyFileName = "public-key.pem";
|
||||
|
||||
private readonly DeploymentLocator _deploymentLocator;
|
||||
@@ -33,6 +34,16 @@ internal sealed class UpdateEngineService
|
||||
|
||||
public LauncherResult CheckPendingUpdate()
|
||||
{
|
||||
var velopackFeedPath = Path.Combine(_incomingRoot, VelopackReleasesFileName);
|
||||
if (File.Exists(velopackFeedPath))
|
||||
{
|
||||
var velopackResult = CheckVelopackPendingUpdate(velopackFeedPath);
|
||||
if (velopackResult is not null)
|
||||
{
|
||||
return velopackResult;
|
||||
}
|
||||
}
|
||||
|
||||
var fileMapPath = Path.Combine(_incomingRoot, SignedFileMapName);
|
||||
var archivePath = Path.Combine(_incomingRoot, ArchiveFileName);
|
||||
var signaturePath = Path.Combine(_incomingRoot, SignatureFileName);
|
||||
@@ -71,6 +82,47 @@ internal sealed class UpdateEngineService
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LauncherResult> DownloadVelopackAsync(
|
||||
string releasesJsonUrl,
|
||||
IReadOnlyList<string> packageUrls,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(releasesJsonUrl))
|
||||
{
|
||||
return Failed("update.download", "invalid_argument", "Missing releases feed url.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(_incomingRoot);
|
||||
|
||||
using var client = new HttpClient
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(2)
|
||||
};
|
||||
|
||||
var releasesPath = Path.Combine(_incomingRoot, VelopackReleasesFileName);
|
||||
await DownloadToFileAsync(client, releasesJsonUrl, releasesPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var url in packageUrls.Where(u => !string.IsNullOrWhiteSpace(u)).Distinct(StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var fileName = Path.GetFileName(new Uri(url).AbsolutePath);
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destination = Path.Combine(_incomingRoot, fileName);
|
||||
await DownloadToFileAsync(client, url, destination, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new LauncherResult
|
||||
{
|
||||
Success = true,
|
||||
Stage = "update.download",
|
||||
Code = "ok",
|
||||
Message = "Velopack update payload downloaded."
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<LauncherResult> DownloadAsync(string manifestUrl, string signatureUrl, string archiveUrl, CancellationToken cancellationToken)
|
||||
{
|
||||
Directory.CreateDirectory(_incomingRoot);
|
||||
@@ -115,6 +167,12 @@ internal sealed class UpdateEngineService
|
||||
Directory.CreateDirectory(_incomingRoot);
|
||||
Directory.CreateDirectory(_snapshotsRoot);
|
||||
|
||||
var velopackFeedPath = Path.Combine(_incomingRoot, VelopackReleasesFileName);
|
||||
if (File.Exists(velopackFeedPath))
|
||||
{
|
||||
return await ApplyVelopackPendingUpdateAsync(velopackFeedPath).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var fileMapPath = Path.Combine(_incomingRoot, SignedFileMapName);
|
||||
var signaturePath = Path.Combine(_incomingRoot, SignatureFileName);
|
||||
var archivePath = Path.Combine(_incomingRoot, ArchiveFileName);
|
||||
@@ -573,7 +631,8 @@ internal sealed class UpdateEngineService
|
||||
{
|
||||
Path.Combine(_incomingRoot, SignedFileMapName),
|
||||
Path.Combine(_incomingRoot, SignatureFileName),
|
||||
Path.Combine(_incomingRoot, ArchiveFileName)
|
||||
Path.Combine(_incomingRoot, ArchiveFileName),
|
||||
Path.Combine(_incomingRoot, VelopackReleasesFileName)
|
||||
})
|
||||
{
|
||||
try
|
||||
@@ -587,6 +646,17 @@ internal sealed class UpdateEngineService
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var nupkgPath in Directory.EnumerateFiles(_incomingRoot, "*.nupkg", SearchOption.TopDirectoryOnly))
|
||||
{
|
||||
File.Delete(nupkgPath);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private (bool Success, string Message) VerifySignature(string fileMapPath, string signaturePath)
|
||||
@@ -654,6 +724,307 @@ internal sealed class UpdateEngineService
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private LauncherResult? CheckVelopackPendingUpdate(string feedPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var feed = JsonSerializer.Deserialize(File.ReadAllText(feedPath), AppJsonContext.Default.VelopackReleaseFeed);
|
||||
if (feed?.Assets is null || feed.Assets.Count == 0)
|
||||
{
|
||||
return Failed("update.check", "invalid_manifest", "releases.win.json is invalid.");
|
||||
}
|
||||
|
||||
var currentVersion = ParseVersionSafe(_deploymentLocator.GetCurrentVersion());
|
||||
var latest = feed.Assets
|
||||
.Where(a => string.Equals(a.Type, "Full", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(a => new { Asset = a, Version = ParseVersionSafe(a.Version) })
|
||||
.Where(x => x.Version > currentVersion)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (latest is null)
|
||||
{
|
||||
return new LauncherResult
|
||||
{
|
||||
Success = true,
|
||||
Stage = "update.check",
|
||||
Code = "noop",
|
||||
Message = "No pending update for current version."
|
||||
};
|
||||
}
|
||||
|
||||
var packagePath = Path.Combine(_incomingRoot, latest.Asset.FileName);
|
||||
if (!File.Exists(packagePath))
|
||||
{
|
||||
return Failed("update.check", "missing_payload", $"Missing Velopack package '{latest.Asset.FileName}'.");
|
||||
}
|
||||
|
||||
return new LauncherResult
|
||||
{
|
||||
Success = true,
|
||||
Stage = "update.check",
|
||||
Code = "available",
|
||||
Message = "Pending Velopack update is available.",
|
||||
CurrentVersion = _deploymentLocator.GetCurrentVersion(),
|
||||
TargetVersion = latest.Asset.Version
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Failed("update.check", "invalid_manifest", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<LauncherResult> ApplyVelopackPendingUpdateAsync(string feedPath)
|
||||
{
|
||||
VelopackReleaseFeed? feed;
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(feedPath).ConfigureAwait(false);
|
||||
feed = JsonSerializer.Deserialize(json, AppJsonContext.Default.VelopackReleaseFeed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Failed("update.apply", "invalid_manifest", $"Invalid releases feed: {ex.Message}");
|
||||
}
|
||||
|
||||
if (feed?.Assets is null || feed.Assets.Count == 0)
|
||||
{
|
||||
return Failed("update.apply", "invalid_manifest", "releases.win.json has no assets.");
|
||||
}
|
||||
|
||||
var currentDeployment = _deploymentLocator.FindCurrentDeploymentDirectory();
|
||||
if (string.IsNullOrWhiteSpace(currentDeployment))
|
||||
{
|
||||
return Failed("update.apply", "no_current_deployment", "Current deployment not found.");
|
||||
}
|
||||
|
||||
var currentVersionText = _deploymentLocator.GetCurrentVersion();
|
||||
var currentVersion = ParseVersionSafe(currentVersionText);
|
||||
var target = feed.Assets
|
||||
.Where(a => string.Equals(a.Type, "Full", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(a => new { Asset = a, Version = ParseVersionSafe(a.Version) })
|
||||
.Where(x => x.Version > currentVersion)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (target is null)
|
||||
{
|
||||
return new LauncherResult
|
||||
{
|
||||
Success = true,
|
||||
Stage = "update.apply",
|
||||
Code = "noop",
|
||||
Message = "No Velopack update payload found."
|
||||
};
|
||||
}
|
||||
|
||||
var packagePath = Path.Combine(_incomingRoot, target.Asset.FileName);
|
||||
if (!File.Exists(packagePath))
|
||||
{
|
||||
return Failed("update.apply", "missing_payload", $"Missing Velopack package '{target.Asset.FileName}'.");
|
||||
}
|
||||
|
||||
if (!VerifyVelopackPackageChecksum(packagePath, target.Asset))
|
||||
{
|
||||
return Failed("update.apply", "checksum_failed", "Velopack package checksum verification failed.");
|
||||
}
|
||||
|
||||
var targetVersion = string.IsNullOrWhiteSpace(target.Asset.Version) ? currentVersionText : target.Asset.Version;
|
||||
var targetDeployment = _deploymentLocator.BuildNextDeploymentDirectory(targetVersion);
|
||||
var partialMarker = Path.Combine(targetDeployment, ".partial");
|
||||
var snapshot = new SnapshotMetadata
|
||||
{
|
||||
SnapshotId = Guid.NewGuid().ToString("N"),
|
||||
SourceVersion = currentVersionText,
|
||||
TargetVersion = targetVersion,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
SourceDirectory = currentDeployment,
|
||||
TargetDirectory = targetDeployment,
|
||||
Status = "pending"
|
||||
};
|
||||
var snapshotPath = Path.Combine(_snapshotsRoot, $"{snapshot.SnapshotId}.json");
|
||||
var extractRoot = Path.Combine(_incomingRoot, "extracted-velopack");
|
||||
|
||||
try
|
||||
{
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
|
||||
if (Directory.Exists(extractRoot))
|
||||
{
|
||||
Directory.Delete(extractRoot, true);
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(extractRoot);
|
||||
ZipFile.ExtractToDirectory(packagePath, extractRoot, overwriteFiles: true);
|
||||
|
||||
var contentRoot = ResolveVelopackContentRoot(extractRoot);
|
||||
if (contentRoot is null)
|
||||
{
|
||||
throw new InvalidOperationException("Unable to locate app payload in Velopack package.");
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetDeployment);
|
||||
File.WriteAllText(partialMarker, string.Empty);
|
||||
CopyDirectory(contentRoot, targetDeployment);
|
||||
|
||||
var hostExecutable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||
if (!File.Exists(Path.Combine(targetDeployment, hostExecutable)))
|
||||
{
|
||||
throw new InvalidOperationException($"Host executable '{hostExecutable}' not found after applying Velopack package.");
|
||||
}
|
||||
|
||||
ActivateDeployment(currentDeployment, targetDeployment);
|
||||
snapshot.Status = "applied";
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
CleanupIncomingArtifacts();
|
||||
CleanupDestroyedDeployments();
|
||||
|
||||
return new LauncherResult
|
||||
{
|
||||
Success = true,
|
||||
Stage = "update.apply",
|
||||
Code = "ok",
|
||||
Message = $"Updated to {targetVersion}.",
|
||||
CurrentVersion = currentVersionText,
|
||||
TargetVersion = targetVersion
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
TryRollbackOnFailure(snapshot);
|
||||
snapshot.Status = "rolled_back";
|
||||
SaveSnapshot(snapshotPath, snapshot);
|
||||
return new LauncherResult
|
||||
{
|
||||
Success = false,
|
||||
Stage = "update.apply",
|
||||
Code = "apply_failed",
|
||||
Message = "Failed to apply update. Rolled back to previous version.",
|
||||
ErrorMessage = ex.Message,
|
||||
CurrentVersion = currentVersionText,
|
||||
RolledBackTo = currentVersionText
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Directory.Exists(extractRoot))
|
||||
{
|
||||
Directory.Delete(extractRoot, true);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Version ParseVersionSafe(string? version)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(version))
|
||||
{
|
||||
return new Version(0, 0, 0);
|
||||
}
|
||||
|
||||
var normalized = version.Trim();
|
||||
var separatorIndex = normalized.IndexOfAny(['-', '+', ' ']);
|
||||
if (separatorIndex > 0)
|
||||
{
|
||||
normalized = normalized[..separatorIndex];
|
||||
}
|
||||
|
||||
return Version.TryParse(normalized, out var parsed) ? parsed : new Version(0, 0, 0);
|
||||
}
|
||||
|
||||
private static bool VerifyVelopackPackageChecksum(string packagePath, VelopackReleaseAsset asset)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(asset.SHA256))
|
||||
{
|
||||
var actualSha256 = ComputeSha256Hex(packagePath);
|
||||
return string.Equals(actualSha256, asset.SHA256, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(asset.SHA1))
|
||||
{
|
||||
using var stream = File.OpenRead(packagePath);
|
||||
var sha1 = SHA1.HashData(stream);
|
||||
var actualSha1 = Convert.ToHexString(sha1);
|
||||
return string.Equals(actualSha1, asset.SHA1, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveVelopackContentRoot(string extractRoot)
|
||||
{
|
||||
var hostExecutable = OperatingSystem.IsWindows() ? "LanMountainDesktop.exe" : "LanMountainDesktop";
|
||||
var hostPath = Directory
|
||||
.EnumerateFiles(extractRoot, hostExecutable, SearchOption.AllDirectories)
|
||||
.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(hostPath))
|
||||
{
|
||||
return Path.GetDirectoryName(hostPath);
|
||||
}
|
||||
|
||||
// common nupkg layout fallback
|
||||
var libRoot = Path.Combine(extractRoot, "lib");
|
||||
if (Directory.Exists(libRoot))
|
||||
{
|
||||
var best = Directory.GetDirectories(libRoot, "*", SearchOption.TopDirectoryOnly)
|
||||
.OrderByDescending(d => Directory.EnumerateFiles(d, "*", SearchOption.AllDirectories).Count())
|
||||
.FirstOrDefault();
|
||||
if (!string.IsNullOrWhiteSpace(best))
|
||||
{
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
var candidate = Directory.GetDirectories(extractRoot, "*", SearchOption.TopDirectoryOnly)
|
||||
.Where(d => !string.Equals(Path.GetFileName(d), "_rels", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(d => !string.Equals(Path.GetFileName(d), "package", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderByDescending(d => Directory.EnumerateFiles(d, "*", SearchOption.AllDirectories).Count())
|
||||
.FirstOrDefault();
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string sourceDir, string targetDir)
|
||||
{
|
||||
foreach (var dirPath in Directory.EnumerateDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relative = Path.GetRelativePath(sourceDir, dirPath);
|
||||
Directory.CreateDirectory(Path.Combine(targetDir, relative));
|
||||
}
|
||||
|
||||
foreach (var sourceFile in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
var relative = Path.GetRelativePath(sourceDir, sourceFile);
|
||||
var destFile = Path.Combine(targetDir, relative);
|
||||
var destDir = Path.GetDirectoryName(destFile);
|
||||
if (!string.IsNullOrWhiteSpace(destDir))
|
||||
{
|
||||
Directory.CreateDirectory(destDir);
|
||||
}
|
||||
File.Copy(sourceFile, destFile, overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task DownloadToFileAsync(HttpClient client, string url, string destination, CancellationToken cancellationToken)
|
||||
{
|
||||
await using var stream = await client.GetStreamAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
await using var output = File.Create(destination);
|
||||
await stream.CopyToAsync(output, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void SaveSnapshot(string path, SnapshotMetadata snapshot)
|
||||
{
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(snapshot, AppJsonContext.Default.SnapshotMetadata));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="clr-namespace:LanMountainDesktop.Launcher.Views"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="600"
|
||||
d:DesignHeight="500"
|
||||
x:Class="LanMountainDesktop.Launcher.Views.LoadingDetailsWindow"
|
||||
Title="阑山桌面 - 加载详情"
|
||||
Title="LanMountain Desktop - Loading Details"
|
||||
Width="600"
|
||||
Height="500"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
@@ -17,18 +18,17 @@
|
||||
Icon="/Assets/logo.ico">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto,Auto">
|
||||
<!-- 标题栏 -->
|
||||
<Border Grid.Row="0"
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
Padding="20,16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="4">
|
||||
<TextBlock Text="正在启动阑山桌面"
|
||||
<TextBlock Text="Starting LanMountain Desktop"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||
<TextBlock x:Name="SubtitleText"
|
||||
Text="初始化系统组件..."
|
||||
Text="Initializing..."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"/>
|
||||
</StackPanel>
|
||||
@@ -46,7 +46,6 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 主要内容区域 -->
|
||||
<Grid Grid.Row="1" Margin="16,12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
@@ -54,7 +53,6 @@
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 整体进度条 -->
|
||||
<ProgressBar x:Name="OverallProgressBar"
|
||||
Grid.Row="0"
|
||||
Height="8"
|
||||
@@ -64,14 +62,12 @@
|
||||
CornerRadius="4"
|
||||
Margin="0,0,0,16"/>
|
||||
|
||||
<!-- 当前活动项 -->
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource CardBackgroundFillColorSecondaryBrush}"
|
||||
CornerRadius="8"
|
||||
Padding="16,12"
|
||||
Margin="0,0,0,12">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="Auto,*">
|
||||
<!-- 图标 -->
|
||||
<Border Grid.Row="0" Grid.RowSpan="3" Grid.Column="0"
|
||||
Width="40"
|
||||
Height="40"
|
||||
@@ -88,23 +84,20 @@
|
||||
VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
|
||||
<!-- 名称 -->
|
||||
<TextBlock x:Name="CurrentItemName"
|
||||
Grid.Row="0" Grid.Column="1"
|
||||
Text="正在初始化..."
|
||||
Text="Initializing..."
|
||||
FontSize="15"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorPrimaryBrush}"/>
|
||||
|
||||
<!-- 描述 -->
|
||||
<TextBlock x:Name="CurrentItemDescription"
|
||||
Grid.Row="1" Grid.Column="1"
|
||||
Text="准备加载系统组件"
|
||||
Text="Preparing components"
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="0,4,0,0"/>
|
||||
|
||||
<!-- 进度 -->
|
||||
<Grid Grid.Row="2" Grid.Column="1" Margin="0,8,0,0">
|
||||
<ProgressBar x:Name="CurrentItemProgress"
|
||||
Height="4"
|
||||
@@ -116,15 +109,13 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 加载项列表 -->
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
CornerRadius="8">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<!-- 列表标题 -->
|
||||
<Grid Grid.Row="0" Margin="12,8" ColumnDefinitions="*,Auto,Auto">
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="加载项"
|
||||
Text="Loading Items"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
@@ -135,22 +126,20 @@
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
||||
Margin="0,0,4,0"/>
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="已完成"
|
||||
Text="Done"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource TextFillColorTertiaryBrush}"/>
|
||||
</Grid>
|
||||
|
||||
<!-- 列表内容 -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Margin="8,0,8,8">
|
||||
<ItemsControl x:Name="LoadingItemsList">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto"
|
||||
<DataTemplate DataType="views:LoadingItemViewModel">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto"
|
||||
Margin="4,3"
|
||||
Opacity="{Binding Opacity}">
|
||||
<!-- 状态图标 -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="{Binding StatusIcon}"
|
||||
FontSize="14"
|
||||
@@ -159,7 +148,6 @@
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- 名称 -->
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Name}"
|
||||
FontSize="13"
|
||||
@@ -167,7 +155,6 @@
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- 进度 -->
|
||||
<TextBlock Grid.Column="2"
|
||||
Text="{Binding ProgressText}"
|
||||
FontSize="12"
|
||||
@@ -175,7 +162,6 @@
|
||||
Margin="8,0"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- 类型标签 -->
|
||||
<Border Grid.Column="3"
|
||||
Background="{Binding TypeBackground}"
|
||||
CornerRadius="4"
|
||||
@@ -194,7 +180,6 @@
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- 错误信息区域 -->
|
||||
<Border x:Name="ErrorPanel"
|
||||
Grid.Row="2"
|
||||
Background="{DynamicResource SystemFillColorCriticalBackgroundBrush}"
|
||||
@@ -214,14 +199,13 @@
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="ErrorText"
|
||||
Grid.Column="1"
|
||||
Text="加载过程中出现错误"
|
||||
Text="An error occurred while loading."
|
||||
FontSize="13"
|
||||
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
|
||||
TextWrapping="Wrap"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<Border Grid.Row="3"
|
||||
Background="{DynamicResource CardBackgroundFillColorDefaultBrush}"
|
||||
Padding="16,12">
|
||||
@@ -234,12 +218,12 @@
|
||||
VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8">
|
||||
<Button x:Name="DetailsButton"
|
||||
Content="查看详情"
|
||||
Content="Details"
|
||||
Width="90"
|
||||
Height="32"
|
||||
FontSize="13"/>
|
||||
<Button x:Name="CancelButton"
|
||||
Content="取消"
|
||||
Content="Cancel"
|
||||
Width="90"
|
||||
Height="32"
|
||||
FontSize="13"/>
|
||||
|
||||
Reference in New Issue
Block a user