2026-06-03 00:50:52 +08:00
|
|
|
using System.Collections.ObjectModel;
|
|
|
|
|
using System.Diagnostics;
|
|
|
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
|
|
|
using CommunityToolkit.Mvvm.Input;
|
2026-06-03 12:32:56 +08:00
|
|
|
using FluentIcons.Common;
|
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;
|
|
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
|
|
|
|
|
[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-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-03 12:32:56 +08:00
|
|
|
new InstallerStepViewModel(InstallerStepId.Welcome, "开始安装", Icon.Play),
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.InstallLocation, "安装位置", Icon.Folder),
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.PrivacyConfirm, "数据确认", Icon.Info),
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.Deploy, "开始部署", Icon.ArrowDownload),
|
|
|
|
|
new InstallerStepViewModel(InstallerStepId.Complete, "完成安装", Icon.Circle)
|
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; }
|
|
|
|
|
|
|
|
|
|
public string WindowTitle => "LanDesktopPLONDS Installer";
|
|
|
|
|
|
|
|
|
|
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-06-03 00:50:52 +08:00
|
|
|
public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling;
|
|
|
|
|
|
|
|
|
|
public bool CanGoNext => CurrentStep switch
|
|
|
|
|
{
|
|
|
|
|
InstallerStepId.Welcome => !IsInstalling,
|
|
|
|
|
InstallerStepId.InstallLocation => !string.IsNullOrWhiteSpace(InstallPath) && !IsInstalling,
|
|
|
|
|
InstallerStepId.PrivacyConfirm => PrivacyConfirmed && !IsInstalling,
|
|
|
|
|
_ => 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-06-03 00:50:52 +08:00
|
|
|
[RelayCommand(CanExecute = nameof(CanGoNext))]
|
|
|
|
|
private async Task NextAsync()
|
|
|
|
|
{
|
|
|
|
|
ErrorMessage = null;
|
|
|
|
|
if (CurrentStep == InstallerStepId.InstallLocation)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
InstallerPathGuard.ValidateInstallPath(InstallPath);
|
|
|
|
|
var info = await _installService.CheckLatestAsync(CancellationToken.None);
|
|
|
|
|
TargetVersion = info.Version;
|
|
|
|
|
SourceId = info.SourceId;
|
|
|
|
|
StatusText = $"准备安装 {info.Version}";
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
ErrorMessage = ex.Message;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[RelayCommand(CanExecute = nameof(CanStartInstall))]
|
|
|
|
|
private async Task StartInstallAsync()
|
|
|
|
|
{
|
|
|
|
|
ErrorMessage = null;
|
|
|
|
|
IsInstalling = true;
|
|
|
|
|
StartInstallCommand.NotifyCanExecuteChanged();
|
|
|
|
|
_installCts?.Dispose();
|
|
|
|
|
_installCts = new CancellationTokenSource();
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void ApplyProgress(InstallerDeployProgress progress)
|
|
|
|
|
{
|
|
|
|
|
StatusText = progress.Stage;
|
|
|
|
|
TargetVersion = progress.TargetVersion ?? TargetVersion;
|
|
|
|
|
DownloadProgress = progress.DownloadProgress;
|
|
|
|
|
InstallProgress = progress.InstallProgress;
|
|
|
|
|
CurrentFile = progress.CurrentFile;
|
|
|
|
|
DownloadBytesText = FormatBytes(progress.BytesDownloaded, progress.TotalBytes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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]}";
|
|
|
|
|
}
|
|
|
|
|
}
|