mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
feat.PLONDS在线安装器继续优化
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:theme="using:Avalonia.Themes.Fluent"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanDesktopPLONDS.Installer.App"
|
||||
RequestedThemeVariant="Default">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<FontFamily x:Key="AppFontFamily">Inter, Segoe UI, Microsoft YaHei UI</FontFamily>
|
||||
<FontFamily x:Key="AppFontFamily">Segoe UI, Microsoft YaHei UI</FontFamily>
|
||||
<FontFamily x:Key="InstallerIconFontFamily">Segoe MDL2 Assets</FontFamily>
|
||||
<CornerRadius x:Key="DesignCornerRadiusMicro">2</CornerRadius>
|
||||
<CornerRadius x:Key="DesignCornerRadiusXs">4</CornerRadius>
|
||||
<CornerRadius x:Key="DesignCornerRadiusSm">4</CornerRadius>
|
||||
@@ -76,10 +76,12 @@
|
||||
<Style Selector="UserControl">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontFamily}" />
|
||||
</Style>
|
||||
<Style Selector="fi|FluentIcon">
|
||||
<Style Selector="TextBlock.installer-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerTextPrimaryBrush}" />
|
||||
<Setter Property="FontFamily" Value="{DynamicResource InstallerIconFontFamily}" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Center" />
|
||||
<Setter Property="TextAlignment" Value="Center" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerTextPrimaryBrush}" />
|
||||
@@ -142,13 +144,13 @@
|
||||
<Setter Property="Background" Value="{DynamicResource InstallerSubtleFillBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerDisabledTextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.primary-command fi|FluentIcon">
|
||||
<Style Selector="Button.primary-command TextBlock.installer-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerOnAccentBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.primary-command TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerOnAccentBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.primary-command:disabled fi|FluentIcon">
|
||||
<Style Selector="Button.primary-command:disabled TextBlock.installer-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerDisabledTextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.primary-command:disabled TextBlock">
|
||||
@@ -174,13 +176,13 @@
|
||||
<Style Selector="Button.secondary-command TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerTextPrimaryBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.secondary-command fi|FluentIcon">
|
||||
<Style Selector="Button.secondary-command TextBlock.installer-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerTextPrimaryBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.secondary-command:disabled TextBlock">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerDisabledTextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.secondary-command:disabled fi|FluentIcon">
|
||||
<Style Selector="Button.secondary-command:disabled TextBlock.installer-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerDisabledTextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="TextBox">
|
||||
|
||||
92
LanDesktopPLONDS.installer/InstallerStartupDiagnostics.cs
Normal file
92
LanDesktopPLONDS.installer/InstallerStartupDiagnostics.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace LanDesktopPLONDS.Installer;
|
||||
|
||||
internal static class InstallerStartupDiagnostics
|
||||
{
|
||||
private const uint MessageBoxIconError = 0x00000010;
|
||||
private const uint MessageBoxOk = 0x00000000;
|
||||
|
||||
private static int _initialized;
|
||||
private static int _fatalMessageShown;
|
||||
|
||||
public static string LogPath => Path.Combine(GetLogDirectory(), "startup.log");
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _initialized, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, args) =>
|
||||
{
|
||||
var exception = args.ExceptionObject as Exception;
|
||||
ReportFatal("The installer encountered an unhandled startup error.", exception);
|
||||
};
|
||||
|
||||
TaskScheduler.UnobservedTaskException += (_, args) =>
|
||||
{
|
||||
ReportFatal("The installer encountered an unobserved background error.", args.Exception);
|
||||
args.SetObserved();
|
||||
};
|
||||
|
||||
Log("Startup diagnostics initialized.");
|
||||
}
|
||||
|
||||
public static void Log(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(GetLogDirectory());
|
||||
File.AppendAllText(
|
||||
LogPath,
|
||||
$"[{DateTimeOffset.Now:O}] {message}{Environment.NewLine}",
|
||||
Encoding.UTF8);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Diagnostics must never become the reason the installer cannot start.
|
||||
}
|
||||
}
|
||||
|
||||
public static void ReportFatal(string message, Exception? exception)
|
||||
{
|
||||
Log(exception is null ? message : $"{message}{Environment.NewLine}{exception}");
|
||||
|
||||
if (!OperatingSystem.IsWindows() || Interlocked.Exchange(ref _fatalMessageShown, 1) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var details = exception is null
|
||||
? message
|
||||
: $"{message}{Environment.NewLine}{Environment.NewLine}{exception.GetType().Name}: {exception.Message}";
|
||||
_ = MessageBox(
|
||||
IntPtr.Zero,
|
||||
$"{details}{Environment.NewLine}{Environment.NewLine}Log: {LogPath}",
|
||||
"LanDesktopPLONDS Installer",
|
||||
MessageBoxOk | MessageBoxIconError);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLogDirectory()
|
||||
{
|
||||
var root = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
if (string.IsNullOrWhiteSpace(root))
|
||||
{
|
||||
root = AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
return Path.Combine(root, "LanMountainDesktop", "Installer", "logs");
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)]
|
||||
private static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
|
||||
}
|
||||
@@ -8,7 +8,14 @@
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<OptimizationPreference>Size</OptimizationPreference>
|
||||
<IlcOptimizationPreference>Size</IlcOptimizationPreference>
|
||||
<PublishReadyToRun>false</PublishReadyToRun>
|
||||
<DebuggerSupport>false</DebuggerSupport>
|
||||
<EventSourceSupport>false</EventSourceSupport>
|
||||
<HttpActivityPropagationSupport>false</HttpActivityPropagationSupport>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<MetadataUpdaterSupport>false</MetadataUpdaterSupport>
|
||||
<UseSystemResourceKeys>true</UseSystemResourceKeys>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
@@ -16,24 +23,11 @@
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(PublishAot)' == 'true'">
|
||||
<TrimmerRootAssembly Include="Avalonia" />
|
||||
<TrimmerRootAssembly Include="Avalonia.Desktop" />
|
||||
<TrimmerRootAssembly Include="Avalonia.Themes.Fluent" />
|
||||
<TrimmerRootAssembly Include="FluentIcons.Avalonia" />
|
||||
<TrimmerRootAssembly Include="LanDesktopPLONDS.installer" />
|
||||
<TrimmerRootAssembly Include="System.Text.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target
|
||||
Name="PrepareInstallerEmbeddedNativeLibraries"
|
||||
BeforeTargets="AssignTargetPaths"
|
||||
Condition="'$(PublishAot)' == 'true' and '$(RuntimeIdentifier)' == 'win-x64'">
|
||||
<ItemGroup>
|
||||
<InstallerNativeLibrary
|
||||
Include="$(PkgAvalonia_Angle_Windows_Natives)\runtimes\win-x64\native\av_libglesv2.dll"
|
||||
CompressedName="av_libglesv2.dll.gz"
|
||||
Condition="Exists('$(PkgAvalonia_Angle_Windows_Natives)\runtimes\win-x64\native\av_libglesv2.dll')" />
|
||||
<InstallerNativeLibrary
|
||||
Include="$(PkgHarfBuzzSharp_NativeAssets_Win32)\runtimes\win-x64\native\libHarfBuzzSharp.dll"
|
||||
CompressedName="libHarfBuzzSharp.dll.gz"
|
||||
@@ -53,9 +47,6 @@
|
||||
Command="powershell -NoProfile -ExecutionPolicy Bypass -File "$(MSBuildThisFileDirectory)Compress-NativeLibrary.ps1" -SourcePath "%(InstallerNativeLibrary.FullPath)" -DestinationPath "$(IntermediateOutputPath)embedded-native\$(RuntimeIdentifier)\%(InstallerNativeLibrary.CompressedName)"" />
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource
|
||||
Include="$(IntermediateOutputPath)embedded-native\$(RuntimeIdentifier)\av_libglesv2.dll.gz"
|
||||
LogicalName="LanDesktopPLONDS.Installer.NativeLibraries.av_libglesv2.dll.gz" />
|
||||
<EmbeddedResource
|
||||
Include="$(IntermediateOutputPath)embedded-native\$(RuntimeIdentifier)\libHarfBuzzSharp.dll.gz"
|
||||
LogicalName="LanDesktopPLONDS.Installer.NativeLibraries.libHarfBuzzSharp.dll.gz" />
|
||||
@@ -68,7 +59,7 @@
|
||||
<PropertyGroup Condition="'$(PublishAot)' == 'true'">
|
||||
<SuppressTrimAnalysisWarnings>false</SuppressTrimAnalysisWarnings>
|
||||
<TrimmerSingleWarn>false</TrimmerSingleWarn>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>true</JsonSerializerIsReflectionEnabledByDefault>
|
||||
<JsonSerializerIsReflectionEnabledByDefault>false</JsonSerializerIsReflectionEnabledByDefault>
|
||||
<IsAotCompatible>true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -20,11 +20,9 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Angle.Windows.Natives" GeneratePathProperty="true" PrivateAssets="all" />
|
||||
<PackageReference Include="Avalonia.Angle.Windows.Natives" ExcludeAssets="all" PrivateAssets="all" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="FluentIcons.Avalonia" />
|
||||
<PackageReference Include="HarfBuzzSharp.NativeAssets.Win32" GeneratePathProperty="true" PrivateAssets="all" />
|
||||
<PackageReference Include="SkiaSharp.NativeAssets.Win32" GeneratePathProperty="true" PrivateAssets="all" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
|
||||
@@ -13,7 +13,6 @@ internal static class NativeDependencyBootstrapper
|
||||
|
||||
private static readonly string[] NativeLibraryNames =
|
||||
[
|
||||
"av_libglesv2.dll",
|
||||
"libHarfBuzzSharp.dll",
|
||||
"libSkiaSharp.dll"
|
||||
];
|
||||
@@ -47,7 +46,7 @@ internal static class NativeDependencyBootstrapper
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[NativeDependencyBootstrapper] Failed to prepare native dependencies: {ex}");
|
||||
InstallerStartupDiagnostics.Log($"Native dependency preparation failed: {ex}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Win32;
|
||||
|
||||
namespace LanDesktopPLONDS.Installer;
|
||||
|
||||
@@ -7,17 +8,21 @@ public static class Program
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
InstallerStartupDiagnostics.Initialize();
|
||||
try
|
||||
{
|
||||
InstallerStartupDiagnostics.Log("Preparing native dependencies.");
|
||||
if (!NativeDependencyBootstrapper.TryPrepare())
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("[Program] Failed to prepare native dependencies, but continuing...");
|
||||
throw new InvalidOperationException("Failed to prepare native dependencies.");
|
||||
}
|
||||
|
||||
InstallerStartupDiagnostics.Log("Starting Avalonia desktop lifetime.");
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Program] Unhandled exception: {ex}");
|
||||
InstallerStartupDiagnostics.ReportFatal("The installer failed to start.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +30,10 @@ public static class Program
|
||||
{
|
||||
return AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
.With(new Win32PlatformOptions
|
||||
{
|
||||
RenderingMode = [Win32RenderingMode.Software],
|
||||
CompositionMode = [Win32CompositionMode.RedirectionSurface]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ internal sealed class FilesPackageInstaller
|
||||
var sourceAppDirectory = ResolveFullPackageAppDirectory(package.ExtractDirectory, package.Version);
|
||||
var targetDeployment = BuildDeploymentDirectory(launcherRoot, package.Version);
|
||||
|
||||
InstallerElevation.EnsureCanInstall(launcherRoot);
|
||||
InstallerPathGuard.EnsureUsableInstallPath(launcherRoot, EstimateRequiredBytes(sourceAppDirectory));
|
||||
Directory.CreateDirectory(launcherRoot);
|
||||
await CopyLauncherRootPayloadAsync(package.ExtractDirectory, sourceAppDirectory, launcherRoot, package.Version, progress, cancellationToken)
|
||||
@@ -299,7 +300,9 @@ internal sealed class FilesPackageInstaller
|
||||
return;
|
||||
}
|
||||
|
||||
var startMenu = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
|
||||
var startMenu = InstallerElevation.IsRunningElevated()
|
||||
? Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu)
|
||||
: Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
|
||||
if (string.IsNullOrWhiteSpace(startMenu))
|
||||
{
|
||||
startMenu = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
|
||||
|
||||
52
LanDesktopPLONDS.installer/Services/InstallerElevation.cs
Normal file
52
LanDesktopPLONDS.installer/Services/InstallerElevation.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System.Security.Principal;
|
||||
|
||||
namespace LanDesktopPLONDS.Installer.Services;
|
||||
|
||||
internal static class InstallerElevation
|
||||
{
|
||||
public static bool IsRunningElevated()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
using var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
|
||||
public static bool RequiresElevation(string installPath)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fullPath = Path.GetFullPath(installPath);
|
||||
return IsUnderSpecialFolder(fullPath, Environment.SpecialFolder.ProgramFiles)
|
||||
|| IsUnderSpecialFolder(fullPath, Environment.SpecialFolder.ProgramFilesX86)
|
||||
|| IsUnderWindowsDirectory(fullPath);
|
||||
}
|
||||
|
||||
public static void EnsureCanInstall(string installPath)
|
||||
{
|
||||
if (RequiresElevation(installPath) && !IsRunningElevated())
|
||||
{
|
||||
throw new UnauthorizedAccessException(
|
||||
"The selected installation path requires administrator permission. Restart the installer as administrator or choose a user-writable folder.");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsUnderSpecialFolder(string fullPath, Environment.SpecialFolder folder)
|
||||
{
|
||||
var root = Environment.GetFolderPath(folder);
|
||||
return !string.IsNullOrWhiteSpace(root) && InstallerPathGuard.IsSameOrChildPath(root, fullPath);
|
||||
}
|
||||
|
||||
private static bool IsUnderWindowsDirectory(string fullPath)
|
||||
{
|
||||
var windows = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
|
||||
return !string.IsNullOrWhiteSpace(windows) && InstallerPathGuard.IsSameOrChildPath(windows, fullPath);
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,13 @@ public static class InstallerPathGuard
|
||||
|
||||
public static string GetDefaultInstallPath()
|
||||
{
|
||||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
if (string.IsNullOrWhiteSpace(programFiles))
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
if (string.IsNullOrWhiteSpace(localAppData))
|
||||
{
|
||||
programFiles = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Programs");
|
||||
localAppData = AppContext.BaseDirectory;
|
||||
}
|
||||
|
||||
return Path.Combine(programFiles, ApplicationDirectoryName);
|
||||
return Path.Combine(localAppData, "Programs", ApplicationDirectoryName);
|
||||
}
|
||||
|
||||
public static string GetInstallPathForSelectedFolder(string selectedFolder)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using FluentIcons.Common;
|
||||
using LanDesktopPLONDS.Installer.Models;
|
||||
|
||||
namespace LanDesktopPLONDS.Installer.ViewModels;
|
||||
@@ -7,7 +6,7 @@ namespace LanDesktopPLONDS.Installer.ViewModels;
|
||||
public sealed partial class InstallerStepViewModel(
|
||||
InstallerStepId stepId,
|
||||
string title,
|
||||
Icon icon) : ObservableObject
|
||||
string iconGlyph) : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private bool _isUnlocked;
|
||||
@@ -19,5 +18,5 @@ public sealed partial class InstallerStepViewModel(
|
||||
|
||||
public string Title { get; } = title;
|
||||
|
||||
public Icon Icon { get; } = icon;
|
||||
public string IconGlyph { get; } = iconGlyph;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -81,11 +80,11 @@ public sealed partial class MainWindowViewModel : ObservableObject
|
||||
_privacyConsentStore = privacyConsentStore ?? new InstallerPrivacyConsentStore();
|
||||
Steps =
|
||||
[
|
||||
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)
|
||||
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")
|
||||
];
|
||||
SyncSteps();
|
||||
DeviceIdPreview = _privacyIdentity.GetOrCreateDeviceId();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
xmlns:vm="using:LanDesktopPLONDS.Installer.ViewModels"
|
||||
x:Class="LanDesktopPLONDS.Installer.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
@@ -62,7 +61,7 @@
|
||||
<Style Selector="Button.step-nav-item:disabled TextBlock.step-title">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerDisabledTextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.step-nav-item:disabled fi|FluentIcon">
|
||||
<Style Selector="Button.step-nav-item:disabled TextBlock.installer-icon">
|
||||
<Setter Property="Foreground" Value="{DynamicResource InstallerDisabledTextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Border.step-nav-selected-fill">
|
||||
@@ -129,10 +128,10 @@
|
||||
Height="28"
|
||||
Background="{DynamicResource InstallerAccentBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusSm}">
|
||||
<fi:FluentIcon Icon="ArrowDownload"
|
||||
IconVariant="Regular"
|
||||
Foreground="{DynamicResource InstallerOnAccentBrush}"
|
||||
FontSize="16" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
Foreground="{DynamicResource InstallerOnAccentBrush}"
|
||||
FontSize="16" />
|
||||
</Border>
|
||||
<TextBlock Text="{Binding WindowTitle}"
|
||||
FontSize="13"
|
||||
@@ -148,16 +147,16 @@
|
||||
<Button Classes="titlebar-icon-button"
|
||||
ToolTip.Tip="最小化"
|
||||
Click="OnMinimizeClick">
|
||||
<fi:FluentIcon Icon="Subtract"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
<Button Classes="titlebar-icon-button"
|
||||
ToolTip.Tip="关闭"
|
||||
Click="OnCloseClick">
|
||||
<fi:FluentIcon Icon="Dismiss"
|
||||
IconVariant="Regular"
|
||||
FontSize="14" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
FontSize="14" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -196,16 +195,16 @@
|
||||
Margin="10,0">
|
||||
<Grid Width="18"
|
||||
VerticalAlignment="Center">
|
||||
<fi:FluentIcon Icon="{Binding Icon}"
|
||||
IconVariant="Regular"
|
||||
Foreground="{DynamicResource InstallerTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
IsVisible="{Binding !IsSelected}" />
|
||||
<fi:FluentIcon Icon="{Binding Icon}"
|
||||
IconVariant="Filled"
|
||||
Foreground="{DynamicResource InstallerTextPrimaryBrush}"
|
||||
FontSize="17"
|
||||
IsVisible="{Binding IsSelected}" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="{Binding IconGlyph}"
|
||||
Foreground="{DynamicResource InstallerTextSecondaryBrush}"
|
||||
FontSize="17"
|
||||
IsVisible="{Binding !IsSelected}" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="{Binding IconGlyph}"
|
||||
Foreground="{DynamicResource InstallerTextPrimaryBrush}"
|
||||
FontSize="17"
|
||||
IsVisible="{Binding IsSelected}" />
|
||||
</Grid>
|
||||
<Grid Grid.Column="1"
|
||||
VerticalAlignment="Center">
|
||||
@@ -257,9 +256,9 @@
|
||||
Height="40"
|
||||
Background="{DynamicResource InstallerSubtleFillBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}">
|
||||
<fi:FluentIcon Icon="CloudArrowDown"
|
||||
IconVariant="Regular"
|
||||
FontSize="20" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
FontSize="20" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="6">
|
||||
@@ -302,8 +301,8 @@
|
||||
Command="{Binding BrowseCommand}">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<fi:FluentIcon Icon="FolderOpen"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="" />
|
||||
<TextBlock Text="浏览" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -341,10 +340,10 @@
|
||||
<Border Classes="info-panel">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="10">
|
||||
<fi:FluentIcon Icon="Shield"
|
||||
IconVariant="Regular"
|
||||
Foreground="{DynamicResource InstallerAccentBrush}"
|
||||
FontSize="18" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
Foreground="{DynamicResource InstallerAccentBrush}"
|
||||
FontSize="18" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="安装器会发送匿名设备码、系统与架构信息、目标版本和请求 IP;不会上传用户名、机器名或安装目录。"
|
||||
Classes="muted" />
|
||||
@@ -416,8 +415,8 @@
|
||||
Command="{Binding StartInstallCommand}">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<fi:FluentIcon Icon="ArrowDownload"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="" />
|
||||
<TextBlock Text="开始安装" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -426,8 +425,8 @@
|
||||
IsEnabled="{Binding IsInstalling}">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<fi:FluentIcon Icon="Dismiss"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="" />
|
||||
<TextBlock Text="取消" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -454,10 +453,10 @@
|
||||
Height="40"
|
||||
Background="{DynamicResource InstallerSubtleFillBrush}"
|
||||
CornerRadius="{DynamicResource DesignCornerRadiusMd}">
|
||||
<fi:FluentIcon Icon="CheckmarkCircle"
|
||||
IconVariant="Regular"
|
||||
Foreground="{DynamicResource InstallerSuccessBrush}"
|
||||
FontSize="22" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
Foreground="{DynamicResource InstallerSuccessBrush}"
|
||||
FontSize="22" />
|
||||
</Border>
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="12">
|
||||
@@ -473,8 +472,8 @@
|
||||
Command="{Binding LaunchCommand}">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<fi:FluentIcon Icon="Play"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="" />
|
||||
<TextBlock Text="打开阑山桌面" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -495,10 +494,10 @@
|
||||
IsVisible="{Binding HasError}">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="10">
|
||||
<fi:FluentIcon Icon="ErrorCircle"
|
||||
IconVariant="Regular"
|
||||
Foreground="{DynamicResource InstallerErrorBrush}"
|
||||
FontSize="18" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text=""
|
||||
Foreground="{DynamicResource InstallerErrorBrush}"
|
||||
FontSize="18" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding ErrorMessage}"
|
||||
Foreground="{DynamicResource InstallerErrorBrush}"
|
||||
@@ -511,8 +510,8 @@
|
||||
Command="{Binding BackCommand}">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<fi:FluentIcon Icon="ArrowLeft"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="" />
|
||||
<TextBlock Text="上一步" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
@@ -522,8 +521,8 @@
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<TextBlock Text="下一步" />
|
||||
<fi:FluentIcon Icon="ArrowRight"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock Classes="installer-icon"
|
||||
Text="" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||
<security>
|
||||
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
@@ -212,6 +212,33 @@ public sealed class OnlineInstallerCoreTests : IDisposable
|
||||
Assert.ThrowsAny<Exception>(() => InstallerPathGuard.NormalizeInstallPath(path));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallerPathGuard_DefaultsToUserWritableProgramsFolder()
|
||||
{
|
||||
var path = InstallerPathGuard.GetDefaultInstallPath();
|
||||
|
||||
Assert.EndsWith(Path.Combine("Programs", InstallerPathGuard.ApplicationDirectoryName), path);
|
||||
Assert.DoesNotContain("Program Files", path, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InstallerElevation_DetectsProtectedProgramFilesPath()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
if (string.IsNullOrWhiteSpace(programFiles))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.True(InstallerElevation.RequiresElevation(Path.Combine(programFiles, InstallerPathGuard.ApplicationDirectoryName)));
|
||||
Assert.False(InstallerElevation.RequiresElevation(Path.Combine(_tempRoot, InstallerPathGuard.ApplicationDirectoryName)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FilesPackageInstaller_DeploysFullPackageWithCurrentMarker()
|
||||
{
|
||||
|
||||
@@ -116,8 +116,8 @@ public sealed class ComponentRegistry
|
||||
"Class Schedule",
|
||||
"CalendarDate",
|
||||
"Date",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 4,
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 3,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
|
||||
@@ -683,18 +683,18 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
var scale = ResolveScale();
|
||||
var cardRadius = ComponentChromeCornerRadiusHelper.Small();
|
||||
var timeFontSize = Math.Clamp(11 * scale, 8, 14);
|
||||
var courseNameFontSize = Math.Clamp(14 * scale, 10, 18);
|
||||
var detailFontSize = Math.Clamp(11 * scale, 8, 14);
|
||||
var progressFontSize = Math.Clamp(10 * scale, 7, 12);
|
||||
var timeFontSize = Math.Clamp(16 * scale, 12, 22);
|
||||
var courseNameFontSize = Math.Clamp(20 * scale, 16, 28);
|
||||
var detailFontSize = Math.Clamp(15 * scale, 12, 20);
|
||||
var progressFontSize = Math.Clamp(13 * scale, 10, 16);
|
||||
var cardPadding = new Thickness(
|
||||
Math.Clamp(10 * scale, 6, 14),
|
||||
Math.Clamp(8 * scale, 5, 12),
|
||||
Math.Clamp(10 * scale, 6, 14),
|
||||
Math.Clamp(8 * scale, 5, 12));
|
||||
var timeColumnWidth = Math.Clamp(44 * scale, 30, 56);
|
||||
var accentBarWidth = Math.Clamp(3 * scale, 2, 4);
|
||||
var progressBarHeight = Math.Clamp(3 * scale, 2, 4);
|
||||
Math.Clamp(14 * scale, 10, 20),
|
||||
Math.Clamp(12 * scale, 8, 18),
|
||||
Math.Clamp(14 * scale, 10, 20),
|
||||
Math.Clamp(12 * scale, 8, 18));
|
||||
var timeColumnWidth = Math.Clamp(60 * scale, 45, 80);
|
||||
var accentBarWidth = Math.Clamp(4 * scale, 3, 6);
|
||||
var progressBarHeight = Math.Clamp(4 * scale, 3, 6);
|
||||
|
||||
for (var i = 0; i < _courseItems.Count; i++)
|
||||
{
|
||||
@@ -894,10 +894,10 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
var itemBorder = new Border
|
||||
{
|
||||
Padding = new Thickness(
|
||||
Math.Clamp(10 * scale, 6, 14),
|
||||
Math.Clamp(2 * scale, 1, 4),
|
||||
Math.Clamp(10 * scale, 6, 14),
|
||||
Math.Clamp(2 * scale, 1, 4)),
|
||||
Math.Clamp(14 * scale, 10, 20),
|
||||
Math.Clamp(4 * scale, 2, 6),
|
||||
Math.Clamp(14 * scale, 10, 20),
|
||||
Math.Clamp(4 * scale, 2, 6)),
|
||||
Background = Brushes.Transparent,
|
||||
Child = itemGrid
|
||||
};
|
||||
@@ -1022,11 +1022,11 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
HeaderGrid.ColumnSpacing = Math.Clamp(8 * scale, 4, 14);
|
||||
DateGroup.Spacing = Math.Clamp(1.5 * scale, 0.5, 3);
|
||||
CourseListPanel.Spacing = Math.Clamp(2 * scale, 0, 6);
|
||||
CourseListPanel.Spacing = Math.Clamp(4 * scale, 2, 8);
|
||||
|
||||
var dateFontByScale = Math.Clamp(28 * scale, 14, 36);
|
||||
var weekdayFontByScale = Math.Clamp(14 * scale, 10, 18);
|
||||
var classCountFontByScale = Math.Clamp(12 * scale, 9, 15);
|
||||
var dateFontByScale = Math.Clamp(36 * scale, 20, 48);
|
||||
var weekdayFontByScale = Math.Clamp(18 * scale, 14, 24);
|
||||
var classCountFontByScale = Math.Clamp(15 * scale, 12, 20);
|
||||
|
||||
var availableWidth = Math.Max(1, Bounds.Width - headerPadding.Left - headerPadding.Right);
|
||||
var dateGroupEstimatedWidth = dateFontByScale * 0.6 * 3 + DateGroup.Spacing * 2;
|
||||
|
||||
Reference in New Issue
Block a user