From bb4e90ea8d2fdaf1238003fe6c91a49599fa681c Mon Sep 17 00:00:00 2001 From: lincube Date: Wed, 3 Jun 2026 12:32:56 +0800 Subject: [PATCH] =?UTF-8?q?fix.=E4=BE=9D=E6=97=A7=E5=9C=A8=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E6=88=91=E4=BB=AC=E7=9A=84=E5=9C=A8=E7=BA=BF=E5=AE=89?= =?UTF-8?q?=E8=A3=85=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LanDesktopPLONDS.installer/App.axaml | 177 ++++- .../Services/FilesPackageInstaller.cs | 26 +- .../Services/PlondsInstallerModels.cs | 6 +- .../ViewModels/InstallerStepViewModel.cs | 39 +- .../ViewModels/MainWindowViewModel.cs | 56 +- .../Views/MainWindow.axaml | 724 ++++++++++++------ .../Views/MainWindow.axaml.cs | 27 +- .../OnlineInstallerCoreTests.cs | 66 +- 8 files changed, 820 insertions(+), 301 deletions(-) diff --git a/LanDesktopPLONDS.installer/App.axaml b/LanDesktopPLONDS.installer/App.axaml index 0154504..681bf05 100644 --- a/LanDesktopPLONDS.installer/App.axaml +++ b/LanDesktopPLONDS.installer/App.axaml @@ -5,19 +5,65 @@ x:Class="LanDesktopPLONDS.Installer.App" RequestedThemeVariant="Default"> - Inter, Segoe UI, Microsoft YaHei UI - 2 - 4 - 6 - 8 - 10 - 12 - 12 - - - - - + + Inter, Segoe UI, Microsoft YaHei UI + 2 + 4 + 4 + 8 + 8 + 12 + 8 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -29,9 +75,14 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanDesktopPLONDS.installer/Services/FilesPackageInstaller.cs b/LanDesktopPLONDS.installer/Services/FilesPackageInstaller.cs index 3dec9c6..8b6734e 100644 --- a/LanDesktopPLONDS.installer/Services/FilesPackageInstaller.cs +++ b/LanDesktopPLONDS.installer/Services/FilesPackageInstaller.cs @@ -56,7 +56,7 @@ internal sealed class FilesPackageInstaller null)); ActivateInitialDeployment(launcherRoot, targetDeployment); - CreateWindowsShortcutsIfAvailable(launcherRoot, options.CreateDesktopShortcut); + CreateWindowsShortcutsIfAvailable(launcherRoot, options); progress?.Report(new InstallerDeployProgress( "Completed", @@ -273,7 +273,7 @@ internal sealed class FilesPackageInstaller return name is ".current" or ".partial" or ".destroy"; } - private static void CreateWindowsShortcutsIfAvailable(string launcherRoot, bool createDesktopShortcut) + private static void CreateWindowsShortcutsIfAvailable(string launcherRoot, OnlineInstallOptions options) { try { @@ -315,19 +315,25 @@ internal sealed class FilesPackageInstaller var shortcutPath = Path.Combine(programs, "LanMountainDesktop.url"); WriteUrlShortcut(shortcutPath, launcherPath); - if (!createDesktopShortcut) + if (options.CreateDesktopShortcut) { - return; + var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); + if (!string.IsNullOrWhiteSpace(desktop)) + { + Directory.CreateDirectory(desktop); + WriteUrlShortcut(Path.Combine(desktop, "LanMountainDesktop.url"), launcherPath); + } } - var desktop = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory); - if (string.IsNullOrWhiteSpace(desktop)) + if (options.CreateStartupShortcut) { - return; + var startup = Environment.GetFolderPath(Environment.SpecialFolder.Startup); + if (!string.IsNullOrWhiteSpace(startup)) + { + Directory.CreateDirectory(startup); + WriteUrlShortcut(Path.Combine(startup, "LanMountainDesktop.url"), launcherPath); + } } - - Directory.CreateDirectory(desktop); - WriteUrlShortcut(Path.Combine(desktop, "LanMountainDesktop.url"), launcherPath); } catch { diff --git a/LanDesktopPLONDS.installer/Services/PlondsInstallerModels.cs b/LanDesktopPLONDS.installer/Services/PlondsInstallerModels.cs index afe5d75..2e6533e 100644 --- a/LanDesktopPLONDS.installer/Services/PlondsInstallerModels.cs +++ b/LanDesktopPLONDS.installer/Services/PlondsInstallerModels.cs @@ -63,9 +63,11 @@ public sealed record OnlineInstallPackageInfo( Uri FilesZipUrl, long EstimatedBytes); -public sealed record OnlineInstallOptions(bool CreateDesktopShortcut) +public sealed record OnlineInstallOptions(bool CreateDesktopShortcut, bool CreateStartupShortcut) { - public static OnlineInstallOptions Default { get; } = new(CreateDesktopShortcut: false); + public static OnlineInstallOptions Default { get; } = new( + CreateDesktopShortcut: false, + CreateStartupShortcut: false); } internal sealed record InstallerPlondsCandidate( diff --git a/LanDesktopPLONDS.installer/ViewModels/InstallerStepViewModel.cs b/LanDesktopPLONDS.installer/ViewModels/InstallerStepViewModel.cs index c4f479f..d4f5b4a 100644 --- a/LanDesktopPLONDS.installer/ViewModels/InstallerStepViewModel.cs +++ b/LanDesktopPLONDS.installer/ViewModels/InstallerStepViewModel.cs @@ -1,4 +1,5 @@ using CommunityToolkit.Mvvm.ComponentModel; +using FluentIcons.Common; using LanDesktopPLONDS.Installer.Models; namespace LanDesktopPLONDS.Installer.ViewModels; @@ -6,7 +7,7 @@ namespace LanDesktopPLONDS.Installer.ViewModels; public sealed partial class InstallerStepViewModel( InstallerStepId stepId, string title, - string iconKey) : ObservableObject + Icon icon) : ObservableObject { [ObservableProperty] private bool _isUnlocked; @@ -14,9 +15,43 @@ public sealed partial class InstallerStepViewModel( [ObservableProperty] private bool _isSelected; + [ObservableProperty] + private bool _isCompleted; + public InstallerStepId StepId { get; } = stepId; public string Title { get; } = title; - public string IconKey { get; } = iconKey; + public Icon Icon { get; } = icon; + + public bool IsLocked => !IsUnlocked; + + public Icon DisplayIcon => IsLocked + ? Icon.LockClosed + : IsCompleted + ? Icon.CheckmarkCircle + : Icon; + + public bool IsAvailable => IsUnlocked && !IsSelected && !IsCompleted; + + partial void OnIsUnlockedChanged(bool value) + { + _ = value; + OnPropertyChanged(nameof(IsLocked)); + OnPropertyChanged(nameof(IsAvailable)); + OnPropertyChanged(nameof(DisplayIcon)); + } + + partial void OnIsSelectedChanged(bool value) + { + _ = value; + OnPropertyChanged(nameof(IsAvailable)); + } + + partial void OnIsCompletedChanged(bool value) + { + _ = value; + OnPropertyChanged(nameof(DisplayIcon)); + OnPropertyChanged(nameof(IsAvailable)); + } } diff --git a/LanDesktopPLONDS.installer/ViewModels/MainWindowViewModel.cs b/LanDesktopPLONDS.installer/ViewModels/MainWindowViewModel.cs index 1b66f3d..cca0586 100644 --- a/LanDesktopPLONDS.installer/ViewModels/MainWindowViewModel.cs +++ b/LanDesktopPLONDS.installer/ViewModels/MainWindowViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using System.Diagnostics; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using FluentIcons.Common; using LanDesktopPLONDS.Installer.Models; using LanDesktopPLONDS.Installer.Services; using LanMountainDesktop.Shared.Contracts.Privacy; @@ -61,11 +62,16 @@ public sealed partial class MainWindowViewModel : ObservableObject [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartInstallCommand))] + [NotifyCanExecuteChangedFor(nameof(BackCommand))] + [NotifyCanExecuteChangedFor(nameof(NextCommand))] private bool _isInstalling; [ObservableProperty] private bool _createDesktopShortcut; + [ObservableProperty] + private bool _createStartupShortcut; + [ObservableProperty] private InstallerStepViewModel? _selectedStep; @@ -79,11 +85,11 @@ public sealed partial class MainWindowViewModel : ObservableObject _privacyConsentStore = privacyConsentStore ?? new InstallerPrivacyConsentStore(); Steps = [ - new InstallerStepViewModel(InstallerStepId.Welcome, "开始安装", "Play"), - new InstallerStepViewModel(InstallerStepId.InstallLocation, "安装位置", "Folder"), - new InstallerStepViewModel(InstallerStepId.PrivacyConfirm, "数据确认", "Info"), - new InstallerStepViewModel(InstallerStepId.Deploy, "开始部署", "Apps"), - new InstallerStepViewModel(InstallerStepId.Complete, "完成安装", "Circle") + 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) ]; SyncSteps(); SelectedStep = Steps[0]; @@ -109,6 +115,8 @@ public sealed partial class MainWindowViewModel : ObservableObject public bool IsCompleteStep => CurrentStep == InstallerStepId.Complete; + public bool HasError => !string.IsNullOrWhiteSpace(ErrorMessage); + public bool CanGoBack => CurrentStep > InstallerStepId.Welcome && !IsInstalling; public bool CanGoNext => CurrentStep switch @@ -145,12 +153,26 @@ public sealed partial class MainWindowViewModel : ObservableObject SyncSteps(); } + partial void OnErrorMessageChanged(string? value) + { + _ = value; + OnPropertyChanged(nameof(HasError)); + } + partial void OnMaxUnlockedStepChanged(InstallerStepId value) { _ = value; SyncSteps(); } + partial void OnIsInstallingChanged(bool value) + { + _ = value; + OnPropertyChanged(nameof(CanGoBack)); + OnPropertyChanged(nameof(CanGoNext)); + OnPropertyChanged(nameof(CanStartInstall)); + } + partial void OnSelectedStepChanged(InstallerStepViewModel? value) { if (_isNavigatingInternally || value is null) @@ -158,7 +180,7 @@ public sealed partial class MainWindowViewModel : ObservableObject return; } - if (value.StepId <= MaxUnlockedStep) + if (!IsInstalling && value.StepId <= MaxUnlockedStep) { CurrentStep = value.StepId; return; @@ -198,6 +220,11 @@ public sealed partial class MainWindowViewModel : ObservableObject [RelayCommand(CanExecute = nameof(CanGoBack))] private void Back() { + if (IsInstalling) + { + return; + } + if (CurrentStep > InstallerStepId.Welcome) { CurrentStep -= 1; @@ -207,15 +234,23 @@ public sealed partial class MainWindowViewModel : ObservableObject [RelayCommand] private async Task BrowseAsync() { + ErrorMessage = null; if (BrowseRequested is null) { return; } - var selected = await BrowseRequested(InstallPath); - if (!string.IsNullOrWhiteSpace(selected)) + try { - InstallPath = selected; + var selected = await BrowseRequested(InstallPath); + if (!string.IsNullOrWhiteSpace(selected)) + { + InstallPath = selected; + } + } + catch (Exception ex) + { + ErrorMessage = $"选择安装位置失败:{ex.Message}"; } } @@ -230,7 +265,7 @@ public sealed partial class MainWindowViewModel : ObservableObject try { var progress = new Progress(ApplyProgress); - var options = new OnlineInstallOptions(CreateDesktopShortcut); + var options = new OnlineInstallOptions(CreateDesktopShortcut, CreateStartupShortcut); await _installService.InstallFreshAsync(InstallPath, options, progress, _installCts.Token); UnlockAndNavigate(InstallerStepId.Complete); StatusText = "安装完成"; @@ -320,6 +355,7 @@ public sealed partial class MainWindowViewModel : ObservableObject { step.IsUnlocked = step.StepId <= MaxUnlockedStep; step.IsSelected = step.StepId == CurrentStep; + step.IsCompleted = step.StepId < CurrentStep; if (step.StepId == CurrentStep && !ReferenceEquals(SelectedStep, step)) { SelectedStep = step; diff --git a/LanDesktopPLONDS.installer/Views/MainWindow.axaml b/LanDesktopPLONDS.installer/Views/MainWindow.axaml index ee90e8e..f0ef70d 100644 --- a/LanDesktopPLONDS.installer/Views/MainWindow.axaml +++ b/LanDesktopPLONDS.installer/Views/MainWindow.axaml @@ -1,17 +1,16 @@ @@ -19,49 +18,114 @@ - - + + + + + + + + + + + + - - + RowDefinitions="48,*" + Background="{DynamicResource InstallerWindowBackgroundBrush}"> - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + diff --git a/LanDesktopPLONDS.installer/Views/MainWindow.axaml.cs b/LanDesktopPLONDS.installer/Views/MainWindow.axaml.cs index 542ae96..34bee2f 100644 --- a/LanDesktopPLONDS.installer/Views/MainWindow.axaml.cs +++ b/LanDesktopPLONDS.installer/Views/MainWindow.axaml.cs @@ -24,9 +24,12 @@ public partial class MainWindow : Window private async Task BrowseForFolderAsync(string currentPath) { - var startFolder = Directory.Exists(currentPath) - ? await StorageProvider.TryGetFolderFromPathAsync(currentPath) - : null; + IStorageFolder? startFolder = null; + if (Directory.Exists(currentPath)) + { + startFolder = await StorageProvider.TryGetFolderFromPathAsync(currentPath); + } + var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions { Title = "选择安装位置", @@ -34,12 +37,28 @@ public partial class MainWindow : Window SuggestedStartLocation = startFolder }); - return result.Count == 0 ? null : result[0].Path.LocalPath; + if (result.Count == 0) + { + return null; + } + + var path = result[0].TryGetLocalPath(); + if (string.IsNullOrWhiteSpace(path)) + { + throw new InvalidOperationException("请选择本机文件夹作为安装位置。"); + } + + return path; } private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e) { _ = sender; + if (e.Source is Button) + { + return; + } + if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) { BeginMoveDrag(e); diff --git a/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs b/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs index 493ee20..ccff0b6 100644 --- a/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs +++ b/LanMountainDesktop.Tests/OnlineInstallerCoreTests.cs @@ -67,6 +67,65 @@ public sealed class OnlineInstallerCoreTests : IDisposable Assert.Equal(InstallerStepId.Welcome, vm.CurrentStep); } + [Fact] + public async Task BrowseCommand_ReportsPickerFailuresWithoutChangingInstallPath() + { + var vm = new MainWindowViewModel( + new FakeInstallService(), + new PrivacyDeviceIdentityProvider(Path.Combine(_tempRoot, "identity.json"))) + { + BrowseRequested = _ => throw new InvalidOperationException("picker failed") + }; + var originalPath = vm.InstallPath; + + await vm.BrowseCommand.ExecuteAsync(null); + + Assert.Equal(originalPath, vm.InstallPath); + Assert.Contains("选择安装位置失败", vm.ErrorMessage); + Assert.Contains("picker failed", vm.ErrorMessage); + } + + [Fact] + public async Task BrowseCommand_UsesSelectedLocalFolder() + { + var selectedPath = Path.Combine(_tempRoot, "selected-install-root"); + var vm = new MainWindowViewModel( + new FakeInstallService(), + new PrivacyDeviceIdentityProvider(Path.Combine(_tempRoot, "identity.json"))) + { + BrowseRequested = _ => Task.FromResult(selectedPath) + }; + + await vm.BrowseCommand.ExecuteAsync(null); + + Assert.Equal(selectedPath, vm.InstallPath); + Assert.Null(vm.ErrorMessage); + } + + [Fact] + public async Task StartInstallCommand_PassesShortcutAndStartupOptions() + { + var installService = new FakeInstallService(); + var vm = new MainWindowViewModel( + installService, + new PrivacyDeviceIdentityProvider(Path.Combine(_tempRoot, "identity.json"))) + { + InstallPath = Path.Combine(_tempRoot, "install", "LanMountainDesktop"), + PrivacyConfirmed = true, + CreateDesktopShortcut = true, + CreateStartupShortcut = true + }; + await vm.NextCommand.ExecuteAsync(null); + await vm.NextCommand.ExecuteAsync(null); + await vm.NextCommand.ExecuteAsync(null); + + await vm.StartInstallCommand.ExecuteAsync(null); + + Assert.NotNull(installService.LastOptions); + Assert.True(installService.LastOptions.CreateDesktopShortcut); + Assert.True(installService.LastOptions.CreateStartupShortcut); + } + [Fact] public void FilesZipUrlResolver_PrefersSourceSpecificThenDerivedThenFallbacks() { @@ -248,6 +307,8 @@ public sealed class OnlineInstallerCoreTests : IDisposable private sealed class FakeInstallService : IOnlineInstallService { + public OnlineInstallOptions? LastOptions { get; private set; } + public Task CheckLatestAsync(CancellationToken cancellationToken) => Task.FromResult(new OnlineInstallPackageInfo("1.2.3", "test", new Uri("https://test/Files.zip"), 1)); @@ -259,7 +320,10 @@ public sealed class OnlineInstallerCoreTests : IDisposable OnlineInstallOptions options, IProgress? progress, CancellationToken cancellationToken) - => Task.CompletedTask; + { + LastOptions = options; + return Task.CompletedTask; + } public Task RepairAsync(string installPath, IProgress? progress, CancellationToken cancellationToken) => throw new NotSupportedException();