feat.PLONDS在线安装器继续优化

This commit is contained in:
lincube
2026-06-09 22:18:27 +08:00
parent 2768b76e1e
commit 13895e0f43
16 changed files with 287 additions and 120 deletions

View File

@@ -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">

View 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);
}

View File

@@ -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 &quot;$(MSBuildThisFileDirectory)Compress-NativeLibrary.ps1&quot; -SourcePath &quot;%(InstallerNativeLibrary.FullPath)&quot; -DestinationPath &quot;$(IntermediateOutputPath)embedded-native\$(RuntimeIdentifier)\%(InstallerNativeLibrary.CompressedName)&quot;" />
<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>

View File

@@ -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" />

View File

@@ -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;
}
}

View File

@@ -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]
});
}
}

View File

@@ -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);

View 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);
}
}

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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,8 +128,8 @@
Height="28"
Background="{DynamicResource InstallerAccentBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}">
<fi:FluentIcon Icon="ArrowDownload"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xE896;"
Foreground="{DynamicResource InstallerOnAccentBrush}"
FontSize="16" />
</Border>
@@ -148,15 +147,15 @@
<Button Classes="titlebar-icon-button"
ToolTip.Tip="最小化"
Click="OnMinimizeClick">
<fi:FluentIcon Icon="Subtract"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xE921;"
FontSize="14" />
</Button>
<Button Classes="titlebar-icon-button"
ToolTip.Tip="关闭"
Click="OnCloseClick">
<fi:FluentIcon Icon="Dismiss"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xE711;"
FontSize="14" />
</Button>
</StackPanel>
@@ -196,13 +195,13 @@
Margin="10,0">
<Grid Width="18"
VerticalAlignment="Center">
<fi:FluentIcon Icon="{Binding Icon}"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="{Binding IconGlyph}"
Foreground="{DynamicResource InstallerTextSecondaryBrush}"
FontSize="17"
IsVisible="{Binding !IsSelected}" />
<fi:FluentIcon Icon="{Binding Icon}"
IconVariant="Filled"
<TextBlock Classes="installer-icon"
Text="{Binding IconGlyph}"
Foreground="{DynamicResource InstallerTextPrimaryBrush}"
FontSize="17"
IsVisible="{Binding IsSelected}" />
@@ -257,8 +256,8 @@
Height="40"
Background="{DynamicResource InstallerSubtleFillBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}">
<fi:FluentIcon Icon="CloudArrowDown"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xE896;"
FontSize="20" />
</Border>
<StackPanel Grid.Column="1"
@@ -302,8 +301,8 @@
Command="{Binding BrowseCommand}">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="FolderOpen"
IconVariant="Regular" />
<TextBlock Classes="installer-icon"
Text="&#xE838;" />
<TextBlock Text="浏览" />
</StackPanel>
</Button>
@@ -341,8 +340,8 @@
<Border Classes="info-panel">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="10">
<fi:FluentIcon Icon="Shield"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xEA18;"
Foreground="{DynamicResource InstallerAccentBrush}"
FontSize="18" />
<TextBlock Grid.Column="1"
@@ -416,8 +415,8 @@
Command="{Binding StartInstallCommand}">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="ArrowDownload"
IconVariant="Regular" />
<TextBlock Classes="installer-icon"
Text="&#xE896;" />
<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="&#xE711;" />
<TextBlock Text="取消" />
</StackPanel>
</Button>
@@ -454,8 +453,8 @@
Height="40"
Background="{DynamicResource InstallerSubtleFillBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusMd}">
<fi:FluentIcon Icon="CheckmarkCircle"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xE73E;"
Foreground="{DynamicResource InstallerSuccessBrush}"
FontSize="22" />
</Border>
@@ -473,8 +472,8 @@
Command="{Binding LaunchCommand}">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="Play"
IconVariant="Regular" />
<TextBlock Classes="installer-icon"
Text="&#xE768;" />
<TextBlock Text="打开阑山桌面" />
</StackPanel>
</Button>
@@ -495,8 +494,8 @@
IsVisible="{Binding HasError}">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="10">
<fi:FluentIcon Icon="ErrorCircle"
IconVariant="Regular"
<TextBlock Classes="installer-icon"
Text="&#xE783;"
Foreground="{DynamicResource InstallerErrorBrush}"
FontSize="18" />
<TextBlock Grid.Column="1"
@@ -511,8 +510,8 @@
Command="{Binding BackCommand}">
<StackPanel Orientation="Horizontal"
Spacing="6">
<fi:FluentIcon Icon="ArrowLeft"
IconVariant="Regular" />
<TextBlock Classes="installer-icon"
Text="&#xE72B;" />
<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="&#xE72A;" />
</StackPanel>
</Button>
</Grid>

View File

@@ -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>

View File

@@ -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()
{

View File

@@ -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),

View File

@@ -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;