完善在线工具、插件运行时与安装卸载流程
This commit is contained in:
+143
-5
@@ -41,6 +41,7 @@ $ServerPublicRoot = Join-Path $Root 'server\update\public'
|
||||
$ServerDownloadRoot = Join-Path $ServerPublicRoot 'downloads'
|
||||
$ToolStateRoot = Join-Path $Root '.cache\tool_state'
|
||||
$NuGetRoot = Join-Path $Root '.cache\nuget'
|
||||
$BuildTempRoot = Join-Path $Root '.cache\build-temp'
|
||||
$TauriHostRoot = Join-Path $Root 'src\YMhut.Box.PluginTauriHost\src-tauri'
|
||||
$RepositoryCargoHome = Join-Path $Root '.cache\rust-toolchain\cargo'
|
||||
$RepositoryRustupHome = Join-Path $Root '.cache\rust-toolchain\rustup'
|
||||
@@ -324,7 +325,10 @@ function Ensure-TauriPluginHost {
|
||||
return $executable
|
||||
}
|
||||
|
||||
Write-Warning 'Cargo was not found, so the optional Tauri plugin host will not be included. Install the Rust stable MSVC toolchain to build ExternalRuntime support.'
|
||||
if ($Configuration -ieq 'Release') {
|
||||
throw 'Cargo was not found and no previously built Release Tauri host is available. Release packages require the bundled plugin host.'
|
||||
}
|
||||
Write-Warning 'Cargo was not found, so the optional Debug Tauri plugin host will not be included.'
|
||||
return $null
|
||||
}
|
||||
|
||||
@@ -516,6 +520,12 @@ function Save-ScaledLogo {
|
||||
}
|
||||
|
||||
function Ensure-MsixAssets {
|
||||
$iconScript = Join-Path $Root 'scripts\generate-app-icons.ps1'
|
||||
& $iconScript -Root $Root
|
||||
if (-not $?) {
|
||||
throw 'Application icon generation failed.'
|
||||
}
|
||||
|
||||
$sourceIcon = Join-Path $Root 'assets\icons\app_icon.png'
|
||||
if (-not (Test-Path -LiteralPath $sourceIcon)) {
|
||||
throw "MSIX source icon was not found: $sourceIcon"
|
||||
@@ -527,6 +537,18 @@ function Ensure-MsixAssets {
|
||||
Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Square150x150Logo.png') 150 150 16 '#00FFFFFF'
|
||||
Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'LockScreenLogo.png') 70 70 8 '#00FFFFFF'
|
||||
Save-ScaledLogo $sourceIcon (Join-Path $ProjectAssets 'Wide310x150Logo.png') 310 150 24 '#F5F6F7' -Wordmark
|
||||
|
||||
$iconPath = Join-Path $ProjectAssets 'app_icon.ico'
|
||||
$bytes = [IO.File]::ReadAllBytes($iconPath)
|
||||
$count = [BitConverter]::ToUInt16($bytes, 4)
|
||||
$sizes = for ($index = 0; $index -lt $count; $index++) {
|
||||
$value = $bytes[6 + ($index * 16)]
|
||||
if ($value -eq 0) { 256 } else { [int] $value }
|
||||
}
|
||||
$missing = @(16, 20, 24, 32, 40, 48, 64, 96, 128, 256) | Where-Object { $_ -notin $sizes }
|
||||
if ($missing.Count -gt 0) {
|
||||
throw "Application icon is missing DPI frames: $($missing -join ', ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-LocalDeveloperCertificate {
|
||||
@@ -712,7 +734,13 @@ function Write-LayoutManifests([string] $Directory) {
|
||||
$configDir = Join-Path $Directory 'config'
|
||||
New-Directory $configDir
|
||||
|
||||
$required = @($files | Where-Object { $_ -in @('YMhutBox.exe', 'YMhutBox.dll', 'WebView2Loader.dll') })
|
||||
$required = @($files | Where-Object { $_ -in @(
|
||||
'YMhutBox.exe',
|
||||
'YMhutBox.dll',
|
||||
'WebView2Loader.dll',
|
||||
'Assets/runtime-assets.dat',
|
||||
'Assets/data/ymhut-data.ybin'
|
||||
) })
|
||||
if ($required.Count -eq 0) {
|
||||
$required = @('YMhutBox.exe')
|
||||
}
|
||||
@@ -720,6 +748,7 @@ function Write-LayoutManifests([string] $Directory) {
|
||||
$manifest = [System.Collections.Generic.List[string]]::new()
|
||||
if ($versionInfo) {
|
||||
$manifest.Add('[Release]')
|
||||
$manifest.Add('ManifestVersion=2')
|
||||
$manifest.Add("Version=$($versionInfo.Version)")
|
||||
$manifest.Add("Build=$($versionInfo.Build)")
|
||||
$manifest.Add("Channel=$($versionInfo.Channel)")
|
||||
@@ -1097,6 +1126,7 @@ function Normalize-PublishLayout([string] $Directory) {
|
||||
Remove-RootHelperDuplicates $Directory
|
||||
Assert-UnpackagedLanguageResourcesInLang $Directory
|
||||
Assert-NoDevelopmentArtifacts $Directory
|
||||
Assert-AuthenticatedAssetLayout $Directory
|
||||
}
|
||||
|
||||
function Assert-UnpackagedLanguageResourcesInLang([string] $Directory) {
|
||||
@@ -1274,8 +1304,14 @@ function Assert-ToolUiPayload([string] $Directory) {
|
||||
}
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $folder -Filter '*.dat' -File -ErrorAction SilentlyContinue)
|
||||
if ($files.Count -lt 202) {
|
||||
throw "Release tool UI payload is incomplete in '$Directory': expected at least 202 .dat definitions, found $($files.Count)"
|
||||
$expectedFolder = Join-Path $Root 'assets\data\tool-ui'
|
||||
$expectedNames = @(Get-ChildItem -LiteralPath $expectedFolder -Filter '*.dat' -File -ErrorAction SilentlyContinue |
|
||||
ForEach-Object Name | Sort-Object)
|
||||
$actualNames = @($files | ForEach-Object Name | Sort-Object)
|
||||
$difference = @(Compare-Object -ReferenceObject $expectedNames -DifferenceObject $actualNames)
|
||||
if ($expectedNames.Count -eq 0 -or $difference.Count -gt 0) {
|
||||
$details = $difference | ForEach-Object { "$($_.SideIndicator) $($_.InputObject)" }
|
||||
throw "Release tool UI payload does not exactly match the repository definitions in '$Directory': $($details -join ', ')"
|
||||
}
|
||||
|
||||
foreach ($file in $files) {
|
||||
@@ -1286,6 +1322,47 @@ function Assert-ToolUiPayload([string] $Directory) {
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-AuthenticatedAssetLayout([string] $Directory, [switch] $AllowMsixLogos) {
|
||||
$assetsRoot = Join-Path $Directory 'Assets'
|
||||
$runtimePackage = Join-Path $assetsRoot 'runtime-assets.dat'
|
||||
$referencePackage = Join-Path $assetsRoot 'data\ymhut-data.ybin'
|
||||
foreach ($package in @($runtimePackage, $referencePackage)) {
|
||||
if (-not (Test-Path -LiteralPath $package -PathType Leaf)) {
|
||||
throw "Authenticated asset package is missing from '$Directory': $package"
|
||||
}
|
||||
|
||||
$bytes = [IO.File]::ReadAllBytes($package)
|
||||
if ($bytes.Length -lt 97 -or [Text.Encoding]::ASCII.GetString($bytes, 0, 8) -ne 'YMHUTAS2' -or $bytes[8] -ne 2) {
|
||||
throw "Authenticated asset package has an invalid header: $package"
|
||||
}
|
||||
}
|
||||
|
||||
$allowedPng = if ($AllowMsixLogos) {
|
||||
@(
|
||||
'Assets/StoreLogo.png',
|
||||
'Assets/Square44x44Logo.png',
|
||||
'Assets/Square150x150Logo.png',
|
||||
'Assets/Wide310x150Logo.png'
|
||||
)
|
||||
} else { @() }
|
||||
$blockedExtensions = @(
|
||||
'.bmp', '.css', '.csv', '.gif', '.glb', '.gltf', '.htm', '.html', '.ico',
|
||||
'.jpeg', '.jpg', '.js', '.json', '.mjs', '.otf', '.svg', '.ttf', '.wasm',
|
||||
'.webp', '.woff', '.woff2', '.xls', '.xlsx'
|
||||
)
|
||||
$blocked = @(Get-ChildItem -LiteralPath $assetsRoot -Recurse -File -ErrorAction SilentlyContinue | Where-Object {
|
||||
$relative = 'Assets/' + ($_.FullName.Substring($assetsRoot.Length).TrimStart('\', '/') -replace '\\', '/')
|
||||
($blockedExtensions -contains $_.Extension.ToLowerInvariant()) -or
|
||||
($_.Extension -ieq '.png' -and $allowedPng -notcontains $relative)
|
||||
})
|
||||
if ($blocked.Count -gt 0) {
|
||||
$relativeList = $blocked | ForEach-Object {
|
||||
'Assets/' + ($_.FullName.Substring($assetsRoot.Length).TrimStart('\', '/') -replace '\\', '/')
|
||||
}
|
||||
throw "Publish Assets contains replaceable plaintext static files: $($relativeList -join ', ')"
|
||||
}
|
||||
}
|
||||
|
||||
function Copy-PublishToLatest {
|
||||
Stop-ProcessesFromDirectory $LatestRoot
|
||||
Reset-DirectoryInsideRepo $LatestRoot
|
||||
@@ -1293,6 +1370,7 @@ function Copy-PublishToLatest {
|
||||
Normalize-PublishLayout $LatestRoot
|
||||
Assert-MusicApiPayload $LatestRoot
|
||||
Assert-ToolUiPayload $LatestRoot
|
||||
Assert-AuthenticatedAssetLayout $LatestRoot
|
||||
Write-LayoutManifests $LatestRoot
|
||||
}
|
||||
|
||||
@@ -1305,6 +1383,7 @@ function Publish-UnpackagedApp([object] $VersionInfo) {
|
||||
'--self-contained', 'true',
|
||||
'--no-restore',
|
||||
'-p:WindowsPackageType=None',
|
||||
'-p:GenerateAppxPackageOnBuild=false',
|
||||
'-p:PublishSingleFile=false',
|
||||
"-p:AssemblyVersion=$($VersionInfo.PackageVersion)",
|
||||
"-p:FileVersion=$($VersionInfo.PackageVersion)",
|
||||
@@ -1314,6 +1393,7 @@ function Publish-UnpackagedApp([object] $VersionInfo) {
|
||||
Normalize-PublishLayout $PublishRoot
|
||||
Assert-MusicApiPayload $PublishRoot
|
||||
Assert-ToolUiPayload $PublishRoot
|
||||
Assert-AuthenticatedAssetLayout $PublishRoot
|
||||
Assert-PerMonitorV2Manifest (Join-Path $PublishRoot 'YMhutBox.exe')
|
||||
Write-LayoutManifests $PublishRoot
|
||||
Copy-PublishToLatest
|
||||
@@ -1387,6 +1467,39 @@ function Build-InstallerBootstrap([object] $VersionInfo, [string] $SignTool) {
|
||||
Invoke-ToolQuiet $setupPath @('/WINDOWSELFTEST') 'WinUI installer window navigation self-test failed' " Installer window self-test: $setupPath"
|
||||
}
|
||||
|
||||
function Build-UninstallerBootstrap([object] $VersionInfo, [string] $SignTool) {
|
||||
$bootstrapRoot = Join-Path $Root 'build\winui\uninstaller-bootstrap'
|
||||
Reset-DirectoryInsideRepo $bootstrapRoot
|
||||
Invoke-DotNet @(
|
||||
'publish', $InstallerBootstrapProject,
|
||||
'-c', $Configuration,
|
||||
'-r', 'win-x64',
|
||||
'--self-contained', 'true',
|
||||
'--no-restore',
|
||||
'-p:BootstrapMode=Uninstall',
|
||||
'-p:AssemblyName=unins000',
|
||||
"-p:Version=$($VersionInfo.PackageVersion)",
|
||||
"-p:FileVersion=$($VersionInfo.PackageVersion)",
|
||||
"-p:InformationalVersion=$($VersionInfo.PackageVersion)",
|
||||
'-o', $bootstrapRoot
|
||||
)
|
||||
|
||||
$bootstrap = Join-Path $bootstrapRoot 'unins000.exe'
|
||||
if (-not (Test-Path -LiteralPath $bootstrap -PathType Leaf)) {
|
||||
throw "WinUI uninstaller bootstrap was not produced: $bootstrap"
|
||||
}
|
||||
if ($SignTool) {
|
||||
Sign-Artifact $SignTool $bootstrap
|
||||
}
|
||||
Assert-PerMonitorV2Manifest $bootstrap
|
||||
|
||||
foreach ($payloadRoot in @($PublishRoot, $LatestRoot)) {
|
||||
Copy-Item -LiteralPath $bootstrap -Destination (Join-Path $payloadRoot 'unins000.exe') -Force
|
||||
Write-LayoutManifests $payloadRoot
|
||||
}
|
||||
Write-Host " Uninstaller bootstrap: $(Join-Path $LatestRoot 'unins000.exe')"
|
||||
}
|
||||
|
||||
function Wait-FileWritable([string] $Path, [int] $Attempts = 20, [int] $DelayMilliseconds = 250) {
|
||||
for ($attempt = 1; $attempt -le $Attempts; $attempt++) {
|
||||
$stream = $null
|
||||
@@ -1501,8 +1614,11 @@ function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $
|
||||
|
||||
Reset-DirectoryInsideRepo $MsixStageRoot
|
||||
Copy-Item -Path (Join-Path $PublishRoot '*') -Destination $MsixStageRoot -Recurse -Force
|
||||
Remove-Item -LiteralPath (Join-Path $MsixStageRoot 'unins000.exe') -Force -ErrorAction SilentlyContinue
|
||||
Write-LayoutManifests $MsixStageRoot
|
||||
Assert-MusicApiPayload $MsixStageRoot
|
||||
Assert-ToolUiPayload $MsixStageRoot
|
||||
Assert-AuthenticatedAssetLayout $MsixStageRoot
|
||||
Assert-PerMonitorV2Manifest (Join-Path $MsixStageRoot 'YMhutBox.exe')
|
||||
Expand-SatelliteResourcePackages $MsixStageRoot -RemovePackages
|
||||
Restore-LangResourceLayoutForMsix $MsixStageRoot
|
||||
@@ -1516,7 +1632,10 @@ function Build-MsixPackage([object] $VersionInfo, [string] $MakeAppx, [string] $
|
||||
|
||||
$stageAssets = Join-Path $MsixStageRoot 'Assets'
|
||||
New-Directory $stageAssets
|
||||
Copy-Item -Path (Join-Path $ProjectAssets '*') -Destination $stageAssets -Recurse -Force
|
||||
foreach ($logo in @('StoreLogo.png', 'Square44x44Logo.png', 'Square150x150Logo.png', 'Wide310x150Logo.png')) {
|
||||
Copy-Item -LiteralPath (Join-Path $ProjectAssets $logo) -Destination (Join-Path $stageAssets $logo) -Force
|
||||
}
|
||||
Assert-AuthenticatedAssetLayout $MsixStageRoot -AllowMsixLogos
|
||||
|
||||
Repair-MsixStageThirdPartyPayloads
|
||||
Invoke-ToolQuiet $MakeAppx @('pack', '/d', $MsixStageRoot, '/p', $msixPath, '/o') 'MakeAppx packaging failed' " MSIX package: $msixPath"
|
||||
@@ -1599,10 +1718,15 @@ pause
|
||||
|
||||
New-Directory $NuGetRoot
|
||||
New-Directory $OutputRoot
|
||||
New-Directory $BuildTempRoot
|
||||
$env:DOTNET_CLI_HOME = Join-Path $ToolStateRoot 'dotnet'
|
||||
$env:NUGET_PACKAGES = $NuGetRoot
|
||||
$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||
$env:TEMP = $BuildTempRoot
|
||||
$env:TMP = $BuildTempRoot
|
||||
$env:DOTNET_BUNDLE_EXTRACT_BASE_DIR = Join-Path $BuildTempRoot 'dotnet-bundle'
|
||||
New-Directory $env:DOTNET_CLI_HOME
|
||||
New-Directory $env:DOTNET_BUNDLE_EXTRACT_BASE_DIR
|
||||
|
||||
$versionInfo = Get-VersionInfo
|
||||
$makeAppx = Find-WindowsSdkTool 'MakeAppx.exe'
|
||||
@@ -1637,12 +1761,26 @@ foreach ($ridProject in @($Project, $InstallerBootstrapProject)) {
|
||||
}
|
||||
|
||||
Publish-UnpackagedApp $versionInfo
|
||||
Build-UninstallerBootstrap $versionInfo $signTool
|
||||
if ($tauriHostExecutable) {
|
||||
$publishedTauriHost = Join-Path $PublishRoot 'tauri-host\ymhut-box-plugin-tauri-host.exe'
|
||||
if (-not (Test-Path -LiteralPath $publishedTauriHost)) {
|
||||
throw "The Tauri plugin host was built but was not included in the publish payload: $publishedTauriHost"
|
||||
}
|
||||
}
|
||||
$tauriRuntimeFiles = @(
|
||||
'tauri-host\runtime.json',
|
||||
'tauri-host\template.ymtemplate',
|
||||
'tauri-host\LICENSES.txt',
|
||||
'tauri-host\template\dist\index.html',
|
||||
'tauri-host\template\README.md'
|
||||
)
|
||||
foreach ($relative in $tauriRuntimeFiles) {
|
||||
$runtimeFile = Join-Path $PublishRoot $relative
|
||||
if (-not (Test-Path -LiteralPath $runtimeFile -PathType Leaf)) {
|
||||
throw "The bundled Tauri runtime dependency is missing from the publish payload: $runtimeFile"
|
||||
}
|
||||
}
|
||||
|
||||
if (($Target -in @('exe', 'both')) -and -not $SkipExe) {
|
||||
$exeBuilt = Build-InnoInstaller $versionInfo $signTool -AllowMissingCompiler:($Target -eq 'both')
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $Root = (Split-Path -Parent $PSScriptRoot)
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$sourcePath = Join-Path $Root 'assets\icons\app_icon.png'
|
||||
if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
|
||||
throw "App icon source was not found: $sourcePath"
|
||||
}
|
||||
|
||||
function New-ResizedBitmap {
|
||||
param(
|
||||
[Drawing.Image] $Source,
|
||||
[int] $Width,
|
||||
[int] $Height,
|
||||
[int] $Padding = 0,
|
||||
[Drawing.Color] $Background = [Drawing.Color]::Transparent,
|
||||
[switch] $SharpenSmallAlpha
|
||||
)
|
||||
|
||||
$bitmap = [Drawing.Bitmap]::new($Width, $Height, [Drawing.Imaging.PixelFormat]::Format32bppArgb)
|
||||
$graphics = [Drawing.Graphics]::FromImage($bitmap)
|
||||
try {
|
||||
$graphics.CompositingMode = [Drawing.Drawing2D.CompositingMode]::SourceCopy
|
||||
$graphics.CompositingQuality = [Drawing.Drawing2D.CompositingQuality]::HighQuality
|
||||
$graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$graphics.PixelOffsetMode = [Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||
$graphics.SmoothingMode = [Drawing.Drawing2D.SmoothingMode]::HighQuality
|
||||
$graphics.Clear($Background)
|
||||
|
||||
$availableWidth = $Width - (2 * $Padding)
|
||||
$availableHeight = $Height - (2 * $Padding)
|
||||
$scale = [Math]::Min($availableWidth / $Source.Width, $availableHeight / $Source.Height)
|
||||
$drawWidth = [Math]::Max(1, [int][Math]::Round($Source.Width * $scale))
|
||||
$drawHeight = [Math]::Max(1, [int][Math]::Round($Source.Height * $scale))
|
||||
$drawX = [int][Math]::Floor(($Width - $drawWidth) / 2)
|
||||
$drawY = [int][Math]::Floor(($Height - $drawHeight) / 2)
|
||||
$attributes = [Drawing.Imaging.ImageAttributes]::new()
|
||||
try {
|
||||
$attributes.SetWrapMode([Drawing.Drawing2D.WrapMode]::TileFlipXY)
|
||||
$graphics.DrawImage(
|
||||
$Source,
|
||||
[Drawing.Rectangle]::new($drawX, $drawY, $drawWidth, $drawHeight),
|
||||
0,
|
||||
0,
|
||||
$Source.Width,
|
||||
$Source.Height,
|
||||
[Drawing.GraphicsUnit]::Pixel,
|
||||
$attributes)
|
||||
} finally {
|
||||
$attributes.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$graphics.Dispose()
|
||||
}
|
||||
|
||||
if ($SharpenSmallAlpha) {
|
||||
for ($y = 0; $y -lt $Height; $y++) {
|
||||
for ($x = 0; $x -lt $Width; $x++) {
|
||||
$color = $bitmap.GetPixel($x, $y)
|
||||
if ($color.A -le 10) {
|
||||
$bitmap.SetPixel($x, $y, [Drawing.Color]::Transparent)
|
||||
} else {
|
||||
$alpha = if ($color.A -ge 244) { 255 } else { [Math]::Min(255, [int](($color.A - 10) * 255 / 234)) }
|
||||
$bitmap.SetPixel($x, $y, [Drawing.Color]::FromArgb($alpha, 0, 114, 255))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $bitmap
|
||||
}
|
||||
|
||||
function Get-PngBytes([Drawing.Bitmap] $Bitmap) {
|
||||
$stream = [IO.MemoryStream]::new()
|
||||
try {
|
||||
$Bitmap.Save($stream, [Drawing.Imaging.ImageFormat]::Png)
|
||||
return $stream.ToArray()
|
||||
} finally {
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Write-PngIconFile([string] $Path, [hashtable] $Frames) {
|
||||
$orderedSizes = @($Frames.Keys | ForEach-Object { [int] $_ } | Sort-Object)
|
||||
$headerSize = 6 + (16 * $orderedSizes.Count)
|
||||
$offset = $headerSize
|
||||
$stream = [IO.MemoryStream]::new()
|
||||
$writer = [IO.BinaryWriter]::new($stream)
|
||||
try {
|
||||
$writer.Write([uint16] 0)
|
||||
$writer.Write([uint16] 1)
|
||||
$writer.Write([uint16] $orderedSizes.Count)
|
||||
foreach ($size in $orderedSizes) {
|
||||
$bytes = [byte[]] $Frames[$size]
|
||||
$writer.Write([byte] $(if ($size -eq 256) { 0 } else { $size }))
|
||||
$writer.Write([byte] $(if ($size -eq 256) { 0 } else { $size }))
|
||||
$writer.Write([byte] 0)
|
||||
$writer.Write([byte] 0)
|
||||
$writer.Write([uint16] 1)
|
||||
$writer.Write([uint16] 32)
|
||||
$writer.Write([uint32] $bytes.Length)
|
||||
$writer.Write([uint32] $offset)
|
||||
$offset += $bytes.Length
|
||||
}
|
||||
foreach ($size in $orderedSizes) {
|
||||
$writer.Write([byte[]] $Frames[$size])
|
||||
}
|
||||
$writer.Flush()
|
||||
$directory = Split-Path -Parent $Path
|
||||
if ($directory) { [IO.Directory]::CreateDirectory($directory) | Out-Null }
|
||||
[IO.File]::WriteAllBytes($Path, $stream.ToArray())
|
||||
} finally {
|
||||
$writer.Dispose()
|
||||
$stream.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
function Save-Logo {
|
||||
param(
|
||||
[Drawing.Image] $Source,
|
||||
[string] $Path,
|
||||
[int] $Width,
|
||||
[int] $Height,
|
||||
[int] $Padding,
|
||||
[Drawing.Color] $Background = [Drawing.Color]::Transparent
|
||||
)
|
||||
$bitmap = New-ResizedBitmap $Source $Width $Height $Padding $Background
|
||||
try {
|
||||
[IO.Directory]::CreateDirectory((Split-Path -Parent $Path)) | Out-Null
|
||||
$bitmap.Save($Path, [Drawing.Imaging.ImageFormat]::Png)
|
||||
} finally {
|
||||
$bitmap.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$loaded = [Drawing.Bitmap]::FromFile($sourcePath)
|
||||
try {
|
||||
$master = [Drawing.Bitmap]::new($loaded)
|
||||
} finally {
|
||||
$loaded.Dispose()
|
||||
}
|
||||
|
||||
try {
|
||||
$frames = @{}
|
||||
foreach ($size in @(16, 20, 24, 32, 40, 48, 64, 96, 128, 256)) {
|
||||
$frame = New-ResizedBitmap $master $size $size 0 ([Drawing.Color]::Transparent) -SharpenSmallAlpha:($size -le 24)
|
||||
try {
|
||||
$frames[$size] = Get-PngBytes $frame
|
||||
} finally {
|
||||
$frame.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$iconTargets = @(
|
||||
(Join-Path $Root 'assets\icon.ico'),
|
||||
(Join-Path $Root 'assets\icons\app_icon.ico'),
|
||||
(Join-Path $Root 'src\box-winUI\Assets\app_icon.ico'),
|
||||
(Join-Path $Root 'src\YMhut.Box.PluginTauriHost\src-tauri\icons\icon.ico')
|
||||
)
|
||||
foreach ($target in $iconTargets) {
|
||||
Write-PngIconFile $target $frames
|
||||
}
|
||||
|
||||
$normalizedSource = Join-Path ([IO.Path]::GetTempPath()) ("ymhut-app-icon-{0}.png" -f [Guid]::NewGuid().ToString('N'))
|
||||
try {
|
||||
$master.Save($normalizedSource, [Drawing.Imaging.ImageFormat]::Png)
|
||||
Copy-Item -LiteralPath $normalizedSource -Destination (Join-Path $Root 'assets\icons\app_icon.png') -Force
|
||||
Copy-Item -LiteralPath $normalizedSource -Destination (Join-Path $Root 'assets\icons\icon.png') -Force
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $normalizedSource -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$projectAssets = Join-Path $Root 'src\box-winUI\Assets'
|
||||
Save-Logo $master (Join-Path $projectAssets 'StoreLogo.png') 50 50 5
|
||||
Save-Logo $master (Join-Path $projectAssets 'Square44x44Logo.png') 44 44 4
|
||||
Save-Logo $master (Join-Path $projectAssets 'Square150x150Logo.png') 150 150 16
|
||||
Save-Logo $master (Join-Path $projectAssets 'LockScreenLogo.png') 70 70 8
|
||||
Save-Logo $master (Join-Path $projectAssets 'Wide310x150Logo.png') 310 150 24 ([Drawing.Color]::FromArgb(255, 245, 246, 247))
|
||||
} finally {
|
||||
$master.Dispose()
|
||||
}
|
||||
|
||||
Write-Host 'Generated DPI-aware YMhut Box application icons.'
|
||||
@@ -0,0 +1,154 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Root,
|
||||
[Parameter(Mandatory = $true)][string]$OutputDirectory
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$rootPath = (Resolve-Path -LiteralPath $Root).Path
|
||||
$outputPath = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
New-Item -ItemType Directory -Force -Path $outputPath | Out-Null
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
|
||||
function Get-Sha256([byte[]]$Bytes) {
|
||||
$sha = [Security.Cryptography.SHA256]::Create()
|
||||
try { return ([BitConverter]::ToString($sha.ComputeHash($Bytes)) -replace '-', '').ToLowerInvariant() }
|
||||
finally { $sha.Dispose() }
|
||||
}
|
||||
|
||||
function Get-KeyMaterial([byte[]]$Salt, [string]$Purpose) {
|
||||
$purposeBytes = [Text.Encoding]::UTF8.GetBytes("YMhut.Box.AssetPackage.v2|$Purpose")
|
||||
$material = [byte[]]::new($purposeBytes.Length + $Salt.Length)
|
||||
[Array]::Copy($purposeBytes, 0, $material, 0, $purposeBytes.Length)
|
||||
[Array]::Copy($Salt, 0, $material, $purposeBytes.Length, $Salt.Length)
|
||||
$sha = [Security.Cryptography.SHA512]::Create()
|
||||
try { return $sha.ComputeHash($material) }
|
||||
finally { $sha.Dispose() }
|
||||
}
|
||||
|
||||
function Write-AuthenticatedPackage([byte[]]$Plain, [string]$Purpose, [string]$Path) {
|
||||
$salt = [byte[]]::new(16)
|
||||
$iv = [byte[]]::new(16)
|
||||
$random = [Security.Cryptography.RandomNumberGenerator]::Create()
|
||||
try {
|
||||
$random.GetBytes($salt)
|
||||
$random.GetBytes($iv)
|
||||
}
|
||||
finally { $random.Dispose() }
|
||||
$keys = Get-KeyMaterial $salt $Purpose
|
||||
$aes = [Security.Cryptography.Aes]::Create()
|
||||
try {
|
||||
$aes.Key = $keys[0..31]
|
||||
$aes.IV = $iv
|
||||
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
|
||||
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
|
||||
$encryptor = $aes.CreateEncryptor()
|
||||
try { $cipher = $encryptor.TransformFinalBlock($Plain, 0, $Plain.Length) }
|
||||
finally { $encryptor.Dispose() }
|
||||
}
|
||||
finally { $aes.Dispose() }
|
||||
|
||||
$magic = [Text.Encoding]::ASCII.GetBytes('YMHUTAS2')
|
||||
$header = [IO.MemoryStream]::new()
|
||||
$writer = [IO.BinaryWriter]::new($header, [Text.Encoding]::UTF8, $true)
|
||||
try {
|
||||
$writer.Write($magic)
|
||||
$writer.Write([byte]2)
|
||||
$writer.Write($salt)
|
||||
$writer.Write($iv)
|
||||
$writer.Write([long]$Plain.Length)
|
||||
$writer.Write([long]$cipher.Length)
|
||||
$writer.Flush()
|
||||
}
|
||||
finally { $writer.Dispose() }
|
||||
|
||||
$signed = [byte[]]::new($header.Length + $cipher.Length)
|
||||
[Array]::Copy($header.ToArray(), 0, $signed, 0, $header.Length)
|
||||
[Array]::Copy($cipher, 0, $signed, $header.Length, $cipher.Length)
|
||||
$hmac = [Security.Cryptography.HMACSHA256]::new($keys[32..63])
|
||||
try { $tag = $hmac.ComputeHash($signed) }
|
||||
finally { $hmac.Dispose() }
|
||||
[IO.File]::WriteAllBytes($Path, $signed + $tag)
|
||||
$header.Dispose()
|
||||
}
|
||||
|
||||
function Add-SourceFile([hashtable]$Entries, [string]$Source, [string]$EntryName) {
|
||||
if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Missing authenticated asset source: $Source" }
|
||||
$normalized = $EntryName.Replace('\', '/').TrimStart('/')
|
||||
if ($normalized -eq '' -or $normalized.Contains('../') -or $normalized.StartsWith('../')) { throw "Unsafe authenticated asset entry: $EntryName" }
|
||||
if ($Entries.ContainsKey($normalized)) { throw "Duplicate authenticated asset entry: $normalized" }
|
||||
$bytes = [IO.File]::ReadAllBytes($Source)
|
||||
$Entries[$normalized] = [pscustomobject]@{ Path = $normalized; Bytes = $bytes; Sha256 = Get-Sha256 $bytes }
|
||||
}
|
||||
|
||||
function Add-SourceTree([hashtable]$Entries, [string]$SourceRoot, [string]$Prefix, [scriptblock]$Include = $null) {
|
||||
if (-not (Test-Path -LiteralPath $SourceRoot -PathType Container)) { throw "Missing authenticated asset directory: $SourceRoot" }
|
||||
foreach ($file in Get-ChildItem -LiteralPath $SourceRoot -Recurse -File) {
|
||||
$relative = $file.FullName.Substring($SourceRoot.TrimEnd('\', '/').Length).TrimStart('\', '/').Replace('\', '/')
|
||||
if ($Include -and -not (& $Include $relative)) { continue }
|
||||
Add-SourceFile $Entries $file.FullName (("$Prefix/$relative").TrimStart('/'))
|
||||
}
|
||||
}
|
||||
|
||||
function New-AssetPackage([hashtable]$Entries, [string]$BundleId, [string]$Purpose, [string]$Path) {
|
||||
$memory = [IO.MemoryStream]::new()
|
||||
$archive = [IO.Compression.ZipArchive]::new($memory, [IO.Compression.ZipArchiveMode]::Create, $true)
|
||||
try {
|
||||
foreach ($item in ($Entries.Values | Sort-Object Path)) {
|
||||
$entry = $archive.CreateEntry($item.Path, [IO.Compression.CompressionLevel]::Optimal)
|
||||
$stream = $entry.Open()
|
||||
try { $stream.Write($item.Bytes, 0, $item.Bytes.Length) }
|
||||
finally { $stream.Dispose() }
|
||||
}
|
||||
$manifest = [ordered]@{
|
||||
schemaVersion = 2
|
||||
bundleId = $BundleId
|
||||
files = @($Entries.Values | Sort-Object Path | ForEach-Object {
|
||||
[ordered]@{ path = $_.Path; length = $_.Bytes.Length; sha256 = $_.Sha256 }
|
||||
})
|
||||
}
|
||||
$manifestBytes = [Text.Encoding]::UTF8.GetBytes(($manifest | ConvertTo-Json -Depth 5 -Compress))
|
||||
$manifestEntry = $archive.CreateEntry('_manifest.json', [IO.Compression.CompressionLevel]::Optimal)
|
||||
$manifestStream = $manifestEntry.Open()
|
||||
try { $manifestStream.Write($manifestBytes, 0, $manifestBytes.Length) }
|
||||
finally { $manifestStream.Dispose() }
|
||||
}
|
||||
finally { $archive.Dispose() }
|
||||
$plain = $memory.ToArray()
|
||||
$memory.Dispose()
|
||||
Write-AuthenticatedPackage $plain $Purpose $Path
|
||||
}
|
||||
|
||||
$runtimeEntries = @{}
|
||||
$projectAssets = Join-Path $rootPath 'src\box-winUI\Assets'
|
||||
$repoAssets = Join-Path $rootPath 'assets'
|
||||
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'startup-splash') 'startup-splash'
|
||||
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'home-globe') 'home-globe' {
|
||||
param($relative)
|
||||
return $relative -notlike 'textures/solar/source/*' -and
|
||||
$relative -notlike 'textures/solar/optimized/*' -and
|
||||
$relative -ne 'textures/solar/solar-texture-manifest.json'
|
||||
}
|
||||
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'tool-pages') 'tool-pages'
|
||||
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'tool-results') 'tool-results'
|
||||
Add-SourceTree $runtimeEntries (Join-Path $repoAssets 'icons') 'icons' { param($relative) return $relative -ne '.gitkeep' }
|
||||
Add-SourceTree $runtimeEntries (Join-Path $repoAssets 'images') 'images' { param($relative) return $relative -ne 'loading.gif' }
|
||||
Add-SourceFile $runtimeEntries (Join-Path $repoAssets 'download.svg') 'download.svg'
|
||||
Add-SourceFile $runtimeEntries (Join-Path $repoAssets 'icons\app_icon.ico') 'app_icon.ico'
|
||||
New-AssetPackage $runtimeEntries 'runtime-assets' 'runtime-assets' (Join-Path $outputPath 'runtime-assets.dat')
|
||||
|
||||
$dataEntries = @{}
|
||||
$dataRoot = Join-Path $repoAssets 'data'
|
||||
foreach ($file in Get-ChildItem -LiteralPath $dataRoot -Recurse -File) {
|
||||
if ($file.Extension -ieq '.dat' -or $file.Name -ieq 'ymhut-data.ybin') { continue }
|
||||
$relative = $file.FullName.Substring($repoAssets.TrimEnd('\', '/').Length).TrimStart('\', '/').Replace('\', '/')
|
||||
Add-SourceFile $dataEntries $file.FullName $relative
|
||||
}
|
||||
$rankingRoot = Join-Path $rootPath 'server\unified-management\internal\reference\data'
|
||||
if (Test-Path -LiteralPath $rankingRoot -PathType Container) {
|
||||
foreach ($file in Get-ChildItem -LiteralPath $rankingRoot -Filter '*.json' -File) {
|
||||
Add-SourceFile $dataEntries $file.FullName ("data/rankings/$($file.Name)")
|
||||
}
|
||||
}
|
||||
New-AssetPackage $dataEntries 'reference-data' 'reference-data' (Join-Path $outputPath 'ymhut-data.ybin')
|
||||
@@ -0,0 +1,64 @@
|
||||
param(
|
||||
[string]$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$catalogPath = Join-Path $Root 'src\YMhut.Box.Core\Api\PearApiCatalog.cs'
|
||||
$outputPath = Join-Path $Root 'src\box-winUI\Views\Tools\GeneratedPearApiToolPages.cs'
|
||||
$pattern = 'D\("(?<id>[^"]+)",\s*\d+,'
|
||||
$ids = [Collections.Generic.List[string]]::new()
|
||||
foreach ($line in (Get-Content -LiteralPath $catalogPath -Encoding UTF8)) {
|
||||
$match = [regex]::Match($line, $pattern)
|
||||
if ($match.Success) { $ids.Add($match.Groups['id'].Value) }
|
||||
}
|
||||
if ($ids.Count -ne 74) { throw "Expected 74 PearAPI definitions, found $($ids.Count)." }
|
||||
if (($ids | Select-Object -Unique).Count -ne $ids.Count) { throw 'PearAPI tool IDs must be unique.' }
|
||||
$legacyAliases = @('baidu_hot', 'bili_hot', 'zhihu_hot')
|
||||
|
||||
function Get-ClassStem([string]$id) {
|
||||
$parts = $id -split '[^A-Za-z0-9]+'
|
||||
return (($parts | Where-Object { $_ } | ForEach-Object {
|
||||
if ($_.Length -eq 1) { $_.ToUpperInvariant() }
|
||||
else { $_.Substring(0, 1).ToUpperInvariant() + $_.Substring(1) }
|
||||
}) -join '')
|
||||
}
|
||||
|
||||
$lines = [Collections.Generic.List[string]]::new()
|
||||
$lines.Add('// <auto-generated />')
|
||||
$lines.Add('#nullable enable')
|
||||
$lines.Add('using YMhut.Box.Core.Api;')
|
||||
$lines.Add('using YMhut.Box.Core.Tools;')
|
||||
$lines.Add('using YMhut.Box.WinUI.ViewModels.Tools;')
|
||||
$lines.Add('')
|
||||
$lines.Add('namespace YMhut.Box.WinUI.Views.Tools;')
|
||||
$lines.Add('')
|
||||
$lines.Add('public sealed partial class ToolPageRegistry')
|
||||
$lines.Add('{')
|
||||
$lines.Add(' private static partial void RegisterPearApi(ToolPageRegistry registry)')
|
||||
$lines.Add(' {')
|
||||
foreach ($id in $ids) {
|
||||
$stem = Get-ClassStem $id
|
||||
$lines.Add(" registry.Register<${stem}PearApiToolPage, ${stem}PearApiToolViewModel>(`"$id`", (module, goBack) => new ${stem}PearApiToolPage(module, goBack));")
|
||||
}
|
||||
foreach ($id in $legacyAliases) {
|
||||
$stem = (($id -split '_') | ForEach-Object { if ($_.Length -gt 0) { $_.Substring(0, 1).ToUpperInvariant() + $_.Substring(1) } }) -join ''
|
||||
$lines.Add(" registry.Register<${stem}PearApiToolPage, ${stem}PearApiToolViewModel>(`"$id`", (module, goBack) => new ${stem}PearApiToolPage(module, goBack));")
|
||||
}
|
||||
$lines.Add(' }')
|
||||
$lines.Add('}')
|
||||
$lines.Add('')
|
||||
foreach ($id in $ids) {
|
||||
$stem = Get-ClassStem $id
|
||||
$lines.Add("public sealed class ${stem}PearApiToolViewModel(IToolModule module) : PearApiToolViewModel(module, PearApiCatalog.GetRequired(`"$id`"));")
|
||||
$lines.Add("public sealed class ${stem}PearApiToolPage(IToolModule module, Action? goBack = null) : PearApiToolPageBase(module, new ${stem}PearApiToolViewModel(module), goBack);")
|
||||
$lines.Add('')
|
||||
}
|
||||
foreach ($id in $legacyAliases) {
|
||||
$stem = (($id -split '_') | ForEach-Object { if ($_.Length -gt 0) { $_.Substring(0, 1).ToUpperInvariant() + $_.Substring(1) } }) -join ''
|
||||
$lines.Add("public sealed class ${stem}PearApiToolViewModel(IToolModule module) : PearApiToolViewModel(module, PearApiCatalog.GetRequired(`"$id`"));")
|
||||
$lines.Add("public sealed class ${stem}PearApiToolPage(IToolModule module, Action? goBack = null) : PearApiToolPageBase(module, new ${stem}PearApiToolViewModel(module), goBack);")
|
||||
$lines.Add('')
|
||||
}
|
||||
|
||||
Set-Content -LiteralPath $outputPath -Value $lines -Encoding UTF8
|
||||
Write-Host "Generated $($ids.Count) PearAPI pages and $($legacyAliases.Count) legacy aliases in $outputPath"
|
||||
@@ -5,6 +5,7 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$catalogPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\ToolCatalog.cs'
|
||||
$pearCatalogPath = Join-Path $Root 'src\YMhut.Box.Core\Api\PearApiCatalog.cs'
|
||||
$nexPath = Join-Path $Root 'src\YMhut.Box.Core\Tools\NexNativeTools.cs'
|
||||
$outputRoot = Join-Path $Root 'assets\data\tool-ui'
|
||||
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
|
||||
@@ -32,6 +33,35 @@ foreach ($line in ($rawMatch.Groups['data'].Value -split "`r?`n")) {
|
||||
}
|
||||
}
|
||||
|
||||
$pearPattern = 'D\("(?<id>[^"]+)",\s*(?<apiId>\d+),\s*"(?<name>[^"]+)",\s*"(?<description>[^"]*)",\s*"(?<category>[^"]+)",\s*"(?<path>[^"]+)",\s*PearApiRequestMethod\.(?<method>\w+),\s*PearApiPrivacyClass\.(?<privacy>\w+),\s*PearApiResultKind\.(?<result>\w+)'
|
||||
foreach ($line in (Get-Content -LiteralPath $pearCatalogPath -Encoding UTF8)) {
|
||||
$match = [regex]::Match($line, $pearPattern)
|
||||
if (-not $match.Success) { continue }
|
||||
$id = $match.Groups['id'].Value
|
||||
[void]$ids.Add($id)
|
||||
$metadata[$id] = [ordered]@{
|
||||
zh = $match.Groups['name'].Value
|
||||
en = $match.Groups['name'].Value
|
||||
descriptionZh = $match.Groups['description'].Value
|
||||
descriptionEn = $match.Groups['description'].Value
|
||||
category = 'network'
|
||||
offline = $false
|
||||
pear = $true
|
||||
pearCategory = $match.Groups['category'].Value
|
||||
privacy = $match.Groups['privacy'].Value
|
||||
result = $match.Groups['result'].Value
|
||||
apiId = $match.Groups['apiId'].Value
|
||||
}
|
||||
}
|
||||
foreach ($alias in @{ baidu_hot = '百度'; bili_hot = '哔哩哔哩'; zhihu_hot = '知乎' }.GetEnumerator()) {
|
||||
if (-not $metadata.ContainsKey($alias.Key)) { continue }
|
||||
$metadata[$alias.Key].pear = $true
|
||||
$metadata[$alias.Key].pearCategory = '网络'
|
||||
$metadata[$alias.Key].privacy = 'Public'
|
||||
$metadata[$alias.Key].result = 'RankedList'
|
||||
$metadata[$alias.Key].apiId = '141'
|
||||
}
|
||||
|
||||
foreach ($match in [regex]::Matches((Get-Content -LiteralPath $nexPath -Raw -Encoding UTF8), 'Tool\("(?<id>[^"]+)"')) {
|
||||
$id = $match.Groups['id'].Value
|
||||
[void]$ids.Add($id)
|
||||
@@ -59,7 +89,13 @@ try {
|
||||
$meta = $metadata[$id]
|
||||
$category = [string]$meta.category
|
||||
$risk = if ($category -in @('security', 'system', 'plugin', 'external')) { 'high' } else { 'normal' }
|
||||
$primaryKind = switch ($category) {
|
||||
$primaryKind = if ($meta.pear) {
|
||||
switch ([string]$meta.privacy) {
|
||||
'Public' { 'Output'; break }
|
||||
'FileUpload' { 'FilePicker'; break }
|
||||
default { 'Query' }
|
||||
}
|
||||
} else { switch ($category) {
|
||||
'image' { 'FilePicker'; break }
|
||||
'calculator' { 'Number'; break }
|
||||
'network' { 'Query'; break }
|
||||
@@ -69,12 +105,12 @@ try {
|
||||
'plugin' { 'WebView'; break }
|
||||
'external' { 'ExternalSurface'; break }
|
||||
default { 'Text' }
|
||||
}
|
||||
} }
|
||||
|
||||
$maxLength = 4096
|
||||
if ($category -eq 'text' -or $category -eq 'dev' -or $category -eq 'security') { $maxLength = 200000 }
|
||||
$placeholder = '输入此工具所需的值'
|
||||
if ($category -eq 'network') { $placeholder = '关键词、URL、域名或 IP' }
|
||||
if ($category -eq 'network') { $placeholder = '按此接口页面的字段填写查询条件' }
|
||||
$primaryControls = [Collections.Generic.List[object]]::new()
|
||||
$primaryControls.Add([ordered]@{
|
||||
id = 'primary'
|
||||
@@ -88,7 +124,9 @@ try {
|
||||
})
|
||||
|
||||
$sections = [Collections.Generic.List[object]]::new()
|
||||
$primaryTitle = switch ($category) {
|
||||
$primaryTitle = if ($meta.pear) {
|
||||
[ordered]@{ zh = "PearAPI · $([string]$meta.pearCategory)"; en = "PearAPI · $([string]$meta.pearCategory)" }
|
||||
} else { switch ($category) {
|
||||
'calculator' { [ordered]@{ zh = '计算参数'; en = 'Calculation parameters' } }
|
||||
'network' { [ordered]@{ zh = '查询条件'; en = 'Query criteria' } }
|
||||
'image' { [ordered]@{ zh = '文件与媒体'; en = 'Files and media' } }
|
||||
@@ -99,14 +137,14 @@ try {
|
||||
'plugin' { [ordered]@{ zh = '插件安全容器'; en = 'Plugin security container' } }
|
||||
'external' { [ordered]@{ zh = '外部程序运行'; en = 'External program launch' } }
|
||||
default { [ordered]@{ zh = '主要操作'; en = 'Primary operation' } }
|
||||
}
|
||||
} }
|
||||
|
||||
if ($category -eq 'calculator') {
|
||||
$primaryControls.Clear()
|
||||
foreach ($numberId in @('number-primary', 'number-secondary', 'number-third', 'number-fourth')) {
|
||||
$primaryControls.Add([ordered]@{ id = $numberId; kind = 'Number'; label = [ordered]@{ zh = '数值参数'; en = 'Numeric parameter' }; defaultValue = '1'; required = $true; isReadOnly = $false; validation = [ordered]@{ minimum = -100000000; maximum = 100000000 } })
|
||||
}
|
||||
} elseif ($category -eq 'network') {
|
||||
} elseif ($category -eq 'network' -and -not $meta.pear) {
|
||||
$primaryControls.Add([ordered]@{ id = 'parameter'; kind = 'ComboBox'; label = [ordered]@{ zh = '来源或分类'; en = 'Source or category' }; defaultValue = ''; required = $false; isReadOnly = $false })
|
||||
} elseif ($category -eq 'image') {
|
||||
$primaryControls.Add([ordered]@{ id = 'file'; kind = 'FilePicker'; label = [ordered]@{ zh = '文件'; en = 'File' }; placeholder = '选择本地文件'; defaultValue = ''; required = $false; isReadOnly = $false })
|
||||
@@ -121,7 +159,7 @@ try {
|
||||
layout = $(if ($category -eq 'calculator') { 'grid' } else { 'stack' })
|
||||
})
|
||||
|
||||
if ($category -in @('text', 'dev', 'security', 'network', 'design')) {
|
||||
if (-not $meta.pear -and $category -in @('text', 'dev', 'security', 'network', 'design')) {
|
||||
$advancedControls = [Collections.Generic.List[object]]::new()
|
||||
if ($category -in @('text', 'dev', 'security')) {
|
||||
$advancedControls.Add([ordered]@{ id = 'rules'; kind = 'ComboBox'; label = [ordered]@{ zh = '处理规则'; en = 'Processing rules' }; defaultValue = ''; required = $false; isReadOnly = $false })
|
||||
@@ -142,7 +180,9 @@ try {
|
||||
})
|
||||
}
|
||||
|
||||
$resultKind = switch ($category) {
|
||||
$resultKind = if ($meta.pear) {
|
||||
"pear-$(([string]$meta.result).ToLowerInvariant())"
|
||||
} else { switch ($category) {
|
||||
'calculator' { 'calculator-table' }
|
||||
'image' { 'image-preview' }
|
||||
'data' { 'ranked-list' }
|
||||
@@ -153,7 +193,7 @@ try {
|
||||
'plugin' { 'status-list' }
|
||||
'external' { 'status-list' }
|
||||
default { 'text' }
|
||||
}
|
||||
} }
|
||||
$actions = [Collections.Generic.List[object]]::new()
|
||||
$actionLabel = switch ($category) {
|
||||
'calculator' { [ordered]@{ zh = '计算'; en = 'Calculate' } }
|
||||
@@ -217,5 +257,10 @@ try {
|
||||
} finally {
|
||||
$aes.Dispose()
|
||||
}
|
||||
foreach ($stale in Get-ChildItem -LiteralPath $outputRoot -Filter '*.dat' -File -ErrorAction SilentlyContinue) {
|
||||
if (-not $ids.Contains($stale.BaseName)) {
|
||||
Remove-Item -LiteralPath $stale.FullName -Force
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Generated $($ids.Count) tool UI definitions in $outputRoot"
|
||||
|
||||
Reference in New Issue
Block a user