diff --git a/.github/workflows/README.md b/.github/workflows/README.md index 643cb9a5..f3903fbe 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -2,11 +2,11 @@ ## 文件总览 -| 文件 | 触发时机 | 职责 | -|---|---|---| -| `pre-publish-branch-ci-check.yml` | push / PR → dev · release · bugfix | 构建 + 单元测试 | -| `branch-protection.yml` | PR → main | 校验源分支合法性 | -| `auto-version-release.yml` | PR 合并到 main | 版本递增 · 打包 · 冒烟测试 · 发布 | +| 文件 | 触发时机 | 职责 | +| ----------------------------------- | ------------------------------------- | ------------------------------------ | +| `pre-publish-branch-ci-check.yml` | push / PR → dev · release · bugfix | 构建 + 单元测试 | +| `branch-protection.yml` | PR → main | 校验源分支合法性 | +| `auto-version-release.yml` | PR 合并到 main | 版本递增 · 打包 · 冒烟测试 · 发布 | --- @@ -35,6 +35,7 @@ 对预发布分支进行持续集成,确保代码在合并到 main 之前质量达标。 **执行内容:** + 1. 构建整个解决方案(Release 配置) 2. 并行运行 4 项单元测试:Core · UI · ML · Shader 3. 汇总测试结果,写入 commit status `ci-check/pre-publish-tests` @@ -50,18 +51,27 @@ 只允许 `dev`、`release`、`bugfix` 三个分支向 main 发起 PR,其他分支一律拦截。 -| 源分支 | 版本变化 | 说明 | -|---|---|---| -| `release` | Minor +1 | 0.4.x.x → 0.5.0.0 | -| `dev` | Build +1 | 0.5.2.x → 0.5.3.0 | -| `bugfix` | Revision +1 | 0.5.3.0 → 0.5.3.1 | +| 源分支 | 版本变化 | 说明 | +| ----------- | ----------- | ------------------ | +| `release` | Minor +1 | 0.4.x.x → 0.5.0.0 | +| `dev` | Build +1 | 0.5.2.x → 0.5.3.0 | +| `bugfix` | Revision +1 | 0.5.3.0 → 0.5.3.1 | --- ## 工作流三:自动发布 > **文件:** `auto-version-release.yml` -> **触发:** PR 成功合并到 `main` +> **触发:** PR 成功合并到 `main`(标题不含 `[skip ci]`) + +### 更新类型判定 + +工作流根据 PR 标签自动区分两种更新类型: + +| 类型 | 判定条件 | 版本号变化 | +| ------------------------ | --------------------------- | ------------------------------------------------------------ | +| **常规安装式更新** | 无 `update-plugin-*` 标签 | 按分支递增(feature→minor, daily→build, bugfix→revision) | +| **重启式更新** | 含 `update-plugin-*` 标签 | 版本号不变,打包插件 | ### 执行流程 @@ -75,15 +85,23 @@ build package │ InnoSetup 编译安装包,上传为 Artifact(保留 30 天) ▼ -smoke_test ← 失败则终止并回滚 - │ 静默安装 → 启动主程序 → 等待 UI 被拉起(最多 20s) +smoke_test + │ 静默安装 → 启动主程序 → 等待 UI 被拉起(最多 10s) │ 若未自动拉起:兜底手动启动 UI │ 再次尝试启动 UI → 断言只有 1 个 UI 进程(单例验证) + │ 验证 UI 无法在无主进程时独立运行(MessageBox 守卫) + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ 以下阶段根据更新类型和测试结果有条件执行(见下表) │ + └─────────────────────────────────────────────────────────────┘ + ▼ +package_restart_update (仅重启式更新) + │ 打包插件 zip + manifest,上传为 Artifact ▼ -bump +bump (仅常规安装式更新) │ 更新 csproj / InnoSetup / README badge 版本号并提交到 main ▼ -sync +sync (仅常规安装式更新) │ 将版本文件同步到 release / dev / bugfix 分支 ▼ summary @@ -93,11 +111,34 @@ release(草稿) 创建 Draft GitHub Release - Tag 和标题:v{版本号} - 正文:本次 PR 的描述 - - 附件:安装包 exe + - 附件:安装包 exe(+ 插件包,如有) - 状态:草稿,不公开,等待人工验证后发布 ``` -> 任意步骤失败时,`rollback` job 自动触发,对 main 及三个预发布分支执行 `git revert`。 +### 各阶段执行条件 + +| 阶段 | 常规安装式更新 | 重启式更新 | +| -------------------------- | ------------------------ | ------------------------- | +| `prepare` | ✅ 始终执行 | ✅ 始终执行 | +| `build` | ✅ 始终执行 | ✅ 始终执行 | +| `package` | ✅ 始终执行 | ✅ 始终执行 | +| `smoke_test` | ✅ 始终执行 | ✅ 始终执行 | +| `package_restart_update` | ❌ 始终跳过 | ✅ smoke_test 通过时执行 | +| `bump` | ✅ smoke_test 通过时执行 | ❌ 始终跳过 | +| `sync` | ✅ bump 成功时执行 | ❌ 始终跳过 | +| `summary` | ✅ 全部成功时执行 | ✅ 全部成功时执行 | +| `release` | ✅ summary 成功时执行 | ✅ summary 成功时执行 | +| `rollback` | ✅ sync 阶段失败时触发 | ❌ 始终跳过(无版本同步) | + +### 失败处理 + +| 失败阶段 | 常规安装式更新 | 重启式更新 | +| -------------------------- | --------------------------------------------------------------- | -------------------------------- | +| `build` / `package` | 流程终止,无后续操作 | 流程终止,无后续操作 | +| `smoke_test` | 流程终止,不触发版本同步和 draft | 流程终止,不触发插件打包和 draft | +| `bump` | 流程终止,不触发 sync | — | +| `sync` | **触发 rollback**,回滚 main + 三个预发布分支到 CI 前 SHA | — | +| `package_restart_update` | — | 流程终止,不触发 draft | --- diff --git a/.github/workflows/auto-version-release.yml b/.github/workflows/auto-version-release.yml index 5d50124c..d3875302 100644 --- a/.github/workflows/auto-version-release.yml +++ b/.github/workflows/auto-version-release.yml @@ -22,7 +22,7 @@ jobs: # Job 1: 准备阶段 - 记录 SHA、确定分支、计算新版本号 # ───────────────────────────────────────────────────────────── prepare: - name: Prepare - Calculate Version & Record SHAs + name: Prepare - Version & Metadata runs-on: windows-latest if: | github.event.pull_request.merged == true && @@ -40,6 +40,12 @@ jobs: release_sha: ${{ steps.shas.outputs.release_sha }} pr_title: ${{ steps.pr_info.outputs.pr_title }} pr_body: ${{ steps.pr_info.outputs.pr_body }} + has_plugin_update: ${{ steps.version.outputs.has_plugin_update }} + update_plugins: ${{ steps.version.outputs.update_plugins }} + build_number: ${{ steps.version.outputs.build_number }} + release_tag: ${{ steps.version.outputs.release_tag }} + previous_tag: ${{ steps.prev_tag.outputs.previous_tag }} + app_comp_manifest: ${{ steps.app_info.outputs.app_comp_manifest }} steps: - name: Checkout with full history @@ -50,27 +56,28 @@ jobs: - name: Record original branch SHAs id: shas - shell: powershell + shell: pwsh run: | $mainSha = git rev-parse HEAD - Write-Output "main_sha=$mainSha" >> $env:GITHUB_OUTPUT + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "main_sha=$mainSha`n", $utf8NoBom) Write-Host "main SHA: $mainSha" $devSha = git rev-parse origin/dev - Write-Output "dev_sha=$devSha" >> $env:GITHUB_OUTPUT + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "dev_sha=$devSha`n", $utf8NoBom) Write-Host "dev SHA: $devSha" $bugfixSha = git rev-parse origin/bugfix - Write-Output "bugfix_sha=$bugfixSha" >> $env:GITHUB_OUTPUT + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "bugfix_sha=$bugfixSha`n", $utf8NoBom) Write-Host "bugfix SHA: $bugfixSha" $releaseSha = git rev-parse origin/release - Write-Output "release_sha=$releaseSha" >> $env:GITHUB_OUTPUT + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "release_sha=$releaseSha`n", $utf8NoBom) Write-Host "release SHA: $releaseSha" - name: Determine source branch and version increment id: branch_info - shell: powershell + shell: pwsh env: PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | @@ -90,12 +97,30 @@ jobs: Write-Host "Unknown source branch: $sourceBranch - defaulting to patch" } - Write-Output "source_branch=$sourceBranch" >> $env:GITHUB_OUTPUT - Write-Output "increment_type=$incrementType" >> $env:GITHUB_OUTPUT + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "source_branch=$sourceBranch`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "increment_type=$incrementType`n", $utf8NoBom) + + - name: Get previous tag + id: prev_tag + shell: pwsh + run: | + # Use for-each-ref sorted by creatordate — not limited to ancestor chain + $prevTag = git for-each-ref --sort=-creatordate --count=1 --format="%(refname:short)" refs/tags/ + if (-not $prevTag) { + Write-Host "No previous tag found, this is the first release" + $prevTag = "" + } + Write-Host "Previous tag: $prevTag" + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "previous_tag=$prevTag`n", $utf8NoBom) - - name: Calculate new version + - name: Prepare release metadata id: version - shell: powershell + shell: pwsh + env: + PR_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + PREV_TAG: ${{ steps.prev_tag.outputs.previous_tag }} run: | $csprojPath = "src/VirtualPaper/VirtualPaper.csproj" $content = Get-Content $csprojPath -Raw @@ -112,32 +137,103 @@ jobs: $revision = [int]$versionMatch.Groups[4].Value $currentVersion = "$major.$minor.$build.$revision" + $currentVersionText = "${major}_${minor}_${build}_${revision}" + + # ── Detect plugin update labels ────────────────────────────── + $labels = "$env:PR_LABELS" -split ',' | ForEach-Object { $_.Trim() } + $isPluginUpdate = $false + $plugins = @() - $incrementType = "${{ steps.branch_info.outputs.increment_type }}" - if ($incrementType -eq "feature") { - $minor = $minor + 1 # 第2位 +1 - $build = 0 # 第3位 清零 - $revision = 0 # 第4位 清零 - } elseif ($incrementType -eq "minor") { - $build = $build + 1 # 第3位 +1 - $revision = 0 # 第4位 清零 + if ($labels -contains "update-plugins") { + $isPluginUpdate = $true + $plugins = @("UI", "PlayerWeb", "ScrSaver", "ML", "Shaders") } else { - $revision = $revision + 1 # 第4位 +1 + if ($labels -contains "update-plugin-ui") { $isPluginUpdate = $true; $plugins += "UI" } + if ($labels -contains "update-plugin-playerweb") { $isPluginUpdate = $true; $plugins += "PlayerWeb" } + if ($labels -contains "update-plugin-scrsaver") { $isPluginUpdate = $true; $plugins += "ScrSaver" } + if ($labels -contains "update-plugin-ml") { $isPluginUpdate = $true; $plugins += "ML" } + if ($labels -contains "update-plugin-shaders") { $isPluginUpdate = $true; $plugins += "Shaders" } + } + + # ── Calculate version ──────────────────────────────────────── + if ($isPluginUpdate) { + Write-Host "Plugin update ($($plugins -join ', ')) - version unchanged: $currentVersion" + } else { + Write-Host "Installer-only release - no plugin updates" + $incrementType = "${{ steps.branch_info.outputs.increment_type }}" + if ($incrementType -eq "feature") { + $minor = $minor + 1 # 第2位 +1 + $build = 0 # 第3位 清零 + $revision = 0 # 第4位 清零 + } elseif ($incrementType -eq "minor") { + $build = $build + 1 # 第3位 +1 + $revision = 0 # 第4位 清零 + } else { + $revision = $revision + 1 # 第4位 +1 + } } $newVersion = "$major.$minor.$build.$revision" # 使用下划线分隔,避免各组件 >= 10 时产生歧义(如 0.10.0.0 → 01000 与 0.1.0.0.0 冲突) $newVersionText = "${major}_${minor}_${build}_${revision}" - Write-Host "Version: $currentVersion -> $newVersion" - Write-Output "current_version=$currentVersion" >> $env:GITHUB_OUTPUT - Write-Output "new_version=$newVersion" >> $env:GITHUB_OUTPUT - Write-Output "new_version_text=$newVersionText" >> $env:GITHUB_OUTPUT + # ── Generate build number: YYMM + R + xx (每月重置) ───────── + # 从 previous tag 解码上一次 build number,同月 +1,跨月重置 + $now = Get-Date + $yearMonth = $now.ToString("yyMM") + + $prevBuildNum = "" + $prevTag = "$env:PREV_TAG" + if ($prevTag -match 'plugin([A-Z]+)$') { + $encoded = $Matches[1] + $prevBuildNum = ($encoded.ToCharArray() | ForEach-Object { + if ($_ -ge 'A' -and $_ -le 'J') { [char]([int][char]'0' + ([int]$_ - [int][char]'A')) } else { $_ } + }) -join '' + } + + $buildSeq = 1 + if ($prevBuildNum -match "^(\d{4})R(\d{2})$") { + $prevYM = $Matches[1] + $prevSeq = [int]$Matches[2] + if ($prevYM -eq $yearMonth) { + $buildSeq = $prevSeq + 1 + } + } + + $buildNumber = "${yearMonth}R{0:D2}" -f $buildSeq + Write-Host "Build number: $buildNumber (prev: $prevBuildNum, seq: $buildSeq)" + + # ── Generate release tag ───────────────────────────────────── + # Plugin 更新:tag 后缀用字母编码 build number,保证旧客户端正则兼容 + # 数字 0-9 → A-J,字母原样保留 + # 例:2607R01 → CGAHRAAB + # tag:v0.4.1.0pluginCGAHRAAB → 正则后 0.4.1.0 + # 常规更新:tag 为 v{version} + if ($isPluginUpdate) { + $encoded = ($buildNumber.ToCharArray() | ForEach-Object { + if ($_ -ge '0' -and $_ -le '9') { [char]([int][char]'A' + ([int]$_ - [int][char]'0')) } else { $_ } + }) -join '' + $releaseTag = "v${newVersion}plugin${encoded}" + } else { + $releaseTag = "v${newVersion}" + } + Write-Host "Release tag: $releaseTag" + + # ── Outputs ────────────────────────────────────────────────── + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "current_version=$currentVersion`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "current_version_text=$currentVersionText`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "new_version=$newVersion`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "new_version_text=$newVersionText`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "has_plugin_update=$($isPluginUpdate.ToString().ToLower())`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "update_plugins=$($plugins -join ';')`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "build_number=$buildNumber`n", $utf8NoBom) + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "release_tag=$releaseTag`n", $utf8NoBom) - name: Capture PR title and body id: pr_info - shell: powershell + shell: pwsh env: PR_TITLE: ${{ github.event.pull_request.title }} PR_BODY: ${{ github.event.pull_request.body }} @@ -158,6 +254,77 @@ jobs: [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "$delimiter`n", $utf8NoBom) Write-Host "PR body captured ($($body.Length) chars)" + - name: Build app_comp_manifest (merge with previous release manifest) + id: app_info + shell: pwsh + env: + PREV_TAG: ${{ steps.prev_tag.outputs.previous_tag }} + BUILD_NUMBER: ${{ steps.version.outputs.build_number }} + HAS_PLUGIN_UPDATE: ${{ steps.version.outputs.has_plugin_update }} + UPDATE_PLUGINS: ${{ steps.version.outputs.update_plugins }} + run: | + $buildNumber = $env:BUILD_NUMBER + $hasPluginUpdate = $env:HAS_PLUGIN_UPDATE -eq "true" + $updatePlugins = $env:UPDATE_PLUGINS -split ';' | Where-Object { $_ -ne "" } + $prevTag = $env:PREV_TAG + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + + $prevPlugins = @{} + + # 从上一个 release 下载 manifest + if ($prevTag) { + try { + gh release download $prevTag --pattern "app_comp_manifest.json" --output "prev_manifest" 2>$null + $global:LASTEXITCODE = 0 + if (Test-Path "prev_manifest") { + $manifest = Get-Content "prev_manifest" -Raw | ConvertFrom-Json + foreach ($prop in $manifest.plugins.PSObject.Properties) { + $prevPlugins[$prop.Name] = $prop.Value + } + Write-Host "[OK] Downloaded previous manifest from release $prevTag" + } + } + catch { + $global:LASTEXITCODE = 0 + } + } + + # 构建所有插件列表 + $allPlugins = @("UI", "PlayerWeb", "ScrSaver", "ML", "Shaders") + $pluginsInfo = @{} + + foreach ($plugin in $allPlugins) { + $pluginBuild = $buildNumber + if ($prevPlugins.ContainsKey($plugin)) { + $pluginBuild = $prevPlugins[$plugin] + } + # 如果是本次更新的插件,使用新的 buildNumber + if ($hasPluginUpdate -and $updatePlugins -contains $plugin) { + $pluginBuild = $buildNumber + } + $pluginsInfo[$plugin] = $pluginBuild + } + + # 输出 app_comp_manifest JSON + $ms = [System.IO.MemoryStream]::new() + $writer = [System.Text.Json.Utf8JsonWriter]::new($ms, [System.Text.Json.JsonWriterOptions]@{ Indented = $false }) + $writer.WriteStartObject() + $writer.WriteString("app_build_number", $buildNumber) + $writer.WritePropertyName("plugins") + $writer.WriteStartObject() + foreach ($kv in $pluginsInfo.GetEnumerator() | Sort-Object Name) { + $writer.WriteString($kv.Key, $kv.Value) + } + $writer.WriteEndObject() + $writer.WriteEndObject() + $writer.Flush() + $json = [System.Text.Encoding]::UTF8.GetString($ms.ToArray()) + $writer.Dispose() + $ms.Dispose() + + [System.IO.File]::AppendAllText($env:GITHUB_OUTPUT, "app_comp_manifest=$json`n", $utf8NoBom) + Write-Host "app_comp_manifest: $json" + # ───────────────────────────────────────────────────────────── # Job 2: 编译 .NET Release # ───────────────────────────────────────────────────────────── @@ -185,7 +352,7 @@ jobs: restore-keys: nuget-${{ runner.os }}- - name: Patch AssemblyVersion in csproj (local only, not committed) - shell: powershell + shell: pwsh run: | $csprojPath = "src/VirtualPaper/VirtualPaper.csproj" $content = Get-Content $csprojPath -Raw @@ -200,6 +367,39 @@ jobs: - name: Rebuild solution run: msbuild src/VirtualPaper.sln /t:Rebuild /p:Configuration=Release /p:Platform="Any CPU" /restore /m + - name: Generate app_comp_manifest.json + shell: pwsh + env: + APP_COMP_MANIFEST: ${{ needs.prepare.outputs.app_comp_manifest }} + run: | + $manifestJson = $env:APP_COMP_MANIFEST + $outDir = "src/VirtualPaper/bin/Release/net8.0-windows10.0.19041.0" + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + + # 格式化 JSON(Indented) + $parsed = $manifestJson | ConvertFrom-Json + $ms = [System.IO.MemoryStream]::new() + $writer = [System.Text.Json.Utf8JsonWriter]::new($ms, [System.Text.Json.JsonWriterOptions]@{ Indented = $true }) + $writer.WriteStartObject() + $writer.WriteString("app_build_number", $parsed.app_build_number) + $writer.WritePropertyName("plugins") + $writer.WriteStartObject() + foreach ($prop in $parsed.plugins.PSObject.Properties | Sort-Object Name) { + $writer.WriteString($prop.Name, $prop.Value) + } + $writer.WriteEndObject() + $writer.WriteEndObject() + $writer.Flush() + $indentedJson = [System.Text.Encoding]::UTF8.GetString($ms.ToArray()) + $writer.Dispose() + $ms.Dispose() + + [System.IO.File]::WriteAllText("$outDir/app_comp_manifest.json", $indentedJson, $utf8NoBom) + [System.IO.File]::WriteAllText("app_comp_manifest.json", $indentedJson, $utf8NoBom) + + Write-Host "[OK] Generated app_comp_manifest.json" + Write-Host $indentedJson + - name: Validate Build Artifacts # 在产物就地校验:主 exe / Plugin exe / 核心 dll 存在且非空。 # 通过后才上传 artifact,确保 package / smoke_test job 拿到的是健康的构建产物。 @@ -223,6 +423,13 @@ jobs: path: src/ retention-days: 1 + - name: Upload app_comp_manifest + uses: actions/upload-artifact@v4 + with: + name: app-comp-manifest + path: app_comp_manifest.json + retention-days: 1 + # ───────────────────────────────────────────────────────────── # Job 3: 制作安装包(依赖 build 完成) # ───────────────────────────────────────────────────────────── @@ -251,11 +458,11 @@ jobs: path: src/ - name: Install Inno Setup 6.7.1 - shell: powershell + shell: pwsh run: choco install innosetup --version=6.7.1 --no-progress -y - name: Patch version in InnoSetup script (local only, not committed) - shell: powershell + shell: pwsh run: | $setupPath = "InnoSetup/setup.iss" $content = Get-Content $setupPath -Raw @@ -267,7 +474,7 @@ jobs: Write-Host "Patched InnoSetup version to $newVersion (build-time only)" - name: Download InnoDependencyInstaller - shell: powershell + shell: pwsh run: | $url = "https://raw.githubusercontent.com/DomGries/InnoDependencyInstaller/master/CodeDependencies.iss" $dest = "InnoSetup\CodeDependencies.iss" @@ -276,7 +483,7 @@ jobs: - name: Compile installer id: build_installer - shell: powershell + shell: pwsh run: | $iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" if (-not (Test-Path $iscc)) { @@ -292,7 +499,7 @@ jobs: - name: Record installer info id: installer_info - shell: powershell + shell: pwsh run: | $outputDir = "InnoSetup\Output" $installerFiles = Get-ChildItem -Path $outputDir -Filter "*.exe" -ErrorAction SilentlyContinue @@ -306,7 +513,7 @@ jobs: Write-Output "installer_name=$($installerFiles[0].Name)" >> $env:GITHUB_OUTPUT - name: Generate SHA256 for installer - shell: powershell + shell: pwsh run: | $hash = (Get-FileHash -Path "${{ steps.installer_info.outputs.installer_path }}" -Algorithm SHA256).Hash.ToLower() Set-Content -Path "${{ github.workspace }}\InnoSetup\Output\SHA256.txt" -Value $hash @@ -346,7 +553,7 @@ jobs: - name: Silent install id: install - shell: powershell + shell: pwsh run: | $installer = Get-ChildItem "dist\" -Filter "*.exe" | Select-Object -First 1 if (-not $installer) { @@ -367,7 +574,7 @@ jobs: Write-Host "[OK] Installation completed" - name: Verify key files exist - shell: powershell + shell: pwsh run: | $base = "${{ steps.install.outputs.install_dir }}" $required = @( @@ -389,7 +596,7 @@ jobs: } - name: Launch and verify UI process - shell: powershell + shell: pwsh run: | $installDir = "${{ steps.install.outputs.install_dir }}" $mainExe = "$installDir\VirtualPaper.exe" @@ -450,7 +657,7 @@ jobs: Write-Host "[OK] Smoke test passed" - name: Verify UI cannot start without main process - shell: powershell + shell: pwsh run: | $installDir = "${{ steps.install.outputs.install_dir }}" $uiExe = "$installDir\Plugins\UI\VirtualPaper.UI.exe" @@ -523,13 +730,156 @@ jobs: } Write-Host "[OK] Standalone-UI guard test passed" + # ───────────────────────────────────────────────────────────── + # Job 4.5: 制作插件更新包(plugin zips + manifest) + # 仅在冒烟测试通过且 PR 有 plugin-packages 标签时执行 + # ───────────────────────────────────────────────────────────── + package_plugin_update: + name: Create Plugin Packages + runs-on: windows-latest + needs: [prepare, build, smoke_test] + if: needs.prepare.outputs.has_plugin_update == 'true' + + steps: + - name: Download build output + uses: actions/download-artifact@v4 + with: + name: build-release-output + path: src/ + + - name: Download app_comp_manifest + uses: actions/download-artifact@v4 + with: + name: app-comp-manifest + path: plugin-packages/ + + - name: Create plugin patch package + shell: pwsh + run: | + $buildDir = "src/VirtualPaper/bin/Release/net8.0-windows10.0.19041.0" + $pluginsDir = "$buildDir/Plugins" + $workDir = "plugin-packages" + $utf8NoBom = [System.Text.UTF8Encoding]::new($false) + + $buildNumber = "${{ needs.prepare.outputs.build_number }}" + $plugins = "${{ needs.prepare.outputs.update_plugins }}" -split ';' | Where-Object { $_ -ne "" } + + # 读取 app_comp_manifest.json + $appCompManifest = Get-Content "$workDir/app_comp_manifest.json" -Raw | ConvertFrom-Json + + # 创建每个插件的 zip 并计算 sha256 + $pendingManifest = @{ plugins = @{} } + $tempPluginZips = @() + + foreach ($plugin in $plugins) { + $pluginDir = "$pluginsDir/$plugin" + if (Test-Path $pluginDir) { + $pluginLower = $plugin.ToLower() + $zipName = "plugin-$pluginLower-$buildNumber.zip" + $zipPath = "$workDir/$zipName" + Compress-Archive -Path $pluginDir -DestinationPath $zipPath -Force + $tempPluginZips += $zipPath + + $hash = (Get-FileHash -Path $zipPath -Algorithm SHA256).Hash.ToLower() + $pluginBuild = $appCompManifest.plugins.$plugin + + $pendingManifest.plugins[$plugin] = @{ + build_number = $pluginBuild + asset = $zipName + sha256 = $hash + } + Write-Host "[OK] Created $zipName (sha256: $hash)" + } else { + Write-Warning "Plugin directory not found: $pluginDir" + } + } + + # 生成 pending_update_plugins_manifest.json + $ms = [System.IO.MemoryStream]::new() + $writer = [System.Text.Json.Utf8JsonWriter]::new($ms, [System.Text.Json.JsonWriterOptions]@{ Indented = $true }) + $writer.WriteStartObject() + $writer.WritePropertyName("plugins") + $writer.WriteStartObject() + foreach ($kv in $pendingManifest.plugins.GetEnumerator() | Sort-Object Name) { + $writer.WritePropertyName($kv.Key) + $writer.WriteStartObject() + $writer.WriteString("build_number", $kv.Value.build_number) + $writer.WriteString("asset", $kv.Value.asset) + $writer.WriteString("sha256", $kv.Value.sha256) + $writer.WriteEndObject() + } + $writer.WriteEndObject() + $writer.WriteEndObject() + $writer.Flush() + $pendingJson = [System.Text.Encoding]::UTF8.GetString($ms.ToArray()) + $writer.Dispose() + $ms.Dispose() + + [System.IO.File]::WriteAllText("$workDir/pending_update_plugins_manifest.json", $pendingJson, $utf8NoBom) + Write-Host "[OK] Generated pending_update_plugins_manifest.json" + Write-Host $pendingJson + + # 生成 app_comp_manifest.json(打包到 zip 里的版本) + $ms2 = [System.IO.MemoryStream]::new() + $writer2 = [System.Text.Json.Utf8JsonWriter]::new($ms2, [System.Text.Json.JsonWriterOptions]@{ Indented = $true }) + $writer2.WriteStartObject() + $writer2.WriteString("app_build_number", $appCompManifest.app_build_number) + $writer2.WritePropertyName("plugins") + $writer2.WriteStartObject() + foreach ($prop in $appCompManifest.plugins.PSObject.Properties | Sort-Object Name) { + $writer2.WriteString($prop.Name, $prop.Value) + } + $writer2.WriteEndObject() + $writer2.WriteEndObject() + $writer2.Flush() + $appCompJson = [System.Text.Encoding]::UTF8.GetString($ms2.ToArray()) + $writer2.Dispose() + $ms2.Dispose() + + [System.IO.File]::WriteAllText("$workDir/app_comp_manifest.json", $appCompJson, $utf8NoBom) + Write-Host "[OK] Generated app_comp_manifest.json for zip" + + # 打包 plugins_patch.zip(包含所有插件 zip + 两个 manifest) + $patchZipPath = "plugins_patch.zip" + $filesToZip = @() + $filesToZip += "$workDir/pending_update_plugins_manifest.json" + $filesToZip += "$workDir/app_comp_manifest.json" + $filesToZip += $tempPluginZips + + Compress-Archive -Path $filesToZip -DestinationPath $patchZipPath -Force + Write-Host "[OK] Created plugins_patch.zip" + + # 生成 PLUGINS_PATCH_SHS256.txt(plugins_patch.zip 的 sha256) + $patchHash = (Get-FileHash -Path $patchZipPath -Algorithm SHA256).Hash.ToLower() + [System.IO.File]::WriteAllText("PLUGINS_PATCH_SHS256.txt", $patchHash, $utf8NoBom) + Write-Host "[OK] Generated PLUGINS_PATCH_SHS256.txt: $patchHash" + + # 输出产物信息 + Write-Host "" + Write-Host "=== Plugin Patch Package ===" + Write-Host "plugins_patch.zip: $((Get-Item $patchZipPath).Length) bytes" + Write-Host "PLUGINS_PATCH_SHS256.txt: $patchHash" + + - name: Upload plugin patch package + uses: actions/upload-artifact@v4 + with: + name: VirtualPaper-Plugins-v${{ needs.prepare.outputs.new_version }} + path: | + plugins_patch.zip + PLUGINS_PATCH_SHS256.txt + retention-days: 30 + # ───────────────────────────────────────────────────────────── # Job 5: 将版本号递增变更提交到 main(冒烟测试通过后才提交) # ───────────────────────────────────────────────────────────── bump: name: Bump Version to main runs-on: windows-latest - needs: [prepare, package, smoke_test] + needs: [prepare, package, smoke_test, package_plugin_update] + if: | + always() && + needs.prepare.outputs.has_plugin_update != 'true' && + needs.smoke_test.result == 'success' steps: - name: Checkout main with full history @@ -539,7 +889,7 @@ jobs: fetch-depth: 0 - name: Update version in csproj - shell: powershell + shell: pwsh run: | $csprojPath = "src/VirtualPaper/VirtualPaper.csproj" $content = Get-Content $csprojPath -Raw @@ -549,7 +899,7 @@ jobs: Write-Host "Updated csproj version to: $newVersion" - name: Update version in InnoSetup script - shell: powershell + shell: pwsh run: | $setupPath = "InnoSetup/setup.iss" $content = Get-Content $setupPath -Raw @@ -561,7 +911,7 @@ jobs: Write-Host "Updated InnoSetup version to: $newVersion" - name: Update version badge in README - shell: powershell + shell: pwsh run: | $readmePath = "README.md" $content = Get-Content $readmePath -Raw @@ -571,7 +921,7 @@ jobs: Write-Host "Updated README badge version to: $newVersion" - name: Commit and push to main - shell: powershell + shell: pwsh run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" @@ -589,6 +939,9 @@ jobs: name: Sync Version to Pre-publish Branches runs-on: windows-latest needs: [prepare, bump] + if: | + always() && + needs.bump.result == 'success' steps: - name: Checkout with full history @@ -598,7 +951,7 @@ jobs: fetch-depth: 0 - name: Sync main -> pre-publish branches (merge with main override on conflict) - shell: powershell + shell: pwsh run: | $newVersion = "${{ needs.prepare.outputs.new_version }}" @@ -632,12 +985,16 @@ jobs: Write-Host "[OK]" -ForegroundColor Green -NoNewline; Write-Host " $branch fully synced with main" } else { - # 策略 2(fallback):merge 无法自动解决 → 放弃,直接 reset --hard 到 main - # 使用 reset --hard 直接移动分支指针,不产生额外 commit, - # 历史看起来如同这次 merge 从未发生,比 git revert 更干净彻底。 - Write-Host " [WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " merge has unresolvable conflicts, force-resetting to origin/main" + # 策略 2(fallback):merge 无法自动解决 → 备份后 reset --hard 到 main + Write-Host " [WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " merge has unresolvable conflicts, backing up and force-resetting to origin/main" git merge --abort + # 备份当前分支状态到 ${branch}_backup,再 reset + $backupBranch = "${branch}_backup" + git branch -f $backupBranch HEAD + git push --force origin $backupBranch + Write-Host " [BACKUP]" -ForegroundColor Cyan -NoNewline; Write-Host " $branch saved to $backupBranch" + git reset --hard origin/main git push --force-with-lease origin $branch Write-Host "[WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " $branch force-reset to origin/main (unresolvable conflicts overridden)" @@ -650,15 +1007,15 @@ jobs: summary: name: Summary runs-on: ubuntu-latest - needs: [prepare, build, package, smoke_test, bump, sync] - if: >- + needs: [prepare, build, package, smoke_test, package_plugin_update, bump, sync] + if: | !cancelled() && needs.prepare.result == 'success' && needs.build.result == 'success' && needs.package.result == 'success' && needs.smoke_test.result == 'success' && - needs.bump.result == 'success' && - needs.sync.result == 'success' + (needs.prepare.outputs.has_plugin_update == 'true' || (needs.bump.result == 'success' && needs.sync.result == 'success')) && + (needs.prepare.outputs.has_plugin_update != 'true' || needs.package_plugin_update.result == 'success') steps: - name: Print summary @@ -675,10 +1032,25 @@ jobs: echo "Source : ${{ needs.prepare.outputs.source_branch }} -> main" echo "Type : $TYPE" echo -e "\033[0;36mPackage : ${{ needs.package.outputs.installer_name }}\033[0m" + + if [ "${{ needs.prepare.outputs.has_plugin_update }}" = "true" ]; then + echo -e "\033[0;36mPlugins Update: YES (plugins: ${{ needs.prepare.outputs.update_plugins }}, build: ${{ needs.prepare.outputs.build_number }})\033[0m" + else + echo "Plugins Update: NO (installer only)" + fi + echo "" - echo -e "\033[0;32m[OK]\033[0m Version updated and committed to main" - echo -e "\033[0;32m[OK]\033[0m Version synced to pre-publish branches (release / dev / bugfix)" + if [ "${{ needs.prepare.outputs.has_plugin_update }}" = "true" ]; then + echo -e "\033[0;33m[SKIP]\033[0m Version bump skipped (plugin update - version unchanged)" + echo -e "\033[0;33m[SKIP]\033[0m Branch sync skipped (plugin update)" + else + echo -e "\033[0;32m[OK]\033[0m Version updated and committed to main" + echo -e "\033[0;32m[OK]\033[0m Version synced to pre-publish branches (release / dev / bugfix)" + fi echo -e "\033[0;32m[OK]\033[0m Installer uploaded as Artifact" + if [ "${{ needs.prepare.outputs.has_plugin_update }}" = "true" ]; then + echo -e "\033[0;32m[OK]\033[0m Plugins update packages uploaded as Artifact" + fi echo -e "\033[0;32m[OK]\033[0m Smoke test passed (install + launch + singleton)" echo -e "\033[0;32m[OK]\033[0m Draft GitHub Release will be created: v${{ needs.prepare.outputs.new_version }}" echo -e "\033[0;32m==========================================\033[0m" @@ -689,45 +1061,91 @@ jobs: # - 以 PR body 作为 Release 正文 # - Tag / Release 标题均为 "v版本号" # - 将 Installer exe 作为 Release 附件上传 + # - 如有重启式更新包,一并上传 # - 创建为草稿(draft),需人工验证后手动点击发布 # ───────────────────────────────────────────────────────────── release: name: Create Draft GitHub Release runs-on: ubuntu-latest - needs: [prepare, package, summary] - if: needs.summary.result == 'success' + needs: [prepare, build, package, smoke_test, package_plugin_update, bump, sync, summary] + if: | + !cancelled() && + needs.prepare.result == 'success' && + needs.build.result == 'success' && + needs.package.result == 'success' && + needs.smoke_test.result == 'success' && + (needs.prepare.outputs.has_plugin_update == 'true' || (needs.bump.result == 'success' && needs.sync.result == 'success')) && + (needs.prepare.outputs.has_plugin_update != 'true' || needs.package_plugin_update.result == 'success') steps: + - name: Checkout (for tag verification) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Download installer artifact uses: actions/download-artifact@v4 with: name: VirtualPaper-Installer-v${{ needs.prepare.outputs.new_version }} path: dist/ + - name: Download plugin patch artifact + if: needs.prepare.outputs.has_plugin_update == 'true' + uses: actions/download-artifact@v4 + with: + name: VirtualPaper-Plugins-v${{ needs.prepare.outputs.new_version }} + path: dist/ + + - name: Download app_comp_manifest + if: needs.prepare.outputs.has_plugin_update == 'true' + uses: actions/download-artifact@v4 + with: + name: app-comp-manifest + path: dist/manifest/ + + - name: Verify tag does not exist + id: tag_check + shell: pwsh + run: | + $tag = "${{ needs.prepare.outputs.release_tag }}" + $existing = git tag -l $tag 2>$null + if ($existing) { + Write-Error "Tag '$tag' already exists! Possible concurrent build race. Aborting release." + exit 1 + } + Write-Host "[OK] Tag '$tag' is unique, safe to create" + - name: Create Draft Release & Tag uses: softprops/action-gh-release@v2 with: - tag_name: v${{ needs.prepare.outputs.new_version }} - name: v${{ needs.prepare.outputs.new_version }} + tag_name: ${{ needs.prepare.outputs.release_tag }} + name: ${{ needs.prepare.outputs.has_plugin_update == 'true' && format('v{0} Plugin Update (Build {1})', needs.prepare.outputs.new_version, needs.prepare.outputs.build_number) || format('v{0} (Build {1})', needs.prepare.outputs.new_version, needs.prepare.outputs.build_number) }} body: ${{ needs.prepare.outputs.pr_body }} + previous_tag: ${{ needs.prepare.outputs.previous_tag }} files: | dist/*.exe dist/SHA256.txt + dist/manifest/* + dist/plugins_patch.zip + dist/PLUGINS_PATCH_SHS256.txt draft: true # 草稿状态,不对外公开,等待人工验证后发布 make_latest: false # 草稿阶段不标记为最新,发布时再生效 token: ${{ secrets.GITHUB_TOKEN }} # ───────────────────────────────────────────────────────────── - # Job 9: 失败回滚(任意 job 失败时执行) + # Job 9: 失败回滚(bump 或 sync 执行后失败时) # ───────────────────────────────────────────────────────────── rollback: name: Rollback on Failure runs-on: windows-latest - needs: [prepare, build, package, smoke_test, bump, sync] - # 触发条件:bump 或 sync 已经开始执行(分支内容已被修改),且流程出现失败或取消。 - # build 失败时 bump/sync 均为 skipped(分支未动),不触发,避免无意义的回滚操作。 + needs: [prepare, build, package, smoke_test, package_plugin_update, bump, sync] + # 触发条件: + # 1. 仅针对常规安装式更新(重启式更新不涉及版本同步,无需回滚) + # 2. bump 或 sync 已经开始执行(分支内容已被修改),且流程出现失败或取消 + # 3. bump 依赖 smoke_test 成功,因此 smoke_test 失败时 bump/sync 均为 skipped,不触发回滚 if: | always() && + needs.prepare.outputs.has_plugin_update != 'true' && needs.prepare.result == 'success' && (needs.bump.result != 'skipped' || needs.sync.result != 'skipped') && (contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) @@ -740,7 +1158,7 @@ jobs: fetch-depth: 0 - name: Rollback changes - shell: powershell + shell: pwsh run: | Write-Host "" Write-Host "[WARN]" -ForegroundColor Yellow -NoNewline; Write-Host " Pipeline failure detected, starting rollback..." diff --git a/src/StaticImg/StaticImg.csproj b/src/StaticImg/StaticImg.csproj index ee03a769..a0586899 100644 --- a/src/StaticImg/StaticImg.csproj +++ b/src/StaticImg/StaticImg.csproj @@ -3,7 +3,7 @@ net8.0-windows10.0.19041.0 10.0.17763.0 Workloads.Creation.StaticImg - win-x86;win-x64;win-arm64 + false true true enable diff --git a/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs b/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs index 82d7294b..e4e7fbdc 100644 --- a/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs +++ b/src/VirtualPaper.AppSettingsPanel/ViewModels/GeneralSettingViewModel.cs @@ -2,19 +2,22 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using System.Windows.Input; using Grpc.Core; using VirtualPaper.Common; -using VirtualPaper.Common.Events; using VirtualPaper.Common.Logging; using VirtualPaper.Common.Utils.Files; using VirtualPaper.Common.Utils.Localization; using VirtualPaper.Common.Utils.Storage; using VirtualPaper.Common.Utils.ThreadContext; +using VirtualPaper.Cores.AppUpdate.Models; using VirtualPaper.Grpc.Client.Interfaces; +using VirtualPaper.Models.AppUpdate; using VirtualPaper.Models.Cores; using VirtualPaper.Models.Cores.Interfaces; +using VirtualPaper.Models.Events; using VirtualPaper.Models.Mvvm; using VirtualPaper.UIComponent; using VirtualPaper.UIComponent.Utils; @@ -34,10 +37,61 @@ public string AppVersionText { ver += "b"; else if (Constants.ApplicationType.IsMSIX) ver += $" {LanguageUtil.GetI18n(Constants.I18n.Settings_General_Version_MsStore)}"; + + var appBuild = LoadAppBuildInfo()?.AppBuildNumber; + if (!string.IsNullOrEmpty(appBuild)) + ver += $" (Build {appBuild})"; + return ver; } } + public List PluginVersionTexts { + get { + var plugins = LoadAppBuildInfo()?.Plugins; + if (plugins == null) return []; + return plugins + .OrderBy(kv => Enum.TryParse(kv.Key, true, out var p) ? (int)p : int.MaxValue) + .Select(kv => $"{kv.Key}: {kv.Value}") + .ToList(); + } + } + + public bool HasPluginVersions => PluginVersionTexts.Count > 0; + + public Microsoft.UI.Xaml.Visibility PluginVersionsVisibility => + HasPluginVersions ? Microsoft.UI.Xaml.Visibility.Visible : Microsoft.UI.Xaml.Visibility.Collapsed; + + private AppBuildInfo? LoadAppBuildInfo() { + if (_buildInfo != null) return _buildInfo; + + // 从 AppData 目录读取 app_comp_manifest.json + var path = Path.Combine(Constants.CommonPaths.AppDataDir, "app_comp_manifest.json"); + if (!File.Exists(path)) { + _buildInfo = null; + return null; + } + + try { + var json = File.ReadAllText(path); + var manifest = JsonSerializer.Deserialize(json, UpdateManifestContext.Default.AppCompManifest); + if (manifest?.Plugins == null) { + _buildInfo = null; + return null; + } + + _buildInfo = new AppBuildInfo { AppBuildNumber = manifest.AppBuildNumber }; + foreach (var (pluginName, buildNumber) in manifest.Plugins) { + _buildInfo.Plugins[pluginName] = buildNumber; + } + } + catch { + _buildInfo = null; + } + + return _buildInfo; + } + public List SystemBackdrops { get; set; } = []; public List Languages { get; set; } = []; @@ -71,6 +125,12 @@ public bool IsUpdateBtnEnable { set { _isUpdateBtnEnable = value; OnPropertyChanged(); } } + private bool _isInstallBtnEnable = true; + public bool IsInstallBtnEnable { + get => _isInstallBtnEnable; + set { _isInstallBtnEnable = value; OnPropertyChanged(); } + } + private bool _isUpdateRingActive = false; public bool IsUpdateRingActive { get => _isUpdateRingActive; @@ -154,6 +214,13 @@ public bool IsWallpaperDirectoryChangeEnable { set { _isWallpaperDirectoryChangeEnable = value; OnPropertyChanged(); } } + private string? _text_UpdateReady; + public string? Text_UpdateReady { + get { return _text_UpdateReady; } + set { _text_UpdateReady = value; OnPropertyChanged(); } + } + + public ICommand? InstallBtnComand { get; private set; } public ICommand? ChangeFileStorageCommand { get; private set; } public ICommand? OpenFileStorageCommand { get; private set; } public ICommand? CheckUpdateCommand { get; private set; } @@ -162,10 +229,12 @@ public bool IsWallpaperDirectoryChangeEnable { public GeneralSettingViewModel( IAppUpdaterClient appUpdater, IUserSettingsClient userSettingsClient, - IWallpaperControlClient wallpaperControlClient) { + IWallpaperControlClient wallpaperControlClient, + ICommandsClient commandsClient) { _appUpdater = appUpdater; _userSettingsClient = userSettingsClient; _wpControlClient = wallpaperControlClient; + _commandsClient = commandsClient; InitText(); InitCollections(); @@ -186,6 +255,9 @@ private void InitCommand() { StartDownloadComand = new RelayCommand(async () => { await StartDownloadAsync(); }); + InstallBtnComand = new RelayCommand(async () => { + await RequstInstallAsync(); + }); } private void InitContent() { @@ -195,6 +267,8 @@ private void InitContent() { IsAutoStart = _userSettingsClient.Settings.IsAutoStart; WallpaperDir = _userSettingsClient.Settings.WallpaperDir; + + LoadAppBuildInfo(); } private void InitText() { @@ -220,14 +294,15 @@ private void ChangeAutoShartStatu(bool isAutoStart) { } private async Task CheckUpdateAsync() { + IsInstallBtnEnable = false; IsUpdateBtnEnable = false; IsUpdateRingActive = true; InfoBarVisibilityRestore(); await _appUpdater.CheckUpdateAsync(); - IsUpdateBtnEnable = true; IsUpdateRingActive = false; + IsUpdateBtnEnable = true; } private void InfoBarVisibilityRestore() { @@ -236,32 +311,41 @@ private void InfoBarVisibilityRestore() { private void AppUpdater_UpdateChecked(object? sender, AppUpdaterEventArgs e) { CrossThreadInvoker.InvokeOnUIThread(() => { - MenuUpdate(e.UpdateStatus, e.UpdateDate, e.UpdateVersion); + MenuUpdate(e.UpdateStatus, e.Release); }); } - private void MenuUpdate(AppUpdateStatus status, DateTime date, Version version) { - Version = $"v{version}"; -//#if DEBUG -// CurrentVersionState = VersionState.FindNew; -//#else + private void MenuUpdate(AppUpdateStatus status, ReleaseInfo? release) { + //CurrentVersionState = VersionState.FindNew; + switch (status) { case AppUpdateStatus.Uptodate: CurrentVersionState = VersionState.UptoNewest; break; case AppUpdateStatus.Available: - Version = $"v{version}"; + Version = release?.IsPluginsUpdate == true + ? $"v{release?.Version} Build ({release?.AppBuild})" + : $"v{release?.Version}"; CurrentVersionState = VersionState.FindNew; break; + case AppUpdateStatus.InstallerReady: + Text_UpdateReady = LanguageUtil.GetI18n(nameof(Constants.I18n.Settings_General_Version_InstallerReady)); + CurrentVersionState = VersionState.InstallReady; + IsInstallBtnEnable = true; + break; + case AppUpdateStatus.PluginsReady: + Text_UpdateReady = LanguageUtil.GetI18n(nameof(Constants.I18n.Settings_General_Version_PluginsReady)); + CurrentVersionState = VersionState.InstallReady; + IsInstallBtnEnable = true; + break; case AppUpdateStatus.Invalid or AppUpdateStatus.Error: CurrentVersionState = VersionState.UpdateErr; break; default: break; } -//#endif Version_LastCheckDate = LanguageUtil.GetI18n(Constants.I18n.Settings_General_Version_LastCheckDate); - Version_LastCheckDate += status == AppUpdateStatus.Notchecked ? "" : $" {date}"; + Version_LastCheckDate += status == AppUpdateStatus.Notchecked ? "" : $" {release?.CheckedTime}"; } private async Task StartDownloadAsync() { @@ -272,6 +356,10 @@ private async Task StartDownloadAsync() { IsUpdateBtnEnable = true; } + private async Task RequstInstallAsync() { + await _commandsClient.RequestInstallAsync(); + } + private async void WallpaperDirectoryChange() { var storagePath = (await WindowsStoragePickers.PickFolderAsync(WindowConsts.WindowHandle))?.Path; if (storagePath == null) return; @@ -335,7 +423,7 @@ internal async Task WallpaperDirectoryChangeAsync(string destRootFolderPath) { GlobalMessageUtil.ShowException(ex); ArcLog.GetLogger().Error(ex.Message); if (destFolderPath != string.Empty) { - FileUtil.EmptyDirectory(destFolderPath); + FileUtil.RemoveDirectory(destFolderPath); } } finally { @@ -376,9 +464,11 @@ internal static async IAsyncEnumerable GetWpBasicDataByInstallFolders WpLibData libData = new(); foreach (string file in files) { if (Path.GetFileName(file) == Constants.Field.WpBasicDataFileName) { - libData.BasicData = await JsonSaver.LoadAsync(file, WpBasicDataContext.Default); + var basicData = await JsonSaver.LoadAsync(file, WpBasicDataContext.Default); + if (basicData == null) continue; - if (libData.BasicData.IsAvailable()) { + libData.BasicData = basicData; + if (libData.BasicData != null && libData.BasicData.IsAvailable()) { libData.Idx = idx++; yield return libData; break; @@ -414,6 +504,8 @@ protected virtual void Dispose(bool disposing) { private readonly IAppUpdaterClient _appUpdater; private readonly IUserSettingsClient _userSettingsClient; private readonly IWallpaperControlClient _wpControlClient; + private readonly ICommandsClient _commandsClient; + private AppBuildInfo? _buildInfo; } public enum VersionState { @@ -424,6 +516,7 @@ public enum VersionState { DownloadFailed, // 下载失败 VerifyFailed, // 校验失败 Downloaded, // 下载完成 - UpdateErr // 网络或更新错误 + UpdateErr, // 网络或更新错误 + InstallReady } } diff --git a/src/VirtualPaper.AppSettingsPanel/ViewModels/SystemSettingViewModel.cs b/src/VirtualPaper.AppSettingsPanel/ViewModels/SystemSettingViewModel.cs index f1a56c1e..7a69c5db 100644 --- a/src/VirtualPaper.AppSettingsPanel/ViewModels/SystemSettingViewModel.cs +++ b/src/VirtualPaper.AppSettingsPanel/ViewModels/SystemSettingViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows.Input; using VirtualPaper.Common.Logging; @@ -33,8 +32,8 @@ private void InitCommand() { }); } - private void OpenDebugView() { - _commandClient.ShowDebugView(); + private async void OpenDebugView() { + await _commandClient.ShowDebugViewAsync(); } public async Task ExportLogsAsync() { diff --git a/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml b/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml index be98836b..c977048e 100644 --- a/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml +++ b/src/VirtualPaper.AppSettingsPanel/Views/GeneralSetting.xaml @@ -18,59 +18,87 @@ Text="{markup:I18n Key=Settings_General_Text_Version}" FontWeight="Bold"/> - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/VirtualPaper/Views/PluginUpdateWindow.xaml.cs b/src/VirtualPaper/Views/PluginUpdateWindow.xaml.cs new file mode 100644 index 00000000..687c0318 --- /dev/null +++ b/src/VirtualPaper/Views/PluginUpdateWindow.xaml.cs @@ -0,0 +1,79 @@ +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Shell; +using Microsoft.Extensions.DependencyInjection; +using VirtualPaper.Common; +using VirtualPaper.Common.Utils.PInvoke; +using VirtualPaper.Cores.AppUpdate.Specific; +using VirtualPaper.lang; +using VirtualPaper.Services.Interfaces; + +namespace VirtualPaper.Views { + public partial class PluginUpdateWindow : Window { + public PluginUpdateWindow() { + InitializeComponent(); + this.Loaded += PluginUpdateWindow_Loaded; + + TaskbarItemInfo = new TaskbarItemInfo(); + } + + private void PluginUpdateWindow_Loaded(object sender, RoutedEventArgs e) { + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.Indeterminate; + } + + private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { + if (e.ChangedButton == MouseButton.Left) + DragMove(); + } + + private void MinimizeButton_Click(object sender, RoutedEventArgs e) { + WindowState = WindowState.Minimized; + } + + private void CloseButton_Click(object sender, RoutedEventArgs e) { + Close(); + _ = App.Services.GetRequiredService().ShowUIAsync(); + } + + private void FlashTaskbar() { + var hwnd = new WindowInteropHelper(this).EnsureHandle(); + var fi = new Native.FLASHWINFO { + cbSize = (uint)System.Runtime.InteropServices.Marshal.SizeOf(), + hwnd = hwnd, + dwFlags = Native.FLASHW_TRAY | Native.FLASHW_TIMERNOFG, + uCount = uint.MaxValue, + dwTimeout = 0 + }; + Native.FlashWindowEx(ref fi); + } + + public void ReportProgress(PluginsUpdateProgress progress) { + Dispatcher.Invoke(() => { + UpdateProgressBar.Value = progress.Percent; + StatusText.Text = progress.Message; + + if (progress.Stage == PluginsUpdateStage.Completed) { + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; + TitleText.Text = LanguageManager.Instance[nameof(Constants.I18n.PluginUpdate_Title_Completed)]; + } + else if (progress.Stage == PluginsUpdateStage.Failed) { + TaskbarItemInfo.ProgressState = TaskbarItemProgressState.None; + TitleText.Text = LanguageManager.Instance[nameof(Constants.I18n.PluginUpdate_Title_Failed)]; + } + }); + } + + public void ShowError(string message) { + Dispatcher.Invoke(() => { + TitleText.Text = LanguageManager.Instance[nameof(Constants.I18n.PluginUpdate_Title_Failed)]; + StatusText.Text = message; + UpdateProgressBar.IsIndeterminate = false; + UpdateProgressBar.Value = 100; + UpdateProgressBar.Foreground = System.Windows.Media.Brushes.Red; + CloseButton.IsEnabled = true; + FlashTaskbar(); + }); + } + } +} diff --git a/src/VirtualPaper/Views/SplashWindow.xaml b/src/VirtualPaper/Views/SplashWindow.xaml index 5a9547e9..ac6ce15e 100644 --- a/src/VirtualPaper/Views/SplashWindow.xaml +++ b/src/VirtualPaper/Views/SplashWindow.xaml @@ -21,7 +21,7 @@ - + diff --git a/src/VirtualPaper/VirtualPaper.csproj b/src/VirtualPaper/VirtualPaper.csproj index d75cdc8c..cc04e379 100644 --- a/src/VirtualPaper/VirtualPaper.csproj +++ b/src/VirtualPaper/VirtualPaper.csproj @@ -11,6 +11,8 @@ 0.5.2.0 virtualpaper.ico true + win-x64 + false @@ -51,7 +53,7 @@ <_PluginFiles Include="$(MSBuildProjectDirectory)\Plugins\**\*" Exclude="$(MSBuildProjectDirectory)\Plugins\**\*.pdb; $(MSBuildProjectDirectory)\Plugins\**\*.xml" /> - + diff --git a/src/WebBackdrop/Core/Utils/WebProjectSession.cs b/src/WebBackdrop/Core/Utils/WebProjectSession.cs new file mode 100644 index 00000000..49f20dcc --- /dev/null +++ b/src/WebBackdrop/Core/Utils/WebProjectSession.cs @@ -0,0 +1,33 @@ +using System; +using VirtualPaper.Common.Utils.UndoRedo.Events; + +namespace Workloads.Creation.WebBackdrop.Core.Utils { + public partial class WebProjectSession : IDisposable { + public event EventHandler? SessionDisposed; + public event EventHandler? IsSavedChanged; + + public string SessionId { get; } = Guid.NewGuid().ToString(); + + private void UnReUtil_IsSavedChanged(object? sender, IsSavedChangedEventArgs e) { + IsSavedChanged?.Invoke(this, e); + } + + #region dispose + private bool _isDisposed; + protected virtual void Dispose(bool disposing) { + if (!_isDisposed) { + if (disposing) { + //UnReUtil?.Dispose(); + SessionDisposed?.Invoke(this, EventArgs.Empty); + } + _isDisposed = true; + } + } + + public void Dispose() { + Dispose(true); + GC.SuppressFinalize(this); + } + #endregion + } +} diff --git a/src/WebBackdrop/MainPage.xaml b/src/WebBackdrop/MainPage.xaml new file mode 100644 index 00000000..2237c597 --- /dev/null +++ b/src/WebBackdrop/MainPage.xaml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/src/WebBackdrop/MainPage.xaml.cs b/src/WebBackdrop/MainPage.xaml.cs new file mode 100644 index 00000000..1fa9ef90 --- /dev/null +++ b/src/WebBackdrop/MainPage.xaml.cs @@ -0,0 +1,112 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using System.Threading.Tasks; +using VirtualPaper.Common.Logging; +using VirtualPaper.Common.Utils.Storage; +using VirtualPaper.Common.Utils.UndoRedo.Events; +using VirtualPaper.UIComponent; +using VirtualPaper.UIComponent.Templates; +using VirtualPaper.UIComponent.Utils; +using Windows.Foundation; +using Windows.Foundation.Collections; +using Workloads.Creation.WebBackdrop.Core.Utils; +using Workloads.Utils.DraftUtils.Interfaces; +using Workloads.Utils.DraftUtils.Models; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace Workloads.Creation.WebBackdrop { + /// + /// An empty page that can be used on its own or navigated to within a Frame. + /// + public sealed partial class MainPage : ArcPage { + public override Type ArcType => typeof(MainPage); + + public MainPage() { + InitializeComponent(); + } + } + //public sealed partial class MainPage : ArcPage, IRuntime { + // public event EventHandler? IsSavedChanged; + // public string FileName => Session.DesignFileUtil.FileName; + // public string FileNameWithoutEx => Session.DesignFileUtil.FileNameWithoutEx; + // public string Id => Session.SessionId; + // public override Type ArcType => typeof(MainPage); + // protected override bool IsMultiInstance => true; + // public WebProjectSession Session { get; private set; } + // public bool IsSavedFromInit => Session.DesignFileUtil.IsSaveFromInit; + + // public MainPage() { + // InitializeComponent(); + // } + + // #region workSpace events + // public async Task SaveAsync() { + // try { + // //var res = await inkCanvas.SaveAsync(); + // //return res; + // throw new NotImplementedException(); + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // return false; + // } + + // public async Task SaveAsAsync() { + // try { + // var format = Session.DesignFileUtil.ExportFormatDefult; + // var path = await ExportAsync(format); + // if (path != null) { + // Session.DesignFileUtil.SetFilePath(path); + // //await inkCanvas.UpdateRecentUsedAsync(path); + // throw new NotImplementedException(); + // } + + // return path; + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // return null; + // } + + // public async Task UndoAsync() { + // try { + // await Session.UnReUtil.UndoAsync(); + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // } + + // public async Task RedoAsync() { + // try { + // await Session.UnReUtil.RedoAsync(); + // } + // catch (Exception ex) { + // ArcLog.GetLogger().Error(ex); + // GlobalMessageUtil.ShowException(ex); + // } + // } + + // public async Task ExportAsync(ExportImageFormat format) { + // throw new NotImplementedException(); + // } + // #endregion + //} +} diff --git a/src/WebBackdrop/WebBackdrop.csproj b/src/WebBackdrop/WebBackdrop.csproj new file mode 100644 index 00000000..a7179529 --- /dev/null +++ b/src/WebBackdrop/WebBackdrop.csproj @@ -0,0 +1,27 @@ + + + net8.0-windows10.0.19041.0 + 10.0.17763.0 + Workloads.Creation.WebBackdrop + false + true + true + enable + + + + + + + + + + + + + + MSBuild:Compile + + + + \ No newline at end of file diff --git a/src/Workloads.Utils/Workloads.Utils.csproj b/src/Workloads.Utils/Workloads.Utils.csproj index 53c3ab39..6f333e14 100644 --- a/src/Workloads.Utils/Workloads.Utils.csproj +++ b/src/Workloads.Utils/Workloads.Utils.csproj @@ -3,7 +3,7 @@ net8.0-windows10.0.19041.0 10.0.19041.0 Workloads.Utils - win-x86;win-x64;win-arm64 + false true false true