mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-19 01:03:25 +08:00
feat.PLONDS系统开发
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Security;
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LanDesktopPLONDS.Installer.Localization;
|
||||
using LanDesktopPLONDS.Installer.Models;
|
||||
using LanDesktopPLONDS.Installer.Services;
|
||||
using LanMountainDesktop.Shared.Contracts.Privacy;
|
||||
@@ -14,6 +17,11 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
private readonly IPrivacyDeviceIdentityProvider _privacyIdentity;
|
||||
private readonly InstallerPrivacyConsentStore _privacyConsentStore;
|
||||
private CancellationTokenSource? _installCts;
|
||||
private CancellationTokenSource? _checkCts;
|
||||
|
||||
// 下载速度计算状态
|
||||
private long _lastBytesDownloaded;
|
||||
private DateTime _lastProgressTime = DateTime.UtcNow;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
||||
@@ -58,6 +66,9 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private string _downloadBytesText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _downloadSpeedText = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(StartInstallCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
|
||||
@@ -70,6 +81,19 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
[ObservableProperty]
|
||||
private bool _createStartupShortcut;
|
||||
|
||||
// === Task 1: 可取消的版本检查 ===
|
||||
[ObservableProperty]
|
||||
[NotifyCanExecuteChangedFor(nameof(NextCommand))]
|
||||
[NotifyCanExecuteChangedFor(nameof(BackCommand))]
|
||||
private bool _isCheckingUpdate;
|
||||
|
||||
// === Task 2: 自动提权 ===
|
||||
[ObservableProperty]
|
||||
private bool _isElevationRequired;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _elevationMessage = "所选安装路径需要管理员权限。";
|
||||
|
||||
public MainWindowViewModel(
|
||||
IOnlineInstallService installService,
|
||||
IPrivacyDeviceIdentityProvider privacyIdentity,
|
||||
@@ -95,7 +119,8 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
public Func<string, Task<string?>>? BrowseRequested { get; set; }
|
||||
|
||||
public string WindowTitle => "LanDesktopPLONDS Installer";
|
||||
/// <summary>窗口标题,已本地化。</summary>
|
||||
public string WindowTitle => "阑山桌面 安装程序";
|
||||
|
||||
public string DeviceIdPreview { get; }
|
||||
|
||||
@@ -111,13 +136,13 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage);
|
||||
|
||||
public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling;
|
||||
public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling && !IsCheckingUpdate;
|
||||
|
||||
public bool CanGoNext => CurrentStep switch
|
||||
{
|
||||
InstallerStepId.Welcome => !IsInstalling,
|
||||
InstallerStepId.InstallLocation => !string.IsNullOrWhiteSpace(InstallPath) && !IsInstalling,
|
||||
InstallerStepId.PrivacyConfirm => PrivacyConfirmed && !IsInstalling,
|
||||
InstallerStepId.Welcome => !IsInstalling && !IsCheckingUpdate,
|
||||
InstallerStepId.InstallLocation => !string.IsNullOrWhiteSpace(InstallPath) && !IsInstalling && !IsCheckingUpdate,
|
||||
InstallerStepId.PrivacyConfirm => PrivacyConfirmed && !IsInstalling && !IsCheckingUpdate,
|
||||
_ => false
|
||||
};
|
||||
|
||||
@@ -167,25 +192,77 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
OnPropertyChanged(nameof(CanStartInstall));
|
||||
}
|
||||
|
||||
partial void OnIsCheckingUpdateChanged(bool value)
|
||||
{
|
||||
_ = value;
|
||||
OnPropertyChanged(nameof(CanGoBack));
|
||||
OnPropertyChanged(nameof(CanGoNext));
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Task 1: 可取消的版本检查 + Task 2: 自动提权
|
||||
// =====================================================================
|
||||
[RelayCommand(CanExecute = nameof(CanGoNext))]
|
||||
private async Task NextAsync()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
IsElevationRequired = false;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
TargetVersion = info.Version;
|
||||
SourceId = info.SourceId;
|
||||
StatusText = $"准备安装 {info.Version}";
|
||||
}
|
||||
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
|
||||
{
|
||||
ErrorMessage = "检查更新超时(30秒),请检查网络连接后重试。";
|
||||
return;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 用户主动取消
|
||||
ErrorMessage = "版本检查已取消。";
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = ex.Message;
|
||||
return;
|
||||
}
|
||||
finally
|
||||
{
|
||||
timeoutCts.Dispose();
|
||||
IsCheckingUpdate = false;
|
||||
}
|
||||
}
|
||||
else if (CurrentStep == InstallerStepId.PrivacyConfirm)
|
||||
{
|
||||
@@ -243,7 +320,49 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanStartInstall))]
|
||||
// =====================================================================
|
||||
// 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]
|
||||
private async Task StartInstallAsync()
|
||||
{
|
||||
ErrorMessage = null;
|
||||
@@ -251,6 +370,12 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
StartInstallCommand.NotifyCanExecuteChanged();
|
||||
_installCts?.Dispose();
|
||||
_installCts = new CancellationTokenSource();
|
||||
|
||||
// 重置下载速度状态
|
||||
_lastBytesDownloaded = 0;
|
||||
_lastProgressTime = DateTime.UtcNow;
|
||||
DownloadSpeedText = string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var progress = new Progress<InstallerDeployProgress>(ApplyProgress);
|
||||
@@ -325,14 +450,29 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
CurrentStep = step;
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// Task 4: 本地化进度阶段 + Task 5: 下载速度显示
|
||||
// =====================================================================
|
||||
private void ApplyProgress(InstallerDeployProgress progress)
|
||||
{
|
||||
StatusText = progress.Stage;
|
||||
// Task 4: 将英文阶段键翻译为中文
|
||||
StatusText = InstallerStrings.TranslateStage(progress.Stage);
|
||||
TargetVersion = progress.TargetVersion ?? TargetVersion;
|
||||
DownloadProgress = progress.DownloadProgress;
|
||||
InstallProgress = progress.InstallProgress;
|
||||
CurrentFile = progress.CurrentFile;
|
||||
DownloadBytesText = FormatBytes(progress.BytesDownloaded, progress.TotalBytes);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
private void SyncSteps()
|
||||
@@ -368,4 +508,31 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
|
||||
return $"{size:0.##} {suffixes[suffix]}";
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user