mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-21 16:14:28 +08:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a70476ce8 |
117
.github/workflows/release.yml
vendored
117
.github/workflows/release.yml
vendored
@@ -98,8 +98,10 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- arch: x64
|
- arch: x64
|
||||||
|
self_contained: true
|
||||||
suffix: ''
|
suffix: ''
|
||||||
- arch: x86
|
- arch: x86
|
||||||
|
self_contained: true
|
||||||
suffix: ''
|
suffix: ''
|
||||||
name: Build_Windows_${{ matrix.arch }}${{ matrix.suffix }}
|
name: Build_Windows_${{ matrix.arch }}${{ matrix.suffix }}
|
||||||
|
|
||||||
@@ -165,55 +167,91 @@ jobs:
|
|||||||
|
|
||||||
- name: Publish Main App
|
- name: Publish Main App
|
||||||
run: |
|
run: |
|
||||||
$publishDir = "publish/windows-${{ matrix.arch }}"
|
$selfContained = "${{ matrix.self_contained }}" -eq "true"
|
||||||
|
$publishDir = if ($selfContained) { "publish/windows-${{ matrix.arch }}" } else { "publish/windows-${{ matrix.arch }}-lite" }
|
||||||
|
|
||||||
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
if ($selfContained) {
|
||||||
-c Release `
|
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
||||||
-o ./$publishDir `
|
-c Release `
|
||||||
--self-contained:false `
|
-o ./$publishDir `
|
||||||
-r win-${{ matrix.arch }} `
|
--self-contained `
|
||||||
-p:SelfContained=false `
|
-r win-${{ matrix.arch }} `
|
||||||
-p:PublishSingleFile=false `
|
-p:PublishSingleFile=false `
|
||||||
-p:DebugType=none `
|
-p:DebugType=none `
|
||||||
-p:DebugSymbols=false `
|
-p:DebugSymbols=false `
|
||||||
-p:SkipAirAppHostBuild=true `
|
-p:SkipAirAppHostBuild=true `
|
||||||
-p:PublishTrimmed=false `
|
-p:PublishTrimmed=false `
|
||||||
-p:PublishReadyToRun=false `
|
-p:PublishReadyToRun=false `
|
||||||
-p:Version=${{ needs.prepare.outputs.version }} `
|
-p:Version=${{ needs.prepare.outputs.version }} `
|
||||||
-p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
|
-p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
-p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
|
-p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
-p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
|
-p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
|
||||||
|
} else {
|
||||||
|
dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
|
||||||
|
-c Release `
|
||||||
|
-o ./$publishDir `
|
||||||
|
--self-contained:false `
|
||||||
|
-p:PublishSingleFile=false `
|
||||||
|
-p:DebugType=none `
|
||||||
|
-p:DebugSymbols=false `
|
||||||
|
-p:SkipAirAppHostBuild=true `
|
||||||
|
-p:PublishTrimmed=false `
|
||||||
|
-p:PublishReadyToRun=false `
|
||||||
|
-p:Version=${{ needs.prepare.outputs.version }} `
|
||||||
|
-p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
|
-p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
|
-p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
|
||||||
|
}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Publish AirAppHost
|
- name: Publish AirAppHost
|
||||||
run: |
|
run: |
|
||||||
$arch = "${{ matrix.arch }}"
|
$arch = "${{ matrix.arch }}"
|
||||||
$publishDir = "publish/windows-$arch"
|
$selfContained = "${{ matrix.self_contained }}" -eq "true"
|
||||||
|
$publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" }
|
||||||
|
|
||||||
dotnet publish LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj `
|
if ($selfContained) {
|
||||||
-c Release `
|
dotnet publish LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj `
|
||||||
-o ./$publishDir `
|
-c Release `
|
||||||
--self-contained:false `
|
-o ./$publishDir `
|
||||||
-r win-$arch `
|
--self-contained:false `
|
||||||
-p:SelfContained=false `
|
-r win-$arch `
|
||||||
-p:PublishSingleFile=false `
|
-p:PublishSingleFile=false `
|
||||||
-p:DebugType=none `
|
-p:DebugType=none `
|
||||||
-p:DebugSymbols=false `
|
-p:DebugSymbols=false `
|
||||||
-p:PublishTrimmed=false `
|
-p:PublishTrimmed=false `
|
||||||
-p:PublishReadyToRun=false `
|
-p:PublishReadyToRun=false `
|
||||||
-p:BuildingAirAppHost=true `
|
-p:BuildingAirAppHost=true `
|
||||||
-p:SkipAirAppHostBuild=true `
|
-p:SkipAirAppHostBuild=true `
|
||||||
-p:Version=${{ needs.prepare.outputs.version }} `
|
-p:Version=${{ needs.prepare.outputs.version }} `
|
||||||
-p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
|
-p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
-p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
|
-p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
-p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
|
-p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
|
||||||
|
} else {
|
||||||
|
dotnet publish LanMountainDesktop.AirAppHost/LanMountainDesktop.AirAppHost.csproj `
|
||||||
|
-c Release `
|
||||||
|
-o ./$publishDir `
|
||||||
|
--self-contained:false `
|
||||||
|
-p:PublishSingleFile=false `
|
||||||
|
-p:DebugType=none `
|
||||||
|
-p:DebugSymbols=false `
|
||||||
|
-p:PublishTrimmed=false `
|
||||||
|
-p:PublishReadyToRun=false `
|
||||||
|
-p:BuildingAirAppHost=true `
|
||||||
|
-p:SkipAirAppHostBuild=true `
|
||||||
|
-p:Version=${{ needs.prepare.outputs.version }} `
|
||||||
|
-p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
|
-p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
|
||||||
|
-p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
|
||||||
|
}
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
|
||||||
- name: Restructure for Launcher
|
- name: Restructure for Launcher
|
||||||
run: |
|
run: |
|
||||||
$version = "${{ needs.prepare.outputs.version }}"
|
$version = "${{ needs.prepare.outputs.version }}"
|
||||||
$arch = "${{ matrix.arch }}"
|
$arch = "${{ matrix.arch }}"
|
||||||
$publishDir = "publish/windows-$arch"
|
$selfContained = "${{ matrix.self_contained }}" -eq "true"
|
||||||
|
$publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" }
|
||||||
$launcherPublishDir = "publish/launcher-win-$arch"
|
$launcherPublishDir = "publish/launcher-win-$arch"
|
||||||
$appDir = "app-$version"
|
$appDir = "app-$version"
|
||||||
$newStructure = "publish-launcher/windows-$arch"
|
$newStructure = "publish-launcher/windows-$arch"
|
||||||
@@ -236,7 +274,8 @@ jobs:
|
|||||||
- name: Optimize and Guard Windows Payload
|
- name: Optimize and Guard Windows Payload
|
||||||
run: |
|
run: |
|
||||||
$arch = "${{ matrix.arch }}"
|
$arch = "${{ matrix.arch }}"
|
||||||
$publishDir = "publish/windows-$arch"
|
$selfContained = "${{ matrix.self_contained }}" -eq "true"
|
||||||
|
$publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" }
|
||||||
|
|
||||||
./LanMountainDesktop/scripts/Optimize-PublishPayload.ps1 `
|
./LanMountainDesktop/scripts/Optimize-PublishPayload.ps1 `
|
||||||
-PublishDir $publishDir `
|
-PublishDir $publishDir `
|
||||||
@@ -255,7 +294,8 @@ jobs:
|
|||||||
$version = "${{ needs.prepare.outputs.version }}"
|
$version = "${{ needs.prepare.outputs.version }}"
|
||||||
$arch = "${{ matrix.arch }}"
|
$arch = "${{ matrix.arch }}"
|
||||||
$suffix = "${{ matrix.suffix }}"
|
$suffix = "${{ matrix.suffix }}"
|
||||||
$publishDir = "publish/windows-$arch"
|
$selfContained = "${{ matrix.self_contained }}" -eq "true"
|
||||||
|
$publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" }
|
||||||
$outputDir = "build-installer"
|
$outputDir = "build-installer"
|
||||||
$installerScript = "LanMountainDesktop/installer/LanMountainDesktop.iss"
|
$installerScript = "LanMountainDesktop/installer/LanMountainDesktop.iss"
|
||||||
|
|
||||||
@@ -289,6 +329,7 @@ jobs:
|
|||||||
"/DMyOutputDir=$outputDir",
|
"/DMyOutputDir=$outputDir",
|
||||||
"/DMyAppArch=$arch",
|
"/DMyAppArch=$arch",
|
||||||
"/DMyAppSuffix=$suffix",
|
"/DMyAppSuffix=$suffix",
|
||||||
|
"/DIsSelfContained=$selfContained",
|
||||||
$installerScript
|
$installerScript
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# Runtime Packaging Fix Checklist
|
|
||||||
|
|
||||||
- [x] `dotnet build LanMountainDesktop.slnx -c Debug -v minimal` succeeds.
|
|
||||||
- [x] Runtime probe, AirAppHost startup, and packaging policy tests pass.
|
|
||||||
- [ ] Full `win-x64` package dry run completes without timeout.
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# Runtime Packaging Fix
|
|
||||||
|
|
||||||
Windows releases use the launcher as the only self-contained bootstrapper. The
|
|
||||||
desktop host and AirAppHost are framework-dependent and rely on an
|
|
||||||
architecture-matched .NET 10 Desktop Runtime installed by the Inno setup flow.
|
|
||||||
|
|
||||||
Acceptance:
|
|
||||||
|
|
||||||
- Windows installer payload does not bundle .NET shared runtime files.
|
|
||||||
- Inno Setup downloads and silently installs the matching .NET 10 Desktop Runtime.
|
|
||||||
- Launcher blocks framework-dependent host startup with `dotnet_runtime_missing` when the runtime is unavailable.
|
|
||||||
- AirAppHost startup uses packaged executables or an explicit architecture-matched dotnet host for DLL fallback.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# Runtime Packaging Fix Tasks
|
|
||||||
|
|
||||||
- [x] Add launcher-side .NET runtime probe and host startup guard.
|
|
||||||
- [x] Update AirAppHost process start behavior for packaged exe and DLL fallback.
|
|
||||||
- [x] Update Windows packaging scripts and CI release workflow.
|
|
||||||
- [x] Update Inno Setup prerequisite download/install flow.
|
|
||||||
- [x] Add regression tests and runtime packaging documentation.
|
|
||||||
@@ -38,15 +38,6 @@ public static class AppearanceCornerRadiusTokenFactory
|
|||||||
Xl: new CornerRadius(40),
|
Xl: new CornerRadius(40),
|
||||||
Island: new CornerRadius(44),
|
Island: new CornerRadius(44),
|
||||||
Component: new CornerRadius(32)),
|
Component: new CornerRadius(32)),
|
||||||
GlobalAppearanceSettings.CornerRadiusStyleFluent => new AppearanceCornerRadiusTokens(
|
|
||||||
Micro: new CornerRadius(2),
|
|
||||||
Xs: new CornerRadius(4),
|
|
||||||
Sm: new CornerRadius(4),
|
|
||||||
Md: new CornerRadius(8),
|
|
||||||
Lg: new CornerRadius(8),
|
|
||||||
Xl: new CornerRadius(12),
|
|
||||||
Island: new CornerRadius(16),
|
|
||||||
Component: new CornerRadius(8)),
|
|
||||||
// Balanced (default)
|
// Balanced (default)
|
||||||
_ => new AppearanceCornerRadiusTokens(
|
_ => new AppearanceCornerRadiusTokens(
|
||||||
Micro: new CornerRadius(6),
|
Micro: new CornerRadius(6),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using LanMountainDesktop.Launcher.Services;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Launcher.Services.AirApp;
|
namespace LanMountainDesktop.Launcher.Services.AirApp;
|
||||||
|
|
||||||
@@ -14,20 +13,17 @@ internal sealed class AirAppProcessStarter : IAirAppProcessStarter
|
|||||||
private readonly Func<string?> _packageRootProvider;
|
private readonly Func<string?> _packageRootProvider;
|
||||||
private readonly Func<string?> _hostPathProvider;
|
private readonly Func<string?> _hostPathProvider;
|
||||||
private readonly Func<string?> _dataRootProvider;
|
private readonly Func<string?> _dataRootProvider;
|
||||||
private readonly DotNetRuntimeProbeOptions? _runtimeProbeOptions;
|
|
||||||
|
|
||||||
public AirAppProcessStarter(
|
public AirAppProcessStarter(
|
||||||
AirAppHostLocator locator,
|
AirAppHostLocator locator,
|
||||||
Func<string?> packageRootProvider,
|
Func<string?> packageRootProvider,
|
||||||
Func<string?> hostPathProvider,
|
Func<string?> hostPathProvider,
|
||||||
Func<string?> dataRootProvider,
|
Func<string?> dataRootProvider)
|
||||||
DotNetRuntimeProbeOptions? runtimeProbeOptions = null)
|
|
||||||
{
|
{
|
||||||
_locator = locator;
|
_locator = locator;
|
||||||
_packageRootProvider = packageRootProvider;
|
_packageRootProvider = packageRootProvider;
|
||||||
_hostPathProvider = hostPathProvider;
|
_hostPathProvider = hostPathProvider;
|
||||||
_dataRootProvider = dataRootProvider;
|
_dataRootProvider = dataRootProvider;
|
||||||
_runtimeProbeOptions = runtimeProbeOptions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Process? Start(
|
public Process? Start(
|
||||||
@@ -38,7 +34,22 @@ internal sealed class AirAppProcessStarter : IAirAppProcessStarter
|
|||||||
string? sourcePlacementId)
|
string? sourcePlacementId)
|
||||||
{
|
{
|
||||||
var hostPath = _locator.Resolve(_packageRootProvider(), _hostPathProvider());
|
var hostPath = _locator.Resolve(_packageRootProvider(), _hostPathProvider());
|
||||||
var startInfo = CreateStartInfo(hostPath, _runtimeProbeOptions);
|
var startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? AppContext.BaseDirectory
|
||||||
|
};
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows() &&
|
||||||
|
string.Equals(Path.GetExtension(hostPath), ".exe", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
startInfo.FileName = hostPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
startInfo.FileName = "dotnet";
|
||||||
|
startInfo.ArgumentList.Add(hostPath);
|
||||||
|
}
|
||||||
|
|
||||||
AddArgument(startInfo, "--app-id", appId);
|
AddArgument(startInfo, "--app-id", appId);
|
||||||
AddArgument(startInfo, "--session-id", sessionId);
|
AddArgument(startInfo, "--session-id", sessionId);
|
||||||
@@ -83,53 +94,6 @@ internal sealed class AirAppProcessStarter : IAirAppProcessStarter
|
|||||||
return process;
|
return process;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static ProcessStartInfo CreateStartInfo(
|
|
||||||
string hostPath,
|
|
||||||
DotNetRuntimeProbeOptions? runtimeProbeOptions = null)
|
|
||||||
{
|
|
||||||
var startInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
UseShellExecute = false,
|
|
||||||
WorkingDirectory = Path.GetDirectoryName(hostPath) ?? AppContext.BaseDirectory
|
|
||||||
};
|
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
if (string.Equals(Path.GetExtension(hostPath), ".exe", StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
if (DotNetRuntimeProbe.IsFrameworkDependentWindowsApp(hostPath))
|
|
||||||
{
|
|
||||||
var executableRuntime = DotNetRuntimeProbe.Probe(runtimeProbeOptions);
|
|
||||||
if (!executableRuntime.IsAvailable)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Unable to start AirAppHost because the architecture-matched .NET 10 runtime was not found. " +
|
|
||||||
executableRuntime.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
startInfo.FileName = hostPath;
|
|
||||||
return startInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
var runtime = DotNetRuntimeProbe.Probe(runtimeProbeOptions);
|
|
||||||
if (!runtime.IsAvailable || string.IsNullOrWhiteSpace(runtime.DotNetHostPath))
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException(
|
|
||||||
"Unable to start AirAppHost because the architecture-matched .NET 10 runtime was not found. " +
|
|
||||||
runtime.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
startInfo.FileName = runtime.DotNetHostPath;
|
|
||||||
startInfo.ArgumentList.Add(hostPath);
|
|
||||||
return startInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
startInfo.FileName = "dotnet";
|
|
||||||
startInfo.ArgumentList.Add(hostPath);
|
|
||||||
return startInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddArgument(ProcessStartInfo startInfo, string name, string value)
|
private static void AddArgument(ProcessStartInfo startInfo, string name, string value)
|
||||||
{
|
{
|
||||||
startInfo.ArgumentList.Add(name);
|
startInfo.ArgumentList.Add(name);
|
||||||
|
|||||||
@@ -1,401 +0,0 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Launcher.Services;
|
|
||||||
|
|
||||||
internal enum DotNetRuntimeArchitecture
|
|
||||||
{
|
|
||||||
X64,
|
|
||||||
X86
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed record DotNetRuntimeInfo(
|
|
||||||
string Name,
|
|
||||||
string Version,
|
|
||||||
string Source,
|
|
||||||
string? Location);
|
|
||||||
|
|
||||||
internal sealed record DotNetRuntimeProbeOptions
|
|
||||||
{
|
|
||||||
public int RequiredMajorVersion { get; init; } = 10;
|
|
||||||
|
|
||||||
public DotNetRuntimeArchitecture Architecture { get; init; } = DotNetRuntimeProbe.GetCurrentArchitecture();
|
|
||||||
|
|
||||||
public string? ProgramFilesPath { get; init; }
|
|
||||||
|
|
||||||
public string? ProgramFilesX86Path { get; init; }
|
|
||||||
|
|
||||||
public string? LocalAppDataPath { get; init; }
|
|
||||||
|
|
||||||
public IReadOnlyList<string>? DotNetHostCandidates { get; init; }
|
|
||||||
|
|
||||||
public bool IncludeRegistry { get; init; } = true;
|
|
||||||
|
|
||||||
public bool IncludeDotNetCli { get; init; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal sealed record DotNetRuntimeProbeResult(
|
|
||||||
bool IsAvailable,
|
|
||||||
int RequiredMajorVersion,
|
|
||||||
DotNetRuntimeArchitecture Architecture,
|
|
||||||
string? DotNetHostPath,
|
|
||||||
IReadOnlyList<string> SearchedPaths,
|
|
||||||
IReadOnlyList<DotNetRuntimeInfo> DetectedRuntimes,
|
|
||||||
string Message)
|
|
||||||
{
|
|
||||||
public Dictionary<string, string> ToDetails(string prefix = "dotnetRuntime")
|
|
||||||
{
|
|
||||||
return new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
[$"{prefix}Available"] = IsAvailable.ToString(),
|
|
||||||
[$"{prefix}RequiredMajorVersion"] = RequiredMajorVersion.ToString(),
|
|
||||||
[$"{prefix}Architecture"] = Architecture.ToString(),
|
|
||||||
[$"{prefix}DotNetHostPath"] = DotNetHostPath ?? string.Empty,
|
|
||||||
[$"{prefix}SearchedPaths"] = string.Join(" | ", SearchedPaths),
|
|
||||||
[$"{prefix}DetectedRuntimes"] = string.Join(
|
|
||||||
" | ",
|
|
||||||
DetectedRuntimes.Select(runtime =>
|
|
||||||
$"{runtime.Name} {runtime.Version} [{runtime.Source}{(string.IsNullOrWhiteSpace(runtime.Location) ? string.Empty : $": {runtime.Location}")}]")),
|
|
||||||
[$"{prefix}Message"] = Message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal static class DotNetRuntimeProbe
|
|
||||||
{
|
|
||||||
public const string RequiredSharedFrameworkName = "Microsoft.NETCore.App";
|
|
||||||
public const string WindowsDesktopSharedFrameworkName = "Microsoft.WindowsDesktop.App";
|
|
||||||
|
|
||||||
private static readonly string[] RequiredSharedFrameworkNames =
|
|
||||||
[
|
|
||||||
RequiredSharedFrameworkName,
|
|
||||||
WindowsDesktopSharedFrameworkName
|
|
||||||
];
|
|
||||||
|
|
||||||
public static DotNetRuntimeProbeResult Probe(DotNetRuntimeProbeOptions? options = null)
|
|
||||||
{
|
|
||||||
options ??= new DotNetRuntimeProbeOptions();
|
|
||||||
|
|
||||||
var searchedPaths = new List<string>();
|
|
||||||
var detected = new List<DotNetRuntimeInfo>();
|
|
||||||
var requiredMajor = options.RequiredMajorVersion;
|
|
||||||
|
|
||||||
var localAppDataRoot = GetLocalAppDataPath(options);
|
|
||||||
var perUserDotnetRoot = !string.IsNullOrWhiteSpace(localAppDataRoot)
|
|
||||||
? Path.Combine(localAppDataRoot, "dotnet")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
foreach (var frameworkName in RequiredSharedFrameworkNames)
|
|
||||||
{
|
|
||||||
foreach (var basePath in EnumerateDotNetInstallRoots(options))
|
|
||||||
{
|
|
||||||
var sharedFrameworkDirectory = Path.Combine(basePath, "shared", frameworkName);
|
|
||||||
searchedPaths.Add(sharedFrameworkDirectory);
|
|
||||||
var isPerUser = perUserDotnetRoot is not null &&
|
|
||||||
string.Equals(basePath, perUserDotnetRoot, StringComparison.OrdinalIgnoreCase);
|
|
||||||
AddDirectoryRuntimes(sharedFrameworkDirectory, frameworkName,
|
|
||||||
isPerUser ? "shared-framework-directory-per-user" : "shared-framework-directory",
|
|
||||||
detected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string? dotNetHostPath = null;
|
|
||||||
foreach (var candidate in EnumerateDotNetHostCandidates(options))
|
|
||||||
{
|
|
||||||
searchedPaths.Add(candidate);
|
|
||||||
if (dotNetHostPath is null && File.Exists(candidate))
|
|
||||||
{
|
|
||||||
dotNetHostPath = candidate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OperatingSystem.IsWindows() && options.IncludeRegistry)
|
|
||||||
{
|
|
||||||
foreach (var frameworkName in RequiredSharedFrameworkNames)
|
|
||||||
{
|
|
||||||
AddRegistryRuntimes(options.Architecture, frameworkName, detected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (options.IncludeDotNetCli)
|
|
||||||
{
|
|
||||||
AddDotNetCliRuntimes(dotNetHostPath, detected);
|
|
||||||
}
|
|
||||||
|
|
||||||
var isAvailable = detected.Any(runtime =>
|
|
||||||
string.Equals(runtime.Name, RequiredSharedFrameworkName, StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
IsRequiredMajor(runtime.Version, requiredMajor));
|
|
||||||
|
|
||||||
var message = isAvailable
|
|
||||||
? $".NET {requiredMajor} runtime found for {options.Architecture}."
|
|
||||||
: $".NET {requiredMajor} runtime was not found for {options.Architecture}.";
|
|
||||||
|
|
||||||
return new DotNetRuntimeProbeResult(
|
|
||||||
isAvailable,
|
|
||||||
requiredMajor,
|
|
||||||
options.Architecture,
|
|
||||||
dotNetHostPath,
|
|
||||||
searchedPaths
|
|
||||||
.Where(path => !string.IsNullOrWhiteSpace(path))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList(),
|
|
||||||
detected
|
|
||||||
.DistinctBy(runtime => $"{runtime.Name}|{runtime.Version}|{runtime.Source}|{runtime.Location}", StringComparer.OrdinalIgnoreCase)
|
|
||||||
.OrderBy(runtime => runtime.Name, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ThenBy(runtime => runtime.Version, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList(),
|
|
||||||
message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static DotNetRuntimeArchitecture GetCurrentArchitecture()
|
|
||||||
{
|
|
||||||
return RuntimeInformation.ProcessArchitecture switch
|
|
||||||
{
|
|
||||||
Architecture.X86 => DotNetRuntimeArchitecture.X86,
|
|
||||||
_ => DotNetRuntimeArchitecture.X64
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string? FindDotNetHostPath(DotNetRuntimeProbeOptions? options = null)
|
|
||||||
{
|
|
||||||
options ??= new DotNetRuntimeProbeOptions();
|
|
||||||
return EnumerateDotNetHostCandidates(options).FirstOrDefault(File.Exists);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool IsFrameworkDependentWindowsApp(string executablePath)
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows() || string.IsNullOrWhiteSpace(executablePath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var directory = Path.GetDirectoryName(Path.GetFullPath(executablePath));
|
|
||||||
if (string.IsNullOrWhiteSpace(directory))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var appName = Path.GetFileNameWithoutExtension(executablePath);
|
|
||||||
var runtimeConfigPath = Path.Combine(directory, $"{appName}.runtimeconfig.json");
|
|
||||||
if (!File.Exists(runtimeConfigPath))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return !File.Exists(Path.Combine(directory, "coreclr.dll")) &&
|
|
||||||
!File.Exists(Path.Combine(directory, "hostfxr.dll")) &&
|
|
||||||
!File.Exists(Path.Combine(directory, "hostpolicy.dll")) &&
|
|
||||||
!File.Exists(Path.Combine(directory, "System.Private.CoreLib.dll"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateDotNetInstallRoots(DotNetRuntimeProbeOptions options)
|
|
||||||
{
|
|
||||||
var programFilesRoot = options.Architecture == DotNetRuntimeArchitecture.X86
|
|
||||||
? GetProgramFilesX86Path(options)
|
|
||||||
: GetProgramFilesPath(options);
|
|
||||||
|
|
||||||
yield return Path.Combine(programFilesRoot, "dotnet");
|
|
||||||
|
|
||||||
var localAppData = GetLocalAppDataPath(options);
|
|
||||||
if (!string.IsNullOrWhiteSpace(localAppData))
|
|
||||||
{
|
|
||||||
var perUserDotnet = Path.Combine(localAppData, "dotnet");
|
|
||||||
if (!string.Equals(perUserDotnet, Path.Combine(programFilesRoot, "dotnet"), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
yield return perUserDotnet;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IEnumerable<string> EnumerateDotNetHostCandidates(DotNetRuntimeProbeOptions options)
|
|
||||||
{
|
|
||||||
if (options.DotNetHostCandidates is not null)
|
|
||||||
{
|
|
||||||
foreach (var candidate in options.DotNetHostCandidates)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(candidate))
|
|
||||||
{
|
|
||||||
yield return Path.GetFullPath(candidate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var programFilesRoot = options.Architecture == DotNetRuntimeArchitecture.X86
|
|
||||||
? GetProgramFilesX86Path(options)
|
|
||||||
: GetProgramFilesPath(options);
|
|
||||||
|
|
||||||
yield return Path.Combine(programFilesRoot, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet");
|
|
||||||
|
|
||||||
var localAppData = GetLocalAppDataPath(options);
|
|
||||||
if (!string.IsNullOrWhiteSpace(localAppData))
|
|
||||||
{
|
|
||||||
var perUserHost = Path.Combine(localAppData, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet");
|
|
||||||
if (!string.Equals(perUserHost, Path.Combine(programFilesRoot, "dotnet", OperatingSystem.IsWindows() ? "dotnet.exe" : "dotnet"), StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
yield return perUserHost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetProgramFilesPath(DotNetRuntimeProbeOptions options)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.ProgramFilesPath))
|
|
||||||
{
|
|
||||||
return Path.GetFullPath(options.ProgramFilesPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Environment.GetEnvironmentVariable("ProgramW6432") ??
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetProgramFilesX86Path(DotNetRuntimeProbeOptions options)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.ProgramFilesX86Path))
|
|
||||||
{
|
|
||||||
return Path.GetFullPath(options.ProgramFilesX86Path);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Environment.GetEnvironmentVariable("ProgramFiles(x86)") ??
|
|
||||||
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetLocalAppDataPath(DotNetRuntimeProbeOptions options)
|
|
||||||
{
|
|
||||||
if (!string.IsNullOrWhiteSpace(options.LocalAppDataPath))
|
|
||||||
{
|
|
||||||
return Path.GetFullPath(options.LocalAppDataPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddDirectoryRuntimes(
|
|
||||||
string sharedFrameworkDirectory,
|
|
||||||
string sharedFrameworkName,
|
|
||||||
string source,
|
|
||||||
List<DotNetRuntimeInfo> detected)
|
|
||||||
{
|
|
||||||
if (!Directory.Exists(sharedFrameworkDirectory))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var directory in Directory.GetDirectories(sharedFrameworkDirectory))
|
|
||||||
{
|
|
||||||
var version = Path.GetFileName(directory);
|
|
||||||
if (!string.IsNullOrWhiteSpace(version))
|
|
||||||
{
|
|
||||||
detected.Add(new DotNetRuntimeInfo(sharedFrameworkName, version, source, directory));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddRegistryRuntimes(
|
|
||||||
DotNetRuntimeArchitecture architecture,
|
|
||||||
string sharedFrameworkName,
|
|
||||||
List<DotNetRuntimeInfo> detected)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var registryView = architecture == DotNetRuntimeArchitecture.X86
|
|
||||||
? RegistryView.Registry32
|
|
||||||
: RegistryView.Registry64;
|
|
||||||
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, registryView);
|
|
||||||
using var key = baseKey.OpenSubKey(
|
|
||||||
$@"SOFTWARE\dotnet\Setup\InstalledVersions\{(architecture == DotNetRuntimeArchitecture.X86 ? "x86" : "x64")}\sharedfx\{sharedFrameworkName}");
|
|
||||||
|
|
||||||
if (key is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var valueName in key.GetValueNames())
|
|
||||||
{
|
|
||||||
if (key.GetValue(valueName) is not null)
|
|
||||||
{
|
|
||||||
detected.Add(new DotNetRuntimeInfo(sharedFrameworkName, valueName, "registry", key.Name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Warn($"Failed to inspect .NET runtime registry keys: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void AddDotNetCliRuntimes(
|
|
||||||
string? dotNetHostPath,
|
|
||||||
List<DotNetRuntimeInfo> detected)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(dotNetHostPath) || !File.Exists(dotNetHostPath))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var process = new Process();
|
|
||||||
process.StartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = dotNetHostPath,
|
|
||||||
Arguments = "--list-runtimes",
|
|
||||||
UseShellExecute = false,
|
|
||||||
RedirectStandardOutput = true,
|
|
||||||
RedirectStandardError = true,
|
|
||||||
CreateNoWindow = true
|
|
||||||
};
|
|
||||||
|
|
||||||
process.Start();
|
|
||||||
var output = process.StandardOutput.ReadToEnd();
|
|
||||||
process.WaitForExit(3000);
|
|
||||||
|
|
||||||
foreach (var line in output.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries))
|
|
||||||
{
|
|
||||||
var parsed = ParseListRuntimeLine(line);
|
|
||||||
if (parsed is not null &&
|
|
||||||
RequiredSharedFrameworkNames.Contains(parsed.Value.Name, StringComparer.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
detected.Add(new DotNetRuntimeInfo(
|
|
||||||
parsed.Value.Name,
|
|
||||||
parsed.Value.Version,
|
|
||||||
"dotnet-cli",
|
|
||||||
parsed.Value.Location));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.Warn($"Failed to inspect .NET runtimes via dotnet CLI: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static (string Name, string Version, string? Location)? ParseListRuntimeLine(string line)
|
|
||||||
{
|
|
||||||
var firstSpace = line.IndexOf(' ');
|
|
||||||
if (firstSpace <= 0 || firstSpace + 1 >= line.Length)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var secondSpace = line.IndexOf(' ', firstSpace + 1);
|
|
||||||
if (secondSpace <= firstSpace)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = line[..firstSpace].Trim();
|
|
||||||
var version = line[(firstSpace + 1)..secondSpace].Trim();
|
|
||||||
var location = line[(secondSpace + 1)..].Trim().Trim('[', ']');
|
|
||||||
return (name, version, string.IsNullOrWhiteSpace(location) ? null : location);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsRequiredMajor(string version, int requiredMajor)
|
|
||||||
{
|
|
||||||
var dotIndex = version.IndexOf('.');
|
|
||||||
var majorText = dotIndex < 0 ? version : version[..dotIndex];
|
|
||||||
return int.TryParse(majorText, out var major) && major == requiredMajor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -930,44 +930,6 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
return LaunchHostWithResolvedPathAsync(resolution, forceDirectMode, retryTag);
|
return LaunchHostWithResolvedPathAsync(resolution, forceDirectMode, retryTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static LauncherResult? ValidateDotNetRuntimePrerequisite(
|
|
||||||
HostLaunchPlan plan,
|
|
||||||
HostResolutionResult resolution,
|
|
||||||
DotNetRuntimeProbeOptions? probeOptions = null)
|
|
||||||
{
|
|
||||||
ArgumentNullException.ThrowIfNull(plan);
|
|
||||||
ArgumentNullException.ThrowIfNull(resolution);
|
|
||||||
|
|
||||||
if (!DotNetRuntimeProbe.IsFrameworkDependentWindowsApp(plan.HostPath))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var runtime = DotNetRuntimeProbe.Probe(probeOptions);
|
|
||||||
Logger.Info(
|
|
||||||
$"Runtime prerequisite check completed. Available={runtime.IsAvailable}; " +
|
|
||||||
$"Architecture={runtime.Architecture}; Message='{runtime.Message}'.");
|
|
||||||
|
|
||||||
if (runtime.IsAvailable)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var details = BuildResolutionDetails(resolution, null, null, "runtime");
|
|
||||||
foreach (var pair in runtime.ToDetails())
|
|
||||||
{
|
|
||||||
details[pair.Key] = pair.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return BuildResult(
|
|
||||||
success: false,
|
|
||||||
stage: "launchHost",
|
|
||||||
code: "dotnet_runtime_missing",
|
|
||||||
message: ".NET 10 Desktop Runtime is required before LanMountainDesktop can start.",
|
|
||||||
details: details,
|
|
||||||
errorMessage: runtime.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<HostLaunchOutcome> LaunchHostWithResolvedPathAsync(
|
private async Task<HostLaunchOutcome> LaunchHostWithResolvedPathAsync(
|
||||||
HostResolutionResult resolution,
|
HostResolutionResult resolution,
|
||||||
bool forceDirectMode,
|
bool forceDirectMode,
|
||||||
@@ -975,12 +937,6 @@ internal sealed class LauncherFlowCoordinator
|
|||||||
{
|
{
|
||||||
var dataRoot = _dataLocationResolver.ResolveDataRoot();
|
var dataRoot = _dataLocationResolver.ResolveDataRoot();
|
||||||
var plan = HostLaunchPlanBuilder.Build(_context, _deploymentLocator, resolution, dataRoot);
|
var plan = HostLaunchPlanBuilder.Build(_context, _deploymentLocator, resolution, dataRoot);
|
||||||
var prerequisiteFailure = ValidateDotNetRuntimePrerequisite(plan, resolution);
|
|
||||||
if (prerequisiteFailure is not null)
|
|
||||||
{
|
|
||||||
return HostLaunchOutcome.FromResult(prerequisiteFailure);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hostPath = plan.HostPath;
|
var hostPath = plan.HostPath;
|
||||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ public static class GlobalAppearanceSettings
|
|||||||
public const string CornerRadiusStyleBalanced = "Balanced";
|
public const string CornerRadiusStyleBalanced = "Balanced";
|
||||||
public const string CornerRadiusStyleRounded = "Rounded";
|
public const string CornerRadiusStyleRounded = "Rounded";
|
||||||
public const string CornerRadiusStyleOpen = "Open";
|
public const string CornerRadiusStyleOpen = "Open";
|
||||||
public const string CornerRadiusStyleFluent = "Fluent";
|
|
||||||
public const string DefaultCornerRadiusStyle = CornerRadiusStyleBalanced;
|
public const string DefaultCornerRadiusStyle = CornerRadiusStyleBalanced;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -44,11 +43,6 @@ public static class GlobalAppearanceSettings
|
|||||||
return CornerRadiusStyleOpen;
|
return CornerRadiusStyleOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(trimmed, CornerRadiusStyleFluent, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
return CornerRadiusStyleFluent;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultCornerRadiusStyle;
|
return DefaultCornerRadiusStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +51,7 @@ public static class GlobalAppearanceSettings
|
|||||||
CornerRadiusStyleSharp,
|
CornerRadiusStyleSharp,
|
||||||
CornerRadiusStyleBalanced,
|
CornerRadiusStyleBalanced,
|
||||||
CornerRadiusStyleRounded,
|
CornerRadiusStyleRounded,
|
||||||
CornerRadiusStyleOpen,
|
CornerRadiusStyleOpen
|
||||||
CornerRadiusStyleFluent
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
using LanMountainDesktop.Launcher.Services;
|
|
||||||
using LanMountainDesktop.Launcher.Services.AirApp;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
|
||||||
|
|
||||||
public sealed class AirAppProcessStarterRuntimeTests : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _root;
|
|
||||||
|
|
||||||
public AirAppProcessStarterRuntimeTests()
|
|
||||||
{
|
|
||||||
_root = Path.Combine(Path.GetTempPath(), "LanMountainDesktop.AirAppProcessStarterRuntimeTests", Guid.NewGuid().ToString("N"));
|
|
||||||
Directory.CreateDirectory(_root);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CreateStartInfo_UsesPackagedExecutable_WhenExeExists()
|
|
||||||
{
|
|
||||||
var hostPath = Path.Combine(_root, OperatingSystem.IsWindows()
|
|
||||||
? "LanMountainDesktop.AirAppHost.exe"
|
|
||||||
: "LanMountainDesktop.AirAppHost");
|
|
||||||
File.WriteAllText(hostPath, string.Empty);
|
|
||||||
|
|
||||||
var startInfo = AirAppProcessStarter.CreateStartInfo(hostPath);
|
|
||||||
|
|
||||||
Assert.Equal(hostPath, startInfo.FileName);
|
|
||||||
Assert.Empty(startInfo.ArgumentList);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void CreateStartInfo_UsesArchitectureMatchedDotnetHost_ForDllFallbackOnWindows()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var programFiles = Path.Combine(_root, "ProgramFiles");
|
|
||||||
var dotnetRoot = Path.Combine(programFiles, "dotnet");
|
|
||||||
Directory.CreateDirectory(dotnetRoot);
|
|
||||||
var dotnetHost = Path.Combine(dotnetRoot, "dotnet.exe");
|
|
||||||
File.WriteAllText(dotnetHost, string.Empty);
|
|
||||||
Directory.CreateDirectory(Path.Combine(
|
|
||||||
dotnetRoot,
|
|
||||||
"shared",
|
|
||||||
DotNetRuntimeProbe.RequiredSharedFrameworkName,
|
|
||||||
"10.0.5"));
|
|
||||||
|
|
||||||
var hostDll = Path.Combine(_root, "LanMountainDesktop.AirAppHost.dll");
|
|
||||||
File.WriteAllText(hostDll, string.Empty);
|
|
||||||
var options = new DotNetRuntimeProbeOptions
|
|
||||||
{
|
|
||||||
Architecture = DotNetRuntimeArchitecture.X64,
|
|
||||||
ProgramFilesPath = programFiles,
|
|
||||||
ProgramFilesX86Path = Path.Combine(_root, "ProgramFilesX86"),
|
|
||||||
IncludeRegistry = false,
|
|
||||||
IncludeDotNetCli = false
|
|
||||||
};
|
|
||||||
|
|
||||||
var startInfo = AirAppProcessStarter.CreateStartInfo(hostDll, options);
|
|
||||||
|
|
||||||
Assert.Equal(dotnetHost, startInfo.FileName);
|
|
||||||
Assert.Equal(hostDll, startInfo.ArgumentList.Single());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (Directory.Exists(_root))
|
|
||||||
{
|
|
||||||
Directory.Delete(_root, recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,6 @@ public sealed class CornerRadiusStyleTests
|
|||||||
[InlineData("Balanced", "Balanced")]
|
[InlineData("Balanced", "Balanced")]
|
||||||
[InlineData("Rounded", "Rounded")]
|
[InlineData("Rounded", "Rounded")]
|
||||||
[InlineData("Open", "Open")]
|
[InlineData("Open", "Open")]
|
||||||
[InlineData("Fluent", "Fluent")]
|
|
||||||
[InlineData("Unknown", "Balanced")]
|
[InlineData("Unknown", "Balanced")]
|
||||||
[InlineData(null, "Balanced")]
|
[InlineData(null, "Balanced")]
|
||||||
public void NormalizeCornerRadiusStyle_ReturnsValidStyleOrDefault(string? input, string expected)
|
public void NormalizeCornerRadiusStyle_ReturnsValidStyleOrDefault(string? input, string expected)
|
||||||
@@ -21,23 +20,6 @@ public sealed class CornerRadiusStyleTests
|
|||||||
Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusStyle(input));
|
Assert.Equal(expected, GlobalAppearanceSettings.NormalizeCornerRadiusStyle(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FluentStyle_ReturnsFluentDesignSystemValues()
|
|
||||||
{
|
|
||||||
var tokens = LanMountainDesktop.Appearance.AppearanceCornerRadiusTokenFactory.Create(
|
|
||||||
GlobalAppearanceSettings.CornerRadiusStyleFluent);
|
|
||||||
|
|
||||||
// Microsoft Fluent Design System: ControlCornerRadius = 4px, OverlayCornerRadius = 8px
|
|
||||||
Assert.Equal(new CornerRadius(2), tokens.Micro);
|
|
||||||
Assert.Equal(new CornerRadius(4), tokens.Xs);
|
|
||||||
Assert.Equal(new CornerRadius(4), tokens.Sm);
|
|
||||||
Assert.Equal(new CornerRadius(8), tokens.Md);
|
|
||||||
Assert.Equal(new CornerRadius(8), tokens.Lg);
|
|
||||||
Assert.Equal(new CornerRadius(12), tokens.Xl);
|
|
||||||
Assert.Equal(new CornerRadius(16), tokens.Island);
|
|
||||||
Assert.Equal(new CornerRadius(8), tokens.Component);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void PluginAppearanceContext_ResolveCornerRadius_ReturnsFixedTokenValues()
|
public void PluginAppearanceContext_ResolveCornerRadius_ReturnsFixedTokenValues()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,238 +0,0 @@
|
|||||||
using LanMountainDesktop.Launcher.Services;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
|
||||||
|
|
||||||
public sealed class DotNetRuntimeProbeTests : IDisposable
|
|
||||||
{
|
|
||||||
private readonly string _root;
|
|
||||||
private readonly string _programFiles;
|
|
||||||
private readonly string _programFilesX86;
|
|
||||||
private readonly string _localAppData;
|
|
||||||
|
|
||||||
public DotNetRuntimeProbeTests()
|
|
||||||
{
|
|
||||||
_root = Path.Combine(Path.GetTempPath(), "LanMountainDesktop.DotNetRuntimeProbeTests", Guid.NewGuid().ToString("N"));
|
|
||||||
_programFiles = Path.Combine(_root, "ProgramFiles");
|
|
||||||
_programFilesX86 = Path.Combine(_root, "ProgramFilesX86");
|
|
||||||
_localAppData = Path.Combine(_root, "LocalAppData");
|
|
||||||
Directory.CreateDirectory(_programFiles);
|
|
||||||
Directory.CreateDirectory(_programFilesX86);
|
|
||||||
Directory.CreateDirectory(_localAppData);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_AcceptsTargetArchitectureRuntime_WhenDotnetHostIsMissing()
|
|
||||||
{
|
|
||||||
CreateRuntime(_programFiles, "10.0.5");
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.True(result.IsAvailable);
|
|
||||||
Assert.Null(result.DotNetHostPath);
|
|
||||||
Assert.Contains(result.DetectedRuntimes, runtime => runtime.Version == "10.0.5");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_X64DoesNotAcceptX86OnlyRuntime()
|
|
||||||
{
|
|
||||||
CreateRuntime(_programFilesX86, "10.0.5");
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.False(result.IsAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_X86DoesNotAcceptX64OnlyRuntime()
|
|
||||||
{
|
|
||||||
CreateRuntime(_programFiles, "10.0.5");
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X86));
|
|
||||||
|
|
||||||
Assert.False(result.IsAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_RejectsOlderMajorVersions()
|
|
||||||
{
|
|
||||||
CreateRuntime(_programFiles, "8.0.25");
|
|
||||||
CreateRuntime(_programFiles, "9.0.14");
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.False(result.IsAvailable);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_DetectsPerUserRuntime()
|
|
||||||
{
|
|
||||||
CreateRuntime(_localAppData, "10.0.5", DotNetRuntimeProbe.RequiredSharedFrameworkName);
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.True(result.IsAvailable);
|
|
||||||
Assert.Contains(result.DetectedRuntimes, runtime =>
|
|
||||||
runtime.Version == "10.0.5" &&
|
|
||||||
runtime.Source == "shared-framework-directory-per-user");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_DetectsWindowsDesktopRuntime()
|
|
||||||
{
|
|
||||||
CreateRuntime(_programFiles, "10.0.5", DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName);
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.False(result.IsAvailable);
|
|
||||||
Assert.Contains(result.DetectedRuntimes, runtime =>
|
|
||||||
runtime.Name == DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName &&
|
|
||||||
runtime.Version == "10.0.5");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_DetectsPerUserWindowsDesktopRuntime()
|
|
||||||
{
|
|
||||||
CreateRuntime(_localAppData, "10.0.5", DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName);
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.Contains(result.DetectedRuntimes, runtime =>
|
|
||||||
runtime.Name == DotNetRuntimeProbe.WindowsDesktopSharedFrameworkName &&
|
|
||||||
runtime.Version == "10.0.5" &&
|
|
||||||
runtime.Source == "shared-framework-directory-per-user");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_FindsDotNetHost_InPerUserPath()
|
|
||||||
{
|
|
||||||
var dotnetDir = Path.Combine(_localAppData, "dotnet");
|
|
||||||
Directory.CreateDirectory(dotnetDir);
|
|
||||||
File.WriteAllText(Path.Combine(dotnetDir, "dotnet.exe"), string.Empty);
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(new DotNetRuntimeProbeOptions
|
|
||||||
{
|
|
||||||
Architecture = DotNetRuntimeArchitecture.X64,
|
|
||||||
ProgramFilesPath = _programFiles,
|
|
||||||
ProgramFilesX86Path = _programFilesX86,
|
|
||||||
LocalAppDataPath = _localAppData,
|
|
||||||
IncludeRegistry = false,
|
|
||||||
IncludeDotNetCli = false
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.NotNull(result.DotNetHostPath);
|
|
||||||
Assert.Contains("LocalAppData", result.DotNetHostPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_PrefersProgramFilesHost_OverPerUserHost()
|
|
||||||
{
|
|
||||||
var systemDotnetDir = Path.Combine(_programFiles, "dotnet");
|
|
||||||
Directory.CreateDirectory(systemDotnetDir);
|
|
||||||
File.WriteAllText(Path.Combine(systemDotnetDir, "dotnet.exe"), string.Empty);
|
|
||||||
|
|
||||||
var perUserDotnetDir = Path.Combine(_localAppData, "dotnet");
|
|
||||||
Directory.CreateDirectory(perUserDotnetDir);
|
|
||||||
File.WriteAllText(Path.Combine(perUserDotnetDir, "dotnet.exe"), string.Empty);
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(new DotNetRuntimeProbeOptions
|
|
||||||
{
|
|
||||||
Architecture = DotNetRuntimeArchitecture.X64,
|
|
||||||
ProgramFilesPath = _programFiles,
|
|
||||||
ProgramFilesX86Path = _programFilesX86,
|
|
||||||
LocalAppDataPath = _localAppData,
|
|
||||||
IncludeRegistry = false,
|
|
||||||
IncludeDotNetCli = false
|
|
||||||
});
|
|
||||||
|
|
||||||
Assert.NotNull(result.DotNetHostPath);
|
|
||||||
Assert.Contains("ProgramFiles", result.DotNetHostPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Probe_CombinesSystemAndPerUserRuntimes()
|
|
||||||
{
|
|
||||||
CreateRuntime(_programFiles, "10.0.5");
|
|
||||||
CreateRuntime(_localAppData, "10.0.3");
|
|
||||||
|
|
||||||
var result = DotNetRuntimeProbe.Probe(CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.True(result.IsAvailable);
|
|
||||||
Assert.Contains(result.DetectedRuntimes, runtime => runtime.Version == "10.0.5");
|
|
||||||
Assert.Contains(result.DetectedRuntimes, runtime => runtime.Version == "10.0.3");
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void ValidateDotNetRuntimePrerequisite_ReturnsStructuredFailure_WhenRuntimeIsMissing()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var appDir = Path.Combine(_root, "app-1.0.0");
|
|
||||||
Directory.CreateDirectory(appDir);
|
|
||||||
var hostPath = Path.Combine(appDir, "LanMountainDesktop.exe");
|
|
||||||
File.WriteAllText(hostPath, string.Empty);
|
|
||||||
File.WriteAllText(Path.Combine(appDir, "LanMountainDesktop.runtimeconfig.json"), "{}");
|
|
||||||
|
|
||||||
var plan = new HostLaunchPlan(
|
|
||||||
hostPath,
|
|
||||||
_root,
|
|
||||||
appDir,
|
|
||||||
[],
|
|
||||||
new Dictionary<string, string>(),
|
|
||||||
new() { Version = "1.0.0", Codename = "Test" });
|
|
||||||
var resolution = new HostResolutionResult
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
ResolvedHostPath = hostPath,
|
|
||||||
AppRoot = _root,
|
|
||||||
ResolutionSource = "test",
|
|
||||||
SearchedPaths = [hostPath]
|
|
||||||
};
|
|
||||||
|
|
||||||
var result = LauncherFlowCoordinator.ValidateDotNetRuntimePrerequisite(
|
|
||||||
plan,
|
|
||||||
resolution,
|
|
||||||
CreateOptions(DotNetRuntimeArchitecture.X64));
|
|
||||||
|
|
||||||
Assert.NotNull(result);
|
|
||||||
Assert.False(result.Success);
|
|
||||||
Assert.Equal("dotnet_runtime_missing", result.Code);
|
|
||||||
Assert.Equal("False", result.Details["dotnetRuntimeAvailable"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private DotNetRuntimeProbeOptions CreateOptions(DotNetRuntimeArchitecture architecture)
|
|
||||||
{
|
|
||||||
return new DotNetRuntimeProbeOptions
|
|
||||||
{
|
|
||||||
Architecture = architecture,
|
|
||||||
ProgramFilesPath = _programFiles,
|
|
||||||
ProgramFilesX86Path = _programFilesX86,
|
|
||||||
LocalAppDataPath = _localAppData,
|
|
||||||
DotNetHostCandidates = [],
|
|
||||||
IncludeRegistry = false,
|
|
||||||
IncludeDotNetCli = false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void CreateRuntime(string root, string version, string? frameworkName = null)
|
|
||||||
{
|
|
||||||
frameworkName ??= DotNetRuntimeProbe.RequiredSharedFrameworkName;
|
|
||||||
Directory.CreateDirectory(Path.Combine(
|
|
||||||
root,
|
|
||||||
"dotnet",
|
|
||||||
"shared",
|
|
||||||
frameworkName,
|
|
||||||
version));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (Directory.Exists(_root))
|
|
||||||
{
|
|
||||||
Directory.Delete(_root, recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace LanMountainDesktop.Tests;
|
|
||||||
|
|
||||||
public sealed class PackagingRuntimePolicyTests
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void WindowsPackageScript_PublishesLauncherRootAndFrameworkDependentAppDirectory()
|
|
||||||
{
|
|
||||||
var script = ReadRepositoryFile("LanMountainDesktop", "scripts", "package.ps1");
|
|
||||||
|
|
||||||
Assert.Contains("Publish-LauncherPayload", script);
|
|
||||||
Assert.Contains("\"app-$Version\"", script);
|
|
||||||
Assert.Contains("Publish-MainAppFrameworkDependentPayload", script);
|
|
||||||
Assert.Contains("\"--self-contained\", \"false\"", script);
|
|
||||||
Assert.Contains("\"-p:SelfContained=false\"", script);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void WindowsPayloadGuard_BlocksBundledDotNetRuntimeFiles()
|
|
||||||
{
|
|
||||||
var script = ReadRepositoryFile("LanMountainDesktop", "scripts", "Optimize-PublishPayload.ps1");
|
|
||||||
|
|
||||||
Assert.Contains("coreclr.dll", script);
|
|
||||||
Assert.Contains("hostfxr.dll", script);
|
|
||||||
Assert.Contains("hostpolicy.dll", script);
|
|
||||||
Assert.Contains("System.Private.CoreLib.dll", script);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void Installer_DownloadsArchitectureSpecificDesktopRuntime()
|
|
||||||
{
|
|
||||||
var installer = ReadRepositoryFile("LanMountainDesktop", "installer", "LanMountainDesktop.iss");
|
|
||||||
|
|
||||||
Assert.Contains("https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe", installer);
|
|
||||||
Assert.Contains("https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x86.exe", installer);
|
|
||||||
Assert.Contains("/install /quiet /norestart", installer);
|
|
||||||
Assert.Contains("ExitCode <> 3010", installer);
|
|
||||||
Assert.DoesNotContain("IsSelfContainedBuild", installer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string ReadRepositoryFile(params string[] pathParts)
|
|
||||||
{
|
|
||||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
|
||||||
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "LanMountainDesktop.slnx")))
|
|
||||||
{
|
|
||||||
directory = directory.Parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (directory is null)
|
|
||||||
{
|
|
||||||
throw new DirectoryNotFoundException("Unable to locate repository root.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return File.ReadAllText(Path.Combine([directory.FullName, .. pathParts]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -44,13 +44,6 @@ pwsh ./scripts/package.ps1 -RuntimeIdentifier osx-x64 -Version 1.0.1
|
|||||||
|
|
||||||
This guide covers local packaging and CI packaging for LanMountainDesktop.
|
This guide covers local packaging and CI packaging for LanMountainDesktop.
|
||||||
|
|
||||||
### Current Windows runtime policy
|
|
||||||
|
|
||||||
- Windows installers do not bundle the .NET shared runtime.
|
|
||||||
- `LanMountainDesktop.Launcher.exe` remains a Native AOT/self-contained bootstrapper at the package root.
|
|
||||||
- `LanMountainDesktop.exe` and `LanMountainDesktop.AirAppHost.exe` are published as framework-dependent, RID-specific apps under `app-<version>/`.
|
|
||||||
- The Inno installer downloads and silently installs the matching .NET 10 Desktop Runtime (`win-x64` or `win-x86`) before copying/launching the app.
|
|
||||||
|
|
||||||
### Key points
|
### Key points
|
||||||
|
|
||||||
- use `scripts/package.ps1` with the target runtime identifier
|
- use `scripts/package.ps1` with the target runtime identifier
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using LanMountainDesktop.Models;
|
using LanMountainDesktop.Models;
|
||||||
@@ -82,9 +82,7 @@ public sealed class AppSettingsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(snapshotToPersist, SerializerOptions);
|
var json = JsonSerializer.Serialize(snapshotToPersist, SerializerOptions);
|
||||||
var tempPath = $"{_settingsPath}.{Guid.NewGuid():N}.tmp";
|
File.WriteAllText(_settingsPath, json);
|
||||||
File.WriteAllText(tempPath, json);
|
|
||||||
File.Move(tempPath, _settingsPath, overwrite: true);
|
|
||||||
|
|
||||||
var writeTimeUtc = File.Exists(_settingsPath)
|
var writeTimeUtc = File.Exists(_settingsPath)
|
||||||
? File.GetLastWriteTimeUtc(_settingsPath)
|
? File.GetLastWriteTimeUtc(_settingsPath)
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ public sealed class ClockAirAppSettingsStore
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempPath = $"{_settingsPath}.{Guid.NewGuid():N}.tmp";
|
File.WriteAllText(_settingsPath, JsonSerializer.Serialize(normalized, SerializerOptions));
|
||||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(normalized, SerializerOptions));
|
|
||||||
File.Move(tempPath, _settingsPath, overwrite: true);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -100,9 +100,8 @@ internal sealed class FusedDesktopLayoutService : IFusedDesktopLayoutService
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempPath = $"{ConfigFilePath}.{Guid.NewGuid():N}.tmp";
|
var json = JsonSerializer.Serialize(snapshot, JsonOptions);
|
||||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(snapshot, JsonOptions));
|
File.WriteAllText(ConfigFilePath, json);
|
||||||
File.Move(tempPath, ConfigFilePath, overwrite: true);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -197,9 +197,7 @@ public sealed class LauncherSettingsService
|
|||||||
}
|
}
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(snapshot, SerializerOptions);
|
var json = JsonSerializer.Serialize(snapshot, SerializerOptions);
|
||||||
var tempPath = $"{_settingsPath}.{Guid.NewGuid():N}.tmp";
|
File.WriteAllText(_settingsPath, json);
|
||||||
File.WriteAllText(tempPath, json);
|
|
||||||
File.Move(tempPath, _settingsPath, overwrite: true);
|
|
||||||
|
|
||||||
return File.Exists(_settingsPath)
|
return File.Exists(_settingsPath)
|
||||||
? File.GetLastWriteTimeUtc(_settingsPath)
|
? File.GetLastWriteTimeUtc(_settingsPath)
|
||||||
|
|||||||
@@ -358,9 +358,7 @@ internal sealed class SettingsService : ISettingsService
|
|||||||
Directory.CreateDirectory(directory);
|
Directory.CreateDirectory(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tempPath = $"{_pluginSettingsPath}.{Guid.NewGuid():N}.tmp";
|
File.WriteAllText(_pluginSettingsPath, JsonSerializer.Serialize(document, SerializerOptions));
|
||||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(document, SerializerOptions));
|
|
||||||
File.Move(tempPath, _pluginSettingsPath, overwrite: true);
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -440,9 +440,7 @@ public sealed class ZhiJiaoHubCacheService : IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_manifestPath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(_manifestPath)!);
|
||||||
var tempPath = $"{_manifestPath}.{Guid.NewGuid():N}.tmp";
|
File.WriteAllText(_manifestPath, JsonSerializer.Serialize(manifest, JsonOptions));
|
||||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(manifest, JsonOptions));
|
|
||||||
File.Move(tempPath, _manifestPath, overwrite: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,9 +469,7 @@ public sealed class ZhiJiaoHubCacheService : IDisposable
|
|||||||
manifest.Entries[source] = new CacheEntry(images, DateTimeOffset.UtcNow);
|
manifest.Entries[source] = new CacheEntry(images, DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_manifestPath)!);
|
Directory.CreateDirectory(Path.GetDirectoryName(_manifestPath)!);
|
||||||
var tempPath = $"{_manifestPath}.{Guid.NewGuid():N}.tmp";
|
File.WriteAllText(_manifestPath, JsonSerializer.Serialize(manifest, JsonOptions));
|
||||||
File.WriteAllText(tempPath, JsonSerializer.Serialize(manifest, JsonOptions));
|
|
||||||
File.Move(tempPath, _manifestPath, overwrite: true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="16" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="16" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<components:WeatherIconView x:Name="MainIcon" Grid.Column="1" Width="56" Height="56" Margin="0,0,10,0" />
|
<components:WeatherIconView x:Name="MainIcon" Grid.Column="1" Width="56" Height="56" Margin="0,0,10,0" />
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Grid.Column="2" Text="--°" FontSize="56" FontWeight="Bold" VerticalAlignment="Center" ClipToBounds="False" Padding="0,2,0,0" />
|
<TextBlock x:Name="TemperatureTextBlock" Grid.Column="2" Text="--°" FontSize="56" FontWeight="Bold" VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<UniformGrid x:Name="MetricGrid" Grid.Row="1" Rows="1" Columns="3" />
|
<UniformGrid x:Name="MetricGrid" Grid.Row="1" Rows="1" Columns="3" />
|
||||||
<Border Grid.Row="2" Background="{DynamicResource SurfaceColor}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="10,8">
|
<Border Grid.Row="2" Background="{DynamicResource SurfaceColor}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="10,8">
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ public partial class ExtendedWeatherWidget : WeatherWidgetBase
|
|||||||
var inner = (StackPanel)panel.Child!;
|
var inner = (StackPanel)panel.Child!;
|
||||||
inner.Children.Add(new TextBlock { Text = FormatTime(item.Time), Foreground = Brush(CurrentPalette.TextSecondary), FontSize = 10, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center });
|
inner.Children.Add(new TextBlock { Text = FormatTime(item.Time), Foreground = Brush(CurrentPalette.TextSecondary), FontSize = 10, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center });
|
||||||
inner.Children.Add(new WeatherIconView { Width = 26, Height = 26, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.WeatherCode, item.WeatherText) });
|
inner.Children.Add(new WeatherIconView { Width = 26, Height = 26, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.WeatherCode, item.WeatherText) });
|
||||||
inner.Children.Add(new TextBlock { Text = FormatTemperature(item.TemperatureC), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 12, ClipToBounds = false });
|
inner.Children.Add(new TextBlock { Text = FormatTemperature(item.TemperatureC), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 12 });
|
||||||
HourlyGrid.Children.Add(panel);
|
HourlyGrid.Children.Add(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,7 +111,7 @@ public partial class ExtendedWeatherWidget : WeatherWidgetBase
|
|||||||
var inner = (StackPanel)panel.Child!;
|
var inner = (StackPanel)panel.Child!;
|
||||||
inner.Children.Add(new TextBlock { Text = ResolveDayLabel(item.Date), Foreground = Brush(CurrentPalette.TextSecondary), FontSize = 10, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center });
|
inner.Children.Add(new TextBlock { Text = ResolveDayLabel(item.Date), Foreground = Brush(CurrentPalette.TextSecondary), FontSize = 10, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center });
|
||||||
inner.Children.Add(new WeatherIconView { Width = 26, Height = 26, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.DayWeatherCode, item.DayWeatherText) });
|
inner.Children.Add(new WeatherIconView { Width = 26, Height = 26, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.DayWeatherCode, item.DayWeatherText) });
|
||||||
inner.Children.Add(new TextBlock { Text = $"{FormatTemperature(item.HighTemperatureC)} / {FormatTemperature(item.LowTemperatureC)}", Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 11, ClipToBounds = false });
|
inner.Children.Add(new TextBlock { Text = $"{FormatTemperature(item.HighTemperatureC)} / {FormatTemperature(item.LowTemperatureC)}", Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextAlignment = Avalonia.Media.TextAlignment.Center, HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 11 });
|
||||||
DailyGrid.Children.Add(panel);
|
DailyGrid.Children.Add(panel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<Border x:Name="OverlayBorder" />
|
<Border x:Name="OverlayBorder" />
|
||||||
<Grid x:Name="ContentGrid" RowDefinitions="Auto,*" Margin="18,14" RowSpacing="12">
|
<Grid x:Name="ContentGrid" RowDefinitions="Auto,*" Margin="18,14" RowSpacing="12">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center">
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto" VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="42" FontWeight="Bold" VerticalAlignment="Center" ClipToBounds="False" Padding="0,1,0,0" />
|
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="42" FontWeight="Bold" VerticalAlignment="Center" />
|
||||||
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" Margin="12,0,0,0" VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="15" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="15" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock x:Name="LocationTextBlock" Text="Weather" FontSize="12" FontWeight="Medium" Opacity="0.72" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="LocationTextBlock" Text="Weather" FontSize="12" FontWeight="Medium" Opacity="0.72" TextTrimming="CharacterEllipsis" />
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ public partial class HourlyWeatherWidget : WeatherWidgetBase
|
|||||||
var inner = (StackPanel)panel.Child!;
|
var inner = (StackPanel)panel.Child!;
|
||||||
inner.Children.Add(new TextBlock { Text = item.Label, FontSize = 10, Foreground = Brush(CurrentPalette.TextSecondary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextAlignment = Avalonia.Media.TextAlignment.Center });
|
inner.Children.Add(new TextBlock { Text = item.Label, FontSize = 10, Foreground = Brush(CurrentPalette.TextSecondary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, TextAlignment = Avalonia.Media.TextAlignment.Center });
|
||||||
inner.Children.Add(new WeatherIconView { Width = 24, Height = 24, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.WeatherCode, item.WeatherText) });
|
inner.Children.Add(new WeatherIconView { Width = 24, Height = 24, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.WeatherCode, item.WeatherText) });
|
||||||
inner.Children.Add(new TextBlock { Text = item.Value, FontWeight = Avalonia.Media.FontWeight.SemiBold, Foreground = Brush(CurrentPalette.TextPrimary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 11, TextAlignment = Avalonia.Media.TextAlignment.Center, ClipToBounds = false });
|
inner.Children.Add(new TextBlock { Text = item.Value, FontWeight = Avalonia.Media.FontWeight.SemiBold, Foreground = Brush(CurrentPalette.TextPrimary), HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Center, FontSize = 11, TextAlignment = Avalonia.Media.TextAlignment.Center });
|
||||||
return panel;
|
return panel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<Grid x:Name="ContentGrid" ColumnDefinitions="1.2*,1.6*" Margin="18,14" ColumnSpacing="14">
|
<Grid x:Name="ContentGrid" ColumnDefinitions="1.2*,1.6*" Margin="18,14" ColumnSpacing="14">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="6">
|
<StackPanel VerticalAlignment="Center" Spacing="6">
|
||||||
<components:WeatherIconView x:Name="MainIcon" Width="64" Height="64" HorizontalAlignment="Left" />
|
<components:WeatherIconView x:Name="MainIcon" Width="64" Height="64" HorizontalAlignment="Left" />
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="42" FontWeight="Bold" ClipToBounds="False" Padding="0,1,0,0" />
|
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="42" FontWeight="Bold" />
|
||||||
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="15" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="15" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||||
<TextBlock x:Name="LocationTextBlock" Text="Weather" FontSize="12" FontWeight="Medium" Opacity="0.72" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="LocationTextBlock" Text="Weather" FontSize="12" FontWeight="Medium" Opacity="0.72" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ public partial class MultiDayWeatherWidget : WeatherWidgetBase
|
|||||||
row.Children.Add(new WeatherIconView { Width = 24, Height = 24, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.DayWeatherCode, item.DayWeatherText), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center });
|
row.Children.Add(new WeatherIconView { Width = 24, Height = 24, Source = WeatherIconAssetResolver.LoadIcon(CurrentVisualStyleId, item.DayWeatherCode, item.DayWeatherText), VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center });
|
||||||
row.Children.Add(new TextBlock { Text = ResolveDayLabel(item.Date), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextTrimming = Avalonia.Media.TextTrimming.CharacterEllipsis, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 });
|
row.Children.Add(new TextBlock { Text = ResolveDayLabel(item.Date), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, TextTrimming = Avalonia.Media.TextTrimming.CharacterEllipsis, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 });
|
||||||
Grid.SetColumn(row.Children[^1], 1);
|
Grid.SetColumn(row.Children[^1], 1);
|
||||||
row.Children.Add(new TextBlock { Text = FormatTemperature(item.HighTemperatureC), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12, ClipToBounds = false });
|
row.Children.Add(new TextBlock { Text = FormatTemperature(item.HighTemperatureC), Foreground = Brush(CurrentPalette.TextPrimary), FontWeight = Avalonia.Media.FontWeight.SemiBold, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 });
|
||||||
Grid.SetColumn(row.Children[^1], 2);
|
Grid.SetColumn(row.Children[^1], 2);
|
||||||
row.Children.Add(new TextBlock { Text = FormatTemperature(item.LowTemperatureC), Foreground = Brush(CurrentPalette.TextSecondary), FontWeight = Avalonia.Media.FontWeight.Medium, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12, ClipToBounds = false });
|
row.Children.Add(new TextBlock { Text = FormatTemperature(item.LowTemperatureC), Foreground = Brush(CurrentPalette.TextSecondary), FontWeight = Avalonia.Media.FontWeight.Medium, VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center, FontSize = 12 });
|
||||||
Grid.SetColumn(row.Children[^1], 3);
|
Grid.SetColumn(row.Children[^1], 3);
|
||||||
|
|
||||||
rowPanel.Children.Add(row);
|
rowPanel.Children.Add(row);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Right" Spacing="1">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Right" Spacing="1">
|
||||||
<components:WeatherIconView x:Name="MainIcon" Width="44" Height="44" HorizontalAlignment="Right" />
|
<components:WeatherIconView x:Name="MainIcon" Width="44" Height="44" HorizontalAlignment="Right" />
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="20" FontWeight="SemiBold" HorizontalAlignment="Right" ClipToBounds="False" />
|
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="20" FontWeight="SemiBold" HorizontalAlignment="Right" />
|
||||||
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="11" FontWeight="Medium" HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" MaxWidth="100" Opacity="0.82" />
|
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="11" FontWeight="Medium" HorizontalAlignment="Right" TextTrimming="CharacterEllipsis" MaxWidth="100" Opacity="0.82" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<Grid x:Name="ContentGrid" RowDefinitions="*,Auto" Margin="20,16,20,14">
|
<Grid x:Name="ContentGrid" RowDefinitions="*,Auto" Margin="20,16,20,14">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="4">
|
<StackPanel VerticalAlignment="Center" Spacing="4">
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="72" FontWeight="Bold" ClipToBounds="False" Padding="0,2,0,0" />
|
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="72" FontWeight="Bold" />
|
||||||
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="18" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
<TextBlock x:Name="ConditionTextBlock" Text="Loading" FontSize="18" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<components:WeatherIconView x:Name="MainIcon" Grid.Column="1" Width="72" Height="72" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,0,4" />
|
<components:WeatherIconView x:Name="MainIcon" Grid.Column="1" Width="72" Height="72" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,0,4" />
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ using Avalonia.Threading;
|
|||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using FluentAvalonia.UI.Windowing;
|
using FluentAvalonia.UI.Windowing;
|
||||||
using LanMountainDesktop.Appearance;
|
|
||||||
using LanMountainDesktop.PluginSdk;
|
using LanMountainDesktop.PluginSdk;
|
||||||
using LanMountainDesktop.Services;
|
using LanMountainDesktop.Services;
|
||||||
using LanMountainDesktop.Services.Settings;
|
using LanMountainDesktop.Services.Settings;
|
||||||
using LanMountainDesktop.Settings.Core;
|
|
||||||
using LanMountainDesktop.ViewModels;
|
using LanMountainDesktop.ViewModels;
|
||||||
using Symbol = FluentIcons.Common.Symbol;
|
using Symbol = FluentIcons.Common.Symbol;
|
||||||
|
|
||||||
@@ -71,7 +69,6 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
SetValue(Window.IconProperty, _appLogoService.CreateWindowIcon());
|
SetValue(Window.IconProperty, _appLogoService.CreateWindowIcon());
|
||||||
ApplyChromeMode(useSystemChrome);
|
ApplyChromeMode(useSystemChrome);
|
||||||
ApplyFluentCornerRadius();
|
|
||||||
|
|
||||||
if (RootNavigationView is not null)
|
if (RootNavigationView is not null)
|
||||||
{
|
{
|
||||||
@@ -801,30 +798,6 @@ public partial class SettingsWindow : FAAppWindow, ISettingsPageHostContext
|
|||||||
TelemetryServices.Usage?.TrackSettingsWindowClosed("SettingsWindow.OnClosed", ViewModel.CurrentPageId);
|
TelemetryServices.Usage?.TrackSettingsWindowClosed("SettingsWindow.OnClosed", ViewModel.CurrentPageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Override global corner radius tokens on the settings window root grid
|
|
||||||
/// so all child controls use Microsoft Fluent Design System values,
|
|
||||||
/// independent of the user's global corner radius preference.
|
|
||||||
/// </summary>
|
|
||||||
private void ApplyFluentCornerRadius()
|
|
||||||
{
|
|
||||||
if (RootGrid is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var tokens = AppearanceCornerRadiusTokenFactory.Create(
|
|
||||||
GlobalAppearanceSettings.CornerRadiusStyleFluent);
|
|
||||||
RootGrid.Resources["DesignCornerRadiusMicro"] = tokens.Micro;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusXs"] = tokens.Xs;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusSm"] = tokens.Sm;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusMd"] = tokens.Md;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusLg"] = tokens.Lg;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusXl"] = tokens.Xl;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusIsland"] = tokens.Island;
|
|
||||||
RootGrid.Resources["DesignCornerRadiusComponent"] = tokens.Component;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTitleBarDragZonePointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnTitleBarDragZonePointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
{
|
{
|
||||||
_ = sender;
|
_ = sender;
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
#define MyAppSuffix ""
|
#define MyAppSuffix ""
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifndef IsSelfContained
|
||||||
|
#define IsSelfContained "true"
|
||||||
|
#endif
|
||||||
|
|
||||||
[Setup]
|
[Setup]
|
||||||
AppId={#MyAppId}
|
AppId={#MyAppId}
|
||||||
AppName={#MyAppName}
|
AppName={#MyAppName}
|
||||||
@@ -108,14 +112,6 @@ english.DotNetRuntimeMissingMessage=This application requires .NET 10.0 Desktop
|
|||||||
chinesesimplified.DotNetRuntimeMissingMessage=此应用程序需要 .NET 10.0 Desktop Runtime 才能运行。
|
chinesesimplified.DotNetRuntimeMissingMessage=此应用程序需要 .NET 10.0 Desktop Runtime 才能运行。
|
||||||
english.DotNetRuntimeMissingAction=Click "Yes" to open the official download page. Install it first, then run this installer again.
|
english.DotNetRuntimeMissingAction=Click "Yes" to open the official download page. Install it first, then run this installer again.
|
||||||
chinesesimplified.DotNetRuntimeMissingAction=单击"是"打开官方下载页面。请先完成安装,然后重新运行此安装程序。
|
chinesesimplified.DotNetRuntimeMissingAction=单击"是"打开官方下载页面。请先完成安装,然后重新运行此安装程序。
|
||||||
english.DotNetRuntimeDownloadCaption=Installing .NET 10 Desktop Runtime
|
|
||||||
chinesesimplified.DotNetRuntimeDownloadCaption=Installing .NET 10 Desktop Runtime
|
|
||||||
english.DotNetRuntimeDownloadDescription=Setup is downloading the required Microsoft .NET runtime.
|
|
||||||
chinesesimplified.DotNetRuntimeDownloadDescription=Setup is downloading the required Microsoft .NET runtime.
|
|
||||||
english.DotNetRuntimeInstallFailed=Setup could not install the required .NET 10 Desktop Runtime.
|
|
||||||
chinesesimplified.DotNetRuntimeInstallFailed=Setup could not install the required .NET 10 Desktop Runtime.
|
|
||||||
english.DotNetRuntimeStillMissing=The .NET 10 Desktop Runtime is still not detected after installation.
|
|
||||||
chinesesimplified.DotNetRuntimeStillMissing=The .NET 10 Desktop Runtime is still not detected after installation.
|
|
||||||
english.DotNetRuntimeOpenFailedMessage=Unable to open the download page automatically.
|
english.DotNetRuntimeOpenFailedMessage=Unable to open the download page automatically.
|
||||||
chinesesimplified.DotNetRuntimeOpenFailedMessage=无法自动打开下载页面。
|
chinesesimplified.DotNetRuntimeOpenFailedMessage=无法自动打开下载页面。
|
||||||
english.DotNetRuntimeOpenFailedAction=Please open this URL manually:
|
english.DotNetRuntimeOpenFailedAction=Please open this URL manually:
|
||||||
@@ -161,8 +157,7 @@ const
|
|||||||
UninstallRegSubkey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppRegistryId}_is1';
|
UninstallRegSubkey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppRegistryId}_is1';
|
||||||
WebView2RuntimeKeyPath = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
|
WebView2RuntimeKeyPath = 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}';
|
||||||
WebView2RuntimeDownloadUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703';
|
WebView2RuntimeDownloadUrl = 'https://go.microsoft.com/fwlink/p/?LinkId=2124703';
|
||||||
DotNetRuntimeDownloadUrlX64 = 'https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe';
|
DotNetRuntimeDownloadUrl = 'https://dotnet.microsoft.com/download/dotnet/10.0';
|
||||||
DotNetRuntimeDownloadUrlX86 = 'https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x86.exe';
|
|
||||||
UpgradeChoiceInPlace = 0;
|
UpgradeChoiceInPlace = 0;
|
||||||
UpgradeChoiceRelocate = 1;
|
UpgradeChoiceRelocate = 1;
|
||||||
|
|
||||||
@@ -552,118 +547,78 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function GetTargetDotNetDesktopRuntimePath(): String;
|
// Returns True when the .NET 10 Desktop Runtime (or the .NET 10 Core Runtime
|
||||||
begin
|
// which is sufficient for Avalonia apps) is found on the system.
|
||||||
if '{#MyAppArch}' = 'x64' then
|
// We check both Microsoft.WindowsDesktop.App and Microsoft.NETCore.App because
|
||||||
begin
|
// the runtimeconfig.json may reference either framework depending on the
|
||||||
Result := ExpandConstant('{commonpf64}\dotnet\shared\Microsoft.WindowsDesktop.App');
|
// publish mode and the app only needs the one it actually references.
|
||||||
end;
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
Result := ExpandConstant('{commonpf}\dotnet\shared\Microsoft.WindowsDesktop.App');
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function GetPerUserDotNetDesktopRuntimePath(): String;
|
|
||||||
begin
|
|
||||||
Result := ExpandConstant('{localappdata}\dotnet\shared\Microsoft.WindowsDesktop.App');
|
|
||||||
end;
|
|
||||||
|
|
||||||
function GetDotNetRuntimeDownloadUrl(): String;
|
|
||||||
begin
|
|
||||||
if '{#MyAppArch}' = 'x64' then
|
|
||||||
begin
|
|
||||||
Result := DotNetRuntimeDownloadUrlX64;
|
|
||||||
end;
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
Result := DotNetRuntimeDownloadUrlX86;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function GetDotNetRuntimeInstallerFileName(): String;
|
|
||||||
begin
|
|
||||||
if '{#MyAppArch}' = 'x64' then
|
|
||||||
begin
|
|
||||||
Result := 'windowsdesktop-runtime-win-x64.exe';
|
|
||||||
end
|
|
||||||
else
|
|
||||||
begin
|
|
||||||
Result := 'windowsdesktop-runtime-win-x86.exe';
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function IsDotNetDesktopRuntimeInstalled(): Boolean;
|
function IsDotNetDesktopRuntimeInstalled(): Boolean;
|
||||||
begin
|
|
||||||
Result := IsDotNet10RuntimePresent(GetTargetDotNetDesktopRuntimePath()) or
|
|
||||||
IsDotNet10RuntimePresent(GetPerUserDotNetDesktopRuntimePath());
|
|
||||||
end;
|
|
||||||
|
|
||||||
function DotNetDownloadProgress(
|
|
||||||
const Url, FileName: String;
|
|
||||||
const Progress, ProgressMax: Int64): Boolean;
|
|
||||||
begin
|
|
||||||
Result := True;
|
|
||||||
end;
|
|
||||||
|
|
||||||
function EnsureDotNetDesktopRuntimeInstalled(var NeedsRestart: Boolean): String;
|
|
||||||
var
|
var
|
||||||
DownloadPage: TDownloadWizardPage;
|
BasePath: String;
|
||||||
InstallerPath: String;
|
|
||||||
ExitCode: Integer;
|
|
||||||
begin
|
begin
|
||||||
Result := '';
|
Result := False;
|
||||||
|
|
||||||
if IsDotNetDesktopRuntimeInstalled() then
|
// Check 64-bit Program Files
|
||||||
|
BasePath := ExpandConstant('{commonpf64}\dotnet\shared\Microsoft.WindowsDesktop.App');
|
||||||
|
if IsDotNet10RuntimePresent(BasePath) then
|
||||||
begin
|
begin
|
||||||
|
Result := True;
|
||||||
exit;
|
exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
DownloadPage := CreateDownloadPage(
|
BasePath := ExpandConstant('{commonpf64}\dotnet\shared\Microsoft.NETCore.App');
|
||||||
CustomMessage('DotNetRuntimeDownloadCaption'),
|
if IsDotNet10RuntimePresent(BasePath) then
|
||||||
CustomMessage('DotNetRuntimeDownloadDescription'),
|
|
||||||
@DotNetDownloadProgress);
|
|
||||||
try
|
|
||||||
DownloadPage.Add(GetDotNetRuntimeDownloadUrl(), GetDotNetRuntimeInstallerFileName(), '');
|
|
||||||
DownloadPage.Show;
|
|
||||||
try
|
|
||||||
DownloadPage.Download;
|
|
||||||
except
|
|
||||||
Result := CustomMessage('DotNetRuntimeInstallFailed') + #13#10 + GetExceptionMessage;
|
|
||||||
exit;
|
|
||||||
end;
|
|
||||||
finally
|
|
||||||
DownloadPage.Hide;
|
|
||||||
end;
|
|
||||||
|
|
||||||
InstallerPath := ExpandConstant('{tmp}\' + GetDotNetRuntimeInstallerFileName());
|
|
||||||
if not Exec(InstallerPath, '/install /quiet /norestart', '', SW_HIDE, ewWaitUntilTerminated, ExitCode) then
|
|
||||||
begin
|
begin
|
||||||
Result := CustomMessage('DotNetRuntimeInstallFailed');
|
Result := True;
|
||||||
exit;
|
exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if (ExitCode <> 0) and (ExitCode <> 3010) then
|
// Check 32-bit Program Files
|
||||||
|
BasePath := ExpandConstant('{commonpf}\dotnet\shared\Microsoft.WindowsDesktop.App');
|
||||||
|
if IsDotNet10RuntimePresent(BasePath) then
|
||||||
begin
|
begin
|
||||||
Result := CustomMessage('DotNetRuntimeInstallFailed') + ' Exit code: ' + IntToStr(ExitCode);
|
Result := True;
|
||||||
exit;
|
exit;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
if ExitCode = 3010 then
|
BasePath := ExpandConstant('{commonpf}\dotnet\shared\Microsoft.NETCore.App');
|
||||||
|
if IsDotNet10RuntimePresent(BasePath) then
|
||||||
begin
|
begin
|
||||||
NeedsRestart := True;
|
Result := True;
|
||||||
end;
|
exit;
|
||||||
|
|
||||||
if not IsDotNetDesktopRuntimeInstalled() then
|
|
||||||
begin
|
|
||||||
Result := CustomMessage('DotNetRuntimeStillMissing') + #13#10 + GetTargetDotNetDesktopRuntimePath();
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function InitializeSetup(): Boolean;
|
function InitializeSetup(): Boolean;
|
||||||
var
|
var
|
||||||
ErrorCode: Integer;
|
ErrorCode: Integer;
|
||||||
|
IsSelfContainedBuild: Boolean;
|
||||||
begin
|
begin
|
||||||
|
IsSelfContainedBuild := ('{#IsSelfContained}' = 'true');
|
||||||
|
|
||||||
|
if not IsSelfContainedBuild then
|
||||||
|
begin
|
||||||
|
if not IsDotNetDesktopRuntimeInstalled() then
|
||||||
|
begin
|
||||||
|
if MsgBox(
|
||||||
|
CustomMessage('DotNetRuntimeMissingMessage') + #13#10#13#10 +
|
||||||
|
CustomMessage('DotNetRuntimeMissingAction'),
|
||||||
|
mbConfirmation,
|
||||||
|
MB_YESNO) = IDYES then
|
||||||
|
begin
|
||||||
|
if not ShellExec('open', DotNetRuntimeDownloadUrl, '', '', SW_SHOWNORMAL, ewNoWait, ErrorCode) then
|
||||||
|
begin
|
||||||
|
MsgBox(
|
||||||
|
CustomMessage('DotNetRuntimeOpenFailedMessage') + #13#10 +
|
||||||
|
CustomMessage('DotNetRuntimeOpenFailedAction') + #13#10 + DotNetRuntimeDownloadUrl,
|
||||||
|
mbError,
|
||||||
|
MB_OK);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
Result := False;
|
||||||
|
exit;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
if IsWebView2RuntimeInstalled() then
|
if IsWebView2RuntimeInstalled() then
|
||||||
begin
|
begin
|
||||||
@@ -690,11 +645,6 @@ begin
|
|||||||
Result := False;
|
Result := False;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
|
||||||
begin
|
|
||||||
Result := EnsureDotNetDesktopRuntimeInstalled(NeedsRestart);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure InitializeWizard;
|
procedure InitializeWizard;
|
||||||
var
|
var
|
||||||
DetailsText: String;
|
DetailsText: String;
|
||||||
|
|||||||
@@ -164,12 +164,6 @@ function Assert-WindowsPayloadClean {
|
|||||||
|
|
||||||
$violations = [System.Collections.Generic.List[string]]::new()
|
$violations = [System.Collections.Generic.List[string]]::new()
|
||||||
$forbiddenExtensions = @(".pdb", ".so", ".dylib", ".a")
|
$forbiddenExtensions = @(".pdb", ".so", ".dylib", ".a")
|
||||||
$forbiddenBundledRuntimeFiles = @(
|
|
||||||
"coreclr.dll",
|
|
||||||
"hostfxr.dll",
|
|
||||||
"hostpolicy.dll",
|
|
||||||
"System.Private.CoreLib.dll"
|
|
||||||
)
|
|
||||||
|
|
||||||
Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue |
|
Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue |
|
||||||
Where-Object { $forbiddenExtensions -contains $_.Extension.ToLowerInvariant() } |
|
Where-Object { $forbiddenExtensions -contains $_.Extension.ToLowerInvariant() } |
|
||||||
@@ -177,12 +171,6 @@ function Assert-WindowsPayloadClean {
|
|||||||
$violations.Add((Get-RelativePathCompat -Root $Root -Path $_.FullName))
|
$violations.Add((Get-RelativePathCompat -Root $Root -Path $_.FullName))
|
||||||
}
|
}
|
||||||
|
|
||||||
Get-ChildItem -LiteralPath $Root -Recurse -File -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $forbiddenBundledRuntimeFiles -contains $_.Name } |
|
|
||||||
ForEach-Object {
|
|
||||||
$violations.Add((Get-RelativePathCompat -Root $Root -Path $_.FullName))
|
|
||||||
}
|
|
||||||
|
|
||||||
Get-ChildItem -LiteralPath $Root -Recurse -Directory -Filter "runtimes" -ErrorAction SilentlyContinue |
|
Get-ChildItem -LiteralPath $Root -Recurse -Directory -Filter "runtimes" -ErrorAction SilentlyContinue |
|
||||||
ForEach-Object {
|
ForEach-Object {
|
||||||
Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue |
|
Get-ChildItem -LiteralPath $_.FullName -Directory -ErrorAction SilentlyContinue |
|
||||||
|
|||||||
@@ -236,7 +236,6 @@ function Publish-AirAppHostPayload {
|
|||||||
"-c", $Configuration,
|
"-c", $Configuration,
|
||||||
"-r", $Rid,
|
"-r", $Rid,
|
||||||
"--self-contained", "false",
|
"--self-contained", "false",
|
||||||
"-p:SelfContained=false",
|
|
||||||
"-p:PublishSingleFile=false",
|
"-p:PublishSingleFile=false",
|
||||||
"-p:PublishTrimmed=false",
|
"-p:PublishTrimmed=false",
|
||||||
"-p:PublishReadyToRun=false",
|
"-p:PublishReadyToRun=false",
|
||||||
@@ -254,70 +253,6 @@ function Publish-AirAppHostPayload {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Publish-LauncherPayload {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$PublishedDirectory,
|
|
||||||
[Parameter(Mandatory = $true)][string]$Rid,
|
|
||||||
[Parameter(Mandatory = $true)][string]$VersionValue
|
|
||||||
)
|
|
||||||
|
|
||||||
$launcherProject = Join-Path $repoRoot "..\LanMountainDesktop.Launcher\LanMountainDesktop.Launcher.csproj"
|
|
||||||
$launcherProject = Resolve-ExistingPath -PathValue $launcherProject
|
|
||||||
Write-Host "Publishing Launcher AOT payload..."
|
|
||||||
$launcherPublishArgs = @(
|
|
||||||
"publish",
|
|
||||||
$launcherProject,
|
|
||||||
"-c", $Configuration,
|
|
||||||
"-r", $Rid,
|
|
||||||
"--self-contained",
|
|
||||||
"-p:PublishAot=true",
|
|
||||||
"-p:PublishSingleFile=true",
|
|
||||||
"-p:IncludeNativeLibrariesForSelfExtract=true",
|
|
||||||
"-p:EnableCompressionInSingleFile=true",
|
|
||||||
"-p:DebugType=None",
|
|
||||||
"-p:DebugSymbols=false",
|
|
||||||
"-p:Version=$VersionValue",
|
|
||||||
"-o", $PublishedDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
& dotnet @launcherPublishArgs
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "Launcher publish failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Publish-MainAppFrameworkDependentPayload {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$ProjectFile,
|
|
||||||
[Parameter(Mandatory = $true)][string]$PublishedDirectory,
|
|
||||||
[Parameter(Mandatory = $true)][string]$Rid,
|
|
||||||
[Parameter(Mandatory = $true)][string]$VersionValue
|
|
||||||
)
|
|
||||||
|
|
||||||
Write-Host "Publishing framework-dependent main app payload..."
|
|
||||||
$publishArgs = @(
|
|
||||||
"publish",
|
|
||||||
$ProjectFile,
|
|
||||||
"-c", $Configuration,
|
|
||||||
"-r", $Rid,
|
|
||||||
"--self-contained", "false",
|
|
||||||
"-p:SelfContained=false",
|
|
||||||
"-p:PublishSingleFile=false",
|
|
||||||
"-p:PublishTrimmed=false",
|
|
||||||
"-p:PublishReadyToRun=false",
|
|
||||||
"-p:DebugType=None",
|
|
||||||
"-p:DebugSymbols=false",
|
|
||||||
"-p:SkipAirAppHostBuild=true",
|
|
||||||
"-p:Version=$VersionValue",
|
|
||||||
"-o", $PublishedDirectory
|
|
||||||
)
|
|
||||||
|
|
||||||
& dotnet @publishArgs
|
|
||||||
if ($LASTEXITCODE -ne 0) {
|
|
||||||
throw "dotnet publish failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
$scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
$repoRoot = Resolve-ExistingPath -PathValue (Join-Path $scriptRoot "..")
|
$repoRoot = Resolve-ExistingPath -PathValue (Join-Path $scriptRoot "..")
|
||||||
|
|
||||||
@@ -339,46 +274,33 @@ if (-not [System.IO.Path]::IsPathRooted($PublishDir)) {
|
|||||||
}
|
}
|
||||||
Clear-DirectoryContents -TargetDirectory $PublishDir
|
Clear-DirectoryContents -TargetDirectory $PublishDir
|
||||||
|
|
||||||
if (Is-WindowsRuntimeIdentifier -Rid $RuntimeIdentifier) {
|
Write-Host "Publishing project..."
|
||||||
$appPublishDir = Join-Path $PublishDir "app-$Version"
|
$publishArgs = @(
|
||||||
[System.IO.Directory]::CreateDirectory($appPublishDir) | Out-Null
|
"publish",
|
||||||
|
$projectPath,
|
||||||
|
"-c", $Configuration,
|
||||||
|
"-r", $RuntimeIdentifier,
|
||||||
|
"--self-contained", "true",
|
||||||
|
"-p:PublishSingleFile=false",
|
||||||
|
"-p:PublishTrimmed=false",
|
||||||
|
"-p:DebugType=None",
|
||||||
|
"-p:DebugSymbols=false",
|
||||||
|
"-p:SkipAirAppHostBuild=true",
|
||||||
|
"-p:Version=$Version",
|
||||||
|
"-o", $PublishDir
|
||||||
|
)
|
||||||
|
|
||||||
Publish-LauncherPayload -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier -VersionValue $Version
|
& dotnet @publishArgs
|
||||||
Publish-MainAppFrameworkDependentPayload -ProjectFile $projectPath -PublishedDirectory $appPublishDir -Rid $RuntimeIdentifier -VersionValue $Version
|
if ($LASTEXITCODE -ne 0) {
|
||||||
Publish-AirAppHostPayload -PublishedDirectory $appPublishDir -Rid $RuntimeIdentifier -VersionValue $Version
|
throw "dotnet publish failed with exit code $LASTEXITCODE."
|
||||||
New-Item -ItemType File -Path (Join-Path $appPublishDir ".current") -Force | Out-Null
|
}
|
||||||
|
|
||||||
Remove-LibVlcForOtherArch -PublishedDirectory $appPublishDir -Rid $RuntimeIdentifier
|
Publish-AirAppHostPayload -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier -VersionValue $Version
|
||||||
Remove-LegacyOutputArtifacts -TargetDirectory $appPublishDir
|
Remove-LibVlcForOtherArch -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier
|
||||||
} else {
|
Remove-LegacyOutputArtifacts -TargetDirectory $PublishDir
|
||||||
Write-Host "Publishing project..."
|
|
||||||
$publishArgs = @(
|
|
||||||
"publish",
|
|
||||||
$projectPath,
|
|
||||||
"-c", $Configuration,
|
|
||||||
"-r", $RuntimeIdentifier,
|
|
||||||
"--self-contained", "true",
|
|
||||||
"-p:PublishSingleFile=false",
|
|
||||||
"-p:PublishTrimmed=false",
|
|
||||||
"-p:DebugType=None",
|
|
||||||
"-p:DebugSymbols=false",
|
|
||||||
"-p:SkipAirAppHostBuild=true",
|
|
||||||
"-p:Version=$Version",
|
|
||||||
"-o", $PublishDir
|
|
||||||
)
|
|
||||||
|
|
||||||
& dotnet @publishArgs
|
if ($RuntimeIdentifier -like "linux-*") {
|
||||||
if ($LASTEXITCODE -ne 0) {
|
Add-LinuxDesktopAssets -PublishedDirectory $PublishDir -RepoRoot $repoRoot
|
||||||
throw "dotnet publish failed with exit code $LASTEXITCODE."
|
|
||||||
}
|
|
||||||
|
|
||||||
Publish-AirAppHostPayload -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier -VersionValue $Version
|
|
||||||
Remove-LibVlcForOtherArch -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier
|
|
||||||
Remove-LegacyOutputArtifacts -TargetDirectory $PublishDir
|
|
||||||
|
|
||||||
if ($RuntimeIdentifier -like "linux-*") {
|
|
||||||
Add-LinuxDesktopAssets -PublishedDirectory $PublishDir -RepoRoot $repoRoot
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Invoke-PublishPayloadOptimization -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier
|
Invoke-PublishPayloadOptimization -PublishedDirectory $PublishDir -Rid $RuntimeIdentifier
|
||||||
|
|||||||
@@ -1,253 +0,0 @@
|
|||||||
# LanMountainDesktop 安全审计报告
|
|
||||||
|
|
||||||
**项目**: LanMountainDesktop
|
|
||||||
**审计日期**: 2026-05-24
|
|
||||||
**审计范围**: 代码库安全性系统性评估
|
|
||||||
**审计方法**: 静态代码分析 + 架构审查 + 攻击面映射
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 执行摘要
|
|
||||||
|
|
||||||
本次审计对 LanMountainDesktop 代码库进行了全面的安全评估,系统性地检查了认证与访问控制、注入向量、外部交互以及敏感数据处理等高风险攻击面。
|
|
||||||
|
|
||||||
**审计结论**: 发现 **5 个已确认的中等及以上严重度漏洞**,均具有可论证的利用路径。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 已确认漏洞
|
|
||||||
|
|
||||||
### 漏洞 #1 - PostHog API Key 硬编码(高严重度)
|
|
||||||
|
|
||||||
| 属性 | 详情 |
|
|
||||||
|------|------|
|
|
||||||
| **严重度** | 高 |
|
|
||||||
| **CWE** | CWE-798 - 使用硬编码凭证 |
|
|
||||||
| **位置** | [PostHogUsageTelemetryService.cs:14](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Services/PostHogUsageTelemetryService.cs#L14) |
|
|
||||||
| **攻击者画像** | 源代码仓库的任何访问者(通过代码泄露、供应链攻击或Git历史) |
|
|
||||||
| **可控输入** | 无(静态硬编码密钥) |
|
|
||||||
|
|
||||||
**代码路径**:
|
|
||||||
```csharp
|
|
||||||
// PostHogUsageTelemetryService.cs:14
|
|
||||||
private const string PostHogApiKey = "phc_bhQZvKDDfsEdLT6kkRFvrWMT8Pc5aCGGsnxoc5ijSf9";
|
|
||||||
```
|
|
||||||
|
|
||||||
**影响**:
|
|
||||||
- 攻击者可滥用此 API Key 向 PostHog 项目发送伪造遥测数据
|
|
||||||
- 可能导致遥测数据污染,干扰产品分析决策
|
|
||||||
- API Key 暴露在公开仓库中,任何人都能获取并滥用
|
|
||||||
|
|
||||||
**修复建议**:
|
|
||||||
```csharp
|
|
||||||
private static string GetPostHogApiKey()
|
|
||||||
{
|
|
||||||
var key = Environment.GetEnvironmentVariable("POSTHOG_API_KEY");
|
|
||||||
if (string.IsNullOrEmpty(key))
|
|
||||||
throw new InvalidOperationException("PostHog API key not configured.");
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 漏洞 #2 - Sentry DSN 硬编码(高严重度)
|
|
||||||
|
|
||||||
| 属性 | 详情 |
|
|
||||||
|------|------|
|
|
||||||
| **严重度** | 高 |
|
|
||||||
| **CWE** | CWE-798 - 使用硬编码凭证 |
|
|
||||||
| **位置** | [SentryCrashTelemetryService.cs:15](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Services/SentryCrashTelemetryService.cs#L15) |
|
|
||||||
| **攻击者画像** | 源代码仓库的任何访问者 |
|
|
||||||
| **可控输入** | 无(静态硬编码密钥) |
|
|
||||||
|
|
||||||
**代码路径**:
|
|
||||||
```csharp
|
|
||||||
// SentryCrashTelemetryService.cs:15
|
|
||||||
private const string SentryDsn = "https://f2aad3a1c63b5f2213ad82683ce93c06@o4511049423257600.ingest.us.sentry.io/4511049425813504";
|
|
||||||
```
|
|
||||||
|
|
||||||
**影响**:
|
|
||||||
- Sentry DSN 等同于项目的访问凭证
|
|
||||||
- 攻击者可利用此 DSN 向项目发送伪造崩溃报告
|
|
||||||
- 可能导致崩溃数据污染,干扰错误追踪
|
|
||||||
- 如 DSN 配置不当,可导致敏感崩溃信息被发送至攻击者控制的端点
|
|
||||||
|
|
||||||
**修复建议**:
|
|
||||||
```csharp
|
|
||||||
private static string GetSentryDsn()
|
|
||||||
{
|
|
||||||
var dsn = Environment.GetEnvironmentVariable("SENTRY_DSN");
|
|
||||||
if (string.IsNullOrEmpty(dsn))
|
|
||||||
throw new InvalidOperationException("Sentry DSN not configured.");
|
|
||||||
return dsn;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 漏洞 #3 - 小米天气 API 签名密钥硬编码(高严重度)
|
|
||||||
|
|
||||||
| 属性 | 详情 |
|
|
||||||
|------|------|
|
|
||||||
| **严重度** | 高 |
|
|
||||||
| **CWE** | CWE-798 - 使用硬编码凭证 |
|
|
||||||
| **位置** | [XiaomiWeatherService.cs:25](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Services/XiaomiWeatherService.cs#L25) |
|
|
||||||
| **攻击者画像** | 源代码仓库的任何访问者 |
|
|
||||||
| **可控输入** | 无(静态硬编码密钥) |
|
|
||||||
|
|
||||||
**代码路径**:
|
|
||||||
```csharp
|
|
||||||
// XiaomiWeatherService.cs:25
|
|
||||||
public string Sign { get; init; } = "zUFJoAR2ZVrDy1vF3D07";
|
|
||||||
```
|
|
||||||
|
|
||||||
**影响**:
|
|
||||||
- API 签名凭证暴露在公开仓库
|
|
||||||
- 攻击者可能利用此凭证访问天气服务 API
|
|
||||||
- 可能导致 API 配额滥用或服务成本增加
|
|
||||||
- 如密钥具有更高权限,可能导致数据泄露
|
|
||||||
|
|
||||||
**修复建议**:
|
|
||||||
```csharp
|
|
||||||
public string Sign { get; init; } = Environment.GetEnvironmentVariable("XIAOMI_WEATHER_SIGN") ?? "";
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 漏洞 #4 - Sentry PII 收集配置(中等严重度)
|
|
||||||
|
|
||||||
| 属性 | 详情 |
|
|
||||||
|------|------|
|
|
||||||
| **严重度** | 中等 |
|
|
||||||
| **CWE** | CWE-359 - 个人身份信息(PII)意外暴露 |
|
|
||||||
| **位置** | [SentryCrashTelemetryService.cs:212](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Services/SentryCrashTelemetryService.cs#L212) |
|
|
||||||
| **攻击者画像** | Sentry 后端管理员、内部威胁或数据泄露事件 |
|
|
||||||
| **可控输入** | 用户环境的机器名、用户名、IP地址等系统信息 |
|
|
||||||
|
|
||||||
**代码路径**:
|
|
||||||
```csharp
|
|
||||||
// SentryCrashTelemetryService.cs:212
|
|
||||||
options.SendDefaultPii = true;
|
|
||||||
```
|
|
||||||
|
|
||||||
**影响**:
|
|
||||||
- `SendDefaultPii = true` 配置会收集和上报用户 IP 地址
|
|
||||||
- 可能违反隐私法规(如 GDPR、中国个人信息保护法)要求
|
|
||||||
- 在崩溃报告中可能暴露用户敏感信息
|
|
||||||
- 用户未明确同意即被收集 PII
|
|
||||||
|
|
||||||
**修复建议**:
|
|
||||||
```csharp
|
|
||||||
// 根据用户同意状态动态设置
|
|
||||||
options.SendDefaultPii = TelemetryEnvironmentInfo.IsTelemetryPiiAllowed();
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 漏洞 #5 - SSL 证书验证被禁用(中等严重度)
|
|
||||||
|
|
||||||
| 属性 | 详情 |
|
|
||||||
|------|------|
|
|
||||||
| **严重度** | 中等 |
|
|
||||||
| **CWE** | CWE-295 - 证书验证不正确 |
|
|
||||||
| **位置** | [RecommendationDataService.cs:105](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Services/RecommendationDataService.cs#L105) |
|
|
||||||
| **攻击者画像** | 网络中间人攻击者(在同一网络环境的攻击者) |
|
|
||||||
| **可控输入** | 用户网络流量 |
|
|
||||||
| **利用路径** | 用户发起API请求 → 攻击者拦截流量 → 伪造响应 |
|
|
||||||
|
|
||||||
**代码路径**:
|
|
||||||
```csharp
|
|
||||||
// RecommendationDataService.cs:100-106
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
|
|
||||||
System.Security.Authentication.SslProtocols.Tls13,
|
|
||||||
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
**影响**:
|
|
||||||
- 禁用了服务器证书验证,使应用程序容易受到中间人(MITM)攻击
|
|
||||||
- 攻击者可以拦截和篡改 API 响应数据
|
|
||||||
- 可能导致注入恶意内容或数据操纵
|
|
||||||
- 即使使用 TLS 1.2/1.3,证书验证被禁用仍然不安全
|
|
||||||
|
|
||||||
**修复建议**:
|
|
||||||
```csharp
|
|
||||||
var handler = new HttpClientHandler
|
|
||||||
{
|
|
||||||
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
|
|
||||||
System.Security.Authentication.SslProtocols.Tls13,
|
|
||||||
// 删除 ServerCertificateCustomValidationCallback 或实现正确的验证
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 未发现漏洞的区域
|
|
||||||
|
|
||||||
经过系统性审计,以下区域未发现中等及以上严重度的已确认漏洞:
|
|
||||||
|
|
||||||
### 认证与访问控制
|
|
||||||
- 单实例服务实现正确(使用互斥体)
|
|
||||||
- IPC 通信使用命名管道,无明显认证绕过风险
|
|
||||||
- 插件隔离使用独立进程边界
|
|
||||||
- 插件加载使用 AppDomain/AssemblyLoadContext 隔离
|
|
||||||
|
|
||||||
### 注入向量
|
|
||||||
- SQLite 使用参数化查询,无 SQL 注入风险 ([ComponentDomainStorage.cs](file:///d:/github/LanMountainDesktop/LanMountainDesktop/Services/Settings/ComponentDomainStorage.cs))
|
|
||||||
- JSON 反序列化使用强类型上下文 (`JsonSerializerContext`),无反序列化漏洞
|
|
||||||
- 文件路径操作使用 `Path.Combine` 和 `Path.GetInvalidFileNameChars()` 过滤
|
|
||||||
- 未发现命令执行注入(Process.Start 使用固定参数)
|
|
||||||
|
|
||||||
### 外部交互
|
|
||||||
- HTTP 请求使用 `HttpClient` 和超时配置
|
|
||||||
- Webhook/回调 URL 使用 `Uri.EscapeDataString` 编码
|
|
||||||
- 下载服务验证目标路径,无路径遍历风险
|
|
||||||
- URL 参数正确使用编码函数
|
|
||||||
|
|
||||||
### 敏感数据处理
|
|
||||||
- 数据库本地存储,使用 WAL 模式
|
|
||||||
- 设置数据通过 JSON 序列化存储在用户目录
|
|
||||||
- 日志文件路径正确隔离在应用数据目录
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 架构安全评估
|
|
||||||
|
|
||||||
| 组件 | 安全评级 | 说明 |
|
|
||||||
|------|----------|------|
|
|
||||||
| 插件系统 | 良好 | 使用独立进程隔离 |
|
|
||||||
| IPC 通信 | 良好 | 命名管道通信,进程边界隔离 |
|
|
||||||
| 更新系统 | 良好 | 支持签名验证 |
|
|
||||||
| 遥测系统 | **需改进** | 存在硬编码凭证和 PII 配置问题 |
|
|
||||||
| 数据存储 | 良好 | 使用标准加密实践 |
|
|
||||||
| 网络通信 | **需改进** | 存在证书验证绕过问题 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 修复优先级
|
|
||||||
|
|
||||||
| 优先级 | 漏洞 | 严重度 | 预计工作量 |
|
|
||||||
|--------|------|--------|------------|
|
|
||||||
| P0 - 紧急 | #1 PostHog API Key | 高 | 低 |
|
|
||||||
| P0 - 紧急 | #2 Sentry DSN | 高 | 低 |
|
|
||||||
| P0 - 紧急 | #3 Xiaomi Weather Sign | 高 | 低 |
|
|
||||||
| P1 - 高 | #4 SendDefaultPii | 中 | 低 |
|
|
||||||
| P1 - 高 | #5 SSL 证书验证禁用 | 中 | 中 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 建议的安全改进
|
|
||||||
|
|
||||||
1. **实施密钥管理**: 使用环境变量或密钥管理服务存储所有 API 凭证
|
|
||||||
2. **添加密钥扫描**: 在 CI/CD 流程中集成 secrets scanning(如 GitGuardian、trufflehog)
|
|
||||||
3. **隐私合规审查**: 确认遥测数据收集符合当地隐私法规要求
|
|
||||||
4. **证书验证修复**: 移除禁用的证书验证,确保 HTTPS 通信安全
|
|
||||||
5. **代码审计**: 建议进行定期安全审计
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*报告生成工具: 自动安全审计系统*
|
|
||||||
*审计方法: 静态代码分析 + 架构审查 + 攻击面映射*
|
|
||||||
@@ -4,13 +4,11 @@
|
|||||||
|
|
||||||
为了确保桌面组件在不同尺寸、缩放比例下都能保持视觉一致性和美感,阑山桌面采用了 **固定圆角风格预设 (Fixed Corner Radius Styles)**,全面参考小米澎湃OS (Xiaomi HyperOS) 的设计语言。
|
为了确保桌面组件在不同尺寸、缩放比例下都能保持视觉一致性和美感,阑山桌面采用了 **固定圆角风格预设 (Fixed Corner Radius Styles)**,全面参考小米澎湃OS (Xiaomi HyperOS) 的设计语言。
|
||||||
|
|
||||||
此外,阑山桌面引入了 **Fluent** 预设,遵循 Microsoft Fluent Design System 规范。设置窗口始终使用 Fluent 圆角,独立于用户选择的全局圆角风格。
|
|
||||||
|
|
||||||
所有的组件和容器必须使用统一的资源键,禁止在 XAML 或代码中使用硬编码的像素值。
|
所有的组件和容器必须使用统一的资源键,禁止在 XAML 或代码中使用硬编码的像素值。
|
||||||
|
|
||||||
## 预设风格 (Preset Styles)
|
## 预设风格 (Preset Styles)
|
||||||
|
|
||||||
用户可以在设置中选择以下五种风格之一。系统会自动根据选中的风格动态映射全局圆角 Token。
|
用户可以在设置中选择以下四种风格之一。系统会自动根据选中的风格动态映射全局圆角 Token。
|
||||||
|
|
||||||
| 风格 (ID) | 名称 (Local) | 组件圆角 (Component) | 设计语义 |
|
| 风格 (ID) | 名称 (Local) | 组件圆角 (Component) | 设计语义 |
|
||||||
| :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- |
|
||||||
@@ -18,33 +16,21 @@
|
|||||||
| **Balanced** | 平衡 | 24px | **默认值**。和谐、自然、普适 |
|
| **Balanced** | 平衡 | 24px | **默认值**。和谐、自然、普适 |
|
||||||
| **Rounded** | 圆润 | 28px | 保守、柔和、亲切 |
|
| **Rounded** | 圆润 | 28px | 保守、柔和、亲切 |
|
||||||
| **Open** | 开放 | 32px | 现代、沉浸、夸张 |
|
| **Open** | 开放 | 32px | 现代、沉浸、夸张 |
|
||||||
| **Fluent** | Fluent | 8px | Microsoft Fluent Design System。标准、规范、一致 |
|
|
||||||
|
|
||||||
## Token 阶梯映射 (Token Step Mapping)
|
## Token 阶梯映射 (Token Step Mapping)
|
||||||
|
|
||||||
每个风格都定义了一套完整的圆角阶梯,以确保在大容器包裹小元素时满足 **圆角嵌套一致性 (Nesting Consistency)**。
|
每个风格都定义了一套完整的圆角阶梯,以确保在大容器包裹小元素时满足 **圆角嵌套一致性 (Nesting Consistency)**。
|
||||||
|
|
||||||
| Token | Sharp | Balanced | Rounded | Open | Fluent | 典型场景 |
|
| Token | Sharp | Balanced | Rounded | Open | 典型场景 |
|
||||||
| :--- | :--- | :--- | :--- | :--- | :--- | :--- |
|
| :--- | :--- | :--- | :--- | :--- | :--- |
|
||||||
| **Micro** | 4px | 6px | 8px | 10px | 2px | 小图标容器、角标 (Badge) |
|
| **Micro** | 4px | 6px | 8px | 10px | 小图标容器、角标 (Badge) |
|
||||||
| **Xs** | 8px | 12px | 14px | 16px | 4px | 小标签 (Tag)、输入框 |
|
| **Xs** | 8px | 12px | 14px | 16px | 小标签 (Tag)、输入框 |
|
||||||
| **Sm** | 10px | 14px | 16px | 20px | 4px | 普通按钮、搜索栏、复选框 |
|
| **Sm** | 10px | 14px | 16px | 20px | 普通按钮、搜索栏、复选框 |
|
||||||
| **Md** | 14px | 20px | 24px | 28px | 8px | 悬浮菜单、小提示框、子卡片 |
|
| **Md** | 14px | 20px | 24px | 28px | 悬浮菜单、小提示框、子卡片 |
|
||||||
| **Lg** | 20px | 28px | 32px | 36px | 8px | 普通面板、对话框内容区 |
|
| **Lg** | 20px | 28px | 32px | 36px | 普通面板、对话框内容区 |
|
||||||
| **Xl** | 24px | 32px | 36px | 40px | 12px | 大尺寸容器、设置中心页面 |
|
| **Xl** | 24px | 32px | 36px | 40px | 大尺寸容器、设置中心页面 |
|
||||||
| **Island** | 28px | 36px | 40px | 44px | 16px | 任务栏、全局大悬浮容器 |
|
| **Island** | 28px | 36px | 40px | 44px | 任务栏、全局大悬浮容器 |
|
||||||
| **Component** | **20px** | **24px** | **28px** | **32px** | **8px** | **所有桌面组件 (Widget) 的主边框** |
|
| **Component** | **20px** | **24px** | **28px** | **32px** | **所有桌面组件 (Widget) 的主边框** |
|
||||||
|
|
||||||
## Fluent Design System 参考 (Fluent Reference)
|
|
||||||
|
|
||||||
Fluent 预设的核心值来源于 Microsoft 官方规范:
|
|
||||||
|
|
||||||
- **ControlCornerRadius = 4px**:用于标准持久 UI 元素(按钮、复选框、输入框等)
|
|
||||||
- **OverlayCornerRadius = 8px**:用于临时覆盖 UI 元素(对话框、浮出菜单等)
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
|
||||||
> **设置窗口强制约束**:
|
|
||||||
> 设置窗口 (`SettingsWindow`) 始终使用 Fluent 圆角 Token,不受用户全局圆角设置影响。这确保设置 UI 作为标准 Windows 应用窗口与 Fluent Design 一致。
|
|
||||||
|
|
||||||
## 开发准则 (Implementation Rules)
|
## 开发准则 (Implementation Rules)
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# Runtime Packaging
|
|
||||||
|
|
||||||
## Windows
|
|
||||||
|
|
||||||
- Windows installers do not bundle the .NET shared runtime.
|
|
||||||
- `LanMountainDesktop.Launcher.exe` is the package-root bootstrapper and remains Native AOT/self-contained.
|
|
||||||
- `LanMountainDesktop.exe` and `LanMountainDesktop.AirAppHost.exe` are framework-dependent, RID-specific apps under `app-<version>/`.
|
|
||||||
- Inno Setup downloads and silently installs the matching .NET 10 Desktop Runtime before continuing:
|
|
||||||
- x64 installer: `https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe`
|
|
||||||
- x86 installer: `https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x86.exe`
|
|
||||||
- Launcher runtime probing validates the architecture-matched `Microsoft.NETCore.App 10.*` shared framework before starting framework-dependent processes.
|
|
||||||
|
|
||||||
If the launcher returns `dotnet_runtime_missing`, verify the runtime architecture:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
dotnet --list-runtimes
|
|
||||||
Test-Path "C:\Program Files\dotnet\shared\Microsoft.NETCore.App"
|
|
||||||
Test-Path "C:\Program Files (x86)\dotnet\shared\Microsoft.NETCore.App"
|
|
||||||
```
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
# Git 提交分析报告
|
|
||||||
|
|
||||||
## 📋 提交基本信息
|
|
||||||
|
|
||||||
| 属性 | 值 |
|
|
||||||
|------|-----|
|
|
||||||
| **完整哈希** | `7a70476ce8093ea6000f25fba7ba404d4f3e8f3c` |
|
|
||||||
| **短哈希** | `7a70476` |
|
|
||||||
| **作者** | lincube <lincube3@hotmail.com> |
|
|
||||||
| **提交日期** | 2026-05-19 |
|
|
||||||
| **提交时间** | 07:55:21 |
|
|
||||||
| **时区** | +0800 |
|
|
||||||
| **提交类型** | 🟡 合并提交 (Merge Commit) |
|
|
||||||
| **关联 PR** | #11 |
|
|
||||||
|
|
||||||
## 📝 提交信息摘要
|
|
||||||
|
|
||||||
```
|
|
||||||
合并对设置系统的更新 (#11)
|
|
||||||
```
|
|
||||||
|
|
||||||
**详细提交说明**:
|
|
||||||
|
|
||||||
本次合并包含了对设置系统的全面更新和改进,主要涉及以下子提交:
|
|
||||||
|
|
||||||
1. **Add Windows system chrome patchers (Harmony)** - 使用 Harmony 修补器添加 Windows 系统 chrome 切换支持
|
|
||||||
2. **Refactor settings window UI and theming** - 重构设置窗口 UI 和主题
|
|
||||||
3. **Add localization and localize settings pages** - 添加本地化和多语言支持
|
|
||||||
4. **Redesign settings window with fluent shell & search** - 使用 Fluent Shell 重新设计设置窗口并添加搜索功能
|
|
||||||
5. **Add OOBE startup presentation and settings merge** - 添加 OOBE 启动演示和设置合并功能
|
|
||||||
6. **Move whiteboard persistence to file storage** - 将白板持久化迁移到文件存储
|
|
||||||
7. **Introduce render gate and chart caching** - 引入渲染门控和图表缓存机制
|
|
||||||
8. **Use MaterialColorSnapshot in appearance flow** - 在外观流程中使用 MaterialColorSnapshot
|
|
||||||
9. **Add material color services, plugin DTOs, and tests** - 添加材质颜色服务、插件 DTO 和测试
|
|
||||||
10. **Add CODE_WIKI and update localization** - 添加 CODE_WIKI 文档和更新本地化
|
|
||||||
11. **Add Data settings page and storage scanner** - 添加数据设置页面和存储扫描器
|
|
||||||
12. **Add IPC backoff/retries and safer disposal** - 添加 IPC 退避/重试和更安全的资源释放
|
|
||||||
13. **Add preview controls and settings UI tweaks** - 添加预览控件和设置 UI 调整
|
|
||||||
14. **Add install checkpoint/resume and DDSS workflows** - 添加安装检查点/恢复和 DDSS 工作流
|
|
||||||
|
|
||||||
## 📊 变更统计
|
|
||||||
|
|
||||||
| 统计项 | 数值 |
|
|
||||||
|--------|------|
|
|
||||||
| **变更文件总数** | 904 个文件 |
|
|
||||||
| **新增代码行数** | +78,048 行 |
|
|
||||||
| **删除代码行数** | -18,362 行 |
|
|
||||||
| **净增代码行数** | +59,686 行 |
|
|
||||||
|
|
||||||
## 📂 详细变更分析(按文件类型分组)
|
|
||||||
|
|
||||||
### 1. 核心代码文件 (.cs)
|
|
||||||
|
|
||||||
#### LanMountainDesktop 核心项目
|
|
||||||
| 文件路径 | 类型 | 变更说明 |
|
|
||||||
|----------|------|----------|
|
|
||||||
| `LanMountainDesktop/Program.cs` | 修改 | 添加 Windows chrome 修补器加载逻辑 |
|
|
||||||
| `LanMountainDesktop/ViewModels/SettingsViewModels.cs` | 修改 | 重构设置视图模型,添加新属性和本地化支持 |
|
|
||||||
| `LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs` | 修改 | 添加材质颜色和壁纸设置 |
|
|
||||||
| `LanMountainDesktop/ViewModels/UpdateSettingsPageViewModel.cs` | 修改 | 更新设置页面视图模型 |
|
|
||||||
| `LanMountainDesktop/Views/SettingsWindow.axaml` | 修改 | Fluent Shell 设置窗口重构,添加自定义标题栏 |
|
|
||||||
| `LanMountainDesktop/Views/SettingsWindow.axaml.cs` | 修改 | 设置窗口代码重构,添加搜索功能 |
|
|
||||||
| `LanMountainDesktop/Views/TransparentOverlayWindow.axaml` | 修改 | 透明覆盖窗口大幅重构 |
|
|
||||||
| `LanMountainDesktop/Services/LocalizationService.cs` | 修改 | 本地化服务更新 |
|
|
||||||
| `LanMountainDesktop/Services/SettingsSearchService.cs` | 新增 | 设置搜索服务(搜索索引、导航、结果高亮) |
|
|
||||||
| `LanMountainDesktop/Services/MaterialSurfaceService.cs` | 修改 | 添加特殊材质参数和窗口材质处理 |
|
|
||||||
| `LanMountainDesktop/Services/GlassEffectService.cs` | 修改 | 添加自适应设置窗口调色笔刷 |
|
|
||||||
| `LanMountainDesktop/Services/SettingsWindowService.cs` | 修改 | 重构主题应用逻辑 |
|
|
||||||
| `LanMountainDesktop/Services/AppearanceThemeService.cs` | 修改 | 依赖 MaterialColorService,更新外观主题处理 |
|
|
||||||
| `LanMountainDesktop/Services/WindowMaterialService.cs` | 修改 | 窗口材质服务和自动材质模式支持 |
|
|
||||||
| `LanMountainDesktop/Services/DataStorageService.cs` | 新增 | 数据存储服务(扫描、磁盘信息、清理操作) |
|
|
||||||
| `LanMountainDesktop/Services/WallpaperColorPipeline.cs` | 新增 | 壁纸颜色管道服务 |
|
|
||||||
| `LanMountainDesktop/Services/Launch/LauncherWindowsStartupService.cs` | 新增 | 启动器 Windows 启动服务 |
|
|
||||||
| `LanMountainDesktop/Services/HostAppSettingsOobeMerger.cs` | 新增 | Host 应用设置 OOBE 合并服务 |
|
|
||||||
| `LanMountainDesktop/Services/UpdateEngineService.cs` | 修改 | 添加检查点加载/保存/恢复逻辑 |
|
|
||||||
| `LanMountainDesktop/Services/IPC/...` | 多文件 | 添加重试逻辑、退避策略、更安全的资源释放 |
|
|
||||||
|
|
||||||
#### 视图模型 (ViewModels)
|
|
||||||
| 文件路径 | 变更说明 |
|
|
||||||
|----------|----------|
|
|
||||||
| `NotificationSettingsPageViewModel.cs` | 添加本地化支持 |
|
|
||||||
| `DevSettingsPageViewModel.cs` | 添加本地化支持 |
|
|
||||||
| `AboutSettingsPageViewModel.cs` | 添加本地化支持 |
|
|
||||||
| `StatusBarSettingsPageViewModel.cs` | 添加本地化支持 |
|
|
||||||
| `MaterialColorSettingsPageViewModel.cs` | 新增材质颜色设置视图模型 |
|
|
||||||
| `DataSettingsPageViewModel.cs` | 新增数据设置视图模型 |
|
|
||||||
| `GeneralSettingsPageViewModel.cs` | 更新通用设置视图模型 |
|
|
||||||
| `AppearanceSettingsPageViewModel.cs` | 更新外观设置视图模型 |
|
|
||||||
| `ComponentsSettingsPageViewModel.cs` | 添加预览控件和实时预览支持 |
|
|
||||||
|
|
||||||
#### 设置页面视图 (Views)
|
|
||||||
| 文件路径 | 变更说明 |
|
|
||||||
|----------|----------|
|
|
||||||
| `NotificationSettingsPage.axaml` | 更新通知设置页面 |
|
|
||||||
| `UpdateSettingsPage.axaml` | 大幅重构更新设置页面(530 行变更) |
|
|
||||||
| `WeatherSettingsPage.axaml` | 更新天气设置页面 |
|
|
||||||
| `GeneralSettingsPage.axaml` | 更新通用设置页面 |
|
|
||||||
| `LauncherSettingsPage.axaml` | 更新启动器设置页面 |
|
|
||||||
| `MaterialColorSettingsPage.axaml` | 新增材质颜色设置页面 |
|
|
||||||
| `MaterialColorSettingsPage.axaml.cs` | 新增材质颜色设置代码 |
|
|
||||||
| `DataSettingsPage.axaml` | 新增数据设置页面 |
|
|
||||||
| `DataSettingsPage.axaml.cs` | 新增数据设置代码 |
|
|
||||||
| `StatusBarSettingsPage.axaml` | 更新状态栏设置页面 |
|
|
||||||
| `WallpaperSettingsPage.axaml` | 更新壁纸设置页面 |
|
|
||||||
|
|
||||||
### 2. 第三方库集成
|
|
||||||
|
|
||||||
#### DotNetCampus.InkCanvas 墨迹画布库(重大新增)
|
|
||||||
|
|
||||||
本次合并添加了完整的 **DotNetCampus.InkCanvas** 库,这是一个功能完整的墨迹/手写画布解决方案:
|
|
||||||
|
|
||||||
| 子项目 | 文件数 | 主要功能 |
|
|
||||||
|--------|--------|----------|
|
|
||||||
| **DotNetCampus.AvaloniaInkCanvas** | 多个 | Avalonia 平台的墨迹画布实现 |
|
|
||||||
| **DotNetCampus.InkCanvas.InkCore** | 30+ | 核心墨迹处理算法和接口 |
|
|
||||||
| **DotNetCampus.InkCanvas.SkiaInk** | 多个 | Skia 渲染引擎的墨迹实现 |
|
|
||||||
|
|
||||||
**主要功能**:
|
|
||||||
- 墨迹绘制和渲染(Stroke rendering)
|
|
||||||
- 橡皮擦功能(Eraser modes)
|
|
||||||
- 点抽稀算法(Drop point algorithm)
|
|
||||||
- 墨迹序列化格式(Ink Serialized Format)
|
|
||||||
- 多平台支持(Skia, WPF, Avalonia)
|
|
||||||
|
|
||||||
**涉及的核心文件**:
|
|
||||||
```
|
|
||||||
ThirdParty/DotNetCampus.InkCanvas/src/
|
|
||||||
├── DotNetCampus.AvaloniaInkCanvas/
|
|
||||||
│ ├── API/InkCanvas.cs
|
|
||||||
│ ├── Caching/InkBitmapCache.cs
|
|
||||||
│ ├── Core/AvaloniaSkiaInkCanvas.cs
|
|
||||||
│ └── Erasing/PointPathEraserManager.cs
|
|
||||||
├── DotNetCampus.InkCanvas.InkCore/
|
|
||||||
│ ├── Inking/Interactives/InkingModeInputDispatcher.cs
|
|
||||||
│ ├── InkSerializedFormat/InkSerializer.cs
|
|
||||||
│ └── System/Windows/Ink/Stroke.cs
|
|
||||||
└── DotNetCampus.InkCanvas.SkiaInk/
|
|
||||||
├── Settings/SkInkCanvasSettings.cs
|
|
||||||
└── Utils/SkiaExtension.cs
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 脚本和工具文件
|
|
||||||
|
|
||||||
#### 分析脚本(新增)
|
|
||||||
| 文件 | 用途 |
|
|
||||||
|------|------|
|
|
||||||
| `parse_git_log.py` | 解析 Git HEAD 日志文件 |
|
|
||||||
| `scripts/Analyze-GitCommits.ps1` | PowerShell 提交分析脚本 |
|
|
||||||
| `scripts/GitCommitAnalyzer.cs` | C# 提交分析器 |
|
|
||||||
| `scripts/analyze_commits.ps1` | 提交分析 PowerShell 脚本 |
|
|
||||||
| `scripts/analyze_commits.py` | Python 提交分析脚本 |
|
|
||||||
| `scripts/analyze_git_commits.py` | Git 提交分析 Python 脚本 |
|
|
||||||
| `scripts/generate_commit_docs.py` | 生成提交 Markdown 文档 |
|
|
||||||
| `scripts/generate_commit_reports.py` | 生成提交报告 |
|
|
||||||
|
|
||||||
#### 构建和发布脚本
|
|
||||||
| 文件 | 变更说明 |
|
|
||||||
|------|----------|
|
|
||||||
| `LanMountainDesktop/scripts/package.ps1` | 包脚本更新 |
|
|
||||||
| `LanMountainDesktop/scripts/Optimize-PublishPayload.ps1` | 新增优化发布脚本(203 行) |
|
|
||||||
|
|
||||||
### 4. 文档文件
|
|
||||||
|
|
||||||
| 文件 | 变更 | 说明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `docs/ARCHITECTURE.md` | +34 行 | 架构文档更新 |
|
|
||||||
| `docs/LAUNCHER.md` | +2 行 | 启动器文档更新 |
|
|
||||||
| `docs/LAUNCHER_COORDINATOR.md` | +11 行 | 启动器协调器文档 |
|
|
||||||
| `docs/PLUGIN_SDK_V5_MIGRATION.md` | +14 行 | 插件 SDK v5 迁移文档 |
|
|
||||||
| `docs/VISUAL_SPEC.md` | +8 行 | 视觉规范文档 |
|
|
||||||
| `docs/ai/CODEBASE_MAP.md` | +1 行 | 代码库地图更新 |
|
|
||||||
| `docs/ai/SETTINGS_WINDOW_DESIGN.md` | +48 行 | 设置窗口设计文档(新增) |
|
|
||||||
| `docs/auto_commit_md/20260518_93758fc0.md` | +321 行 | 自动提交分析文档 |
|
|
||||||
| `SECURITY_AUDIT_REPORT.md` | +196 行 | 安全审计报告(新增) |
|
|
||||||
| `CODE_WIKI.md` | 新增 | 综合代码维基文档 |
|
|
||||||
| `design.md` | +2 行 | 设计文档更新 |
|
|
||||||
|
|
||||||
### 5. 配置文件
|
|
||||||
|
|
||||||
| 文件 | 变更说明 |
|
|
||||||
|------|----------|
|
|
||||||
| `NuGet.Config` | +7 行,新增本地 NuGet 包文件夹配置 |
|
|
||||||
| `LanMountainDesktop/HostApp.csproj` | 添加 Lib.Harmony.Thin 包引用 |
|
|
||||||
| `LanMountainDesktop/LanMountainDesktop.csproj` | 添加 PostHog 包更新到 2.6.0 |
|
|
||||||
| `LanMountainDesktop/plugins/PluginLoader.cs` | +61 行,插件加载器更新 |
|
|
||||||
| `LanMountainDesktop/plugins/PluginRuntimeService.cs` | +38 行,新增插件运行时服务 |
|
|
||||||
|
|
||||||
### 6. Mockup 和原型文件
|
|
||||||
|
|
||||||
| 文件 | 说明 |
|
|
||||||
|------|------|
|
|
||||||
| `mocks/class-schedule-mock.html` | +459 行,课程表 Mockup |
|
|
||||||
| `mocks/weather-widget-mock.html` | +209 行,天气组件 Mockup |
|
|
||||||
| `mockup-noise-level.html` | +898 行,噪音级别 Mockup |
|
|
||||||
|
|
||||||
## 🔍 代码审查要点
|
|
||||||
|
|
||||||
### ✅ 优点
|
|
||||||
|
|
||||||
1. **功能完整性**:此次合并涵盖了设置系统的多个重要方面,包括 UI 改进、本地化、搜索功能、数据管理等,是一个全面的更新。
|
|
||||||
|
|
||||||
2. **代码质量**:
|
|
||||||
- 添加了大量的单元测试
|
|
||||||
- 引入了渲染门控机制,避免不必要的重绘,提升性能
|
|
||||||
- 使用 MaterialColorSnapshot 作为统一的数据源,简化了主题管理
|
|
||||||
|
|
||||||
3. **第三方库集成**:
|
|
||||||
- 引入 DotNetCampus.InkCanvas 库,提供了完整的墨迹画布功能
|
|
||||||
- 使用 Harmony 进行系统级修补,提供了更灵活的系统集成方式
|
|
||||||
|
|
||||||
4. **安全性**:
|
|
||||||
- 添加了安全审计报告
|
|
||||||
- 改进了 IPC 通信的健壮性(退避、重试、资源释放)
|
|
||||||
|
|
||||||
5. **用户体验**:
|
|
||||||
- Fluent Shell 设计语言的应用,使设置窗口更加现代化
|
|
||||||
- 添加了设置搜索功能,提升了可访问性
|
|
||||||
- 数据存储管理功能让用户可以更好地管理应用空间
|
|
||||||
|
|
||||||
### ⚠️ 需要注意的点
|
|
||||||
|
|
||||||
1. **合并提交风险**:
|
|
||||||
- 这是一个大型合并提交(904 个文件),增加了代码审查的难度
|
|
||||||
- 建议:未来考虑拆分为更小的、功能明确的合并请求
|
|
||||||
|
|
||||||
2. **二进制文件**:
|
|
||||||
- `diff.txt` 是二进制文件(303KB),可能是补丁或差异文件
|
|
||||||
- 建议:检查 .gitattributes 确保二进制文件处理正确
|
|
||||||
|
|
||||||
3. **大量文件变更**:
|
|
||||||
- 78,048 行新增代码是一次性引入的,虽然功能完整,但风险集中
|
|
||||||
- 建议:确保有足够的测试覆盖,特别是对新集成的 DotNetCampus.InkCanvas 库
|
|
||||||
|
|
||||||
4. **本地化工作量**:
|
|
||||||
- 添加了多个语言的本地化字符串
|
|
||||||
- 建议:验证所有新增字符串的翻译准确性和一致性
|
|
||||||
|
|
||||||
5. **性能考虑**:
|
|
||||||
- 透明覆盖窗口有 1,258 行代码变更,需要特别关注渲染性能
|
|
||||||
- 建议:进行性能测试,特别是在不同硬件配置下
|
|
||||||
|
|
||||||
6. **依赖管理**:
|
|
||||||
- 添加了新的第三方库依赖
|
|
||||||
- 建议:评估库的维护状态和长期支持情况
|
|
||||||
|
|
||||||
### 📌 建议后续行动
|
|
||||||
|
|
||||||
1. **测试覆盖**:确保对新功能有充分的单元测试和集成测试
|
|
||||||
2. **文档更新**:更新用户文档以反映新的设置选项和功能
|
|
||||||
3. **性能监控**:部署后监控应用性能,特别是启动时间和内存使用
|
|
||||||
4. **用户体验反馈**:收集用户对新设置界面和搜索功能的反馈
|
|
||||||
5. **版本发布说明**:准备详细的发布说明,记录所有新增功能和重大变更
|
|
||||||
|
|
||||||
## 📈 影响范围评估
|
|
||||||
|
|
||||||
| 影响领域 | 评级 | 说明 |
|
|
||||||
|----------|------|------|
|
|
||||||
| **用户体验** | 🟢 高正面 | Fluent Shell 设计、搜索功能、本地化 |
|
|
||||||
| **系统性能** | 🟢 正面 | 渲染门控、图表缓存、IPC 优化 |
|
|
||||||
| **代码架构** | 🟢 正面 | MaterialColorSnapshot 统一数据源 |
|
|
||||||
| **功能完整性** | 🟢 正面 | InkCanvas 集成、数据管理 |
|
|
||||||
| **安全性** | 🟢 正面 | IPC 健壮性改进、安全审计 |
|
|
||||||
| **维护成本** | 🟡 中性 | 新增第三方库依赖需要维护 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*此报告由自动提交分析工具生成*
|
|
||||||
*生成时间: 2026-05-19 10:30:00*
|
|
||||||
*工具版本: Git Commit Analyzer v1.0*
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
# Git 提交分析报告
|
|
||||||
|
|
||||||
## 📋 提交基本信息
|
|
||||||
|
|
||||||
| 属性 | 值 |
|
|
||||||
|------|-----|
|
|
||||||
| **完整哈希** | `ac8ee8dc5467d51cc09ad614aac2c783a6c5dad5` |
|
|
||||||
| **短哈希** | `ac8ee8d` |
|
|
||||||
| **作者** | lincube <lincube3@hotmail.com> |
|
|
||||||
| **提交日期** | 2026-05-23 |
|
|
||||||
| **提交时间** | 02:49:01 |
|
|
||||||
| **时区** | +0800 |
|
|
||||||
| **提交类型** | 🟢 常规提交 |
|
|
||||||
|
|
||||||
## 📝 提交信息摘要
|
|
||||||
|
|
||||||
```
|
|
||||||
changed.优化了天气组件
|
|
||||||
```
|
|
||||||
|
|
||||||
**详细分析**:
|
|
||||||
|
|
||||||
本次提交主要针对天气组件进行了 UI 优化,重点解决了文本显示被截断的问题。通过调整 `ClipToBounds` 属性和添加适当的 `Padding`,改善了天气组件在各种分辨率下的文本可见性。
|
|
||||||
|
|
||||||
## 📊 变更统计
|
|
||||||
|
|
||||||
| 统计项 | 数值 |
|
|
||||||
|--------|------|
|
|
||||||
| **变更文件总数** | 15 个文件 |
|
|
||||||
| **新增代码行数** | +301 行 |
|
|
||||||
| **删除代码行数** | -19 行 |
|
|
||||||
| **净增代码行数** | +282 行 |
|
|
||||||
|
|
||||||
### 变更文件类型分布
|
|
||||||
|
|
||||||
| 文件类型 | 文件数量 | 说明 |
|
|
||||||
|----------|----------|------|
|
|
||||||
| **C# 服务文件** | 6 个 | 后端服务逻辑 |
|
|
||||||
| **XAML 视图文件** | 6 个 | UI 组件定义 |
|
|
||||||
| **XAML.CS 代码文件** | 4 个 | 视图代码逻辑 |
|
|
||||||
| **文档文件** | 1 个 | 自动生成的分析文档 |
|
|
||||||
|
|
||||||
## 📂 详细变更分析
|
|
||||||
|
|
||||||
### 1. 服务层文件变更 (Services)
|
|
||||||
|
|
||||||
#### LanMountainDesktop/Services/AppSettingsService.cs
|
|
||||||
- **变更类型**: 修改
|
|
||||||
- **变更行数**: ±6 行
|
|
||||||
- **变更说明**: 更新应用设置服务,可能涉及天气相关配置的调整
|
|
||||||
|
|
||||||
#### LanMountainDesktop/Services/FusedDesktopLayoutService.cs
|
|
||||||
- **变更类型**: 修改
|
|
||||||
- **变更行数**: ±5 行
|
|
||||||
- **变更说明**: 融合桌面布局服务更新
|
|
||||||
|
|
||||||
#### LanMountainDesktop/Services/LauncherSettingsService.cs
|
|
||||||
- **变更类型**: 修改
|
|
||||||
- **变更行数**: ±4 行
|
|
||||||
- **变更说明**: 启动器设置服务更新
|
|
||||||
|
|
||||||
#### LanMountainDesktop/Services/Settings/SettingsService.cs
|
|
||||||
- **变更类型**: 修改
|
|
||||||
- **变更行数**: ±4 行
|
|
||||||
- **变更说明**: 通用设置服务更新
|
|
||||||
|
|
||||||
#### LanMountainDesktop/Services/ZhiJiaoHubCacheService.cs
|
|
||||||
- **变更类型**: 修改
|
|
||||||
- **变更行数**: ±8 行
|
|
||||||
- **变更说明**: 智慧教育缓存服务更新(变更较大)
|
|
||||||
|
|
||||||
### 2. ClockAirApp 相关文件
|
|
||||||
|
|
||||||
#### LanMountainDesktop/ClockAirApp/ClockAirAppSettingsStore.cs
|
|
||||||
- **变更类型**: 修改
|
|
||||||
- **变更行数**: ±4 行
|
|
||||||
- **变更说明**: 时钟应用设置存储更新
|
|
||||||
|
|
||||||
### 3. 天气组件核心变更 (Weather Widgets) 🔥
|
|
||||||
|
|
||||||
这是本次提交的核心变更区域,涉及多个天气组件的 UI 优化:
|
|
||||||
|
|
||||||
#### ExtendedWeatherWidget 扩展天气组件
|
|
||||||
| 文件 | 变更类型 | 变更说明 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `Views/Components/ExtendedWeatherWidget.axaml` | 修改 | 添加 `ClipToBounds="False"` |
|
|
||||||
| `Views/Components/ExtendedWeatherWidget.axaml.cs` | 修改 | 动态生成 TextBlock 时添加 `ClipToBounds = false` |
|
|
||||||
|
|
||||||
**关键代码变更**:
|
|
||||||
```csharp
|
|
||||||
// ExtendedWeatherWidget.axaml.cs
|
|
||||||
// 变更前
|
|
||||||
inner.Children.Add(new TextBlock { ... });
|
|
||||||
|
|
||||||
// 变更后
|
|
||||||
inner.Children.Add(new TextBlock {
|
|
||||||
Text = item.Value,
|
|
||||||
ClipToBounds = false // 允许文本溢出显示
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
#### HourlyWeatherWidget 小时天气组件
|
|
||||||
| 文件 | 变更类型 | 变更说明 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `Views/Components/HourlyWeatherWidget.axaml` | 修改 | 添加 `ClipToBounds="False"` |
|
|
||||||
| `Views/Components/HourlyWeatherWidget.axaml.cs` | 修改 | 添加 `ClipToBounds = false` |
|
|
||||||
|
|
||||||
#### MultiDayWeatherWidget 多日天气组件
|
|
||||||
| 文件 | 变更类型 | 变更说明 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `Views/Components/MultiDayWeatherWidget.axaml` | 修改 | 添加 `ClipToBounds="False" Padding="0,1,0,0"` |
|
|
||||||
| `Views/Components/MultiDayWeatherWidget.axaml.cs` | 修改 | 多处 TextBlock 添加 `ClipToBounds = false` |
|
|
||||||
|
|
||||||
**关键 UI 调整**:
|
|
||||||
```xml
|
|
||||||
<!-- MultiDayWeatherWidget.axaml -->
|
|
||||||
<!-- 变更前 -->
|
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="42" FontWeight="Bold" />
|
|
||||||
|
|
||||||
<!-- 变更后 -->
|
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="42" FontWeight="Bold"
|
|
||||||
ClipToBounds="False" Padding="0,1,0,0" />
|
|
||||||
```
|
|
||||||
|
|
||||||
**代码层变更**:
|
|
||||||
```csharp
|
|
||||||
// 多处高低温 TextBlock 添加了 ClipToBounds = false
|
|
||||||
new TextBlock {
|
|
||||||
Text = FormatTemperature(item.HighTemperatureC),
|
|
||||||
ClipToBounds = false // 允许温度值完整显示
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### WeatherClockWidget 天气时钟组件
|
|
||||||
| 文件 | 变更类型 | 变更说明 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `Views/Components/WeatherClockWidget.axaml` | 修改 | 温度文本块添加 `ClipToBounds="False"` |
|
|
||||||
|
|
||||||
#### WeatherWidget 主天气组件
|
|
||||||
| 文件 | 变更类型 | 变更说明 |
|
|
||||||
|------|----------|----------|
|
|
||||||
| `Views/Components/WeatherWidget.axaml` | 修改 | 温度文本块添加 `ClipToBounds="False" Padding="0,2,0,0"` |
|
|
||||||
|
|
||||||
**WeatherWidget 详细变更**:
|
|
||||||
```xml
|
|
||||||
<!-- 变更前 -->
|
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="72" FontWeight="Bold" />
|
|
||||||
|
|
||||||
<!-- 变更后 -->
|
|
||||||
<TextBlock x:Name="TemperatureTextBlock" Text="--°" FontSize="72" FontWeight="Bold"
|
|
||||||
ClipToBounds="False" Padding="0,2,0,0" />
|
|
||||||
```
|
|
||||||
- 添加了 `ClipToBounds="False"` 以允许文本溢出显示
|
|
||||||
- 添加了 `Padding="0,2,0,0"` 调整文本垂直位置,避免上下被截断
|
|
||||||
|
|
||||||
### 4. 文档变更
|
|
||||||
|
|
||||||
#### docs/auto_commit_md/20260519_7a70476.md
|
|
||||||
- **变更类型**: 新增
|
|
||||||
- **变更行数**: +269 行
|
|
||||||
- **变更说明**: 自动生成的历史提交分析文档
|
|
||||||
|
|
||||||
## 🔍 代码审查要点
|
|
||||||
|
|
||||||
### ✅ 优点
|
|
||||||
|
|
||||||
1. **UI 修复明确**:
|
|
||||||
- 清楚地识别了文本截断问题
|
|
||||||
- 使用 `ClipToBounds="False"` 是解决文本溢出显示的标准做法
|
|
||||||
- 添加适当的 `Padding` 调整了文本位置,避免被边框裁切
|
|
||||||
|
|
||||||
2. **一致性处理**:
|
|
||||||
- 在所有相关天气组件中都应用了相同的修复策略
|
|
||||||
- 保持了 UI 调整的一致性,包括 XAML 和 C# 代码
|
|
||||||
|
|
||||||
3. **性能考虑**:
|
|
||||||
- `ClipToBounds="False"` 的使用是局部的、针对性的
|
|
||||||
- 不会对整体渲染性能产生显著影响
|
|
||||||
|
|
||||||
4. **向后兼容**:
|
|
||||||
- 修改仅影响文本显示方式,不影响数据逻辑
|
|
||||||
- 用户不会感知到底层数据的变化
|
|
||||||
|
|
||||||
### ⚠️ 需要注意的点
|
|
||||||
|
|
||||||
1. **字体渲染差异**:
|
|
||||||
- `Padding` 值(0,1,0,0 或 0,2,0,0)可能需要根据不同字体进行微调
|
|
||||||
- 建议在不同字体、不同 DPI 设置下测试显示效果
|
|
||||||
|
|
||||||
2. **文本溢出风险**:
|
|
||||||
- 虽然允许文本溢出,但需要确保有足够的容器空间
|
|
||||||
- 极端情况下文本可能仍然会被父容器裁切
|
|
||||||
|
|
||||||
3. **多语言支持**:
|
|
||||||
- 不同的语言文本长度不同,需要确保各种语言的文本都能正确显示
|
|
||||||
- 建议测试中文、英文、日文等多种语言的天气描述文本
|
|
||||||
|
|
||||||
4. **动态内容**:
|
|
||||||
- 温度值在不同单位(°C/°F)下长度可能不同
|
|
||||||
- 需要测试各种温度值的显示效果
|
|
||||||
|
|
||||||
### 📌 建议后续行动
|
|
||||||
|
|
||||||
1. **UI 测试**:在多种分辨率和 DPI 设置下测试天气组件
|
|
||||||
2. **多语言测试**:确保各种语言环境下文本显示正常
|
|
||||||
3. **边界测试**:测试温度值在极端情况下的显示(如 -40°C 或 50°C)
|
|
||||||
4. **性能监控**:监控修改后的渲染性能,确保没有性能退化
|
|
||||||
|
|
||||||
## 📈 技术分析
|
|
||||||
|
|
||||||
### 变更的技术背景
|
|
||||||
|
|
||||||
在 Avalonia UI 框架中,`ClipToBounds` 属性默认值为 `true`,这会导致子元素在超出容器边界时被裁切。对于天气组件中的温度文本、天气描述等动态内容,这种裁切会导致文本显示不完整。
|
|
||||||
|
|
||||||
### 解决方案的有效性
|
|
||||||
|
|
||||||
| 解决方案 | 效果 | 风险 |
|
|
||||||
|----------|------|------|
|
|
||||||
| `ClipToBounds="False"` | ✅ 允许文本完整显示 | ⚠️ 可能溢出到其他元素 |
|
|
||||||
| `Padding="0,2,0,0"` | ✅ 调整文本位置 | ⚠️ 需要精确调整数值 |
|
|
||||||
|
|
||||||
### 相关设计模式
|
|
||||||
|
|
||||||
本次修改涉及以下 UI 设计考虑:
|
|
||||||
- **溢出处理**:在固定尺寸容器中显示动态内容
|
|
||||||
- **对齐策略**:通过 Padding 微调元素位置
|
|
||||||
- **层级管理**:避免文本溢出影响其他 UI 元素
|
|
||||||
|
|
||||||
## 📊 影响范围评估
|
|
||||||
|
|
||||||
| 影响领域 | 评级 | 说明 |
|
|
||||||
|----------|------|------|
|
|
||||||
| **用户体验** | 🟢 正面 | 修复了文本截断问题,提升可读性 |
|
|
||||||
| **系统性能** | 🟢 无影响 | UI 属性调整,无性能影响 |
|
|
||||||
| **代码维护性** | 🟢 正面 | 统一了天气组件的文本显示处理方式 |
|
|
||||||
| **兼容性** | 🟢 正面 | 向后兼容,无破坏性变更 |
|
|
||||||
| **测试覆盖率** | 🟡 需补充 | 建议增加 UI 显示测试用例 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 总结
|
|
||||||
|
|
||||||
本次提交 `ac8ee8d` 主要解决了天气组件的文本显示问题,通过在所有相关组件中添加 `ClipToBounds="False"` 和适当的 `Padding`,确保了温度、天气描述等文本能够完整显示。
|
|
||||||
|
|
||||||
**关键成果**:
|
|
||||||
- ✅ 修复了 5 个天气组件的文本截断问题
|
|
||||||
- ✅ 保持了一致的 UI 处理方式
|
|
||||||
- ✅ 代码变更精确、风险低
|
|
||||||
|
|
||||||
**建议关注**:
|
|
||||||
- 多语言文本显示效果
|
|
||||||
- 不同 DPI 下的字体渲染
|
|
||||||
- 极端温度值的显示
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
*此报告由自动提交分析工具生成*
|
|
||||||
*生成时间: 2026-05-23*
|
|
||||||
*工具版本: Git Commit Analyzer v1.0*
|
|
||||||
Reference in New Issue
Block a user