From f013f32556f85610517f01b471086a59eb05a2c2 Mon Sep 17 00:00:00 2001
From: backspace135 <46274807+backspace135@users.noreply.github.com>
Date: Thu, 9 Jul 2026 15:43:56 +0800
Subject: [PATCH 01/18] Fix macOS CLI terminal and add app bundle icon
- CliLauncher: branch by platform. macOS now writes an equivalent .command
shell script with the same numbered menu and opens it in Terminal.app;
previously it hardcoded cmd.exe + .bat and failed silently on macOS.
- Surface launch failures via Notifier (toast) instead of only logging.
- Add cli.unsupported / cli.launchFailed strings to both ResX files.
- Packaging: generate AppIcon.icns from atv-icon.png (sips + iconutil) and
set CFBundleIconFile so the macOS .app shows an icon in Dock/Finder.
---
CLAUDE.md | 163 +++++++++++++++++
packaging/build-update-zip.ps1 | 81 ++++++++-
.../Controls/SkeletonBlock.axaml | 41 +++--
.../Controls/SkeletonBlock.axaml.cs | 15 ++
.../Resources/Strings.resx | 6 +
.../Resources/Strings.zh-Hans.resx | 6 +
.../Services/CliLauncher.cs | 164 ++++++++++++++++--
.../ViewModels/MainWindowViewModel.cs | 43 +++--
8 files changed, 471 insertions(+), 48 deletions(-)
create mode 100644 CLAUDE.md
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..4dafdeb
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,163 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## What this is
+
+AndroidTreeView is a **.NET 10 + Avalonia** Windows desktop tool for inspecting, testing and managing
+Android devices over ADB. Despite the name it is a C#/.NET solution, **not** a Java/Kotlin Android app.
+It has two shipping executables that share the same core:
+
+- **App** (`src/AndroidTreeView.App`) — the full Avalonia UI: device overview, per-category details,
+ screen mirroring (scrcpy), CLI tools, settings, updates.
+- **Mini** (`src/AndroidTreeView.Mini`) — a lightweight WinForms tray/monitor app that stays resident,
+ watches for devices, and auto-launches scrcpy once a device is connected and authorized. Mini
+ deliberately does **not** carry the Avalonia/Skia runtime.
+
+`Mini.Mac` is an Avalonia-based Mini variant for macOS.
+
+## Commands
+
+Requires the **.NET 10 SDK**. Run from the repo root.
+
+```bash
+dotnet restore AndroidTreeView.sln
+dotnet build AndroidTreeView.sln
+dotnet test AndroidTreeView.sln
+dotnet run --project src/AndroidTreeView.App
+dotnet run --project src/AndroidTreeView.Mini
+```
+
+Building on non-Windows (App/Mini target Windows) needs the targeting pack flag — CI uses it everywhere:
+
+```bash
+dotnet build AndroidTreeView.sln -p:EnableWindowsTargeting=true
+```
+
+Run a single test project / a single test:
+
+```bash
+dotnet test tests/AndroidTreeView.Adb.Tests
+dotnet test AndroidTreeView.sln --filter "FullyQualifiedName~BatteryParser"
+```
+
+Format check (CI runs this, non-blocking — but keep it clean):
+
+```bash
+dotnet format AndroidTreeView.sln --verify-no-changes
+```
+
+Package/verify the release ZIP link locally (real releases only ship via the GitHub `Publish` workflow,
+x64 only):
+
+```powershell
+./packaging/build-update-zip.ps1 -Product App -Rid win-x64
+./packaging/build-update-zip.ps1 -Product Mini -Rid win-x64
+```
+
+There are ready-made Rider/VS run configs under `.run/`.
+
+## Architecture
+
+The layering is strict and enforced by project references — respect the dependency direction. Lower
+layers never reference upper ones.
+
+```
+Models domain records/enums, no project deps
+ ↑
+Core interfaces, options (AppSettings, AdbOptions), typed exceptions, DeviceMonitor
+ ↑
+Adb / Infrastructure Adb: ProcessRunner, command builders, parsers, AdbDeviceService, LogcatService
+ ↑ Infrastructure: SettingsService (JSON), logging, update check/install
+Shared the shared composition root — AddAndroidTreeViewSharedServices() wires ADB, monitoring,
+ ↑ scrcpy, settings, and update services identically for both App and Mini
+App / Mini / Mini.Mac executables (each references Shared)
+```
+
+Key architectural rules (see `docs/architecture.md` for the full binding type/interface contract, and
+`docs/app-contract.md`):
+
+- **Shared is the single wiring point.** Both App and Mini call
+ `AddAndroidTreeViewSharedServices(...)` so their ADB/scrcpy/settings/update behavior can't drift.
+ When adding a service that both should use, register it there (it uses `TryAdd*`), not per-app.
+- **App is strict MVVM** with CommunityToolkit.Mvvm (`[ObservableProperty]` / `[RelayCommand]`) and
+ `Microsoft.Extensions` DI/Hosting/Logging. `Program.cs` builds the host + `IServiceProvider`, then
+ Avalonia resolves `MainWindowViewModel`. `ViewLocator` maps `*.ViewModels.XxxViewModel` →
+ `*.Views.XxxView` by name convention.
+- **All ADB is out-of-process** via `ProcessRunner` (async stdout/stderr, kills the whole process tree
+ on cancel/timeout). Device info comes from parsing ADB text output.
+- **Parsers are pure and unit-tested.** ADB output parsing lives in `AndroidTreeView.Adb.Parsers` as
+ stateless/static functions (`GetPropParser`, `BatteryParser`, `AdbDevicesParser`, etc.). Any change to
+ parsing logic must come with a parser test — fixtures live under the Adb.Tests project.
+- `AdbLocator` resolves adb (configured path → PATH → common SDK locations); `IAdbEnvironment` holds the
+ current location as a singleton. If adb is missing the App shows a Setup page instead of crashing.
+- `DeviceMonitor` owns a `PeriodicTimer` background loop and raises `DevicesChanged`; VMs marshal to the
+ UI thread themselves.
+
+## Conventions (from `.editorconfig` + `CONTRIBUTING.md`)
+
+- File-scoped namespaces, 4-space indent, interfaces `I`-prefixed, `using` outside the namespace,
+ CRLF line endings, UTF-8.
+- **Async-only for I/O**: `async` + `CancellationToken` propagated everywhere. No `.Result` / `.Wait()` /
+ `Task.Run` for ADB/process work. UI mutations only on the UI thread (`Dispatcher.UIThread.Post`).
+- Views use **compiled bindings** (`x:DataType`) and bind only to members that exist on the VM. No
+ business logic or ADB calls in views.
+- **No hardcoded user-facing strings.** Use localization for every user-visible string (see below).
+- **Read-only / safe by design**: do not introduce ADB commands that modify, wipe, or flash a device.
+- Keep files under ~400 lines; the App never crashes on normal ADB/device errors — map them to friendly
+ `ErrorMessage` state.
+
+## Localization
+
+Every user-facing string is localized — never hardcode display text. Default language is **Simplified
+Chinese**; English is the neutral/fallback resource.
+
+- **Contract**: `ILocalizationService` (`AndroidTreeView.Core/Interfaces`) — `Get(key)`, `Format(key, args)`,
+ the `this[key]` indexer, `SetLanguage(AppLanguage)`, and a `LanguageChanged` event. `AppLanguage` lives
+ in `AndroidTreeView.Core/Options/SettingsEnums.cs`. `Get` returns the key itself if a resource is
+ missing, so a missing translation is visible, not a crash.
+- **Implementation**: `LocalizationService` in `src/AndroidTreeView.App/Localization`, backed by two ResX
+ files with **identical key sets** (keep them in sync — currently 215 keys each):
+ - `src/AndroidTreeView.App/Resources/Strings.resx` — English (neutral fallback)
+ - `src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx` — Simplified Chinese (default)
+ - Keys are dotted/namespaced: `app.title`, `nav.devices`, `common.refresh`, etc.
+- **In ViewModels**: inject `ILocalizationService` and call `_localization.Get("key")` / `.Format(...)`.
+- **In XAML**: use the `{loc:Localize Key=some.key}` markup extension (`LocalizeExtension` +
+ `LocalizeKeyConverter`). Bindings watch `LocalizationService.LanguageTick` so text refreshes live when
+ the language changes (the indexer's own `INotifyPropertyChanged` was unreliable in this Avalonia
+ version — that's why the plain `LanguageTick` counter exists; keep using it for live-refresh bindings).
+
+**When adding a string**: add the key to **both** ResX files, then reference it via `Get`/`Format` (VMs)
+or `{loc:Localize}` (XAML). Never add a key to only one file.
+
+## Tests
+
+xUnit across four projects (`dotnet test AndroidTreeView.sln`, or target one project — see Commands).
+Fixtures are **inline `const` strings** inside the test classes, not external `.txt` files.
+
+- **`Adb.Tests`** — the bulk of the suite. `Parsers/` has one test class per parser/builder
+ (`BatteryParserTests`, `StorageParserTests`, `AdbDevicesParserTests`, `GetPropParserTests`,
+ `RootStatusParserTests`, `NetworkParserTests`, the `*BuilderTests`, etc.), `Commands/` covers argv
+ building (`AdbArgsTests`), `Services/` covers device-action/screen-capture services. **Any change to ADB
+ output parsing must add/update the matching parser test here** — this is where correctness is pinned.
+- **`Core.Tests`** — `AppSettings` clone/serialize, `DeviceMonitor` start/stop (with a fake
+ `IDeviceService`), `SemanticVersion`, file-transfer service.
+- **`Infrastructure.Tests`** — `SettingsService` persistence, update check (`NekoIndexUpdateService`) and
+ `UpdateInstaller`, using test doubles in `TestDoubles.cs`.
+- **`App.Tests`** — ViewModel logic with fake services (`Fakes.cs`): device-list reconcile keeps
+ selection, RawProperties filtering, Logcat bounding, DI graph resolves (`ServiceGraphTests`), and a boot
+ smoke test via `Avalonia.Headless` (`TestAppBuilder.cs`, `BootSmokeTests.cs`). No real ADB, no on-screen
+ rendering.
+
+## Versioning & updates
+
+Version is unified across runtime version, App/Mini assembly versions, manifests, and
+`packaging/build-update-zip.ps1` — keep them in sync when bumping (currently `1.0.5`). Update channels:
+`android-tree-view-app` (App) and `android-tree-view-mini` (Mini). `NekoIndexUpdateService` checks the
+channel and compares semver; `UpdateInstaller` downloads, verifies SHA-256, unpacks the x64 ZIP, and runs
+a local update script. Loose ZIPs without a supported `release.json` manifest are rejected.
+
+## Bundled tools
+
+`build/AndroidTreeView.Scrcpy.targets` downloads and bundles scrcpy (which ships adb) into `tools/scrcpy`
+at build/publish time on Windows. `tools/verify-scrcpy-latest.ps1` checks for newer scrcpy releases.
diff --git a/packaging/build-update-zip.ps1 b/packaging/build-update-zip.ps1
index fa688df..0b47896 100644
--- a/packaging/build-update-zip.ps1
+++ b/packaging/build-update-zip.ps1
@@ -250,6 +250,60 @@ function ConvertTo-PlistString {
return [System.Security.SecurityElement]::Escape($Value)
}
+function New-IcnsFromPng {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$SourcePng,
+
+ [Parameter(Mandatory = $true)]
+ [string]$OutputIcns
+ )
+
+ # macOS .app icons are .icns files; the repo only ships a 256x256 PNG, so build the .icns from it
+ # with the system tools (sips resizes each slot, iconutil packs the iconset). Requires macOS.
+ if (-not (Get-Command sips -ErrorAction SilentlyContinue) -or -not (Get-Command iconutil -ErrorAction SilentlyContinue)) {
+ Write-Warning 'sips/iconutil not found; skipping macOS app icon generation.'
+ return $false
+ }
+
+ $iconsetDir = "$OutputIcns.iconset"
+ if (Test-Path -LiteralPath $iconsetDir) {
+ Remove-Item -Recurse -Force -LiteralPath $iconsetDir
+ }
+ New-Item -ItemType Directory -Force -Path $iconsetDir | Out-Null
+
+ # name => pixel size for the standard iconset slots (1x + @2x). Upscaled slots beyond the
+ # 256px source are slightly soft but valid; Dock/Finder common sizes stay crisp.
+ $slots = [ordered]@{
+ 'icon_16x16.png' = 16
+ 'icon_16x16@2x.png' = 32
+ 'icon_32x32.png' = 32
+ 'icon_32x32@2x.png' = 64
+ 'icon_128x128.png' = 128
+ 'icon_128x128@2x.png' = 256
+ 'icon_256x256.png' = 256
+ 'icon_256x256@2x.png' = 512
+ 'icon_512x512.png' = 512
+ 'icon_512x512@2x.png' = 1024
+ }
+
+ foreach ($entry in $slots.GetEnumerator()) {
+ $target = Join-Path $iconsetDir $entry.Key
+ & sips -z $entry.Value $entry.Value $SourcePng --out $target *> $null
+ if ($LASTEXITCODE -ne 0) {
+ throw "sips failed to render '$($entry.Key)' from '$SourcePng'."
+ }
+ }
+
+ & iconutil -c icns $iconsetDir -o $OutputIcns
+ if ($LASTEXITCODE -ne 0) {
+ throw "iconutil failed to build '$OutputIcns'."
+ }
+
+ Remove-Item -Recurse -Force -LiteralPath $iconsetDir
+ return $true
+}
+
function New-MacOSAppBundle {
param(
[Parameter(Mandatory = $true)]
@@ -306,6 +360,25 @@ function New-MacOSAppBundle {
$bundleIdentifier = "com.birditch.$($ProductConfig.AppKey.Replace('-', '.'))"
$displayName = $ProductConfig.ProductName
$executableName = $ProductConfig.Executable
+
+ # Build the app icon (.icns) from the App's PNG so the bundle shows in Dock/Finder.
+ $iconSourcePng = Join-Path $repoRoot 'src/AndroidTreeView.App/Assets/atv-icon.png'
+ $iconFileName = ''
+ if (Test-Path -LiteralPath $iconSourcePng) {
+ $icnsPath = Join-Path $resourcesDir 'AppIcon.icns'
+ if (New-IcnsFromPng -SourcePng $iconSourcePng -OutputIcns $icnsPath) {
+ $iconFileName = 'AppIcon'
+ }
+ } else {
+ Write-Warning "Icon source '$iconSourcePng' not found; bundle will have no icon."
+ }
+
+ $iconPlistEntry = if ($iconFileName) {
+ " CFBundleIconFile`n $(ConvertTo-PlistString $iconFileName)`n"
+ } else {
+ ''
+ }
+
$infoPlist = @"
@@ -317,7 +390,7 @@ function New-MacOSAppBundle {
$(ConvertTo-PlistString $displayName)CFBundleExecutable$(ConvertTo-PlistString $executableName)
- CFBundleIdentifier
+$iconPlistEntry CFBundleIdentifier$(ConvertTo-PlistString $bundleIdentifier)CFBundleInfoDictionaryVersion6.0
@@ -491,6 +564,10 @@ $zipPath = Join-Path $artifacts "$baseName.zip"
$zipChecksumPath = "$zipPath.sha256"
$packageKind = if ($ridInfo.IsWindows) { 'portable-x64' } else { "portable-$($ridInfo.Rid)" }
$bundleFastbootValue = if ($productConfig.BundleFastboot) { 'true' } else { 'false' }
+# macOS has no system-level "install .NET runtime" prompt like Windows, so a framework-dependent
+# .app silently fails to launch on machines without the runtime. Bundle the runtime into the macOS
+# .app (self-contained). Windows stays framework-dependent: its apphost shows the OS download prompt.
+$selfContainedValue = if ($ridInfo.IsMacOS) { 'true' } else { 'false' }
New-Item -ItemType Directory -Force -Path $artifacts | Out-Null
Assert-UnderDirectory -Path $publishDir -Parent $artifacts
@@ -513,7 +590,7 @@ $publishArgs = @(
$productConfig.Project,
'--configuration', $Configuration,
'--runtime', $ridInfo.Rid,
- '--self-contained', 'false',
+ '--self-contained', $selfContainedValue,
"-p:Version=$Version",
"-p:AssemblyVersion=$Version.0",
"-p:FileVersion=$Version.0",
diff --git a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml
index c0230e1..2038e5c 100644
--- a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml
+++ b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml
@@ -4,27 +4,30 @@
x:Class="AndroidTreeView.App.Controls.SkeletonBlock"
x:CompileBindings="False"
MinHeight="16">
+
+
+
+
-
-
-
-
+ CornerRadius="{Binding CornerRadius, RelativeSource={RelativeSource AncestorType=controls:SkeletonBlock}}" />
diff --git a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs
index 619a9db..50045f2 100644
--- a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs
+++ b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs
@@ -1,4 +1,5 @@
using Avalonia.Controls;
+using Avalonia.Layout;
namespace AndroidTreeView.App.Controls;
@@ -9,5 +10,19 @@ public partial class SkeletonBlock : UserControl
public SkeletonBlock()
{
InitializeComponent();
+
+ // The shimmer is an INFINITE animation. Avalonia keeps such a clock ticking even when this
+ // control is only collapsed by a parent (the loading panel toggles IsVisible), which drives
+ // the render loop to repaint every frame — the app feels laggy and runs hot while "doing
+ // nothing". EffectiveViewportChanged fires when an ancestor's visibility collapses/expands
+ // our on-screen viewport; gate the animation on a non-empty viewport via the `:active`
+ // pseudo-class so the clock stops whenever the skeleton isn't actually shown.
+ EffectiveViewportChanged += OnEffectiveViewportChanged;
+ }
+
+ private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
+ {
+ var visible = e.EffectiveViewport.Width > 0 && e.EffectiveViewport.Height > 0;
+ PseudoClasses.Set(":active", visible);
}
}
diff --git a/src/AndroidTreeView.App/Resources/Strings.resx b/src/AndroidTreeView.App/Resources/Strings.resx
index ff3f94f..62acc49 100644
--- a/src/AndroidTreeView.App/Resources/Strings.resx
+++ b/src/AndroidTreeView.App/Resources/Strings.resx
@@ -171,6 +171,12 @@
CLI Terminal
+
+ The CLI terminal is not supported on this operating system.
+
+
+ Failed to open the CLI terminal.
+
Details
diff --git a/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx b/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx
index 830a777..ee83efa 100644
--- a/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx
+++ b/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx
@@ -171,6 +171,12 @@
CLI 终端
+
+ 当前操作系统不支持 CLI 终端。
+
+
+ 打开 CLI 终端失败。
+
详情
diff --git a/src/AndroidTreeView.App/Services/CliLauncher.cs b/src/AndroidTreeView.App/Services/CliLauncher.cs
index 552c5ba..a38ad0f 100644
--- a/src/AndroidTreeView.App/Services/CliLauncher.cs
+++ b/src/AndroidTreeView.App/Services/CliLauncher.cs
@@ -27,13 +27,15 @@ public interface ICliLauncher
public sealed class CliLauncher : ICliLauncher
{
private readonly IAdbEnvironment _environment;
+ private readonly ILocalizationService _localization;
private readonly ILogger _logger;
private readonly List _processes = new();
private readonly object _gate = new();
- public CliLauncher(IAdbEnvironment environment, ILogger logger)
+ public CliLauncher(IAdbEnvironment environment, ILocalizationService localization, ILogger logger)
{
_environment = environment;
+ _localization = localization;
_logger = logger;
}
@@ -42,29 +44,81 @@ public void Open(string serial, string title, bool fastboot)
try
{
var toolsDir = ResolveToolsDir() ?? AppContext.BaseDirectory;
- var script = fastboot ? BuildFastbootMenu(serial, toolsDir) : BuildAdbMenu(serial, toolsDir);
-
var safe = new string(serial.Where(char.IsLetterOrDigit).ToArray());
- var batPath = Path.Combine(Path.GetTempPath(), $"atv-cli-{(safe.Length == 0 ? "device" : safe)}.bat");
- File.WriteAllText(batPath, script);
-
- var psi = new ProcessStartInfo
+ if (safe.Length == 0)
{
- FileName = "cmd.exe",
- Arguments = $"/K \"{batPath}\"",
- WorkingDirectory = toolsDir,
- UseShellExecute = true, // reliably opens a visible console window from a GUI process
- };
+ safe = "device";
+ }
- var process = Process.Start(psi);
- if (process is not null)
+ if (OperatingSystem.IsWindows())
+ {
+ OpenWindows(serial, safe, toolsDir, fastboot);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ OpenMac(serial, safe, toolsDir, fastboot);
+ }
+ else
{
- Track(process);
+ Notifier.Notify(_localization.Get("cli.unsupported"), NotifierLevel.Warning);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to open CLI terminal for {Serial}.", serial);
+ Notifier.Notify(_localization.Get("cli.launchFailed"), NotifierLevel.Error);
+ }
+ }
+
+ // Windows: write a menu .bat and open it in a cmd.exe /K console.
+ private void OpenWindows(string serial, string safe, string toolsDir, bool fastboot)
+ {
+ var script = fastboot ? BuildFastbootMenu(serial, toolsDir) : BuildAdbMenu(serial, toolsDir);
+ var batPath = Path.Combine(Path.GetTempPath(), $"atv-cli-{safe}.bat");
+ File.WriteAllText(batPath, script);
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ Arguments = $"/K \"{batPath}\"",
+ WorkingDirectory = toolsDir,
+ UseShellExecute = true, // reliably opens a visible console window from a GUI process
+ };
+
+ var process = Process.Start(psi);
+ if (process is not null)
+ {
+ Track(process);
+ }
+ }
+
+ // macOS: write an equivalent .command shell script and open it in Terminal.app.
+ // Note: `open` returns immediately and the Terminal window is an independent process, so
+ // ShutdownAll() cannot close it — that's intentional; we don't force-close a user's terminal.
+ [System.Runtime.Versioning.SupportedOSPlatform("macos")]
+ private void OpenMac(string serial, string safe, string toolsDir, bool fastboot)
+ {
+ var script = fastboot ? BuildFastbootMenuSh(serial, toolsDir) : BuildAdbMenuSh(serial, toolsDir);
+ var scriptPath = Path.Combine(Path.GetTempPath(), $"atv-cli-{safe}.command");
+ File.WriteAllText(scriptPath, script);
+ File.SetUnixFileMode(
+ scriptPath,
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "open",
+ Arguments = $"-a Terminal.app \"{scriptPath}\"",
+ WorkingDirectory = toolsDir,
+ UseShellExecute = false,
+ };
+
+ var process = Process.Start(psi);
+ if (process is not null)
+ {
+ Track(process);
}
}
@@ -279,8 +333,88 @@ private static string BuildAdbMenu(string serial, string toolsDir) => Join(new[]
"exit",
});
+ // macOS fastboot menu — bash equivalent of BuildFastbootMenu (universal + non-destructive only).
+ private static string BuildFastbootMenuSh(string serial, string toolsDir) => JoinLf(new[]
+ {
+ "#!/bin/bash",
+ // ANSI colors (cyan headers / green numbers / yellow values) matching the Windows menu.
+ "H=$'\\033[96m'; N=$'\\033[92m'; V=$'\\033[93m'; R=$'\\033[0m'",
+ $"export PATH=\"{ShEscape(toolsDir)}:$PATH\"",
+ $"SERIAL=\"{ShEscape(serial)}\"",
+ "while true; do",
+ " clear",
+ " echo \"${H}================ AndroidTreeView Fastboot CLI ================${R}\"",
+ " echo \" 设备 Device: ${V}${SERIAL}${R} (fastboot / bootloader)\"",
+ " echo \"${H}-------------------------------------------------------------${R}\"",
+ " echo \" ${N}[1]${R} 设备信息 Device info (getvar all)\"",
+ " echo \" ${N}[2]${R} 重启到系统 Reboot to system\"",
+ " echo \" ${N}[3]${R} 重启到 Bootloader Reboot to bootloader\"",
+ " echo \" ${N}[4]${R} 重启到 Recovery Reboot to recovery\"",
+ " echo \" ${N}[5]${R} 关机 Power off\"",
+ " echo \" ${N}[6]${R} 切换到 A 槽 Set active slot A\"",
+ " echo \" ${N}[7]${R} 切换到 B 槽 Set active slot B\"",
+ " echo \" ${N}[9]${R} 原生命令行 Raw CLI (adb + fastboot on PATH)\"",
+ " echo \" ${N}[0]${R} 退出 Exit\"",
+ " echo \"${H}-------------------------------------------------------------${R}\"",
+ " read -r -p \"${H}输入序号并回车 Enter choice >${R} \" sel",
+ " case \"$sel\" in",
+ " 1) fastboot -s \"$SERIAL\" getvar all; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 2) fastboot -s \"$SERIAL\" reboot; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 3) fastboot -s \"$SERIAL\" reboot bootloader; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 4) fastboot -s \"$SERIAL\" reboot recovery; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 5) fastboot -s \"$SERIAL\" oem poweroff; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 6) fastboot -s \"$SERIAL\" set_active a; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 7) fastboot -s \"$SERIAL\" set_active b; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 9) echo \"adb / fastboot 已在 PATH。输入 exit 返回菜单 (type exit to return).\"; \"${SHELL:-/bin/bash}\" ;;",
+ " 0) exit 0 ;;",
+ " esac",
+ "done",
+ });
+
+ // macOS adb menu — bash equivalent of BuildAdbMenu (info / shell / logcat / reboots / power / raw).
+ private static string BuildAdbMenuSh(string serial, string toolsDir) => JoinLf(new[]
+ {
+ "#!/bin/bash",
+ "H=$'\\033[96m'; N=$'\\033[92m'; V=$'\\033[93m'; R=$'\\033[0m'",
+ $"export PATH=\"{ShEscape(toolsDir)}:$PATH\"",
+ $"SERIAL=\"{ShEscape(serial)}\"",
+ "while true; do",
+ " clear",
+ " echo \"${H}================== AndroidTreeView ADB CLI ==================${R}\"",
+ " echo \" 设备 Device: ${V}${SERIAL}${R}\"",
+ " echo \"${H}------------------------------------------------------------${R}\"",
+ " echo \" ${N}[1]${R} 设备信息 Device info (getprop)\"",
+ " echo \" ${N}[2]${R} 进入 Shell adb shell\"",
+ " echo \" ${N}[3]${R} 查看日志 Logcat (Ctrl+C 停止)\"",
+ " echo \" ${N}[4]${R} 重启 Reboot\"",
+ " echo \" ${N}[5]${R} 重启到 Bootloader Reboot to bootloader\"",
+ " echo \" ${N}[6]${R} 重启到 Recovery Reboot to recovery\"",
+ " echo \" ${N}[7]${R} 关机 Power off\"",
+ " echo \" ${N}[9]${R} 原生命令行 Raw CLI (adb + fastboot on PATH)\"",
+ " echo \" ${N}[0]${R} 退出 Exit\"",
+ " echo \"${H}------------------------------------------------------------${R}\"",
+ " read -r -p \"${H}输入序号并回车 Enter choice >${R} \" sel",
+ " case \"$sel\" in",
+ " 1) adb -s \"$SERIAL\" shell getprop; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 2) adb -s \"$SERIAL\" shell ;;",
+ " 3) adb -s \"$SERIAL\" logcat ;;",
+ " 4) adb -s \"$SERIAL\" reboot; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 5) adb -s \"$SERIAL\" reboot bootloader; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 6) adb -s \"$SERIAL\" reboot recovery; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 7) adb -s \"$SERIAL\" shell reboot -p; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 9) echo \"adb / fastboot 已在 PATH。输入 exit 返回菜单 (type exit to return).\"; \"${SHELL:-/bin/bash}\" ;;",
+ " 0) exit 0 ;;",
+ " esac",
+ "done",
+ });
+
+ // Single-quote escape for embedding a literal into a double-quoted bash string safely.
+ private static string ShEscape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("$", "\\$").Replace("`", "\\`");
+
private static string Join(IEnumerable lines) => string.Join("\r\n", lines) + "\r\n";
+ private static string JoinLf(IEnumerable lines) => string.Join("\n", lines) + "\n";
+
private string? ResolveToolsDir()
{
var adb = _environment.Location?.ExecutablePath;
diff --git a/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs b/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs
index 156d965..61ae0b2 100644
--- a/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs
+++ b/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs
@@ -7,6 +7,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
+using System.Linq;
namespace AndroidTreeView.App.ViewModels;
@@ -37,13 +38,19 @@ public sealed partial class MainWindowViewModel : ViewModelBase
private readonly ILogger _logger;
// Per-device enrichment state (accessed only on the UI thread from OnDevicesChanged).
- // Identity + root are re-probed on this cadence (not just once) so a card never drifts out of sync
- // with the detail page (e.g. root becoming visible after a grant).
- private static readonly TimeSpan StaticRefreshInterval = TimeSpan.FromSeconds(8);
- private readonly Dictionary _staticEnrichedAt = new(StringComparer.Ordinal);
+ // Identity + root are static per connection: probed once when a device first appears and not
+ // re-polled on a timer. Re-probing all devices every few seconds forked a burst of adb processes
+ // (root alone is 6 probes/device) that stalled the main page when several devices were connected.
+ // A device that changes state (e.g. root granted) disconnects/reconnects or is refreshed on demand,
+ // which clears its entry and re-enriches it.
+ private readonly HashSet _staticEnriched = new(StringComparer.Ordinal);
private readonly HashSet _enriching = new(StringComparer.Ordinal);
private readonly HashSet _fastbootEnriching = new(StringComparer.Ordinal);
+ // Device-list poll interval, loaded from settings at startup (was hardcoded to 1s, which ignored
+ // the DeviceRefreshIntervalSeconds setting and hammered adb once per second per device).
+ private TimeSpan _refreshInterval = TimeSpan.FromSeconds(3);
+
private string? _releaseUrl;
private UpdateCheckResult? _latestUpdate;
private bool _monitorSubscribed;
@@ -221,6 +228,8 @@ public async Task InitializeAsync()
_themeService.Apply(settings.Theme);
_themeService.ApplyAccent(settings.AccentColor);
+ _refreshInterval = TimeSpan.FromSeconds(Math.Max(1, settings.DeviceRefreshIntervalSeconds));
+
var location = await _locator.LocateAsync(settings.AdbPath).ConfigureAwait(true);
_environment.Set(location);
@@ -239,7 +248,7 @@ public async Task InitializeAsync()
{
IsAdbAvailable = true;
AdbStatusText = _localization.Get("adb.status.ready");
- _monitor.UpdateInterval(TimeSpan.FromSeconds(1));
+ _monitor.UpdateInterval(_refreshInterval);
_monitor.Start();
CurrentContent = Devices;
}
@@ -349,6 +358,9 @@ private async Task RefreshDevicesAsync()
{
try
{
+ // A manual refresh is the user asking for fresh facts, so drop the one-time static cache
+ // to force identity/root to be re-probed on the next enrichment pass.
+ _staticEnriched.Clear();
await _monitor.RefreshNowAsync().ConfigureAwait(true);
}
catch (Exception ex)
@@ -474,7 +486,7 @@ private void OnAdbReady(object? sender, EventArgs e)
if (!_monitor.IsRunning)
{
- _monitor.UpdateInterval(TimeSpan.FromSeconds(1));
+ _monitor.UpdateInterval(_refreshInterval);
_monitor.Start();
}
@@ -509,9 +521,17 @@ private void OnLanguageChanged(object? sender, EventArgs e)
}
// Fills each online card with real data: battery every tick (fast), and static identity + root once
- // per device. A per-serial guard prevents overlapping runs from piling up under the 1 Hz monitor.
+ // per device (first time it appears). A per-serial guard prevents overlapping runs from piling up.
private void EnrichDevices(IReadOnlyList devices)
{
+ // Forget devices that dropped off the list so a later reconnect re-runs the one-time static
+ // enrichment (identity / root) instead of showing stale data.
+ if (_staticEnriched.Count > 0)
+ {
+ var present = new HashSet(devices.Select(d => d.Serial), StringComparer.Ordinal);
+ _staticEnriched.RemoveWhere(serial => !present.Contains(serial));
+ }
+
foreach (var device in devices)
{
// Fastboot devices have no OS to query over adb — read their fastboot getvar facts instead.
@@ -552,9 +572,10 @@ private async Task EnrichOneAsync(string serial)
var battery = await _deviceService.GetBatteryAsync(serial).ConfigureAwait(true);
card.ApplyBattery(battery);
- var due = !_staticEnrichedAt.TryGetValue(serial, out var last)
- || DateTimeOffset.Now - last > StaticRefreshInterval;
- if (due)
+ // Static identity + root are probed once per connection, not on a timer. Re-probing every
+ // device every few seconds was the main-page stall: root alone forks 6 adb processes per
+ // device, so N devices produced a periodic burst of dozens of concurrent adb calls.
+ if (_staticEnriched.Add(serial))
{
var overview = await _deviceService.GetOverviewAsync(serial).ConfigureAwait(true);
card.ApplyOverview(overview);
@@ -563,8 +584,6 @@ private async Task EnrichOneAsync(string serial)
card.ApplyRoot(root);
await card.RefreshActionStateAsync().ConfigureAwait(true);
-
- _staticEnrichedAt[serial] = DateTimeOffset.Now;
}
}
catch (Exception ex)
From d5c82c4d3ab7c1ef01b1f9a1e83716b260c88b3f Mon Sep 17 00:00:00 2001
From: backspace135 <46274807+backspace135@users.noreply.github.com>
Date: Thu, 9 Jul 2026 15:49:37 +0800
Subject: [PATCH 02/18] Bump version to 1.0.6
Sync version across AppInfo, App/Mini/Mini.Mac project metadata,
app.manifest, packaging scripts, wix, docs, and README.
---
.github/workflows/publish.yml | 2 +-
CLAUDE.md | 2 +-
README-CN.md | 14 ++++----
README.en.md | 14 ++++----
README.md | 14 ++++----
docs/app-contract.md | 2 +-
docs/packaging.md | 24 ++++++-------
docs/publishing.md | 34 +++++++++----------
docs/requirements-v1.md | 2 +-
packaging/AndroidTreeView.Package.wixproj | 6 ++--
packaging/Bundle.wxs | 6 ++--
packaging/Product.wxs | 4 +--
packaging/build-msi.ps1 | 4 +--
packaging/build-update-zip.ps1 | 2 +-
.../AndroidTreeView.App.csproj | 8 ++---
src/AndroidTreeView.App/app.manifest | 2 +-
src/AndroidTreeView.Core/AppInfo.cs | 2 +-
.../Interfaces/IUpdateService.cs | 2 +-
.../AndroidTreeView.Mini.Mac.csproj | 8 ++---
.../AndroidTreeView.Mini.csproj | 8 ++---
.../UpdateCheckResult.cs | 2 +-
21 files changed, 81 insertions(+), 81 deletions(-)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 98b3de5..af90abb 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
inputs:
version:
- description: "Release version, for example 1.0.5"
+ description: "Release version, for example 1.0.6"
required: true
type: string
diff --git a/CLAUDE.md b/CLAUDE.md
index 4dafdeb..1bdf281 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -152,7 +152,7 @@ Fixtures are **inline `const` strings** inside the test classes, not external `.
## Versioning & updates
Version is unified across runtime version, App/Mini assembly versions, manifests, and
-`packaging/build-update-zip.ps1` — keep them in sync when bumping (currently `1.0.5`). Update channels:
+`packaging/build-update-zip.ps1` — keep them in sync when bumping (currently `1.0.6`). Update channels:
`android-tree-view-app` (App) and `android-tree-view-mini` (Mini). `NekoIndexUpdateService` checks the
channel and compares semver; `UpdateInstaller` downloads, verifies SHA-256, unpacks the x64 ZIP, and runs
a local update script. Loose ZIPs without a supported `release.json` manifest are rejected.
diff --git a/README-CN.md b/README-CN.md
index d84bcdd..ce942d7 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -9,11 +9,11 @@
AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的 Windows 桌面工具。主程序负责设备总览、详情、投屏、基础工具和设置;Mini 版本保持独立运行,常驻监听设备并自动投屏。
-当前版本:**v1.0.5**。
+当前版本:**v1.0.6**。
## 产品样式展示
-
+
## 功能
@@ -61,13 +61,13 @@ ADB 安装与排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
./packaging/build-update-zip.ps1 -Product Mini -Rid win-x64
```
-默认产物版本是 `1.0.5`。只发布 x64 架构。验证输出包含主程序和 Mini 的上传 ZIP,例如:
+默认产物版本是 `1.0.6`。只发布 x64 架构。验证输出包含主程序和 Mini 的上传 ZIP,例如:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
```
更多细节见 [docs/packaging.md](docs/packaging.md)。
diff --git a/README.en.md b/README.en.md
index 3865168..c72aaae 100644
--- a/README.en.md
+++ b/README.en.md
@@ -9,11 +9,11 @@
AndroidTreeView is a desktop tool for inspecting, testing, and managing Android devices through ADB. The full app shows device cards, detail pages, mirroring, tools, settings, and updates. The Mini app stays resident, watches for devices, and starts mirroring automatically after authorization.
-Current version: **v1.0.5**. Current verification target: solution build passes, all tests pass, and GitHub Actions creates Windows x64 and macOS Apple Silicon ZIP artifacts for both App and Mini.
+Current version: **v1.0.6**. Current verification target: solution build passes, all tests pass, and GitHub Actions creates Windows x64 and macOS Apple Silicon ZIP artifacts for both App and Mini.
## Product Preview
-
+
## Features
@@ -78,7 +78,7 @@ See [docs/adb-requirements.md](docs/adb-requirements.md) for platform-tools setu
## Release ZIP Packaging
-The product version is currently `1.0.5` and is kept in sync across runtime version, App/Mini assembly metadata, manifest, and the ZIP build script. Official releases are produced only by the GitHub Actions `Publish` workflow.
+The product version is currently `1.0.6` and is kept in sync across runtime version, App/Mini assembly metadata, manifest, and the ZIP build script. Official releases are produced only by the GitHub Actions `Publish` workflow.
Local commands are for packaging validation only:
@@ -90,10 +90,10 @@ Local commands are for packaging validation only:
Example output:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
```
## Auto Update
diff --git a/README.md b/README.md
index 441c240..b2e08ac 100644
--- a/README.md
+++ b/README.md
@@ -9,11 +9,11 @@
AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的 Windows 桌面工具。主程序负责设备总览、详情、投屏、基础工具、设置和更新;Mini 版本保持独立运行,常驻监听设备,并在授权后自动启动投屏。
-当前版本:**v1.0.5**。当前验证目标:App 构建通过、Mini 构建通过、全量测试通过,打包链路能为 App 和 Mini 分别生成 x64 上传 ZIP。
+当前版本:**v1.0.6**。当前验证目标:App 构建通过、Mini 构建通过、全量测试通过,打包链路能为 App 和 Mini 分别生成 x64 上传 ZIP。
## 产品样式展示
-
+
## 核心能力
@@ -78,7 +78,7 @@ ADB 安装和排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
## Release ZIP 打包
-当前版本号统一为 `1.0.5`,运行时版本、App/Mini 程序集版本、manifest 和 `build-update-zip.ps1` 默认版本保持一致。正式发布只通过 GitHub Actions 的 `Publish` 工作流完成,发布只接受 x64。
+当前版本号统一为 `1.0.6`,运行时版本、App/Mini 程序集版本、manifest 和 `build-update-zip.ps1` 默认版本保持一致。正式发布只通过 GitHub Actions 的 `Publish` 工作流完成,发布只接受 x64。
本地命令仅用于验证打包链路:
@@ -90,10 +90,10 @@ ADB 安装和排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
示例输出:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
```
## 自动更新
diff --git a/docs/app-contract.md b/docs/app-contract.md
index 5e79538..0f9d351 100644
--- a/docs/app-contract.md
+++ b/docs/app-contract.md
@@ -2,7 +2,7 @@
This document records the App-layer contract for the full app and the Mini companion.
-Current product version: `1.0.5`.
+Current product version: `1.0.6`.
## Shared Services
diff --git a/docs/packaging.md b/docs/packaging.md
index 3cc2a6d..2c2d9f0 100644
--- a/docs/packaging.md
+++ b/docs/packaging.md
@@ -2,7 +2,7 @@
Packaging files live under `packaging/`.
-Current default product version: `1.0.5`.
+Current default product version: `1.0.6`.
Official release artifacts are created by GitHub Actions (`.github/workflows/publish.yml`) only. Local packaging commands are validation helpers and must not be used as the authority for publishing a release.
@@ -11,14 +11,14 @@ Official release artifacts are created by GitHub Actions (`.github/workflows/pub
Every GitHub Release must contain exactly these four ZIP packages plus matching `.sha256` sidecars:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-win-x64.zip.sha256
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip.sha256
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip.sha256
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip.sha256
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip.sha256
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip.sha256
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip.sha256
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip.sha256
```
Windows ZIPs contain the published application files, the platform-matched `scrcpy` bundle, and `release.json` at the ZIP root. macOS ZIPs contain a top-level `.app` bundle (`AndroidTreeView.app` or `AndroidTreeView Mini.app`); the published files, `scrcpy`, and `release.json` live inside the bundle.
@@ -74,7 +74,7 @@ The updater uses `release.json` to distinguish an automated release ZIP from a r
"product": "App",
"productName": "AndroidTreeView",
"appKey": "android-tree-view-app",
- "version": "1.0.5",
+ "version": "1.0.6",
"platform": "win",
"arch": "x64",
"rid": "win-x64",
@@ -102,8 +102,8 @@ The WiX project rejects non-x64 platforms.
`build-update-zip.ps1` writes checksums automatically. Manual verification:
```powershell
-Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-1.0.5-win-x64.zip
-Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-Mini-1.0.5-win-x64.zip
+Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-1.0.6-win-x64.zip
+Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-Mini-1.0.6-win-x64.zip
```
The sidecar uses ` *` format for compatibility with `sha256sum -c`.
diff --git a/docs/publishing.md b/docs/publishing.md
index 63585c5..cbe0b4f 100644
--- a/docs/publishing.md
+++ b/docs/publishing.md
@@ -2,7 +2,7 @@
Repository: `Birditch/AndroidTreeView`.
-Current product version: `1.0.5`.
+Current product version: `1.0.6`.
## Release Policy
@@ -37,24 +37,24 @@ Keep these values identical for every release:
- `src/AndroidTreeView.App/app.manifest` -> `assemblyIdentity version`
- `packaging/build-update-zip.ps1` -> default `Version`
-For `1.0.5`, release artifacts are named:
+For `1.0.6`, release artifacts are named:
```text
-AndroidTreeView-1.0.5-win-x64.zip
-AndroidTreeView-1.0.5-win-x64.zip.sha256
-AndroidTreeView-1.0.5-osx-arm64.zip
-AndroidTreeView-1.0.5-osx-arm64.zip.sha256
-AndroidTreeView-Mini-1.0.5-win-x64.zip
-AndroidTreeView-Mini-1.0.5-win-x64.zip.sha256
-AndroidTreeView-Mini-1.0.5-osx-arm64.zip
-AndroidTreeView-Mini-1.0.5-osx-arm64.zip.sha256
+AndroidTreeView-1.0.6-win-x64.zip
+AndroidTreeView-1.0.6-win-x64.zip.sha256
+AndroidTreeView-1.0.6-osx-arm64.zip
+AndroidTreeView-1.0.6-osx-arm64.zip.sha256
+AndroidTreeView-Mini-1.0.6-win-x64.zip
+AndroidTreeView-Mini-1.0.6-win-x64.zip.sha256
+AndroidTreeView-Mini-1.0.6-osx-arm64.zip
+AndroidTreeView-Mini-1.0.6-osx-arm64.zip.sha256
```
## Build Upload ZIP Packages
Official publication happens through the `Publish` GitHub Actions workflow:
-- push a tag named `v..`, for example `v1.0.5`
+- push a tag named `v..`, for example `v1.0.6`
- or run `workflow_dispatch` and provide the same version string
The workflow validates on Windows, verifies the pinned scrcpy release against the latest upstream release, builds the two Windows portable ZIPs and two macOS `.app` bundle ZIPs, writes SHA-256 sidecars, uploads workflow artifacts, and creates the GitHub Release.
@@ -77,8 +77,8 @@ The full app and Mini share the same update implementation but use different app
The Windows update channel must point each product key at its own Windows ZIP:
-- `android-tree-view-app` -> `AndroidTreeView-1.0.5-win-x64.zip`
-- `android-tree-view-mini` -> `AndroidTreeView-Mini-1.0.5-win-x64.zip`
+- `android-tree-view-app` -> `AndroidTreeView-1.0.6-win-x64.zip`
+- `android-tree-view-mini` -> `AndroidTreeView-Mini-1.0.6-win-x64.zip`
`NekoIndexUpdateService` queries the configured internal update API and compares the returned version with `AppInfo.Version`. `UpdateInstaller` downloads the package, verifies SHA-256 metadata when present, extracts the ZIP, verifies the portable Windows x64 manifest, and starts the automated update script.
@@ -97,7 +97,7 @@ Minimum response contract:
"data": {
"appKey": "android-tree-view-app",
"title": "AndroidTreeView",
- "version": "1.0.5",
+ "version": "1.0.6",
"zip": {
"sha256": "<64-character sha256>",
"downloadUrl": "/api/resources/android-tree-view-app/versions/latest/archive"
@@ -114,11 +114,11 @@ Minimum response contract:
Use this flow for the intranet update server:
-1. Push a `v1.0.5` tag or run the `Publish` GitHub Actions workflow manually.
+1. Push a `v1.0.6` tag or run the `Publish` GitHub Actions workflow manually.
2. Download the GitHub Actions-built Windows ZIP and `.sha256` sidecar for each Windows update channel.
3. Upload the ZIPs to the internal server storage.
-4. Configure `/api/update/android-tree-view-app/latest` to return `AndroidTreeView-1.0.5-win-x64.zip` metadata.
-5. Configure `/api/update/android-tree-view-mini/latest` to return `AndroidTreeView-Mini-1.0.5-win-x64.zip` metadata.
+4. Configure `/api/update/android-tree-view-app/latest` to return `AndroidTreeView-1.0.6-win-x64.zip` metadata.
+5. Configure `/api/update/android-tree-view-mini/latest` to return `AndroidTreeView-Mini-1.0.6-win-x64.zip` metadata.
6. Confirm the `sha256` value matches the uploaded ZIP.
7. Use the app "Check for updates" action to test download, validation, replacement, cleanup, and restart.
diff --git a/docs/requirements-v1.md b/docs/requirements-v1.md
index bf4147f..179ee4a 100644
--- a/docs/requirements-v1.md
+++ b/docs/requirements-v1.md
@@ -1,6 +1,6 @@
# AndroidTreeView Requirements Addendum
-Current product version: `1.0.5`.
+Current product version: `1.0.6`.
This addendum extends `docs/architecture.md` and reflects the current App + Mini direction.
diff --git a/packaging/AndroidTreeView.Package.wixproj b/packaging/AndroidTreeView.Package.wixproj
index bcf193e..27147e4 100644
--- a/packaging/AndroidTreeView.Package.wixproj
+++ b/packaging/AndroidTreeView.Package.wixproj
@@ -9,7 +9,7 @@
app and then invokes this project with the right properties, e.g.:
dotnet build packaging/AndroidTreeView.Package.wixproj -c Release ^
- -p:Platform=x64 -p:ProductVersion=1.0.5 -p:SelfContained=false ^
+ -p:Platform=x64 -p:ProductVersion=1.0.6 -p:SelfContained=false ^
-p:PublishDir=
Requires the WiX v5 SDK, restored automatically from nuget.org via the Sdk attribute.
@@ -23,7 +23,7 @@
- 1.0.5
+ 1.0.6AndroidTreeViewAndroidTreeView.App.exe{6C0B8E2A-4F1D-4B7C-9A3E-2D5F8B1C0E4A}
@@ -41,7 +41,7 @@
harvest. build-msi.ps1 sets this; the default is only for manual runs. -->
$(MSBuildThisFileDirectory)..\artifacts\publish\$(Platform)\
-
+
$(ProductArtifactName)-$(ProductVersion)-$(Platform)Packagefalse
diff --git a/packaging/Bundle.wxs b/packaging/Bundle.wxs
index 45dd3b6..e305dad 100644
--- a/packaging/Bundle.wxs
+++ b/packaging/Bundle.wxs
@@ -8,13 +8,13 @@
Build after the MSI has been produced:
wix build packaging/Bundle.wxs -arch x64 ^
- -d ProductVersion=1.0.5 ^
+ -d ProductVersion=1.0.6 ^
-d Platform=x64 ^
- -d MsiPath=artifacts\AndroidTreeView-1.0.5-x64.msi ^
+ -d MsiPath=artifacts\AndroidTreeView-1.0.6-x64.msi ^
-d DotNetRuntimeExe=C:\path\windowsdesktop-runtime-10.0.x-win-x64.exe ^
-ext WixToolset.BootstrapperApplications.wixext ^
-ext WixToolset.Netfx.wixext ^
- -o artifacts\AndroidTreeView-1.0.5-x64-setup.exe
+ -o artifacts\AndroidTreeView-1.0.6-x64-setup.exe
-->
-
+
diff --git a/packaging/build-msi.ps1 b/packaging/build-msi.ps1
index d40e71c..3266be6 100644
--- a/packaging/build-msi.ps1
+++ b/packaging/build-msi.ps1
@@ -27,7 +27,7 @@
Build configuration. Default: Release.
.PARAMETER Version
- Product version. Default: 1.0.5. Must match AndroidTreeView.Core.AppInfo.Version.
+ Product version. Default: 1.0.6. Must match AndroidTreeView.Core.AppInfo.Version.
.EXAMPLE
./build-msi.ps1 -Product App -Arch x64
@@ -49,7 +49,7 @@ param(
[string]$Configuration = 'Release',
- [string]$Version = '1.0.5'
+ [string]$Version = '1.0.6'
)
$ErrorActionPreference = 'Stop'
diff --git a/packaging/build-update-zip.ps1 b/packaging/build-update-zip.ps1
index 0b47896..e5f807f 100644
--- a/packaging/build-update-zip.ps1
+++ b/packaging/build-update-zip.ps1
@@ -27,7 +27,7 @@ param(
[string]$Configuration = 'Release',
- [string]$Version = '1.0.5'
+ [string]$Version = '1.0.6'
)
$ErrorActionPreference = 'Stop'
diff --git a/src/AndroidTreeView.App/AndroidTreeView.App.csproj b/src/AndroidTreeView.App/AndroidTreeView.App.csproj
index 94aded3..de7dc78 100644
--- a/src/AndroidTreeView.App/AndroidTreeView.App.csproj
+++ b/src/AndroidTreeView.App/AndroidTreeView.App.csproj
@@ -8,10 +8,10 @@
Assets/atv-icon.icotruetrue
- 1.0.5
- 1.0.5.0
- 1.0.5.0
- 1.0.5
+ 1.0.6
+ 1.0.6.0
+ 1.0.6.0
+ 1.0.6false
diff --git a/src/AndroidTreeView.App/app.manifest b/src/AndroidTreeView.App/app.manifest
index 5f15658..41e62b4 100644
--- a/src/AndroidTreeView.App/app.manifest
+++ b/src/AndroidTreeView.App/app.manifest
@@ -1,6 +1,6 @@
-
+ AndroidTreeView.App.mini
- 1.0.5
- 1.0.5.0
- 1.0.5.0
- 1.0.5
+ 1.0.6
+ 1.0.6.0
+ 1.0.6.0
+ 1.0.6falseAssets\atv-icon.ico
diff --git a/src/AndroidTreeView.Models/UpdateCheckResult.cs b/src/AndroidTreeView.Models/UpdateCheckResult.cs
index e1e494c..bc6c9b3 100644
--- a/src/AndroidTreeView.Models/UpdateCheckResult.cs
+++ b/src/AndroidTreeView.Models/UpdateCheckResult.cs
@@ -31,7 +31,7 @@ public enum UpdateCheckStatus
///
public sealed class UpdateCheckResult
{
- /// The version currently running (e.g. 1.0.5).
+ /// The version currently running (e.g. 1.0.6).
public required string CurrentVersion { get; init; }
/// The latest published version, when known.
From 8564eca61f1fa7740ac78ecca89c25a2617fa304 Mon Sep 17 00:00:00 2001
From: backspace135 <46274807+backspace135@users.noreply.github.com>
Date: Thu, 9 Jul 2026 15:54:42 +0800
Subject: [PATCH 03/18] docs: reflect macOS support in Chinese READMEs
- Platform badge Windows -> Windows + macOS
- Describe as cross-platform (Windows + macOS Apple Silicon), not Windows-only
- Generalize the usage section and add a macOS subsection (Terminal.app CLI,
drag to /Applications, Gatekeeper). English README already covered this.
---
README-CN.md | 12 +++++++++---
README.md | 13 ++++++++++---
2 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/README-CN.md b/README-CN.md
index ce942d7..df1c3cf 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -3,11 +3,11 @@
[](LICENSE)
[](https://dotnet.microsoft.com/)
[](https://avaloniaui.net/)
-[](#windows-使用说明)
+[](#使用说明)
[主文档](README.md) | **简体中文** | [English](README.en.md)
-AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的 Windows 桌面工具。主程序负责设备总览、详情、投屏、基础工具和设置;Mini 版本保持独立运行,常驻监听设备并自动投屏。
+AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的桌面工具,支持 Windows 与 macOS(Apple Silicon)。主程序负责设备总览、详情、投屏、基础工具和设置;Mini 版本保持独立运行,常驻监听设备并自动投屏。
当前版本:**v1.0.6**。
@@ -36,13 +36,19 @@ dotnet run --project src/AndroidTreeView.App
dotnet run --project src/AndroidTreeView.Mini
```
-## Windows 使用说明
+## 使用说明
1. 安装 Android platform-tools,或让应用在启动时引导选择 `adb.exe`。
2. 在手机上开启开发者选项和 USB 调试。
3. 连接手机并允许 USB 调试授权。
4. 主程序会显示设备卡片;Mini 会自动监听并启动投屏。
+### macOS 说明
+
+- 从 Release 下载 `AndroidTreeView-<版本>-osx-arm64.zip`,解压后将 `AndroidTreeView.app` 拖入 `/Applications`。
+- 首次打开若被 Gatekeeper 拦截,右键「打开」,或执行 `xattr -dr com.apple.quarantine AndroidTreeView.app` 放行。
+- 设备卡片的「CLI 终端」在 macOS 上通过 Terminal.app 打开,编号菜单与 Windows 一致。
+
ADB 安装与排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
## 自动更新
diff --git a/README.md b/README.md
index b2e08ac..7fc49e1 100644
--- a/README.md
+++ b/README.md
@@ -3,11 +3,11 @@
[](LICENSE)
[](https://dotnet.microsoft.com/)
[](https://avaloniaui.net/)
-[](#windows-使用说明)
+[](#使用说明)
**简体中文** | [English](README.en.md) | [中文副本](README-CN.md)
-AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的 Windows 桌面工具。主程序负责设备总览、详情、投屏、基础工具、设置和更新;Mini 版本保持独立运行,常驻监听设备,并在授权后自动启动投屏。
+AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的桌面工具,支持 Windows 与 macOS(Apple Silicon)。主程序负责设备总览、详情、投屏、基础工具、设置和更新;Mini 版本保持独立运行,常驻监听设备,并在授权后自动启动投屏。
当前版本:**v1.0.6**。当前验证目标:App 构建通过、Mini 构建通过、全量测试通过,打包链路能为 App 和 Mini 分别生成 x64 上传 ZIP。
@@ -38,6 +38,7 @@ src/
AndroidTreeView.Shared
AndroidTreeView.App
AndroidTreeView.Mini
+ AndroidTreeView.Mini.Mac
tests/
AndroidTreeView.*.Tests
packaging/
@@ -69,13 +70,19 @@ dotnet run --project src/AndroidTreeView.Mini
ADB 安装和排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
-## Windows 使用说明
+## 使用说明
1. 启动 AndroidTreeView。
2. 连接已开启并授权 USB 调试的 Android 设备。
3. 使用设备卡片查看状态、投屏、打开 CLI 或执行非破坏性工具。
4. 通过设置或关于页检查并安装更新。
+### macOS 说明
+
+- 从 Release 下载 `AndroidTreeView-<版本>-osx-arm64.zip`,解压后将 `AndroidTreeView.app` 拖入 `/Applications`。
+- 首次打开若被 Gatekeeper 拦截(未签名),右键选择「打开」,或执行 `xattr -dr com.apple.quarantine AndroidTreeView.app` 放行。
+- 设备卡片的「CLI 终端」在 macOS 上通过 Terminal.app 打开,提供与 Windows 一致的编号菜单(设备信息 / adb shell / logcat / 重启 / 关机 等)。
+
## Release ZIP 打包
当前版本号统一为 `1.0.6`,运行时版本、App/Mini 程序集版本、manifest 和 `build-update-zip.ps1` 默认版本保持一致。正式发布只通过 GitHub Actions 的 `Publish` 工作流完成,发布只接受 x64。
From b596ebd90335df48f468be638c703314736bde50 Mon Sep 17 00:00:00 2001
From: backspace135 <46274807+backspace135@users.noreply.github.com>
Date: Thu, 9 Jul 2026 16:42:44 +0800
Subject: [PATCH 04/18] =?UTF-8?q?docs:=20=E5=8D=8A=E8=87=AA=E5=8A=A8=20Roo?=
=?UTF-8?q?t=20=E5=8A=9F=E8=83=BD=E8=AE=BE=E8=AE=A1=E8=A7=84=E6=A0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
分步向导:上传刷机包→提取 boot.img→手机端官方 Magisk 修补→进 bootloader→flash boot,写操作前强制确认+自动备份原始 boot。双平台(Windows/macOS)。
---
.../specs/2026-07-09-semi-auto-root-design.md | 276 ++++++++++++++++++
1 file changed, 276 insertions(+)
create mode 100644 docs/superpowers/specs/2026-07-09-semi-auto-root-design.md
diff --git a/docs/superpowers/specs/2026-07-09-semi-auto-root-design.md b/docs/superpowers/specs/2026-07-09-semi-auto-root-design.md
new file mode 100644
index 0000000..9334615
--- /dev/null
+++ b/docs/superpowers/specs/2026-07-09-semi-auto-root-design.md
@@ -0,0 +1,276 @@
+# 半自动 Root 功能 — 设计规格
+
+- 日期:2026-07-09
+- 状态:已通过设计评审,待用户审阅规格文档
+- 关联记忆:`root-feature-relaxes-safety`
+
+## 1. 目标与范围
+
+为 AndroidTreeView 增加一个**半自动 Root 功能**:用户手动上传刷机包,工具自动提取
+`boot.img`、用官方 Magisk 组件修补、引导进入 bootloader、把修补后的 `boot.img` 刷入设备。
+以**分步向导**形态交付,写操作前强制确认,刷入前自动备份原始 `boot.img`。
+
+### 1.1 第一版明确要做
+
+- 上传刷机包(本地文件选择)。
+- 提取 `boot.img`:
+ - zip 内含裸 `boot.img`(含 Pixel factory 的嵌套 `image-*.zip`)。
+ - `payload.bin`(A/B OTA),通过打包的 `payload_dumper` 解出。
+- Magisk 修补:工具自带官方 Magisk 组件(各 ABI),push 到手机 `/data/local/tmp`,执行官方
+ `boot_patch.sh` 完成修补,pull 回修补后镜像。
+- 写操作链路:`adb push` → `adb reboot bootloader` → `fastboot flash boot` → `fastboot reboot`。
+- 只读检测:`fastboot getvar unlocked`(解锁状态)、设备 ABI 探测。
+- **刷入前自动备份原始未修补 `boot.img`** 到本地用户目录。
+- 平台:**Windows + macOS 双平台**,共用同一套 MVVM 代码。
+
+### 1.2 第一版明确不做(后续再议)
+
+- `fastboot flashing unlock`(解锁 bootloader,会抹数据)——**仅检测状态 + 文字引导,不代执行**。
+- 刷其他分区(system / vbmeta / recovery / super 等)。
+- 卸载 root / 一键恢复原厂 boot(仅提供备份文件与引导,不做自动回刷向导)。
+- 厂商私有包格式(`.ozip`、动态分区 super.img 拆分等)。
+
+## 2. 方向性决策(已与用户确认)
+
+| 决策 | 选择 | 理由 |
+|---|---|---|
+| 与"只读/安全"定位冲突 | **放宽定位**,接受工具含刷机写操作 | 用户明确要 fastboot 刷入的半自动 root |
+| 支持平台 | **Windows + macOS 双平台** | 两平台都要能真正刷机 |
+| Magisk 修补执行位置 | **手机端执行官方 magiskboot/组件** | 官方 Android 二进制天然可跑,比桌面第三方二进制可靠 |
+| 自动化程度 | **分步向导 + 写操作前强制确认** | 变砖风险高,不做无人值守一键刷 |
+| 包格式范围 | **zip(含 Pixel 嵌套) + payload.bin** | 覆盖 factory image 与现代 A/B OTA |
+| 工具打包 | **构建时按平台下载打包进 `tools/`** | 对齐现有 scrcpy/adb 打包,开箱即用 |
+| UI 落点 | **App 新导航页,扩展 App 到 macOS** | Avalonia 本就跨平台,双平台共用 MVVM |
+| 修补第 4 步实现 | **自带完整 Magisk 组件跑官方 `boot_patch.sh`** | 最贴近官方;实机验证列为高风险里程碑 |
+
+## 3. 可行性结论
+
+核心链路技术上全部可实现,跨平台可行。真正的风险**不在技术**,而在两个工具无法根除、
+必须由用户承担的前提:
+
+- **前提 1 — bootloader 已解锁**:未解锁则 `fastboot flash` 必然失败。工具只检测并引导,
+ 不自动解锁(解锁抹数据、需在手机上按键、部分厂商需解锁码)。
+- **前提 2 — 包与设备匹配**:刷错版本/架构可能变砖。工具做基本校验与警告,但无法保证包正确。
+
+设计上以**分步向导 + 强制确认 + 自动备份原始 boot** 将风险降到可控。
+
+**最高技术不确定性**:Magisk 修补的第 4 步(执行 `boot_patch.sh`)不是单条命令,需一并打包
+`magiskinit` / `magisk` 并复现官方脚本流程,**必须实机验证**(见 §8 里程碑)。
+
+## 4. 分层与工程落点
+
+严格遵循项目分层(下层不引用上层):
+
+```
+Models FlashPackage, BootImageInfo(Path, Source, OriginalPackageName),
+ RootWizardState(enum), DeviceFastbootStatus, PackageType(enum)
+ ↑
+Core 接口:IBootImageExtractor, IMagiskPatcher, IFastbootService, IRootWizardService
+ IFastbootEnvironment(对齐 IAdbEnvironment),RootException 类型
+ ↑
+Adb / Infrastructure
+ Adb:FastbootService(ProcessRunner)、FastbootLocator、
+ BootImageExtractor(zip + payload.bin)、MagiskPatcher(push+执行)、
+ Parsers:FastbootVarParser、PackageTypeDetector、CpuAbiParser
+ Infra:BootBackupService(备份原始 boot 到用户目录)
+ ↑
+Shared AddAndroidTreeViewSharedServices() 内 TryAdd 注册全部新服务
+ ↑
+App RootWizardViewModel + RootWizardView(新导航页,分步向导)
+```
+
+- 所有 fastboot/adb 写操作走 `ProcessRunner`(异步、可取消、杀进程树)。
+- 解析类为纯函数放 `AndroidTreeView.Adb.Parsers`,配套解析测试(项目硬规则)。
+- 工具二进制通过新建 `build/AndroidTreeView.RootTools.targets` 按平台下载打包进 `tools/`。
+
+## 5. 向导状态机与流程
+
+### 5.1 状态枚举 `RootWizardState`
+
+```
+Idle 初始
+PackageSelected 已选刷机包
+Extracting 提取 boot.img 中(自动)
+BootExtracted 已提取(含来源:PlainZip / NestedZip / Payload)
+Patching 修补中(push magiskboot → 手机执行 boot_patch.sh → pull 回)
+BootPatched 已得修补后 boot + 已本地备份原始 boot
+AwaitingBootloaderConfirm ⛔ 强制确认点 1:即将重启进 bootloader
+RebootingToBootloader adb reboot bootloader(自动)
+InFastboot 已进 fastboot,检测解锁状态
+Blocked_Locked 未解锁:引导用户,终止自动流程(可重新检测)
+AwaitingFlashConfirm ⛔ 强制确认点 2:即将 flash boot
+Flashing fastboot flash boot(自动)
+Rebooting fastboot reboot(自动)
+Completed 完成
+Failed 任一步失败(带错误信息 + 重试当前步 / 中止)
+```
+
+### 5.2 Happy Path 时序
+
+```
+选包 → [自动]提取 → [自动]修补+备份 → ⛔确认1 → [自动]进bootloader
+ → 检测解锁 → ⛔确认2 → [自动]flash → [自动]reboot → 完成
+```
+
+### 5.3 两个强制确认点
+
+1. **确认 1(进 bootloader 前)**:提示手机将重启进 fastboot、保持数据线稳定。
+2. **确认 2(flash 前)**:显示目标分区 `boot`、修补后镜像、**原始 boot 备份路径**、
+ 红色"刷错可能变砖"警告;用户勾选"我已了解风险"后方可点"刷入"。
+
+### 5.4 关键分支
+
+- **Blocked_Locked(未解锁)**:向导停止,显示解锁引导(开发者选项 OEM 解锁、
+ `fastboot flashing unlock` 会抹数据、部分厂商需解锁码)。**工具不代执行解锁**。
+ 用户自行解锁后可"重新检测"继续。
+- **Failed(任一步失败)**:显示该步 stderr/错误摘要(映射成友好文案),提供"重试当前步"或
+ "中止"。中止不留危险中间态(若已在 fastboot,引导用户 `fastboot reboot` 回系统)。
+- **可取消**:每个自动步走 `CancellationToken`,用户可随时取消(取消后进入 Failed/可重试)。
+
+## 6. 核心机制
+
+### 6.1 boot.img 提取(`BootImageExtractor`)
+
+输入包文件,按类型分派,输出 `boot.img` 本地路径 + 来源标记
+(`BootImageInfo { Path, Source, OriginalPackageName }`)。
+
+**包类型判定**(纯函数 `PackageTypeDetector`,读文件头/枚举 zip 条目):
+
+- zip 顶层有 `boot.img` → **PlainZip**
+- zip 含 `image-*.zip`(Pixel factory)→ **NestedZip**(解一层内层 zip 再取 `boot.img`)
+- zip 含 `payload.bin` → **Payload**
+- 裸 `payload.bin` 文件 → **Payload**
+- 都不匹配 → 抛 `RootException`,友好提示"未在包内找到 boot.img"
+
+**提取**:
+
+- PlainZip / NestedZip → 纯 C# `System.IO.Compression`,无外部依赖。
+- Payload → 调用打包的 `payload_dumper`(走 `ProcessRunner`)解出 `boot` 分区。
+- 输出到工作区 `~/.androidtreeview/root-work//`。
+
+**可测试性**:`PackageTypeDetector` 纯函数(zip 条目列表/文件头样本做测试);payload 解包用假
+`ProcessRunner` 测试流程/错误。
+
+### 6.2 Magisk 修补(`MagiskPatcher`,手机端执行官方组件)
+
+前提:设备已连接、adb 已授权(复用现有 `DeviceMonitor` / 设备状态)。
+
+1. **探测 ABI**:`adb shell getprop ro.product.cpu.abi`(纯函数 `CpuAbiParser`),据此选
+ `tools/magisk//` 下对应二进制。
+2. **push 组件与镜像**到 `/data/local/tmp/atv_root/`:
+ `magiskboot`、`magiskinit`、`magisk`、`boot_patch.sh`、`boot.img`;`chmod 755` 可执行文件。
+3. **本地备份原始 boot.img**(`BootBackupService`):复制到
+ `~/.androidtreeview/root-backups/--boot-original.img`,路径供确认 2 显示。
+4. **修补**:在手机上执行官方 `boot_patch.sh`(内部调用 `magiskboot unpack/cpio/repack` +
+ `magiskinit`/`magisk`),产出 `new-boot.img`。
+5. **pull 回**:`adb pull .../new-boot.img /boot-patched.img`。
+6. **清理**手机临时目录。
+
+> ⚠️ 第 4 步为最高技术不确定性环节,需实机验证(见 §8)。
+
+### 6.3 fastboot 服务(`FastbootService`,走 `ProcessRunner`,全部 async + CancellationToken)
+
+| 方法 | 命令 | 说明 |
+|---|---|---|
+| `RebootToBootloaderAsync` | `adb reboot bootloader` | 从系统进 fastboot |
+| `WaitForFastbootDeviceAsync` | `fastboot devices` 轮询 | 等设备在 fastboot 出现(带超时) |
+| `GetUnlockStatusAsync` | `fastboot getvar unlocked` | 只读,`FastbootVarParser` 解析 `unlocked: yes/no` |
+| `FlashBootAsync` | `fastboot flash boot ` | 核心写操作 |
+| `RebootAsync` | `fastboot reboot` | 刷完回系统 |
+
+解析 `getvar` / `fastboot devices` 输出为纯函数 + 解析测试。
+
+### 6.4 fastboot 定位(`FastbootLocator` + `IFastbootEnvironment`)
+
+对齐 `AdbLocator` / `IAdbEnvironment`:配置路径 → 打包
+`tools/platform-tools//fastboot` → PATH → 常见 SDK 位置。单例持有当前路径。缺失时向导页
+显示"未找到 fastboot"引导(类比 App 现有 adb 缺失 Setup 页),不崩溃。
+
+### 6.5 备份服务(`BootBackupService`,Infrastructure 层)
+
+- 修补前把原始 boot.img 存到 `~/.androidtreeview/root-backups/`,文件名含序列号 + 时间戳。
+- 提供"列出/打开备份目录"给 UI,便于变砖时找回。
+
+## 7. 工具打包 — `build/AndroidTreeView.RootTools.targets`
+
+仿 `AndroidTreeView.Scrcpy.targets`,构建/发布时按平台下载并打包:
+
+```
+tools/
+ platform-tools/
+ win-x64/ fastboot.exe (+ adb.exe)
+ osx-x64/ fastboot
+ osx-arm64/ fastboot
+ payload-dumper/
+ win-x64/ osx-x64/ osx-arm64/ payload_dumper
+ magisk/
+ arm64-v8a/ armeabi-v7a/ x86_64/ x86/
+ magiskboot magiskinit magisk
+ boot_patch.sh
+```
+
+- **需支持 macOS 平台产出**(现有 scrcpy 仅 Windows 打包)。
+- 下载源写明 URL + SHA-256 校验,文档登记版本,对齐项目"下载工具需校验 SHA-256"风格:
+ - platform-tools:Google 官方 dl 链接。
+ - magisk 组件:官方 Magisk GitHub release 的 APK(zip)抽取各 ABI。
+ - payload_dumper:可信开源发布。
+- 新增 `tools/verify-roottools-latest.ps1`(仿 `verify-scrcpy-latest.ps1`)检查上游新版本。
+
+## 8. 高风险里程碑(实机验证)
+
+以下无法单测,须实机验证并在开发中预留调整轮次:
+
+1. **`boot_patch.sh` 修补链路** — push 完整 Magisk 组件后能在真机产出可用 `new-boot.img`。
+2. **真实 flash** — `fastboot flash boot` 在已解锁真机上成功且能正常开机。
+3. **payload.bin 解包** — `payload_dumper` 在三种 RID 上均能正确解出 boot 分区。
+4. **跨平台 fastboot/adb** — Windows 与 macOS(x64/arm64) 下二进制均能定位与执行。
+
+## 9. App UI
+
+- 新导航页,命名遵循 ViewLocator 约定(`RootWizardViewModel` → `RootWizardView`),严格 MVVM
+ (`[ObservableProperty]` / `[RelayCommand]`),编译绑定 `x:DataType`。
+- 侧栏新增导航项(新 loc key `nav.root`)。页面:顶部步骤条反映 `RootWizardState`,中间当前步
+ 内容/进度/日志,底部操作按钮(下一步/确认/取消/重试)。
+- 两个强制确认为醒目对话框 + 复选框(未勾选"我已了解风险"则"刷入"禁用)。
+- flash 步骤用红色警告样式,与只读功能区分。
+- `RootWizardViewModel` 仅编排 `IRootWizardService`,负责状态→UI 映射、UI 线程 marshal
+ (`Dispatcher.UIThread.Post`)、错误映射成友好 `ErrorMessage`(App 永不因正常错误崩溃)。
+- macOS 支持:让 App 能在 macOS 构建/运行,Root 页双平台共用同一 MVVM 代码。
+
+## 10. 本地化
+
+- 所有向导文案、按钮、警告、错误映射、引导说明新增 key 到**两个 ResX 都加**
+ (`Strings.resx` 英文 + `Strings.zh-Hans.resx` 中文),键集保持一致。
+- 命名示例:`root.wizard.title`、`root.step.extract`、`root.confirm.flash.warning`、
+ `root.blocked.locked.guide`、`root.error.*`。
+- XAML 用 `{loc:Localize Key=...}`,VM 用 `_localization.Get/Format`。
+
+## 11. 测试
+
+- **Adb.Tests** 新增:
+ - `Parsers/FastbootVarParserTests`(`getvar unlocked` / `fastboot devices`)
+ - `Parsers/PackageTypeDetectorTests`(zip 条目 → 包类型)
+ - `Parsers/CpuAbiParserTests`(`ro.product.cpu.abi` 解析)
+ - `Commands/`:fastboot/adb argv 构建测试
+ - `Services/`:`BootImageExtractor`、`MagiskPatcher`、`FastbootService`(假 `ProcessRunner`,
+ 覆盖流程与错误路径)
+- **App.Tests** 新增:`RootWizardViewModel` 状态机推进、确认门控(未勾选不能 flash)、错误映射、
+ 未解锁→Blocked 分支;DI 图解析新服务(`ServiceGraphTests`)。
+- **实机验证**(§8):无法单测,作为手动验证清单。
+
+## 12. CLAUDE.md 更新
+
+因放宽只读定位,实现时需:
+
+- 修改"Read-only / safe by design"条目:说明工具现含**受控、需多重确认的刷机写操作**(仅
+ Root 向导内 boot 刷入链路),其余功能仍保持只读;解锁 bootloader 仍不自动执行。
+- Architecture / Bundled tools 段补充 fastboot、magisk 组件、payload_dumper 打包说明。
+- 版本按发布规则统一 bump。
+
+## 13. 明确的非目标(YAGNI)
+
+- 不做 bootloader 自动解锁。
+- 不做 boot 以外分区刷写。
+- 不做一键恢复/卸载 root 向导(仅备份 + 引导)。
+- 不支持厂商私有包格式。
+- 不引入桌面版第三方 `magiskboot`(改用手机端官方组件)。
From 5ea42a0e8af19629a2a14e86673dc3707e9c2559 Mon Sep 17 00:00:00 2001
From: Birditch <58v5tb4sjt@privaterelay.appleid.com>
Date: Thu, 9 Jul 2026 16:51:00 +0800
Subject: [PATCH 05/18] Clean up macOS CLI PR
---
CLAUDE.md | 163 ------------------
.../Controls/SkeletonBlock.axaml | 41 ++---
.../Controls/SkeletonBlock.axaml.cs | 15 --
.../Services/CliLauncher.cs | 12 +-
.../ViewModels/MainWindowViewModel.cs | 43 ++---
5 files changed, 40 insertions(+), 234 deletions(-)
delete mode 100644 CLAUDE.md
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 1bdf281..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,163 +0,0 @@
-# CLAUDE.md
-
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
-
-## What this is
-
-AndroidTreeView is a **.NET 10 + Avalonia** Windows desktop tool for inspecting, testing and managing
-Android devices over ADB. Despite the name it is a C#/.NET solution, **not** a Java/Kotlin Android app.
-It has two shipping executables that share the same core:
-
-- **App** (`src/AndroidTreeView.App`) — the full Avalonia UI: device overview, per-category details,
- screen mirroring (scrcpy), CLI tools, settings, updates.
-- **Mini** (`src/AndroidTreeView.Mini`) — a lightweight WinForms tray/monitor app that stays resident,
- watches for devices, and auto-launches scrcpy once a device is connected and authorized. Mini
- deliberately does **not** carry the Avalonia/Skia runtime.
-
-`Mini.Mac` is an Avalonia-based Mini variant for macOS.
-
-## Commands
-
-Requires the **.NET 10 SDK**. Run from the repo root.
-
-```bash
-dotnet restore AndroidTreeView.sln
-dotnet build AndroidTreeView.sln
-dotnet test AndroidTreeView.sln
-dotnet run --project src/AndroidTreeView.App
-dotnet run --project src/AndroidTreeView.Mini
-```
-
-Building on non-Windows (App/Mini target Windows) needs the targeting pack flag — CI uses it everywhere:
-
-```bash
-dotnet build AndroidTreeView.sln -p:EnableWindowsTargeting=true
-```
-
-Run a single test project / a single test:
-
-```bash
-dotnet test tests/AndroidTreeView.Adb.Tests
-dotnet test AndroidTreeView.sln --filter "FullyQualifiedName~BatteryParser"
-```
-
-Format check (CI runs this, non-blocking — but keep it clean):
-
-```bash
-dotnet format AndroidTreeView.sln --verify-no-changes
-```
-
-Package/verify the release ZIP link locally (real releases only ship via the GitHub `Publish` workflow,
-x64 only):
-
-```powershell
-./packaging/build-update-zip.ps1 -Product App -Rid win-x64
-./packaging/build-update-zip.ps1 -Product Mini -Rid win-x64
-```
-
-There are ready-made Rider/VS run configs under `.run/`.
-
-## Architecture
-
-The layering is strict and enforced by project references — respect the dependency direction. Lower
-layers never reference upper ones.
-
-```
-Models domain records/enums, no project deps
- ↑
-Core interfaces, options (AppSettings, AdbOptions), typed exceptions, DeviceMonitor
- ↑
-Adb / Infrastructure Adb: ProcessRunner, command builders, parsers, AdbDeviceService, LogcatService
- ↑ Infrastructure: SettingsService (JSON), logging, update check/install
-Shared the shared composition root — AddAndroidTreeViewSharedServices() wires ADB, monitoring,
- ↑ scrcpy, settings, and update services identically for both App and Mini
-App / Mini / Mini.Mac executables (each references Shared)
-```
-
-Key architectural rules (see `docs/architecture.md` for the full binding type/interface contract, and
-`docs/app-contract.md`):
-
-- **Shared is the single wiring point.** Both App and Mini call
- `AddAndroidTreeViewSharedServices(...)` so their ADB/scrcpy/settings/update behavior can't drift.
- When adding a service that both should use, register it there (it uses `TryAdd*`), not per-app.
-- **App is strict MVVM** with CommunityToolkit.Mvvm (`[ObservableProperty]` / `[RelayCommand]`) and
- `Microsoft.Extensions` DI/Hosting/Logging. `Program.cs` builds the host + `IServiceProvider`, then
- Avalonia resolves `MainWindowViewModel`. `ViewLocator` maps `*.ViewModels.XxxViewModel` →
- `*.Views.XxxView` by name convention.
-- **All ADB is out-of-process** via `ProcessRunner` (async stdout/stderr, kills the whole process tree
- on cancel/timeout). Device info comes from parsing ADB text output.
-- **Parsers are pure and unit-tested.** ADB output parsing lives in `AndroidTreeView.Adb.Parsers` as
- stateless/static functions (`GetPropParser`, `BatteryParser`, `AdbDevicesParser`, etc.). Any change to
- parsing logic must come with a parser test — fixtures live under the Adb.Tests project.
-- `AdbLocator` resolves adb (configured path → PATH → common SDK locations); `IAdbEnvironment` holds the
- current location as a singleton. If adb is missing the App shows a Setup page instead of crashing.
-- `DeviceMonitor` owns a `PeriodicTimer` background loop and raises `DevicesChanged`; VMs marshal to the
- UI thread themselves.
-
-## Conventions (from `.editorconfig` + `CONTRIBUTING.md`)
-
-- File-scoped namespaces, 4-space indent, interfaces `I`-prefixed, `using` outside the namespace,
- CRLF line endings, UTF-8.
-- **Async-only for I/O**: `async` + `CancellationToken` propagated everywhere. No `.Result` / `.Wait()` /
- `Task.Run` for ADB/process work. UI mutations only on the UI thread (`Dispatcher.UIThread.Post`).
-- Views use **compiled bindings** (`x:DataType`) and bind only to members that exist on the VM. No
- business logic or ADB calls in views.
-- **No hardcoded user-facing strings.** Use localization for every user-visible string (see below).
-- **Read-only / safe by design**: do not introduce ADB commands that modify, wipe, or flash a device.
-- Keep files under ~400 lines; the App never crashes on normal ADB/device errors — map them to friendly
- `ErrorMessage` state.
-
-## Localization
-
-Every user-facing string is localized — never hardcode display text. Default language is **Simplified
-Chinese**; English is the neutral/fallback resource.
-
-- **Contract**: `ILocalizationService` (`AndroidTreeView.Core/Interfaces`) — `Get(key)`, `Format(key, args)`,
- the `this[key]` indexer, `SetLanguage(AppLanguage)`, and a `LanguageChanged` event. `AppLanguage` lives
- in `AndroidTreeView.Core/Options/SettingsEnums.cs`. `Get` returns the key itself if a resource is
- missing, so a missing translation is visible, not a crash.
-- **Implementation**: `LocalizationService` in `src/AndroidTreeView.App/Localization`, backed by two ResX
- files with **identical key sets** (keep them in sync — currently 215 keys each):
- - `src/AndroidTreeView.App/Resources/Strings.resx` — English (neutral fallback)
- - `src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx` — Simplified Chinese (default)
- - Keys are dotted/namespaced: `app.title`, `nav.devices`, `common.refresh`, etc.
-- **In ViewModels**: inject `ILocalizationService` and call `_localization.Get("key")` / `.Format(...)`.
-- **In XAML**: use the `{loc:Localize Key=some.key}` markup extension (`LocalizeExtension` +
- `LocalizeKeyConverter`). Bindings watch `LocalizationService.LanguageTick` so text refreshes live when
- the language changes (the indexer's own `INotifyPropertyChanged` was unreliable in this Avalonia
- version — that's why the plain `LanguageTick` counter exists; keep using it for live-refresh bindings).
-
-**When adding a string**: add the key to **both** ResX files, then reference it via `Get`/`Format` (VMs)
-or `{loc:Localize}` (XAML). Never add a key to only one file.
-
-## Tests
-
-xUnit across four projects (`dotnet test AndroidTreeView.sln`, or target one project — see Commands).
-Fixtures are **inline `const` strings** inside the test classes, not external `.txt` files.
-
-- **`Adb.Tests`** — the bulk of the suite. `Parsers/` has one test class per parser/builder
- (`BatteryParserTests`, `StorageParserTests`, `AdbDevicesParserTests`, `GetPropParserTests`,
- `RootStatusParserTests`, `NetworkParserTests`, the `*BuilderTests`, etc.), `Commands/` covers argv
- building (`AdbArgsTests`), `Services/` covers device-action/screen-capture services. **Any change to ADB
- output parsing must add/update the matching parser test here** — this is where correctness is pinned.
-- **`Core.Tests`** — `AppSettings` clone/serialize, `DeviceMonitor` start/stop (with a fake
- `IDeviceService`), `SemanticVersion`, file-transfer service.
-- **`Infrastructure.Tests`** — `SettingsService` persistence, update check (`NekoIndexUpdateService`) and
- `UpdateInstaller`, using test doubles in `TestDoubles.cs`.
-- **`App.Tests`** — ViewModel logic with fake services (`Fakes.cs`): device-list reconcile keeps
- selection, RawProperties filtering, Logcat bounding, DI graph resolves (`ServiceGraphTests`), and a boot
- smoke test via `Avalonia.Headless` (`TestAppBuilder.cs`, `BootSmokeTests.cs`). No real ADB, no on-screen
- rendering.
-
-## Versioning & updates
-
-Version is unified across runtime version, App/Mini assembly versions, manifests, and
-`packaging/build-update-zip.ps1` — keep them in sync when bumping (currently `1.0.6`). Update channels:
-`android-tree-view-app` (App) and `android-tree-view-mini` (Mini). `NekoIndexUpdateService` checks the
-channel and compares semver; `UpdateInstaller` downloads, verifies SHA-256, unpacks the x64 ZIP, and runs
-a local update script. Loose ZIPs without a supported `release.json` manifest are rejected.
-
-## Bundled tools
-
-`build/AndroidTreeView.Scrcpy.targets` downloads and bundles scrcpy (which ships adb) into `tools/scrcpy`
-at build/publish time on Windows. `tools/verify-scrcpy-latest.ps1` checks for newer scrcpy releases.
diff --git a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml
index 2038e5c..c0230e1 100644
--- a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml
+++ b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml
@@ -4,30 +4,27 @@
x:Class="AndroidTreeView.App.Controls.SkeletonBlock"
x:CompileBindings="False"
MinHeight="16">
-
-
-
-
+ CornerRadius="{Binding CornerRadius, RelativeSource={RelativeSource AncestorType=controls:SkeletonBlock}}">
+
+
+
+
diff --git a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs
index 50045f2..619a9db 100644
--- a/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs
+++ b/src/AndroidTreeView.App/Controls/SkeletonBlock.axaml.cs
@@ -1,5 +1,4 @@
using Avalonia.Controls;
-using Avalonia.Layout;
namespace AndroidTreeView.App.Controls;
@@ -10,19 +9,5 @@ public partial class SkeletonBlock : UserControl
public SkeletonBlock()
{
InitializeComponent();
-
- // The shimmer is an INFINITE animation. Avalonia keeps such a clock ticking even when this
- // control is only collapsed by a parent (the loading panel toggles IsVisible), which drives
- // the render loop to repaint every frame — the app feels laggy and runs hot while "doing
- // nothing". EffectiveViewportChanged fires when an ancestor's visibility collapses/expands
- // our on-screen viewport; gate the animation on a non-empty viewport via the `:active`
- // pseudo-class so the clock stops whenever the skeleton isn't actually shown.
- EffectiveViewportChanged += OnEffectiveViewportChanged;
- }
-
- private void OnEffectiveViewportChanged(object? sender, EffectiveViewportChangedEventArgs e)
- {
- var visible = e.EffectiveViewport.Width > 0 && e.EffectiveViewport.Height > 0;
- PseudoClasses.Set(":active", visible);
}
}
diff --git a/src/AndroidTreeView.App/Services/CliLauncher.cs b/src/AndroidTreeView.App/Services/CliLauncher.cs
index a38ad0f..feb6e58 100644
--- a/src/AndroidTreeView.App/Services/CliLauncher.cs
+++ b/src/AndroidTreeView.App/Services/CliLauncher.cs
@@ -110,10 +110,12 @@ private void OpenMac(string serial, string safe, string toolsDir, bool fastboot)
var psi = new ProcessStartInfo
{
FileName = "open",
- Arguments = $"-a Terminal.app \"{scriptPath}\"",
WorkingDirectory = toolsDir,
UseShellExecute = false,
};
+ psi.ArgumentList.Add("-a");
+ psi.ArgumentList.Add("Terminal.app");
+ psi.ArgumentList.Add(scriptPath);
var process = Process.Start(psi);
if (process is not null)
@@ -408,8 +410,12 @@ private static string BuildAdbMenuSh(string serial, string toolsDir) => JoinLf(n
"done",
});
- // Single-quote escape for embedding a literal into a double-quoted bash string safely.
- private static string ShEscape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("$", "\\$").Replace("`", "\\`");
+ // Escape for embedding a literal into a double-quoted bash string safely.
+ private static string ShEscape(string value) =>
+ value.Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("$", "\\$")
+ .Replace("`", "\\`");
private static string Join(IEnumerable lines) => string.Join("\r\n", lines) + "\r\n";
diff --git a/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs b/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs
index 61ae0b2..156d965 100644
--- a/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs
+++ b/src/AndroidTreeView.App/ViewModels/MainWindowViewModel.cs
@@ -7,7 +7,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging;
-using System.Linq;
namespace AndroidTreeView.App.ViewModels;
@@ -38,19 +37,13 @@ public sealed partial class MainWindowViewModel : ViewModelBase
private readonly ILogger _logger;
// Per-device enrichment state (accessed only on the UI thread from OnDevicesChanged).
- // Identity + root are static per connection: probed once when a device first appears and not
- // re-polled on a timer. Re-probing all devices every few seconds forked a burst of adb processes
- // (root alone is 6 probes/device) that stalled the main page when several devices were connected.
- // A device that changes state (e.g. root granted) disconnects/reconnects or is refreshed on demand,
- // which clears its entry and re-enriches it.
- private readonly HashSet _staticEnriched = new(StringComparer.Ordinal);
+ // Identity + root are re-probed on this cadence (not just once) so a card never drifts out of sync
+ // with the detail page (e.g. root becoming visible after a grant).
+ private static readonly TimeSpan StaticRefreshInterval = TimeSpan.FromSeconds(8);
+ private readonly Dictionary _staticEnrichedAt = new(StringComparer.Ordinal);
private readonly HashSet _enriching = new(StringComparer.Ordinal);
private readonly HashSet _fastbootEnriching = new(StringComparer.Ordinal);
- // Device-list poll interval, loaded from settings at startup (was hardcoded to 1s, which ignored
- // the DeviceRefreshIntervalSeconds setting and hammered adb once per second per device).
- private TimeSpan _refreshInterval = TimeSpan.FromSeconds(3);
-
private string? _releaseUrl;
private UpdateCheckResult? _latestUpdate;
private bool _monitorSubscribed;
@@ -228,8 +221,6 @@ public async Task InitializeAsync()
_themeService.Apply(settings.Theme);
_themeService.ApplyAccent(settings.AccentColor);
- _refreshInterval = TimeSpan.FromSeconds(Math.Max(1, settings.DeviceRefreshIntervalSeconds));
-
var location = await _locator.LocateAsync(settings.AdbPath).ConfigureAwait(true);
_environment.Set(location);
@@ -248,7 +239,7 @@ public async Task InitializeAsync()
{
IsAdbAvailable = true;
AdbStatusText = _localization.Get("adb.status.ready");
- _monitor.UpdateInterval(_refreshInterval);
+ _monitor.UpdateInterval(TimeSpan.FromSeconds(1));
_monitor.Start();
CurrentContent = Devices;
}
@@ -358,9 +349,6 @@ private async Task RefreshDevicesAsync()
{
try
{
- // A manual refresh is the user asking for fresh facts, so drop the one-time static cache
- // to force identity/root to be re-probed on the next enrichment pass.
- _staticEnriched.Clear();
await _monitor.RefreshNowAsync().ConfigureAwait(true);
}
catch (Exception ex)
@@ -486,7 +474,7 @@ private void OnAdbReady(object? sender, EventArgs e)
if (!_monitor.IsRunning)
{
- _monitor.UpdateInterval(_refreshInterval);
+ _monitor.UpdateInterval(TimeSpan.FromSeconds(1));
_monitor.Start();
}
@@ -521,17 +509,9 @@ private void OnLanguageChanged(object? sender, EventArgs e)
}
// Fills each online card with real data: battery every tick (fast), and static identity + root once
- // per device (first time it appears). A per-serial guard prevents overlapping runs from piling up.
+ // per device. A per-serial guard prevents overlapping runs from piling up under the 1 Hz monitor.
private void EnrichDevices(IReadOnlyList devices)
{
- // Forget devices that dropped off the list so a later reconnect re-runs the one-time static
- // enrichment (identity / root) instead of showing stale data.
- if (_staticEnriched.Count > 0)
- {
- var present = new HashSet(devices.Select(d => d.Serial), StringComparer.Ordinal);
- _staticEnriched.RemoveWhere(serial => !present.Contains(serial));
- }
-
foreach (var device in devices)
{
// Fastboot devices have no OS to query over adb — read their fastboot getvar facts instead.
@@ -572,10 +552,9 @@ private async Task EnrichOneAsync(string serial)
var battery = await _deviceService.GetBatteryAsync(serial).ConfigureAwait(true);
card.ApplyBattery(battery);
- // Static identity + root are probed once per connection, not on a timer. Re-probing every
- // device every few seconds was the main-page stall: root alone forks 6 adb processes per
- // device, so N devices produced a periodic burst of dozens of concurrent adb calls.
- if (_staticEnriched.Add(serial))
+ var due = !_staticEnrichedAt.TryGetValue(serial, out var last)
+ || DateTimeOffset.Now - last > StaticRefreshInterval;
+ if (due)
{
var overview = await _deviceService.GetOverviewAsync(serial).ConfigureAwait(true);
card.ApplyOverview(overview);
@@ -584,6 +563,8 @@ private async Task EnrichOneAsync(string serial)
card.ApplyRoot(root);
await card.RefreshActionStateAsync().ConfigureAwait(true);
+
+ _staticEnrichedAt[serial] = DateTimeOffset.Now;
}
}
catch (Exception ex)
From 61428ae72669bde82ed9efa535deda7c52155a52 Mon Sep 17 00:00:00 2001
From: null <46274807+backspace135@users.noreply.github.com>
Date: Thu, 9 Jul 2026 17:10:49 +0800
Subject: [PATCH 06/18] Fix macOS CLI terminal, app icon, and version 1.0.6
Fix macOS CLI terminal launch, add macOS app bundle icon generation, update release metadata/docs to 1.0.6, and remove unrelated changes from the PR.
---
.github/workflows/publish.yml | 2 +-
README-CN.md | 26 +--
README.en.md | 14 +-
README.md | 27 +--
docs/app-contract.md | 2 +-
docs/packaging.md | 24 +--
docs/publishing.md | 34 ++--
docs/requirements-v1.md | 2 +-
packaging/AndroidTreeView.Package.wixproj | 6 +-
packaging/Bundle.wxs | 6 +-
packaging/Product.wxs | 4 +-
packaging/build-msi.ps1 | 4 +-
packaging/build-update-zip.ps1 | 83 ++++++++-
.../AndroidTreeView.App.csproj | 8 +-
.../Resources/Strings.resx | 6 +
.../Resources/Strings.zh-Hans.resx | 6 +
.../Services/CliLauncher.cs | 170 ++++++++++++++++--
src/AndroidTreeView.App/app.manifest | 2 +-
src/AndroidTreeView.Core/AppInfo.cs | 2 +-
.../Interfaces/IUpdateService.cs | 2 +-
.../AndroidTreeView.Mini.Mac.csproj | 8 +-
.../AndroidTreeView.Mini.csproj | 8 +-
.../UpdateCheckResult.cs | 2 +-
23 files changed, 345 insertions(+), 103 deletions(-)
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 98b3de5..af90abb 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -10,7 +10,7 @@ on:
workflow_dispatch:
inputs:
version:
- description: "Release version, for example 1.0.5"
+ description: "Release version, for example 1.0.6"
required: true
type: string
diff --git a/README-CN.md b/README-CN.md
index d84bcdd..df1c3cf 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -3,17 +3,17 @@
[](LICENSE)
[](https://dotnet.microsoft.com/)
[](https://avaloniaui.net/)
-[](#windows-使用说明)
+[](#使用说明)
[主文档](README.md) | **简体中文** | [English](README.en.md)
-AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的 Windows 桌面工具。主程序负责设备总览、详情、投屏、基础工具和设置;Mini 版本保持独立运行,常驻监听设备并自动投屏。
+AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的桌面工具,支持 Windows 与 macOS(Apple Silicon)。主程序负责设备总览、详情、投屏、基础工具和设置;Mini 版本保持独立运行,常驻监听设备并自动投屏。
-当前版本:**v1.0.5**。
+当前版本:**v1.0.6**。
## 产品样式展示
-
+
## 功能
@@ -36,13 +36,19 @@ dotnet run --project src/AndroidTreeView.App
dotnet run --project src/AndroidTreeView.Mini
```
-## Windows 使用说明
+## 使用说明
1. 安装 Android platform-tools,或让应用在启动时引导选择 `adb.exe`。
2. 在手机上开启开发者选项和 USB 调试。
3. 连接手机并允许 USB 调试授权。
4. 主程序会显示设备卡片;Mini 会自动监听并启动投屏。
+### macOS 说明
+
+- 从 Release 下载 `AndroidTreeView-<版本>-osx-arm64.zip`,解压后将 `AndroidTreeView.app` 拖入 `/Applications`。
+- 首次打开若被 Gatekeeper 拦截,右键「打开」,或执行 `xattr -dr com.apple.quarantine AndroidTreeView.app` 放行。
+- 设备卡片的「CLI 终端」在 macOS 上通过 Terminal.app 打开,编号菜单与 Windows 一致。
+
ADB 安装与排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
## 自动更新
@@ -61,13 +67,13 @@ ADB 安装与排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
./packaging/build-update-zip.ps1 -Product Mini -Rid win-x64
```
-默认产物版本是 `1.0.5`。只发布 x64 架构。验证输出包含主程序和 Mini 的上传 ZIP,例如:
+默认产物版本是 `1.0.6`。只发布 x64 架构。验证输出包含主程序和 Mini 的上传 ZIP,例如:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
```
更多细节见 [docs/packaging.md](docs/packaging.md)。
diff --git a/README.en.md b/README.en.md
index 3865168..c72aaae 100644
--- a/README.en.md
+++ b/README.en.md
@@ -9,11 +9,11 @@
AndroidTreeView is a desktop tool for inspecting, testing, and managing Android devices through ADB. The full app shows device cards, detail pages, mirroring, tools, settings, and updates. The Mini app stays resident, watches for devices, and starts mirroring automatically after authorization.
-Current version: **v1.0.5**. Current verification target: solution build passes, all tests pass, and GitHub Actions creates Windows x64 and macOS Apple Silicon ZIP artifacts for both App and Mini.
+Current version: **v1.0.6**. Current verification target: solution build passes, all tests pass, and GitHub Actions creates Windows x64 and macOS Apple Silicon ZIP artifacts for both App and Mini.
## Product Preview
-
+
## Features
@@ -78,7 +78,7 @@ See [docs/adb-requirements.md](docs/adb-requirements.md) for platform-tools setu
## Release ZIP Packaging
-The product version is currently `1.0.5` and is kept in sync across runtime version, App/Mini assembly metadata, manifest, and the ZIP build script. Official releases are produced only by the GitHub Actions `Publish` workflow.
+The product version is currently `1.0.6` and is kept in sync across runtime version, App/Mini assembly metadata, manifest, and the ZIP build script. Official releases are produced only by the GitHub Actions `Publish` workflow.
Local commands are for packaging validation only:
@@ -90,10 +90,10 @@ Local commands are for packaging validation only:
Example output:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
```
## Auto Update
diff --git a/README.md b/README.md
index 441c240..7fc49e1 100644
--- a/README.md
+++ b/README.md
@@ -3,17 +3,17 @@
[](LICENSE)
[](https://dotnet.microsoft.com/)
[](https://avaloniaui.net/)
-[](#windows-使用说明)
+[](#使用说明)
**简体中文** | [English](README.en.md) | [中文副本](README-CN.md)
-AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的 Windows 桌面工具。主程序负责设备总览、详情、投屏、基础工具、设置和更新;Mini 版本保持独立运行,常驻监听设备,并在授权后自动启动投屏。
+AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的桌面工具,支持 Windows 与 macOS(Apple Silicon)。主程序负责设备总览、详情、投屏、基础工具、设置和更新;Mini 版本保持独立运行,常驻监听设备,并在授权后自动启动投屏。
-当前版本:**v1.0.5**。当前验证目标:App 构建通过、Mini 构建通过、全量测试通过,打包链路能为 App 和 Mini 分别生成 x64 上传 ZIP。
+当前版本:**v1.0.6**。当前验证目标:App 构建通过、Mini 构建通过、全量测试通过,打包链路能为 App 和 Mini 分别生成 x64 上传 ZIP。
## 产品样式展示
-
+
## 核心能力
@@ -38,6 +38,7 @@ src/
AndroidTreeView.Shared
AndroidTreeView.App
AndroidTreeView.Mini
+ AndroidTreeView.Mini.Mac
tests/
AndroidTreeView.*.Tests
packaging/
@@ -69,16 +70,22 @@ dotnet run --project src/AndroidTreeView.Mini
ADB 安装和排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
-## Windows 使用说明
+## 使用说明
1. 启动 AndroidTreeView。
2. 连接已开启并授权 USB 调试的 Android 设备。
3. 使用设备卡片查看状态、投屏、打开 CLI 或执行非破坏性工具。
4. 通过设置或关于页检查并安装更新。
+### macOS 说明
+
+- 从 Release 下载 `AndroidTreeView-<版本>-osx-arm64.zip`,解压后将 `AndroidTreeView.app` 拖入 `/Applications`。
+- 首次打开若被 Gatekeeper 拦截(未签名),右键选择「打开」,或执行 `xattr -dr com.apple.quarantine AndroidTreeView.app` 放行。
+- 设备卡片的「CLI 终端」在 macOS 上通过 Terminal.app 打开,提供与 Windows 一致的编号菜单(设备信息 / adb shell / logcat / 重启 / 关机 等)。
+
## Release ZIP 打包
-当前版本号统一为 `1.0.5`,运行时版本、App/Mini 程序集版本、manifest 和 `build-update-zip.ps1` 默认版本保持一致。正式发布只通过 GitHub Actions 的 `Publish` 工作流完成,发布只接受 x64。
+当前版本号统一为 `1.0.6`,运行时版本、App/Mini 程序集版本、manifest 和 `build-update-zip.ps1` 默认版本保持一致。正式发布只通过 GitHub Actions 的 `Publish` 工作流完成,发布只接受 x64。
本地命令仅用于验证打包链路:
@@ -90,10 +97,10 @@ ADB 安装和排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
示例输出:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
```
## 自动更新
diff --git a/docs/app-contract.md b/docs/app-contract.md
index 5e79538..0f9d351 100644
--- a/docs/app-contract.md
+++ b/docs/app-contract.md
@@ -2,7 +2,7 @@
This document records the App-layer contract for the full app and the Mini companion.
-Current product version: `1.0.5`.
+Current product version: `1.0.6`.
## Shared Services
diff --git a/docs/packaging.md b/docs/packaging.md
index 3cc2a6d..2c2d9f0 100644
--- a/docs/packaging.md
+++ b/docs/packaging.md
@@ -2,7 +2,7 @@
Packaging files live under `packaging/`.
-Current default product version: `1.0.5`.
+Current default product version: `1.0.6`.
Official release artifacts are created by GitHub Actions (`.github/workflows/publish.yml`) only. Local packaging commands are validation helpers and must not be used as the authority for publishing a release.
@@ -11,14 +11,14 @@ Official release artifacts are created by GitHub Actions (`.github/workflows/pub
Every GitHub Release must contain exactly these four ZIP packages plus matching `.sha256` sidecars:
```text
-artifacts/AndroidTreeView-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-1.0.5-win-x64.zip.sha256
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-1.0.5-osx-arm64.zip.sha256
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-win-x64.zip.sha256
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.5-osx-arm64.zip.sha256
+artifacts/AndroidTreeView-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-1.0.6-win-x64.zip.sha256
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-1.0.6-osx-arm64.zip.sha256
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip.sha256
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
+artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip.sha256
```
Windows ZIPs contain the published application files, the platform-matched `scrcpy` bundle, and `release.json` at the ZIP root. macOS ZIPs contain a top-level `.app` bundle (`AndroidTreeView.app` or `AndroidTreeView Mini.app`); the published files, `scrcpy`, and `release.json` live inside the bundle.
@@ -74,7 +74,7 @@ The updater uses `release.json` to distinguish an automated release ZIP from a r
"product": "App",
"productName": "AndroidTreeView",
"appKey": "android-tree-view-app",
- "version": "1.0.5",
+ "version": "1.0.6",
"platform": "win",
"arch": "x64",
"rid": "win-x64",
@@ -102,8 +102,8 @@ The WiX project rejects non-x64 platforms.
`build-update-zip.ps1` writes checksums automatically. Manual verification:
```powershell
-Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-1.0.5-win-x64.zip
-Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-Mini-1.0.5-win-x64.zip
+Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-1.0.6-win-x64.zip
+Get-FileHash -Algorithm SHA256 artifacts\AndroidTreeView-Mini-1.0.6-win-x64.zip
```
The sidecar uses ` *` format for compatibility with `sha256sum -c`.
diff --git a/docs/publishing.md b/docs/publishing.md
index 63585c5..cbe0b4f 100644
--- a/docs/publishing.md
+++ b/docs/publishing.md
@@ -2,7 +2,7 @@
Repository: `Birditch/AndroidTreeView`.
-Current product version: `1.0.5`.
+Current product version: `1.0.6`.
## Release Policy
@@ -37,24 +37,24 @@ Keep these values identical for every release:
- `src/AndroidTreeView.App/app.manifest` -> `assemblyIdentity version`
- `packaging/build-update-zip.ps1` -> default `Version`
-For `1.0.5`, release artifacts are named:
+For `1.0.6`, release artifacts are named:
```text
-AndroidTreeView-1.0.5-win-x64.zip
-AndroidTreeView-1.0.5-win-x64.zip.sha256
-AndroidTreeView-1.0.5-osx-arm64.zip
-AndroidTreeView-1.0.5-osx-arm64.zip.sha256
-AndroidTreeView-Mini-1.0.5-win-x64.zip
-AndroidTreeView-Mini-1.0.5-win-x64.zip.sha256
-AndroidTreeView-Mini-1.0.5-osx-arm64.zip
-AndroidTreeView-Mini-1.0.5-osx-arm64.zip.sha256
+AndroidTreeView-1.0.6-win-x64.zip
+AndroidTreeView-1.0.6-win-x64.zip.sha256
+AndroidTreeView-1.0.6-osx-arm64.zip
+AndroidTreeView-1.0.6-osx-arm64.zip.sha256
+AndroidTreeView-Mini-1.0.6-win-x64.zip
+AndroidTreeView-Mini-1.0.6-win-x64.zip.sha256
+AndroidTreeView-Mini-1.0.6-osx-arm64.zip
+AndroidTreeView-Mini-1.0.6-osx-arm64.zip.sha256
```
## Build Upload ZIP Packages
Official publication happens through the `Publish` GitHub Actions workflow:
-- push a tag named `v..`, for example `v1.0.5`
+- push a tag named `v..`, for example `v1.0.6`
- or run `workflow_dispatch` and provide the same version string
The workflow validates on Windows, verifies the pinned scrcpy release against the latest upstream release, builds the two Windows portable ZIPs and two macOS `.app` bundle ZIPs, writes SHA-256 sidecars, uploads workflow artifacts, and creates the GitHub Release.
@@ -77,8 +77,8 @@ The full app and Mini share the same update implementation but use different app
The Windows update channel must point each product key at its own Windows ZIP:
-- `android-tree-view-app` -> `AndroidTreeView-1.0.5-win-x64.zip`
-- `android-tree-view-mini` -> `AndroidTreeView-Mini-1.0.5-win-x64.zip`
+- `android-tree-view-app` -> `AndroidTreeView-1.0.6-win-x64.zip`
+- `android-tree-view-mini` -> `AndroidTreeView-Mini-1.0.6-win-x64.zip`
`NekoIndexUpdateService` queries the configured internal update API and compares the returned version with `AppInfo.Version`. `UpdateInstaller` downloads the package, verifies SHA-256 metadata when present, extracts the ZIP, verifies the portable Windows x64 manifest, and starts the automated update script.
@@ -97,7 +97,7 @@ Minimum response contract:
"data": {
"appKey": "android-tree-view-app",
"title": "AndroidTreeView",
- "version": "1.0.5",
+ "version": "1.0.6",
"zip": {
"sha256": "<64-character sha256>",
"downloadUrl": "/api/resources/android-tree-view-app/versions/latest/archive"
@@ -114,11 +114,11 @@ Minimum response contract:
Use this flow for the intranet update server:
-1. Push a `v1.0.5` tag or run the `Publish` GitHub Actions workflow manually.
+1. Push a `v1.0.6` tag or run the `Publish` GitHub Actions workflow manually.
2. Download the GitHub Actions-built Windows ZIP and `.sha256` sidecar for each Windows update channel.
3. Upload the ZIPs to the internal server storage.
-4. Configure `/api/update/android-tree-view-app/latest` to return `AndroidTreeView-1.0.5-win-x64.zip` metadata.
-5. Configure `/api/update/android-tree-view-mini/latest` to return `AndroidTreeView-Mini-1.0.5-win-x64.zip` metadata.
+4. Configure `/api/update/android-tree-view-app/latest` to return `AndroidTreeView-1.0.6-win-x64.zip` metadata.
+5. Configure `/api/update/android-tree-view-mini/latest` to return `AndroidTreeView-Mini-1.0.6-win-x64.zip` metadata.
6. Confirm the `sha256` value matches the uploaded ZIP.
7. Use the app "Check for updates" action to test download, validation, replacement, cleanup, and restart.
diff --git a/docs/requirements-v1.md b/docs/requirements-v1.md
index bf4147f..179ee4a 100644
--- a/docs/requirements-v1.md
+++ b/docs/requirements-v1.md
@@ -1,6 +1,6 @@
# AndroidTreeView Requirements Addendum
-Current product version: `1.0.5`.
+Current product version: `1.0.6`.
This addendum extends `docs/architecture.md` and reflects the current App + Mini direction.
diff --git a/packaging/AndroidTreeView.Package.wixproj b/packaging/AndroidTreeView.Package.wixproj
index bcf193e..27147e4 100644
--- a/packaging/AndroidTreeView.Package.wixproj
+++ b/packaging/AndroidTreeView.Package.wixproj
@@ -9,7 +9,7 @@
app and then invokes this project with the right properties, e.g.:
dotnet build packaging/AndroidTreeView.Package.wixproj -c Release ^
- -p:Platform=x64 -p:ProductVersion=1.0.5 -p:SelfContained=false ^
+ -p:Platform=x64 -p:ProductVersion=1.0.6 -p:SelfContained=false ^
-p:PublishDir=
Requires the WiX v5 SDK, restored automatically from nuget.org via the Sdk attribute.
@@ -23,7 +23,7 @@
- 1.0.5
+ 1.0.6AndroidTreeViewAndroidTreeView.App.exe{6C0B8E2A-4F1D-4B7C-9A3E-2D5F8B1C0E4A}
@@ -41,7 +41,7 @@
harvest. build-msi.ps1 sets this; the default is only for manual runs. -->
$(MSBuildThisFileDirectory)..\artifacts\publish\$(Platform)\
-
+
$(ProductArtifactName)-$(ProductVersion)-$(Platform)Packagefalse
diff --git a/packaging/Bundle.wxs b/packaging/Bundle.wxs
index 45dd3b6..e305dad 100644
--- a/packaging/Bundle.wxs
+++ b/packaging/Bundle.wxs
@@ -8,13 +8,13 @@
Build after the MSI has been produced:
wix build packaging/Bundle.wxs -arch x64 ^
- -d ProductVersion=1.0.5 ^
+ -d ProductVersion=1.0.6 ^
-d Platform=x64 ^
- -d MsiPath=artifacts\AndroidTreeView-1.0.5-x64.msi ^
+ -d MsiPath=artifacts\AndroidTreeView-1.0.6-x64.msi ^
-d DotNetRuntimeExe=C:\path\windowsdesktop-runtime-10.0.x-win-x64.exe ^
-ext WixToolset.BootstrapperApplications.wixext ^
-ext WixToolset.Netfx.wixext ^
- -o artifacts\AndroidTreeView-1.0.5-x64-setup.exe
+ -o artifacts\AndroidTreeView-1.0.6-x64-setup.exe
-->
-
+
diff --git a/packaging/build-msi.ps1 b/packaging/build-msi.ps1
index d40e71c..3266be6 100644
--- a/packaging/build-msi.ps1
+++ b/packaging/build-msi.ps1
@@ -27,7 +27,7 @@
Build configuration. Default: Release.
.PARAMETER Version
- Product version. Default: 1.0.5. Must match AndroidTreeView.Core.AppInfo.Version.
+ Product version. Default: 1.0.6. Must match AndroidTreeView.Core.AppInfo.Version.
.EXAMPLE
./build-msi.ps1 -Product App -Arch x64
@@ -49,7 +49,7 @@ param(
[string]$Configuration = 'Release',
- [string]$Version = '1.0.5'
+ [string]$Version = '1.0.6'
)
$ErrorActionPreference = 'Stop'
diff --git a/packaging/build-update-zip.ps1 b/packaging/build-update-zip.ps1
index fa688df..e5f807f 100644
--- a/packaging/build-update-zip.ps1
+++ b/packaging/build-update-zip.ps1
@@ -27,7 +27,7 @@ param(
[string]$Configuration = 'Release',
- [string]$Version = '1.0.5'
+ [string]$Version = '1.0.6'
)
$ErrorActionPreference = 'Stop'
@@ -250,6 +250,60 @@ function ConvertTo-PlistString {
return [System.Security.SecurityElement]::Escape($Value)
}
+function New-IcnsFromPng {
+ param(
+ [Parameter(Mandatory = $true)]
+ [string]$SourcePng,
+
+ [Parameter(Mandatory = $true)]
+ [string]$OutputIcns
+ )
+
+ # macOS .app icons are .icns files; the repo only ships a 256x256 PNG, so build the .icns from it
+ # with the system tools (sips resizes each slot, iconutil packs the iconset). Requires macOS.
+ if (-not (Get-Command sips -ErrorAction SilentlyContinue) -or -not (Get-Command iconutil -ErrorAction SilentlyContinue)) {
+ Write-Warning 'sips/iconutil not found; skipping macOS app icon generation.'
+ return $false
+ }
+
+ $iconsetDir = "$OutputIcns.iconset"
+ if (Test-Path -LiteralPath $iconsetDir) {
+ Remove-Item -Recurse -Force -LiteralPath $iconsetDir
+ }
+ New-Item -ItemType Directory -Force -Path $iconsetDir | Out-Null
+
+ # name => pixel size for the standard iconset slots (1x + @2x). Upscaled slots beyond the
+ # 256px source are slightly soft but valid; Dock/Finder common sizes stay crisp.
+ $slots = [ordered]@{
+ 'icon_16x16.png' = 16
+ 'icon_16x16@2x.png' = 32
+ 'icon_32x32.png' = 32
+ 'icon_32x32@2x.png' = 64
+ 'icon_128x128.png' = 128
+ 'icon_128x128@2x.png' = 256
+ 'icon_256x256.png' = 256
+ 'icon_256x256@2x.png' = 512
+ 'icon_512x512.png' = 512
+ 'icon_512x512@2x.png' = 1024
+ }
+
+ foreach ($entry in $slots.GetEnumerator()) {
+ $target = Join-Path $iconsetDir $entry.Key
+ & sips -z $entry.Value $entry.Value $SourcePng --out $target *> $null
+ if ($LASTEXITCODE -ne 0) {
+ throw "sips failed to render '$($entry.Key)' from '$SourcePng'."
+ }
+ }
+
+ & iconutil -c icns $iconsetDir -o $OutputIcns
+ if ($LASTEXITCODE -ne 0) {
+ throw "iconutil failed to build '$OutputIcns'."
+ }
+
+ Remove-Item -Recurse -Force -LiteralPath $iconsetDir
+ return $true
+}
+
function New-MacOSAppBundle {
param(
[Parameter(Mandatory = $true)]
@@ -306,6 +360,25 @@ function New-MacOSAppBundle {
$bundleIdentifier = "com.birditch.$($ProductConfig.AppKey.Replace('-', '.'))"
$displayName = $ProductConfig.ProductName
$executableName = $ProductConfig.Executable
+
+ # Build the app icon (.icns) from the App's PNG so the bundle shows in Dock/Finder.
+ $iconSourcePng = Join-Path $repoRoot 'src/AndroidTreeView.App/Assets/atv-icon.png'
+ $iconFileName = ''
+ if (Test-Path -LiteralPath $iconSourcePng) {
+ $icnsPath = Join-Path $resourcesDir 'AppIcon.icns'
+ if (New-IcnsFromPng -SourcePng $iconSourcePng -OutputIcns $icnsPath) {
+ $iconFileName = 'AppIcon'
+ }
+ } else {
+ Write-Warning "Icon source '$iconSourcePng' not found; bundle will have no icon."
+ }
+
+ $iconPlistEntry = if ($iconFileName) {
+ " CFBundleIconFile`n $(ConvertTo-PlistString $iconFileName)`n"
+ } else {
+ ''
+ }
+
$infoPlist = @"
@@ -317,7 +390,7 @@ function New-MacOSAppBundle {
$(ConvertTo-PlistString $displayName)CFBundleExecutable$(ConvertTo-PlistString $executableName)
- CFBundleIdentifier
+$iconPlistEntry CFBundleIdentifier$(ConvertTo-PlistString $bundleIdentifier)CFBundleInfoDictionaryVersion6.0
@@ -491,6 +564,10 @@ $zipPath = Join-Path $artifacts "$baseName.zip"
$zipChecksumPath = "$zipPath.sha256"
$packageKind = if ($ridInfo.IsWindows) { 'portable-x64' } else { "portable-$($ridInfo.Rid)" }
$bundleFastbootValue = if ($productConfig.BundleFastboot) { 'true' } else { 'false' }
+# macOS has no system-level "install .NET runtime" prompt like Windows, so a framework-dependent
+# .app silently fails to launch on machines without the runtime. Bundle the runtime into the macOS
+# .app (self-contained). Windows stays framework-dependent: its apphost shows the OS download prompt.
+$selfContainedValue = if ($ridInfo.IsMacOS) { 'true' } else { 'false' }
New-Item -ItemType Directory -Force -Path $artifacts | Out-Null
Assert-UnderDirectory -Path $publishDir -Parent $artifacts
@@ -513,7 +590,7 @@ $publishArgs = @(
$productConfig.Project,
'--configuration', $Configuration,
'--runtime', $ridInfo.Rid,
- '--self-contained', 'false',
+ '--self-contained', $selfContainedValue,
"-p:Version=$Version",
"-p:AssemblyVersion=$Version.0",
"-p:FileVersion=$Version.0",
diff --git a/src/AndroidTreeView.App/AndroidTreeView.App.csproj b/src/AndroidTreeView.App/AndroidTreeView.App.csproj
index 94aded3..de7dc78 100644
--- a/src/AndroidTreeView.App/AndroidTreeView.App.csproj
+++ b/src/AndroidTreeView.App/AndroidTreeView.App.csproj
@@ -8,10 +8,10 @@
Assets/atv-icon.icotruetrue
- 1.0.5
- 1.0.5.0
- 1.0.5.0
- 1.0.5
+ 1.0.6
+ 1.0.6.0
+ 1.0.6.0
+ 1.0.6false
diff --git a/src/AndroidTreeView.App/Resources/Strings.resx b/src/AndroidTreeView.App/Resources/Strings.resx
index ff3f94f..62acc49 100644
--- a/src/AndroidTreeView.App/Resources/Strings.resx
+++ b/src/AndroidTreeView.App/Resources/Strings.resx
@@ -171,6 +171,12 @@
CLI Terminal
+
+ The CLI terminal is not supported on this operating system.
+
+
+ Failed to open the CLI terminal.
+
Details
diff --git a/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx b/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx
index 830a777..ee83efa 100644
--- a/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx
+++ b/src/AndroidTreeView.App/Resources/Strings.zh-Hans.resx
@@ -171,6 +171,12 @@
CLI 终端
+
+ 当前操作系统不支持 CLI 终端。
+
+
+ 打开 CLI 终端失败。
+
详情
diff --git a/src/AndroidTreeView.App/Services/CliLauncher.cs b/src/AndroidTreeView.App/Services/CliLauncher.cs
index 552c5ba..feb6e58 100644
--- a/src/AndroidTreeView.App/Services/CliLauncher.cs
+++ b/src/AndroidTreeView.App/Services/CliLauncher.cs
@@ -27,13 +27,15 @@ public interface ICliLauncher
public sealed class CliLauncher : ICliLauncher
{
private readonly IAdbEnvironment _environment;
+ private readonly ILocalizationService _localization;
private readonly ILogger _logger;
private readonly List _processes = new();
private readonly object _gate = new();
- public CliLauncher(IAdbEnvironment environment, ILogger logger)
+ public CliLauncher(IAdbEnvironment environment, ILocalizationService localization, ILogger logger)
{
_environment = environment;
+ _localization = localization;
_logger = logger;
}
@@ -42,29 +44,83 @@ public void Open(string serial, string title, bool fastboot)
try
{
var toolsDir = ResolveToolsDir() ?? AppContext.BaseDirectory;
- var script = fastboot ? BuildFastbootMenu(serial, toolsDir) : BuildAdbMenu(serial, toolsDir);
-
var safe = new string(serial.Where(char.IsLetterOrDigit).ToArray());
- var batPath = Path.Combine(Path.GetTempPath(), $"atv-cli-{(safe.Length == 0 ? "device" : safe)}.bat");
- File.WriteAllText(batPath, script);
-
- var psi = new ProcessStartInfo
+ if (safe.Length == 0)
{
- FileName = "cmd.exe",
- Arguments = $"/K \"{batPath}\"",
- WorkingDirectory = toolsDir,
- UseShellExecute = true, // reliably opens a visible console window from a GUI process
- };
+ safe = "device";
+ }
- var process = Process.Start(psi);
- if (process is not null)
+ if (OperatingSystem.IsWindows())
+ {
+ OpenWindows(serial, safe, toolsDir, fastboot);
+ }
+ else if (OperatingSystem.IsMacOS())
+ {
+ OpenMac(serial, safe, toolsDir, fastboot);
+ }
+ else
{
- Track(process);
+ Notifier.Notify(_localization.Get("cli.unsupported"), NotifierLevel.Warning);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to open CLI terminal for {Serial}.", serial);
+ Notifier.Notify(_localization.Get("cli.launchFailed"), NotifierLevel.Error);
+ }
+ }
+
+ // Windows: write a menu .bat and open it in a cmd.exe /K console.
+ private void OpenWindows(string serial, string safe, string toolsDir, bool fastboot)
+ {
+ var script = fastboot ? BuildFastbootMenu(serial, toolsDir) : BuildAdbMenu(serial, toolsDir);
+ var batPath = Path.Combine(Path.GetTempPath(), $"atv-cli-{safe}.bat");
+ File.WriteAllText(batPath, script);
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ Arguments = $"/K \"{batPath}\"",
+ WorkingDirectory = toolsDir,
+ UseShellExecute = true, // reliably opens a visible console window from a GUI process
+ };
+
+ var process = Process.Start(psi);
+ if (process is not null)
+ {
+ Track(process);
+ }
+ }
+
+ // macOS: write an equivalent .command shell script and open it in Terminal.app.
+ // Note: `open` returns immediately and the Terminal window is an independent process, so
+ // ShutdownAll() cannot close it — that's intentional; we don't force-close a user's terminal.
+ [System.Runtime.Versioning.SupportedOSPlatform("macos")]
+ private void OpenMac(string serial, string safe, string toolsDir, bool fastboot)
+ {
+ var script = fastboot ? BuildFastbootMenuSh(serial, toolsDir) : BuildAdbMenuSh(serial, toolsDir);
+ var scriptPath = Path.Combine(Path.GetTempPath(), $"atv-cli-{safe}.command");
+ File.WriteAllText(scriptPath, script);
+ File.SetUnixFileMode(
+ scriptPath,
+ UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
+ UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
+ UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
+
+ var psi = new ProcessStartInfo
+ {
+ FileName = "open",
+ WorkingDirectory = toolsDir,
+ UseShellExecute = false,
+ };
+ psi.ArgumentList.Add("-a");
+ psi.ArgumentList.Add("Terminal.app");
+ psi.ArgumentList.Add(scriptPath);
+
+ var process = Process.Start(psi);
+ if (process is not null)
+ {
+ Track(process);
}
}
@@ -279,8 +335,92 @@ private static string BuildAdbMenu(string serial, string toolsDir) => Join(new[]
"exit",
});
+ // macOS fastboot menu — bash equivalent of BuildFastbootMenu (universal + non-destructive only).
+ private static string BuildFastbootMenuSh(string serial, string toolsDir) => JoinLf(new[]
+ {
+ "#!/bin/bash",
+ // ANSI colors (cyan headers / green numbers / yellow values) matching the Windows menu.
+ "H=$'\\033[96m'; N=$'\\033[92m'; V=$'\\033[93m'; R=$'\\033[0m'",
+ $"export PATH=\"{ShEscape(toolsDir)}:$PATH\"",
+ $"SERIAL=\"{ShEscape(serial)}\"",
+ "while true; do",
+ " clear",
+ " echo \"${H}================ AndroidTreeView Fastboot CLI ================${R}\"",
+ " echo \" 设备 Device: ${V}${SERIAL}${R} (fastboot / bootloader)\"",
+ " echo \"${H}-------------------------------------------------------------${R}\"",
+ " echo \" ${N}[1]${R} 设备信息 Device info (getvar all)\"",
+ " echo \" ${N}[2]${R} 重启到系统 Reboot to system\"",
+ " echo \" ${N}[3]${R} 重启到 Bootloader Reboot to bootloader\"",
+ " echo \" ${N}[4]${R} 重启到 Recovery Reboot to recovery\"",
+ " echo \" ${N}[5]${R} 关机 Power off\"",
+ " echo \" ${N}[6]${R} 切换到 A 槽 Set active slot A\"",
+ " echo \" ${N}[7]${R} 切换到 B 槽 Set active slot B\"",
+ " echo \" ${N}[9]${R} 原生命令行 Raw CLI (adb + fastboot on PATH)\"",
+ " echo \" ${N}[0]${R} 退出 Exit\"",
+ " echo \"${H}-------------------------------------------------------------${R}\"",
+ " read -r -p \"${H}输入序号并回车 Enter choice >${R} \" sel",
+ " case \"$sel\" in",
+ " 1) fastboot -s \"$SERIAL\" getvar all; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 2) fastboot -s \"$SERIAL\" reboot; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 3) fastboot -s \"$SERIAL\" reboot bootloader; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 4) fastboot -s \"$SERIAL\" reboot recovery; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 5) fastboot -s \"$SERIAL\" oem poweroff; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 6) fastboot -s \"$SERIAL\" set_active a; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 7) fastboot -s \"$SERIAL\" set_active b; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 9) echo \"adb / fastboot 已在 PATH。输入 exit 返回菜单 (type exit to return).\"; \"${SHELL:-/bin/bash}\" ;;",
+ " 0) exit 0 ;;",
+ " esac",
+ "done",
+ });
+
+ // macOS adb menu — bash equivalent of BuildAdbMenu (info / shell / logcat / reboots / power / raw).
+ private static string BuildAdbMenuSh(string serial, string toolsDir) => JoinLf(new[]
+ {
+ "#!/bin/bash",
+ "H=$'\\033[96m'; N=$'\\033[92m'; V=$'\\033[93m'; R=$'\\033[0m'",
+ $"export PATH=\"{ShEscape(toolsDir)}:$PATH\"",
+ $"SERIAL=\"{ShEscape(serial)}\"",
+ "while true; do",
+ " clear",
+ " echo \"${H}================== AndroidTreeView ADB CLI ==================${R}\"",
+ " echo \" 设备 Device: ${V}${SERIAL}${R}\"",
+ " echo \"${H}------------------------------------------------------------${R}\"",
+ " echo \" ${N}[1]${R} 设备信息 Device info (getprop)\"",
+ " echo \" ${N}[2]${R} 进入 Shell adb shell\"",
+ " echo \" ${N}[3]${R} 查看日志 Logcat (Ctrl+C 停止)\"",
+ " echo \" ${N}[4]${R} 重启 Reboot\"",
+ " echo \" ${N}[5]${R} 重启到 Bootloader Reboot to bootloader\"",
+ " echo \" ${N}[6]${R} 重启到 Recovery Reboot to recovery\"",
+ " echo \" ${N}[7]${R} 关机 Power off\"",
+ " echo \" ${N}[9]${R} 原生命令行 Raw CLI (adb + fastboot on PATH)\"",
+ " echo \" ${N}[0]${R} 退出 Exit\"",
+ " echo \"${H}------------------------------------------------------------${R}\"",
+ " read -r -p \"${H}输入序号并回车 Enter choice >${R} \" sel",
+ " case \"$sel\" in",
+ " 1) adb -s \"$SERIAL\" shell getprop; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 2) adb -s \"$SERIAL\" shell ;;",
+ " 3) adb -s \"$SERIAL\" logcat ;;",
+ " 4) adb -s \"$SERIAL\" reboot; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 5) adb -s \"$SERIAL\" reboot bootloader; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 6) adb -s \"$SERIAL\" reboot recovery; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 7) adb -s \"$SERIAL\" shell reboot -p; read -n1 -s -r -p \"按任意键返回菜单 Press any key... \" ;;",
+ " 9) echo \"adb / fastboot 已在 PATH。输入 exit 返回菜单 (type exit to return).\"; \"${SHELL:-/bin/bash}\" ;;",
+ " 0) exit 0 ;;",
+ " esac",
+ "done",
+ });
+
+ // Escape for embedding a literal into a double-quoted bash string safely.
+ private static string ShEscape(string value) =>
+ value.Replace("\\", "\\\\")
+ .Replace("\"", "\\\"")
+ .Replace("$", "\\$")
+ .Replace("`", "\\`");
+
private static string Join(IEnumerable lines) => string.Join("\r\n", lines) + "\r\n";
+ private static string JoinLf(IEnumerable lines) => string.Join("\n", lines) + "\n";
+
private string? ResolveToolsDir()
{
var adb = _environment.Location?.ExecutablePath;
diff --git a/src/AndroidTreeView.App/app.manifest b/src/AndroidTreeView.App/app.manifest
index 5f15658..41e62b4 100644
--- a/src/AndroidTreeView.App/app.manifest
+++ b/src/AndroidTreeView.App/app.manifest
@@ -1,6 +1,6 @@
-
+ AndroidTreeView.App.mini
- 1.0.5
- 1.0.5.0
- 1.0.5.0
- 1.0.5
+ 1.0.6
+ 1.0.6.0
+ 1.0.6.0
+ 1.0.6falseAssets\atv-icon.ico
diff --git a/src/AndroidTreeView.Models/UpdateCheckResult.cs b/src/AndroidTreeView.Models/UpdateCheckResult.cs
index e1e494c..bc6c9b3 100644
--- a/src/AndroidTreeView.Models/UpdateCheckResult.cs
+++ b/src/AndroidTreeView.Models/UpdateCheckResult.cs
@@ -31,7 +31,7 @@ public enum UpdateCheckStatus
///
public sealed class UpdateCheckResult
{
- /// The version currently running (e.g. 1.0.5).
+ /// The version currently running (e.g. 1.0.6).
public required string CurrentVersion { get; init; }
/// The latest published version, when known.
From af695aade6de3f512929fd4f3f1ae23119387ab7 Mon Sep 17 00:00:00 2001
From: Birditch <58v5tb4sjt@privaterelay.appleid.com>
Date: Thu, 9 Jul 2026 18:10:52 +0800
Subject: [PATCH 07/18] Fix CI workflow failures
---
.editorconfig | 2 +-
.gitattributes | 1 +
.github/workflows/ci.yml | 22 ++++++++++++-------
.../Update/NekoIndexUpdateService.cs | 17 ++++++++++----
4 files changed, 29 insertions(+), 13 deletions(-)
create mode 100644 .gitattributes
diff --git a/.editorconfig b/.editorconfig
index c91cfbe..95db9e1 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -3,7 +3,7 @@ root = true
# ---- All files ----
[*]
charset = utf-8
-end_of_line = crlf
+end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b1630b9..190ca1e 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,6 +16,7 @@ env:
DOTNET_NOLOGO: "true"
DOTNET_CLI_TELEMETRY_OPTOUT: "true"
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: "true"
+ EnableWindowsTargeting: "true"
CONFIGURATION: Release
SOLUTION: AndroidTreeView.sln
@@ -37,17 +38,16 @@ jobs:
dotnet-version: 10.0.x
- name: Restore
- run: dotnet restore ${{ env.SOLUTION }} -p:EnableWindowsTargeting=true
+ run: dotnet restore ${{ env.SOLUTION }}
- name: Build
- run: dotnet build ${{ env.SOLUTION }} -c ${{ env.CONFIGURATION }} --no-restore -p:EnableWindowsTargeting=true
+ run: dotnet build ${{ env.SOLUTION }} -c ${{ env.CONFIGURATION }} --no-restore
- name: Test
run: >-
dotnet test ${{ env.SOLUTION }}
-c ${{ env.CONFIGURATION }}
--no-build
- -p:EnableWindowsTargeting=true
--logger trx
--results-directory test-results
@@ -72,13 +72,13 @@ jobs:
dotnet-version: 10.0.x
- name: Restore
- run: dotnet restore ${{ env.SOLUTION }} -p:EnableWindowsTargeting=true
+ run: dotnet restore ${{ env.SOLUTION }}
- name: Check for vulnerable packages
shell: bash
run: |
set -euo pipefail
- dotnet list ${{ env.SOLUTION }} package --vulnerable --include-transitive 2>&1 | tee vulnerable.log
+ dotnet list ${{ env.SOLUTION }} package --vulnerable --include-transitive --no-restore 2>&1 | tee vulnerable.log
if grep -q "has the following vulnerable packages" vulnerable.log; then
echo "::error::Vulnerable NuGet packages detected. See the log above."
exit 1
@@ -88,7 +88,6 @@ jobs:
format:
name: Format check (non-blocking)
runs-on: ubuntu-latest
- continue-on-error: true
steps:
- name: Checkout
uses: actions/checkout@v7
@@ -99,7 +98,14 @@ jobs:
dotnet-version: 10.0.x
- name: Restore
- run: dotnet restore ${{ env.SOLUTION }} -p:EnableWindowsTargeting=true
+ run: dotnet restore ${{ env.SOLUTION }}
- name: dotnet format --verify-no-changes
- run: dotnet format ${{ env.SOLUTION }} --verify-no-changes --verbosity diagnostic
+ shell: bash
+ run: |
+ set +e
+ dotnet format "${{ env.SOLUTION }}" --no-restore --verify-no-changes --verbosity diagnostic
+ exit_code=$?
+ if [ "$exit_code" -ne 0 ]; then
+ echo "::warning::dotnet format found formatting differences. This check is informational and does not fail CI."
+ fi
diff --git a/src/AndroidTreeView.Infrastructure/Update/NekoIndexUpdateService.cs b/src/AndroidTreeView.Infrastructure/Update/NekoIndexUpdateService.cs
index 50fbd42..04bb3b9 100644
--- a/src/AndroidTreeView.Infrastructure/Update/NekoIndexUpdateService.cs
+++ b/src/AndroidTreeView.Infrastructure/Update/NekoIndexUpdateService.cs
@@ -223,12 +223,21 @@ private UpdateCheckResult Invalid(string message) =>
return null;
}
- if (Uri.TryCreate(url, UriKind.Absolute, out var absolute))
+ var trimmedUrl = url.Trim();
+ var baseUri = new Uri(_product.UpdateServerBaseUrl.TrimEnd('/') + "/");
+ if (trimmedUrl.StartsWith("/", StringComparison.Ordinal) && !trimmedUrl.StartsWith("//", StringComparison.Ordinal))
{
- return absolute.ToString();
+ return new Uri(baseUri, trimmedUrl.TrimStart('/')).ToString();
}
- var baseUri = new Uri(_product.UpdateServerBaseUrl.TrimEnd('/') + "/");
- return new Uri(baseUri, url).ToString();
+ if (Uri.TryCreate(trimmedUrl, UriKind.Absolute, out var absolute))
+ {
+ return absolute.Scheme.Equals(Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase)
+ || absolute.Scheme.Equals(Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)
+ ? absolute.ToString()
+ : null;
+ }
+
+ return new Uri(baseUri, trimmedUrl).ToString();
}
}
From ac91ab62e631e1560788572e2ab9ceafe8cd169a Mon Sep 17 00:00:00 2001
From: Birditch <58v5tb4sjt@privaterelay.appleid.com>
Date: Thu, 9 Jul 2026 18:26:08 +0800
Subject: [PATCH 08/18] Add PR automation and reusable README
---
.github/PR_AUTOMATION.md | 39 ++++
.github/PULL_REQUEST_TEMPLATE.md | 12 ++
.github/workflows/chatgpt-pr-review.yml | 253 ++++++++++++++++++++++++
.github/workflows/pr-gate.yml | 147 ++++++++++++++
README-CN.md | 41 ++--
README.en.md | 63 ++----
README.md | 63 ++----
7 files changed, 492 insertions(+), 126 deletions(-)
create mode 100644 .github/PR_AUTOMATION.md
create mode 100644 .github/workflows/chatgpt-pr-review.yml
create mode 100644 .github/workflows/pr-gate.yml
diff --git a/.github/PR_AUTOMATION.md b/.github/PR_AUTOMATION.md
new file mode 100644
index 0000000..b3439c6
--- /dev/null
+++ b/.github/PR_AUTOMATION.md
@@ -0,0 +1,39 @@
+# PR Automation
+
+This repository uses automated checks to make pull requests easier for maintainers and safer for third-party contributions.
+
+## Required PR Body
+
+Every PR should fill in the template and check the `SIGN` declaration. The `PR Gate` workflow verifies:
+
+- The PR has a meaningful title and summary.
+- The `SIGN` declaration is checked.
+- Third-party PRs do not change privileged automation, packaging, script, or binary artifact paths without maintainer handling.
+
+The gate uses `pull_request_target` but does not check out or execute contributor code.
+
+## ChatGPT Preliminary Review
+
+`ChatGPT PR Review` runs automatically when a PR is opened or updated. Maintainers can request another pass by commenting:
+
+```text
+@ChatGPT
+```
+
+or:
+
+```text
+/chatgpt-review
+```
+
+The workflow reads PR metadata and diff through the GitHub API, then posts or updates one bot comment with the preliminary review. It does not execute pull request code.
+
+## OpenAI Configuration
+
+Set these repository secrets or variables:
+
+- `OPENAI_API_KEY` secret: required for ChatGPT review. If absent, the review job skips safely.
+- `OPENAI_BASE_URL` secret: optional OpenAI-compatible gateway URL, for example a relay ending in `/v1`. If absent, the official OpenAI API base URL is used.
+- `OPENAI_REVIEW_MODEL` repository variable: optional model override. If absent, the workflow uses its built-in default.
+
+The review workflow tries the Responses API first. If a gateway does not support it, the workflow falls back to an OpenAI-compatible chat completions endpoint.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6f6db2e..6c9300a 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -4,6 +4,11 @@
+## SIGN 声明 (SIGN Declaration)
+
+- [ ] SIGN: 我确认我有权提交这些改动,并同意它们按本仓库许可证发布。
+ (I certify that I have the right to submit this work and agree it is provided under this repository's license.)
+
## 类型 (Type)
- [ ] 缺陷修复 (Bug fix)
@@ -17,6 +22,13 @@
+## 安全影响 (Security Impact)
+
+- [ ] 未修改 GitHub Actions、发布脚本、打包脚本或二进制产物。
+ (No GitHub Actions, release scripts, packaging scripts, or binary artifacts were changed.)
+- [ ] 如修改了上述敏感区域,我已在摘要或细节中说明原因。
+ (If sensitive areas were changed, I explained why in the summary or details.)
+
## 验证 (Verification)
- [ ] `dotnet build -c Release` 通过 (build succeeds).
diff --git a/.github/workflows/chatgpt-pr-review.yml b/.github/workflows/chatgpt-pr-review.yml
new file mode 100644
index 0000000..d9ba557
--- /dev/null
+++ b/.github/workflows/chatgpt-pr-review.yml
@@ -0,0 +1,253 @@
+name: ChatGPT PR Review
+
+on:
+ pull_request_target:
+ types: [opened, synchronize, reopened, ready_for_review]
+ issue_comment:
+ types: [created]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: read
+
+jobs:
+ review:
+ name: Preliminary ChatGPT review
+ if: github.event_name == 'pull_request_target' || github.event_name == 'issue_comment'
+ runs-on: ubuntu-latest
+ steps:
+ - name: Review pull request diff
+ shell: bash
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
+ OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
+ OPENAI_MODEL: ${{ vars.OPENAI_REVIEW_MODEL }}
+ run: |
+ node <<'NODE'
+ const fs = require('fs');
+
+ (async () => {
+ const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
+ if (event.issue && !event.issue.pull_request) {
+ console.log('Comment is not on a pull request; skipping.');
+ process.exit(0);
+ }
+
+ if (event.comment) {
+ const command = (event.comment.body || '').toLowerCase();
+ if (!command.includes('@chatgpt') && !command.includes('/chatgpt-review')) {
+ console.log('Comment does not request ChatGPT review; skipping.');
+ process.exit(0);
+ }
+ }
+
+ const prNumber = event.pull_request?.number || event.issue?.number;
+ if (!prNumber) {
+ console.log('No pull request number found; skipping.');
+ process.exit(0);
+ }
+
+ if (!process.env.OPENAI_API_KEY) {
+ fs.appendFileSync(
+ process.env.GITHUB_STEP_SUMMARY,
+ '# ChatGPT PR Review\n\nOPENAI_API_KEY is not configured, so the automatic review was skipped.\n');
+ process.exit(0);
+ }
+
+ const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
+ const githubHeaders = {
+ Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
+ Accept: 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28',
+ };
+
+ async function gh(path, options = {}) {
+ const response = await fetch(`https://api.github.com${path}`, {
+ ...options,
+ headers: {
+ ...githubHeaders,
+ ...(options.headers || {}),
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`GitHub API ${path} failed: ${response.status} ${await response.text()}`);
+ }
+ if (response.status === 204) {
+ return null;
+ }
+ return response.json();
+ }
+
+ async function ghText(path, accept) {
+ const response = await fetch(`https://api.github.com${path}`, {
+ headers: {
+ ...githubHeaders,
+ Accept: accept,
+ },
+ });
+ if (!response.ok) {
+ throw new Error(`GitHub API ${path} failed: ${response.status} ${await response.text()}`);
+ }
+ return response.text();
+ }
+
+ async function listFiles() {
+ const files = [];
+ for (let page = 1; page <= 20; page += 1) {
+ const batch = await gh(`/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100&page=${page}`);
+ files.push(...batch);
+ if (batch.length < 100) {
+ break;
+ }
+ }
+ return files;
+ }
+
+ function extractText(json) {
+ if (typeof json.output_text === 'string' && json.output_text.trim()) {
+ return json.output_text.trim();
+ }
+ if (Array.isArray(json.output)) {
+ return json.output
+ .flatMap(item => item.content || [])
+ .map(part => part.text || '')
+ .join('\n')
+ .trim();
+ }
+ if (Array.isArray(json.choices)) {
+ return json.choices
+ .map(choice => choice.message?.content || choice.text || '')
+ .join('\n')
+ .trim();
+ }
+ return '';
+ }
+
+ async function callOpenAI(prompt) {
+ const model = process.env.OPENAI_MODEL || 'gpt-5-mini';
+ const baseUrl = (process.env.OPENAI_BASE_URL || 'https://api.openai.com/v1').replace(/\/+$/, '');
+ const headers = {
+ Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
+ 'Content-Type': 'application/json',
+ };
+ const system = [
+ 'You are doing a preliminary pull request review for AndroidTreeView.',
+ 'Focus on correctness, security, CI risk, release risk, and missing tests.',
+ 'Be concise. Write in Simplified Chinese unless a quoted code identifier is English.',
+ 'Do not claim approval. This is an initial automated review only.',
+ ].join(' ');
+
+ const responsesBody = {
+ model,
+ input: [
+ { role: 'system', content: system },
+ { role: 'user', content: prompt },
+ ],
+ max_output_tokens: 2000,
+ };
+
+ const responses = await fetch(`${baseUrl}/responses`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(responsesBody),
+ });
+
+ if (responses.ok) {
+ const text = extractText(await responses.json());
+ if (text) {
+ return text;
+ }
+ } else if (![400, 404, 405].includes(responses.status)) {
+ throw new Error(`OpenAI Responses API failed: ${responses.status} ${await responses.text()}`);
+ }
+
+ const chat = await fetch(`${baseUrl}/chat/completions`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify({
+ model,
+ messages: [
+ { role: 'system', content: system },
+ { role: 'user', content: prompt },
+ ],
+ max_tokens: 2000,
+ }),
+ });
+
+ if (!chat.ok) {
+ throw new Error(`OpenAI-compatible chat completion failed: ${chat.status} ${await chat.text()}`);
+ }
+
+ const text = extractText(await chat.json());
+ if (!text) {
+ throw new Error('OpenAI-compatible endpoint returned an empty review.');
+ }
+ return text;
+ }
+
+ async function upsertComment(body) {
+ const marker = '';
+ const comments = await gh(`/repos/${owner}/${repo}/issues/${prNumber}/comments?per_page=100`);
+ const previous = comments.find(comment =>
+ comment.user?.type === 'Bot' && typeof comment.body === 'string' && comment.body.includes(marker));
+ const payload = JSON.stringify({ body: `${marker}\n${body}` });
+ if (previous) {
+ await gh(`/repos/${owner}/${repo}/issues/comments/${previous.id}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: payload,
+ });
+ } else {
+ await gh(`/repos/${owner}/${repo}/issues/${prNumber}/comments`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: payload,
+ });
+ }
+ }
+
+ const pr = await gh(`/repos/${owner}/${repo}/pulls/${prNumber}`);
+ const files = await listFiles();
+ const diff = await ghText(`/repos/${owner}/${repo}/pulls/${prNumber}`, 'application/vnd.github.v3.diff');
+ const fileSummary = files
+ .map(file => `${file.status} ${file.filename} (+${file.additions}/-${file.deletions})`)
+ .join('\n');
+ const truncatedDiff = diff.length > 60000
+ ? `${diff.slice(0, 60000)}\n\n[Diff truncated for automated review.]`
+ : diff;
+
+ const prompt = [
+ `PR #${pr.number}: ${pr.title}`,
+ `Author: ${pr.user?.login} (${pr.author_association})`,
+ `Base: ${pr.base.ref}`,
+ `Head: ${pr.head.label}`,
+ '',
+ 'PR body:',
+ pr.body || '(empty)',
+ '',
+ 'Changed files:',
+ fileSummary || '(none)',
+ '',
+ 'Unified diff:',
+ truncatedDiff,
+ ].join('\n');
+
+ const review = await callOpenAI(prompt);
+ const commentBody = [
+ '## ChatGPT PR 初审',
+ '',
+ review,
+ '',
+ '---',
+ '这是自动初审,不代表维护者批准。第三方 PR 的代码没有在此 workflow 中执行。',
+ ].join('\n');
+
+ await upsertComment(commentBody);
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${commentBody}\n`);
+ })().catch(error => {
+ console.error(`::error::${error.message}`);
+ process.exit(1);
+ });
+ NODE
diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml
new file mode 100644
index 0000000..9c49f4f
--- /dev/null
+++ b/.github/workflows/pr-gate.yml
@@ -0,0 +1,147 @@
+name: PR Gate
+
+on:
+ pull_request_target:
+ types: [opened, edited, synchronize, reopened, ready_for_review]
+
+permissions:
+ contents: read
+ pull-requests: read
+
+jobs:
+ metadata-and-safety:
+ name: Metadata and safety gate
+ runs-on: ubuntu-latest
+ steps:
+ - name: Validate PR metadata and third-party safety
+ shell: bash
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ node <<'NODE'
+ const fs = require('fs');
+
+ (async () => {
+ const event = JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, 'utf8'));
+ const pr = event.pull_request;
+ if (!pr) {
+ console.log('No pull_request payload; skipping.');
+ process.exit(0);
+ }
+
+ const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/');
+ const token = process.env.GITHUB_TOKEN;
+ const headers = {
+ Authorization: `Bearer ${token}`,
+ Accept: 'application/vnd.github+json',
+ 'X-GitHub-Api-Version': '2022-11-28',
+ };
+
+ async function gh(path) {
+ const response = await fetch(`https://api.github.com${path}`, { headers });
+ if (!response.ok) {
+ throw new Error(`GitHub API ${path} failed: ${response.status} ${await response.text()}`);
+ }
+ return response.json();
+ }
+
+ async function listFiles() {
+ const files = [];
+ for (let page = 1; page <= 20; page += 1) {
+ const batch = await gh(`/repos/${owner}/${repo}/pulls/${pr.number}/files?per_page=100&page=${page}`);
+ files.push(...batch);
+ if (batch.length < 100) {
+ break;
+ }
+ }
+ return files;
+ }
+
+ function section(body, heading) {
+ const lines = body.split(/\r?\n/);
+ const start = lines.findIndex(line => line.trim() === `## ${heading}`);
+ if (start < 0) {
+ return '';
+ }
+
+ const collected = [];
+ for (const line of lines.slice(start + 1)) {
+ if (/^##\s+/.test(line)) {
+ break;
+ }
+ collected.push(line);
+ }
+
+ return collected.join('\n').replace(//g, '').trim();
+ }
+
+ const failures = [];
+ const warnings = [];
+ const body = pr.body || '';
+ const summary = section(body, '摘要 (Summary)');
+ const hasSign = /-\s*\[[xX]\]\s*SIGN:/i.test(body);
+
+ if (!hasSign) {
+ failures.push('PR must check the SIGN declaration in the pull request body.');
+ }
+
+ if (summary.length < 10) {
+ failures.push('PR summary is missing or too short.');
+ }
+
+ if ((pr.title || '').trim().length < 8) {
+ failures.push('PR title is too short to be useful.');
+ }
+
+ const trustedAssociations = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']);
+ const sameRepository = pr.head.repo?.full_name === pr.base.repo?.full_name;
+ const trustedAuthor = sameRepository || trustedAssociations.has(pr.author_association);
+ const files = await listFiles();
+ const sensitivePatterns = [
+ /^\.github\/workflows\//,
+ /^\.github\/actions\//,
+ /^\.github\/CODEOWNERS$/,
+ /^build\//,
+ /^packaging\//,
+ /^tools\//,
+ /\.(bat|cmd|dll|dmg|exe|msi|pkg|ps1|psm1|sh|zip)$/i,
+ ];
+ const sensitiveFiles = files
+ .map(file => file.filename)
+ .filter(name => sensitivePatterns.some(pattern => pattern.test(name)));
+
+ if (!trustedAuthor && sensitiveFiles.length > 0) {
+ failures.push(`Third-party PR changes security-sensitive files: ${sensitiveFiles.join(', ')}`);
+ } else if (sensitiveFiles.length > 0) {
+ warnings.push(`Sensitive files changed by trusted author: ${sensitiveFiles.join(', ')}`);
+ }
+
+ const summaryLines = [
+ '# PR Gate',
+ '',
+ `- PR: #${pr.number} ${pr.title}`,
+ `- Author association: ${pr.author_association}`,
+ `- Same repository branch: ${sameRepository ? 'yes' : 'no'}`,
+ `- Files changed: ${files.length}`,
+ ];
+
+ if (warnings.length > 0) {
+ summaryLines.push('', '## Warnings', ...warnings.map(warning => `- ${warning}`));
+ }
+
+ if (failures.length > 0) {
+ summaryLines.push('', '## Failures', ...failures.map(failure => `- ${failure}`));
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summaryLines.join('\n')}\n`);
+ for (const failure of failures) {
+ console.error(`::error::${failure}`);
+ }
+ process.exit(1);
+ }
+
+ summaryLines.push('', 'PR metadata and third-party safety checks passed.');
+ fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${summaryLines.join('\n')}\n`);
+ })().catch(error => {
+ console.error(`::error::${error.message}`);
+ process.exit(1);
+ });
+ NODE
diff --git a/README-CN.md b/README-CN.md
index df1c3cf..1c653a0 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -1,19 +1,17 @@
# AndroidTreeView
[](LICENSE)
-[](https://dotnet.microsoft.com/)
-[](https://avaloniaui.net/)
+[](https://github.com/Birditch/AndroidTreeView/releases/latest)
+[](https://github.com/Birditch/AndroidTreeView/actions/workflows/ci.yml)
+[](https://dotnet.microsoft.com/)
+[](https://avaloniaui.net/)
[](#使用说明)
[主文档](README.md) | **简体中文** | [English](README.en.md)
AndroidTreeView 是一个用于 Android 设备巡检、测试与管理的桌面工具,支持 Windows 与 macOS(Apple Silicon)。主程序负责设备总览、详情、投屏、基础工具和设置;Mini 版本保持独立运行,常驻监听设备并自动投屏。
-当前版本:**v1.0.6**。
-
-## 产品样式展示
-
-
+当前发布请查看上方 Release 徽章或 [GitHub Releases](https://github.com/Birditch/AndroidTreeView/releases/latest)。运行时版本、目标框架和打包配置以项目文件与发布工作流为准。
## 功能
@@ -58,26 +56,6 @@ ADB 安装与排错见 [docs/adb-requirements.md](docs/adb-requirements.md)。
- Windows 更新包使用带 `release.json` 的 x64 ZIP;GitHub Release 同时包含 macOS Apple Silicon ZIP。
- ZIP 中没有受支持的发布清单时会拒绝安装,避免用户手动替换文件。
-## 打包
-
-正式发布只通过 GitHub Actions 的 `Publish` 工作流完成。本地命令仅用于验证打包链路:
-
-```powershell
-./packaging/build-update-zip.ps1 -Product App -Rid win-x64
-./packaging/build-update-zip.ps1 -Product Mini -Rid win-x64
-```
-
-默认产物版本是 `1.0.6`。只发布 x64 架构。验证输出包含主程序和 Mini 的上传 ZIP,例如:
-
-```text
-artifacts/AndroidTreeView-1.0.6-win-x64.zip
-artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
-```
-
-更多细节见 [docs/packaging.md](docs/packaging.md)。
-
## 验证
```bash
@@ -87,6 +65,13 @@ dotnet build src/AndroidTreeView.Mini.Mac/AndroidTreeView.Mini.Mac.csproj --no-r
dotnet test AndroidTreeView.sln --no-restore
```
-## 许可证
+## 许可证与致谢
+
+
+
+
+
+
+
AndroidTreeView 基于 [MIT License](LICENSE) 开源。
diff --git a/README.en.md b/README.en.md
index c72aaae..e3e1681 100644
--- a/README.en.md
+++ b/README.en.md
@@ -1,19 +1,17 @@
# AndroidTreeView
[](LICENSE)
-[](https://dotnet.microsoft.com/)
-[](https://avaloniaui.net/)
+[](https://github.com/Birditch/AndroidTreeView/releases/latest)
+[](https://github.com/Birditch/AndroidTreeView/actions/workflows/ci.yml)
+[](https://dotnet.microsoft.com/)
+[](https://avaloniaui.net/)
[](#usage)
[Simplified Chinese](README.md) | **English**
AndroidTreeView is a desktop tool for inspecting, testing, and managing Android devices through ADB. The full app shows device cards, detail pages, mirroring, tools, settings, and updates. The Mini app stays resident, watches for devices, and starts mirroring automatically after authorization.
-Current version: **v1.0.6**. Current verification target: solution build passes, all tests pass, and GitHub Actions creates Windows x64 and macOS Apple Silicon ZIP artifacts for both App and Mini.
-
-## Product Preview
-
-
+See the Release badge above or [GitHub Releases](https://github.com/Birditch/AndroidTreeView/releases/latest) for the current published version. Runtime versions, target frameworks, and packaging settings are defined by the project files and release workflow.
## Features
@@ -26,29 +24,9 @@ Current version: **v1.0.6**. Current verification target: solution build passes,
- Update automation downloads packages, verifies SHA-256 metadata when available, extracts Windows x64 ZIP packages, and starts a local update script so users are not asked to manually replace files.
- Simplified Chinese and English UI, with Light / Dark / System theme modes.
-## Repository Layout
-
-```text
-src/
- AndroidTreeView.Models
- AndroidTreeView.Core
- AndroidTreeView.Adb
- AndroidTreeView.Infrastructure
- AndroidTreeView.Shared
- AndroidTreeView.App
- AndroidTreeView.Mini
- AndroidTreeView.Mini.Mac
-tests/
- AndroidTreeView.*.Tests
-packaging/
- win-x64 / osx-arm64 ZIP packaging and optional WiX MSI packaging
-build/
- Shared MSBuild targets
-```
-
## Build And Run
-Requires the .NET 10 SDK:
+Install the .NET SDK targeted by the project files:
```bash
dotnet restore AndroidTreeView.sln
@@ -76,26 +54,6 @@ See [docs/adb-requirements.md](docs/adb-requirements.md) for platform-tools setu
3. Use the device cards for status, mirroring, CLI access, and non-destructive tools.
4. Use Settings or About to check and install updates.
-## Release ZIP Packaging
-
-The product version is currently `1.0.6` and is kept in sync across runtime version, App/Mini assembly metadata, manifest, and the ZIP build script. Official releases are produced only by the GitHub Actions `Publish` workflow.
-
-Local commands are for packaging validation only:
-
-```powershell
-./packaging/build-update-zip.ps1 -Product App -Rid win-x64
-./packaging/build-update-zip.ps1 -Product Mini -Rid win-x64
-```
-
-Example output:
-
-```text
-artifacts/AndroidTreeView-1.0.6-win-x64.zip
-artifacts/AndroidTreeView-1.0.6-osx-arm64.zip
-artifacts/AndroidTreeView-Mini-1.0.6-win-x64.zip
-artifacts/AndroidTreeView-Mini-1.0.6-osx-arm64.zip
-```
-
## Auto Update
- Full app update key: `android-tree-view-app`.
@@ -114,6 +72,13 @@ dotnet build src/AndroidTreeView.Mini.Mac/AndroidTreeView.Mini.Mac.csproj --no-r
dotnet test AndroidTreeView.sln --no-restore
```
-## License
+## License And Credits
+
+