2026-07-31 11:43:50 +09:00
|
|
|
|
using System.ComponentModel;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
using System.Diagnostics;
|
2026-07-31 11:43:50 +09:00
|
|
|
|
using System.Security;
|
|
|
|
|
|
using System.Collections.ObjectModel;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-07-31 11:43:50 +09:00
|
|
|
|
using LanDesktopPLONDS.Installer.Localization;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
using LanDesktopPLONDS.Installer.Models;
|
|
|
|
|
|
using LanDesktopPLONDS.Installer.Services;
|
|
|
|
|
|
using LanMountainDesktop.Shared.Contracts.Privacy;
|
|
|
|
|
|
|
|
|
|
|
|
namespace LanDesktopPLONDS.Installer.ViewModels;
|
|
|
|
|
|
|
|
|
|
|
|
public sealed partial class MainWindowViewModel : ObservableObject
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly IOnlineInstallService _installService;
|
|
|
|
|
|
private readonly IPrivacyDeviceIdentityProvider _privacyIdentity;
|
|
|
|
|
|
private readonly InstallerPrivacyConsentStore _privacyConsentStore;
|
|
|
|
|
|
private CancellationTokenSource? _installCts;
|
2026-07-31 11:43:50 +09:00
|
|
|
|
private CancellationTokenSource? _checkCts;
|
|
|
|
|
|
|
|
|
|
|
|
// 下载速度计算状态
|
|
|
|
|
|
private long _lastBytesDownloaded;
|
|
|
|
|
|
private DateTime _lastProgressTime = DateTime.UtcNow;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(StartInstallCommand))]
|
|
|
|
|
|
private InstallerStepId _currentStep = InstallerStepId.Welcome;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private InstallerStepId _maxUnlockedStep = InstallerStepId.Welcome;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(StartInstallCommand))]
|
|
|
|
|
|
private string _installPath = InstallerPathGuard.GetDefaultInstallPath();
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(StartInstallCommand))]
|
|
|
|
|
|
private bool _privacyConfirmed;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string? _targetVersion;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string? _sourceId;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string? _errorMessage;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string _statusText = "准备开始安装";
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private double _downloadProgress;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private double _installProgress;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string? _currentFile;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string _downloadBytesText = string.Empty;
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string _downloadSpeedText = string.Empty;
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(StartInstallCommand))]
|
2026-06-03 12:32:56 +08:00
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
2026-06-03 00:50:52 +08:00
|
|
|
|
private bool _isInstalling;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private bool _createDesktopShortcut;
|
|
|
|
|
|
|
2026-06-03 12:32:56 +08:00
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private bool _createStartupShortcut;
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
// === Task 1: 可取消的版本检查 ===
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
|
|
|
|
|
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
|
|
|
|
|
|
private bool _isCheckingUpdate;
|
|
|
|
|
|
|
|
|
|
|
|
// === Task 2: 自动提权 ===
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private bool _isElevationRequired;
|
|
|
|
|
|
|
|
|
|
|
|
[ObservableProperty]
|
|
|
|
|
|
private string _elevationMessage = "所选安装路径需要管理员权限。";
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
public MainWindowViewModel(
|
|
|
|
|
|
IOnlineInstallService installService,
|
|
|
|
|
|
IPrivacyDeviceIdentityProvider privacyIdentity,
|
|
|
|
|
|
InstallerPrivacyConsentStore? privacyConsentStore = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
_installService = installService;
|
|
|
|
|
|
_privacyIdentity = privacyIdentity;
|
|
|
|
|
|
_privacyConsentStore = privacyConsentStore ?? new InstallerPrivacyConsentStore();
|
|
|
|
|
|
Steps =
|
|
|
|
|
|
[
|
2026-06-09 22:18:27 +08:00
|
|
|
|
new InstallerStepViewModel(InstallerStepId.Welcome, "开始安装", "\uE768"),
|
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.InstallLocation, "安装位置", "\uE838"),
|
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.PrivacyConfirm, "数据确认", "\uE946"),
|
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.Deploy, "开始部署", "\uE896"),
|
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.Complete, "完成安装", "\uE73E")
|
2026-06-03 00:50:52 +08:00
|
|
|
|
];
|
|
|
|
|
|
SyncSteps();
|
|
|
|
|
|
DeviceIdPreview = _privacyIdentity.GetOrCreateDeviceId();
|
|
|
|
|
|
PrivacyConfirmed = _privacyConsentStore.HasConfirmed(DeviceIdPreview);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ObservableCollection<InstallerStepViewModel> Steps { get; }
|
|
|
|
|
|
|
|
|
|
|
|
public Func<string, Task<string?>>? BrowseRequested { get; set; }
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
/// <summary>窗口标题,已本地化。</summary>
|
|
|
|
|
|
public string WindowTitle => "阑山桌面 安装程序";
|
2026-06-03 00:50:52 +08:00
|
|
|
|
|
|
|
|
|
|
public string DeviceIdPreview { get; }
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsWelcomeStep => CurrentStep == InstallerStepId.Welcome;
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsLocationStep => CurrentStep == InstallerStepId.InstallLocation;
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsPrivacyStep => CurrentStep == InstallerStepId.PrivacyConfirm;
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsDeployStep => CurrentStep == InstallerStepId.Deploy;
|
|
|
|
|
|
|
|
|
|
|
|
public bool IsCompleteStep => CurrentStep == InstallerStepId.Complete;
|
|
|
|
|
|
|
2026-06-03 12:32:56 +08:00
|
|
|
|
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling && !IsCheckingUpdate;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
|
|
|
|
|
|
public bool CanGoNext => CurrentStep switch
|
|
|
|
|
|
{
|
2026-07-31 11:43:50 +09:00
|
|
|
|
InstallerStepId.Welcome => !IsInstalling && !IsCheckingUpdate,
|
|
|
|
|
|
InstallerStepId.InstallLocation => !string.IsNullOrWhiteSpace(InstallPath) && !IsInstalling && !IsCheckingUpdate,
|
|
|
|
|
|
InstallerStepId.PrivacyConfirm => PrivacyConfirmed && !IsInstalling && !IsCheckingUpdate,
|
2026-06-03 00:50:52 +08:00
|
|
|
|
_ => false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
public bool CanStartInstall => CurrentStep == InstallerStepId.Deploy &&
|
|
|
|
|
|
PrivacyConfirmed &&
|
|
|
|
|
|
!string.IsNullOrWhiteSpace(InstallPath) &&
|
|
|
|
|
|
!IsInstalling;
|
|
|
|
|
|
|
|
|
|
|
|
public InstallerWorkflowState Snapshot => new(
|
|
|
|
|
|
CurrentStep,
|
|
|
|
|
|
MaxUnlockedStep,
|
|
|
|
|
|
InstallPath,
|
|
|
|
|
|
PrivacyConfirmed,
|
|
|
|
|
|
TargetVersion,
|
|
|
|
|
|
ErrorMessage);
|
|
|
|
|
|
|
|
|
|
|
|
partial void OnCurrentStepChanged(InstallerStepId value)
|
|
|
|
|
|
{
|
|
|
|
|
|
OnPropertyChanged(nameof(IsWelcomeStep));
|
|
|
|
|
|
OnPropertyChanged(nameof(IsLocationStep));
|
|
|
|
|
|
OnPropertyChanged(nameof(IsPrivacyStep));
|
|
|
|
|
|
OnPropertyChanged(nameof(IsDeployStep));
|
|
|
|
|
|
OnPropertyChanged(nameof(IsCompleteStep));
|
|
|
|
|
|
OnPropertyChanged(nameof(CanGoBack));
|
|
|
|
|
|
OnPropertyChanged(nameof(CanGoNext));
|
|
|
|
|
|
OnPropertyChanged(nameof(CanStartInstall));
|
|
|
|
|
|
SyncSteps();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 12:32:56 +08:00
|
|
|
|
partial void OnErrorMessageChanged(string? value)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ = value;
|
|
|
|
|
|
OnPropertyChanged(nameof(HasError));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
partial void OnMaxUnlockedStepChanged(InstallerStepId value)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ = value;
|
|
|
|
|
|
SyncSteps();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 12:32:56 +08:00
|
|
|
|
partial void OnIsInstallingChanged(bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ = value;
|
|
|
|
|
|
OnPropertyChanged(nameof(CanGoBack));
|
|
|
|
|
|
OnPropertyChanged(nameof(CanGoNext));
|
|
|
|
|
|
OnPropertyChanged(nameof(CanStartInstall));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
partial void OnIsCheckingUpdateChanged(bool value)
|
|
|
|
|
|
{
|
|
|
|
|
|
_ = value;
|
|
|
|
|
|
OnPropertyChanged(nameof(CanGoBack));
|
|
|
|
|
|
OnPropertyChanged(nameof(CanGoNext));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// Task 1: 可取消的版本检查 + Task 2: 自动提权
|
|
|
|
|
|
// =====================================================================
|
2026-06-03 00:50:52 +08:00
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGoNext))]
|
|
|
|
|
|
private async Task NextAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = null;
|
2026-07-31 11:43:50 +09:00
|
|
|
|
IsElevationRequired = false;
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
if (CurrentStep == InstallerStepId.InstallLocation)
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
InstallerPathGuard.ValidateInstallPath(InstallPath);
|
2026-07-31 11:43:50 +09:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = ex.Message;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Task 2: 自动提权检查
|
|
|
|
|
|
if (InstallerElevation.RequiresElevation(InstallPath) && !InstallerElevation.IsRunningElevated())
|
|
|
|
|
|
{
|
|
|
|
|
|
IsElevationRequired = true;
|
|
|
|
|
|
ElevationMessage = $"所选安装路径 {InstallPath} 需要管理员权限才能写入。";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Task 1: 带 CTS 和30秒超时的版本检查
|
|
|
|
|
|
_checkCts?.Dispose();
|
|
|
|
|
|
_checkCts = new CancellationTokenSource();
|
|
|
|
|
|
IsCheckingUpdate = true;
|
|
|
|
|
|
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
|
|
|
|
|
|
timeoutCts.Token, _checkCts.Token);
|
|
|
|
|
|
var info = await _installService.CheckLatestAsync(linkedCts.Token);
|
2026-06-03 00:50:52 +08:00
|
|
|
|
TargetVersion = info.Version;
|
|
|
|
|
|
SourceId = info.SourceId;
|
|
|
|
|
|
StatusText = $"准备安装 {info.Version}";
|
|
|
|
|
|
}
|
2026-07-31 11:43:50 +09:00
|
|
|
|
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = "检查更新超时(30秒),请检查网络连接后重试。";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 用户主动取消
|
|
|
|
|
|
ErrorMessage = "版本检查已取消。";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-06-03 00:50:52 +08:00
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = ex.Message;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-31 11:43:50 +09:00
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
timeoutCts.Dispose();
|
|
|
|
|
|
IsCheckingUpdate = false;
|
|
|
|
|
|
}
|
2026-06-03 00:50:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
else if (CurrentStep == InstallerStepId.PrivacyConfirm)
|
|
|
|
|
|
{
|
|
|
|
|
|
_privacyConsentStore.SaveConfirmed(DeviceIdPreview);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
UnlockAndNavigate(CurrentStep + 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand(CanExecute = nameof(CanGoBack))]
|
|
|
|
|
|
private void Back()
|
|
|
|
|
|
{
|
2026-06-03 12:32:56 +08:00
|
|
|
|
if (IsInstalling)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
if (CurrentStep > InstallerStepId.Welcome)
|
|
|
|
|
|
{
|
|
|
|
|
|
CurrentStep -= 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-05 11:08:11 +08:00
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void SelectStep(InstallerStepViewModel? step)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (step is null || IsInstalling || step.StepId > MaxUnlockedStep)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CurrentStep = step.StepId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private async Task BrowseAsync()
|
|
|
|
|
|
{
|
2026-06-03 12:32:56 +08:00
|
|
|
|
ErrorMessage = null;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
if (BrowseRequested is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-03 12:32:56 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var selected = await BrowseRequested(InstallPath);
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(selected))
|
|
|
|
|
|
{
|
2026-06-05 11:08:11 +08:00
|
|
|
|
InstallPath = InstallerPathGuard.GetInstallPathForSelectedFolder(selected);
|
2026-06-03 12:32:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
2026-06-03 00:50:52 +08:00
|
|
|
|
{
|
2026-06-03 12:32:56 +08:00
|
|
|
|
ErrorMessage = $"选择安装位置失败:{ex.Message}";
|
2026-06-03 00:50:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// Task 1: 取消版本检查
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void CancelCheck()
|
|
|
|
|
|
{
|
|
|
|
|
|
_checkCts?.Cancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// Task 2: 自动提权 — 以管理员身份重新启动
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void RelaunchElevated()
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var exePath = Environment.ProcessPath ?? System.Reflection.Assembly.GetExecutingAssembly().Location;
|
|
|
|
|
|
var args = $"--install-path \"{InstallPath}\"";
|
|
|
|
|
|
|
|
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
FileName = exePath,
|
|
|
|
|
|
Arguments = args,
|
|
|
|
|
|
UseShellExecute = true,
|
|
|
|
|
|
Verb = "runas"
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
Environment.Exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 用户在 UAC 对话框中点击了"否"(拒绝提权)
|
|
|
|
|
|
ErrorMessage = "已拒绝管理员权限请求。请选择一个不需要管理员权限的安装路径,或手动以管理员身份运行安装程序。";
|
|
|
|
|
|
IsElevationRequired = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = $"重新启动失败:{ex.Message}";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
2026-06-03 00:50:52 +08:00
|
|
|
|
private async Task StartInstallAsync()
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = null;
|
|
|
|
|
|
IsInstalling = true;
|
|
|
|
|
|
StartInstallCommand.NotifyCanExecuteChanged();
|
|
|
|
|
|
_installCts?.Dispose();
|
|
|
|
|
|
_installCts = new CancellationTokenSource();
|
2026-07-31 11:43:50 +09:00
|
|
|
|
|
|
|
|
|
|
// 重置下载速度状态
|
|
|
|
|
|
_lastBytesDownloaded = 0;
|
|
|
|
|
|
_lastProgressTime = DateTime.UtcNow;
|
|
|
|
|
|
DownloadSpeedText = string.Empty;
|
|
|
|
|
|
|
2026-06-03 00:50:52 +08:00
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var progress = new Progress<InstallerDeployProgress>(ApplyProgress);
|
2026-06-03 12:32:56 +08:00
|
|
|
|
var options = new OnlineInstallOptions(CreateDesktopShortcut, CreateStartupShortcut);
|
2026-06-03 00:50:52 +08:00
|
|
|
|
await _installService.InstallFreshAsync(InstallPath, options, progress, _installCts.Token);
|
|
|
|
|
|
UnlockAndNavigate(InstallerStepId.Complete);
|
|
|
|
|
|
StatusText = "安装完成";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (OperationCanceledException)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = "安装已取消。";
|
|
|
|
|
|
StatusText = "安装已取消";
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = ex.Message;
|
|
|
|
|
|
StatusText = "安装失败";
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
IsInstalling = false;
|
|
|
|
|
|
StartInstallCommand.NotifyCanExecuteChanged();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void CancelInstall()
|
|
|
|
|
|
{
|
|
|
|
|
|
_installCts?.Cancel();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[RelayCommand]
|
|
|
|
|
|
private void Launch()
|
|
|
|
|
|
{
|
|
|
|
|
|
LaunchCore();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void LaunchCore()
|
|
|
|
|
|
{
|
|
|
|
|
|
var launcher = Path.Combine(InstallPath, OperatingSystem.IsWindows()
|
|
|
|
|
|
? "LanMountainDesktop.Launcher.exe"
|
|
|
|
|
|
: "LanMountainDesktop.Launcher");
|
|
|
|
|
|
if (!File.Exists(launcher))
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = "未找到 LanMountainDesktop.Launcher。";
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Process.Start(new ProcessStartInfo
|
|
|
|
|
|
{
|
|
|
|
|
|
FileName = launcher,
|
|
|
|
|
|
Arguments = "--launch-source postinstall",
|
|
|
|
|
|
WorkingDirectory = InstallPath,
|
|
|
|
|
|
UseShellExecute = true
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
ErrorMessage = ex.Message;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void UnlockAndNavigate(InstallerStepId step)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (step > MaxUnlockedStep)
|
|
|
|
|
|
{
|
|
|
|
|
|
MaxUnlockedStep = step;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
CurrentStep = step;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-31 11:43:50 +09:00
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// Task 4: 本地化进度阶段 + Task 5: 下载速度显示
|
|
|
|
|
|
// =====================================================================
|
2026-06-03 00:50:52 +08:00
|
|
|
|
private void ApplyProgress(InstallerDeployProgress progress)
|
|
|
|
|
|
{
|
2026-07-31 11:43:50 +09:00
|
|
|
|
// Task 4: 将英文阶段键翻译为中文
|
|
|
|
|
|
StatusText = InstallerStrings.TranslateStage(progress.Stage);
|
2026-06-03 00:50:52 +08:00
|
|
|
|
TargetVersion = progress.TargetVersion ?? TargetVersion;
|
|
|
|
|
|
DownloadProgress = progress.DownloadProgress;
|
|
|
|
|
|
InstallProgress = progress.InstallProgress;
|
|
|
|
|
|
CurrentFile = progress.CurrentFile;
|
|
|
|
|
|
DownloadBytesText = FormatBytes(progress.BytesDownloaded, progress.TotalBytes);
|
2026-07-31 11:43:50 +09:00
|
|
|
|
|
|
|
|
|
|
// Task 5: 计算下载速度
|
|
|
|
|
|
var now = DateTime.UtcNow;
|
|
|
|
|
|
var elapsed = (now - _lastProgressTime).TotalSeconds;
|
|
|
|
|
|
if (elapsed > 0.5 && progress.BytesDownloaded > _lastBytesDownloaded && progress.BytesDownloaded > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var speedBytesPerSec = (progress.BytesDownloaded - _lastBytesDownloaded) / elapsed;
|
|
|
|
|
|
DownloadSpeedText = $"{ToSize((long)speedBytesPerSec)}/s";
|
|
|
|
|
|
_lastBytesDownloaded = progress.BytesDownloaded;
|
|
|
|
|
|
_lastProgressTime = now;
|
|
|
|
|
|
}
|
2026-06-03 00:50:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void SyncSteps()
|
|
|
|
|
|
{
|
2026-06-05 11:08:11 +08:00
|
|
|
|
foreach (var step in Steps)
|
2026-06-03 00:50:52 +08:00
|
|
|
|
{
|
2026-06-05 11:08:11 +08:00
|
|
|
|
step.IsUnlocked = step.StepId <= MaxUnlockedStep;
|
|
|
|
|
|
step.IsSelected = step.StepId == CurrentStep;
|
2026-06-03 00:50:52 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string FormatBytes(long downloaded, long? total)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (downloaded <= 0 && total is not > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var downloadedText = ToSize(downloaded);
|
|
|
|
|
|
return total is > 0 ? $"{downloadedText} / {ToSize(total.Value)}" : downloadedText;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string ToSize(long value)
|
|
|
|
|
|
{
|
|
|
|
|
|
string[] suffixes = ["B", "KB", "MB", "GB"];
|
|
|
|
|
|
var size = (double)value;
|
|
|
|
|
|
var suffix = 0;
|
|
|
|
|
|
while (size >= 1024 && suffix < suffixes.Length - 1)
|
|
|
|
|
|
{
|
|
|
|
|
|
size /= 1024;
|
|
|
|
|
|
suffix++;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $"{size:0.##} {suffixes[suffix]}";
|
|
|
|
|
|
}
|
2026-07-31 11:43:50 +09:00
|
|
|
|
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
// Task 2: 解析 --install-path 命令行参数
|
|
|
|
|
|
// =====================================================================
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 从命令行参数中解析 --install-path 值。
|
|
|
|
|
|
/// 由 App.axaml.cs 在启动时调用。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public static string? ParseInstallPath(string[]? args)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (args is null || args.Length == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for (var i = 0; i < args.Length - 1; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.Equals(args[i], "--install-path", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
var value = args[i + 1].Trim('"');
|
|
|
|
|
|
return string.IsNullOrWhiteSpace(value) ? null : value;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-06-03 00:50:52 +08:00
|
|
|
|
}
|