diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b677951..8b44dc8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -42,18 +42,18 @@ jobs:
dotnet:
name: Avalonia (Core/Windows/Avalonia 三层)
runs-on: windows-latest
- if: hashFiles('SmartSideBAR.sln') != ''
+ if: hashFiles('SmartSideBAR.slnx') != ''
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore
- run: dotnet restore SmartSideBAR.sln
+ run: dotnet restore SmartSideBAR.slnx
- name: Build
- run: dotnet build SmartSideBAR.sln -c Release --no-restore
+ run: dotnet build SmartSideBAR.slnx -c Release --no-restore
- name: Test (覆盖率采集)
- run: dotnet test SmartSideBAR.sln -c Release --no-build --collect:"XPlat Code Coverage" --results-directory TestResults
+ run: dotnet test SmartSideBAR.slnx -c Release --no-build --collect:"XPlat Code Coverage" --results-directory TestResults
# 覆盖率门禁:Wave A 先落机制、阈值 0;Wave D(§10.1)提升至 80
- name: Coverage gate
shell: pwsh
@@ -65,10 +65,11 @@ jobs:
Write-Host "Line coverage: $line%"
$min = 0 # TODO(Wave D): 80
if ([double]$line -lt $min) { throw "coverage $line% < $min%" }
- - name: Publish (自包含单文件)
+ - name: Publish (自包含单文件, 部分裁剪+压缩 —— 实测 14.7MB 冒烟全绿)
run: >
dotnet publish src/SmartSideBAR.Avalonia -c Release -r win-x64
--self-contained -p:PublishSingleFile=true -p:PublishTrimmed=true
+ -p:TrimMode=partial -p:EnableCompressionInSingleFile=true
-o artifacts/app
- uses: actions/upload-artifact@v4
with:
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..139aae8
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,11 @@
+
+
+
+ enable
+ enable
+ latest
+ nullable
+ true
+ 2.0.0-waveA
+
+
diff --git a/SmartSideBAR.slnx b/SmartSideBAR.slnx
new file mode 100644
index 0000000..ba19d73
--- /dev/null
+++ b/SmartSideBAR.slnx
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/mlp-wave-a-report.md b/docs/mlp-wave-a-report.md
new file mode 100644
index 0000000..138cea6
--- /dev/null
+++ b/docs/mlp-wave-a-report.md
@@ -0,0 +1,170 @@
+# MLP Wave A 交付报告(骨架 + AppBar 一等公民化)
+
+> 依据《优化方案_Master_MLP_v1.md》(M1.0, 2026-09-06)。分支:`avalonia`(主线开发);
+> `main` 已完成 Wave 0 治理并冻结维护(仅安全补丁)。
+
+## 1. Wave 0 仓库治理(已交付,main @ 263db0a)
+
+| 项 | 结果 |
+|---|---|
+| G1 密钥治理 | **勘误**:`cert/` 私钥从未被 git 跟踪(`git log --all -- cert/` 为空),无需历史重写。处置:`.gitignore` 改为按文件类型精确忽略(pfx/p12/password/thumbprint),公钥 `SmartSideBAR-root.cer` 与信任脚本入库可审计;新增 `uninstall-trust.cmd`(安装包默认附带,卸载/轮换时移除旧信任根)。密钥轮换步骤见 `cert/README.md`(附录 C.1),留作用户手动动作(需交互式口令) |
+| G3 CI | `.github/workflows/ci.yml`:gitleaks 前置(全历史扫描)+ Electron 冻结门禁(typecheck/lint/test 全绿本地验证)+ Avalonia dotnet 门禁(构建/测试/覆盖率机制,阈值 Wave D 提升至 80)+ v2* tag release job(签名走 secrets) |
+| G6 | `aede938` 无关提交已在历史,按方案默认 3a 不重写(gitleaks 防再犯) |
+| 分支策略 | main 冻结 / avalonia 主开发线(ADR-M1 绞杀者迁移) |
+
+## 2. Wave A 解决方案(本报告范围)
+
+### 2.1 结构(方案 §4.1 落地)
+
+```
+SmartSideBAR.slnx
+├── src/SmartSideBAR.Core net10.0(平台无关,100% 单测)
+│ ├── Configuration/ SidekickConfig(逐字段同构 types.ts) · ConfigService(三层合并+迁移)
+│ │ ConfigMigrator(内嵌 Load) · ConfigJsonContext(源生成) · Appearance/FloatBall 钳制
+│ ├── Scheduling/ SchedulerService(TimeProvider 注入,C1 变更检测落盘)
+│ ├── Links/ LinksService(E1 全量回写)
+│ ├── Policy/ PolicyService + ModuleIds
+│ ├── FloatBall/ FloatBallLayout(floatball-layout.ts 逐函数移植)
+│ └── Messaging/ EventBus(替代 IPC 事件推送)
+├── src/SmartSideBAR.Windows net10.0-windows(互操作层,零 UI 依赖)
+│ ├── AppBar/ AppBarNative(LibraryImport) · IAppBarApi/AppBarApi · AppBarService(状态机)
+│ └── Native/ WndProcHook(SetWindowLongPtr 子类化) · Win32Display
+├── src/SmartSideBAR.Avalonia net10.0-windows(UI 层,Avalonia 11.3.20)
+│ ├── Program.cs(DI 装配 §4.2) · App.axaml · WindowManager · Views/SidebarWindow(Demo rail)
+│ └── Logging/FileLoggerProvider(Wave D 换 Serilog)
+└── tests/ Core.Tests(40) + Windows.Tests(14) = 54 用例全绿
+```
+
+### 2.2 AppBar 缺口对照(方案 §5.1 → 现状)
+
+| 缺口 | 状态 | 落点 |
+|---|---|---|
+| K1 ABN 通知无人接收 | ✅ 闭环 | `WndProcHook` 子类化 + `AppBarService.OnWndProc` 状态机(POSCHANGED/STATECHANGE/FULLSCREENAPP/WINDOWARRANGE 全处理) |
+| K2 ACTIVATE/WINDOWPOSCHANGED 未上报 | ✅ | WM_ACTIVATE → ABM_ACTIVATE;每次重排后 ABM_WINDOWPOSCHANGED |
+| K3 ABN_FULLSCREENAPP | ✅ | 全屏收起 rail / 退出按用户偏好恢复(V5),`FullscreenAppChanged` 事件 |
+| K4 坐标双轨 | ✅ 单轨 | 物理像素全链路:`PhysicalDpiScope` 强制 PMv2 线程上下文 + `MoveWindow` 直达,零 DIP 换算 |
+| K5 构建链脆弱 | ✅ 消灭 | node-gyp/N-API 全退役,P/Invoke 编译期绑定 |
+| K6 自动隐藏 | ⏳ P2 | 常量已备(ABM_SETAUTOHIDEBAR),按方案列 P2 |
+| K7 多显示器/热插拔 | ✅ | WM_DISPLAYCHANGE / WM_DPICHANGED → Reposition |
+| V6 孤儿注册自愈 | ✅ | Attach 前补发 ABM_REMOVE(对未注册 hwnd 无副作用) |
+| V8 退出恢复 WorkArea | ✅ 实测 | Detach → ABM_REMOVE;进程死亡后系统回收亦实测确认 |
+
+### 2.3 对方案的两处实现勘误(重要)
+
+1. **§5.3 `Reposition` 锚点:`rcWork` → `rcMonitor`**。方案蓝本以工作区为锚,但注册后 rcWork
+ 已扣除自身 AppBar 占位——注册期 ABN_POSCHANGED 风暴中占位带每次向屏幕内侧漂移一个带宽
+ (实测 150% 缩放下工作区被蚕食至 43px)。已改为全屏矩形锚定,沿边裁剪交给 ABM_QUERYPOS
+ (与 v1.2.0 appbar.cc 语义一致),并加了漂移回归测试 `Reposition_IsStable_UnderRepeatedPosChanged`。
+2. **§5.3 `WinProcHook`:Avalonia `AddWndProcCallback` 非公开 API**,改用 `SetWindowLongPtr(GWLP_WNDPROC)`
+ 经典子类化——等效且零 UI 框架耦合,Wave B 的全局热键(WM_HOTKEY)与 USB(WM_DEVICECHANGE)复用同一钩子。
+
+另:宿主(Avalonia)线程的 DPI 感知上下文不受我们控制,实测坐标被虚拟化(÷1.5)。
+`AppBarApi` 所有调用经 `SetThreadDpiAwarenessContext(PMv2)` 作用域包裹——物理像素单轨由此硬保证。
+
+### 2.4 开发期修复的 bug 清单
+
+| # | Bug | 修复 |
+|---|---|---|
+| 1 | EventBus 退订实现错误 | Unsubscription 携带事件类型 |
+| 2 | 调度器启动追赶循环遍历时修改集合 | 快照遍历 |
+| 3 | `IReadOnlyList` 缺 JSON 源生成元数据 | Context 补注册 |
+| 4 | `NormalizeAccent` 3 位色号未校验 hex 字符 | 对齐 appearance.ts 正则语义 |
+| 5 | `NextFullHour` 跨日对齐错误 | 按本地时间整点重构 |
+| 6 | rcWork 锚点反蚀漂移(见勘误 1) | rcMonitor 锚定 + 回归测试 |
+| 7 | 线程 DPI 上下文虚拟化(见勘误 3) | PhysicalDpiScope 强制 PMv2 |
+
+### 2.5 验证证据
+
+- **单测 54/54 全绿**(Core 40 + Windows 14,零警告零错误构建)。AppBarService 状态机经
+ FakeAppBarApi 断言操作序列:Attach→NEW/QUERYPOS/SETPOS/WINDOWPOSCHANGED/MOVE;
+ POSCHANGED→重协商;FULLSCREENAPP→rail/恢复;ACTIVATE 上报;DISPLAYCHANGE→重排;Detach→REMOVE。
+- **真机冒烟(2560×1600 @150%)**:
+ - `ABM_NEW → True`,侧栏窗口物理贴右缘(虚拟坐标 1664-1707 = 物理 2496-2560,64px 宽)
+ - 工作区稳定扣除恰好一个带宽(0,0)-(1664,1019),多次 ABN_POSCHANGED 无漂移
+ - 退出:ABM_REMOVE → 工作区完全恢复 (0,0)-(1707,1019)
+ - 冒烟自动化:`SSB_SMOKE_EXIT_MS` 环境变量 N 毫秒自动退出(CI 可用)
+- **配置互认**:`%APPDATA%\SmartSideBAR\config.json` camelCase 与 Electron 版逐字段互认;
+ 遗留目录(`%APPDATA%\smartsidebar`、`%ProgramData%\SeewoSidekick`)首启自动迁移(.migrated.bak)。
+
+### 2.6 已落点的 IPC 通道(附录 A 进度)
+
+| 区块 | 状态 |
+|---|---|
+| A.7 config:get/set/updated、power(部分) | ✅ ConfigService(B3 键白名单根修) |
+| A.8 reminder:add/remove/list/due | ✅ SchedulerService |
+| A.9 window:resize(rail/panel 切换) | ✅ Demo(SetExpanded→AppBar 重排) |
+| A.10 diag:update(配置变更广播) | ✅ ConfigChanged |
+| 其余(IME/USB/捕获族/悬浮球/OOBE…) | Wave B/C(接口桩已在 DI 预留) |
+
+### 2.7 Wave B 入口提示
+
+- `IWndProcHook` 支持同窗口多回调叠加——HotkeyService/UsbDeviceWatcher 直接 `Add`。
+- `ModuleIds` + `PolicyService` 就绪,服务启动跳过逻辑按 P2-1 语义接入。
+- 铃声播放 (NAudio)、IME (InputLanguage)、USB (WM_DEVICECHANGE) 按方案 §6 对应节实现。
+
+---
+
+# 追加:Wave B / C / D 交付报告(2026-09-06 晚,分支 avalonia)
+
+## Wave B 常驻服务族(b0ae19e)
+
+| 模块 | 落点 | 真机状态 |
+|---|---|---|
+| 输入法 (§6.1) | ImeService:键盘布局枚举 + WM_INPUTLANGCHANGEREQUEST,零 PS 子进程 | rail 按钮可切 |
+| U 盘 (§6.6) | UsbDeviceWatcher:WM_DEVICECHANGE 顶层窗口广播(勘误见下) | 监听中 |
+| 打印机 (§6.7) | PrinterMonitor:WMI 轮询 + DetectedErrorState 映射(E4 根修,单测锁定) | 2 台实检 |
+| 热键 (§6.13) | HotkeyService:RegisterHotKey 系统级冲突事实 + 替代建议 | 5 槽位注册零冲突 |
+| Shell (§6.8) | taskmgr / 外链白名单 (B2) / HKCU 自启 | — |
+| 铃声 | winmm PlaySound 别名 + MCI 播 MP3(PS 守护退役) | — |
+| 托盘 (§6.14) | 运行时图标 + 显示侧栏/设置/退出 | — |
+
+## Wave C 捕获族 + UX(8daa938 / 3e88891)
+
+- **区域截图**(§6.2):全屏透明覆盖层拖选 → BitBlt 物理像素裁剪 → PNG/JPG 落盘 → Toast。
+- **屏幕批注**(§6.3/D1/D3/P1-8):InkCanvas 统一 Pointer(触屏/笔/鼠标);导出 仅笔迹/含背景 双模式。
+- **长截图**(§6.4/A5/C3):EnumWindows(剔 cloaked)+ DWM 扩展框 + 行哈希线性拼接
+ (`LongshotStitcher` 纯函数单测)+ 倒计时/进度事件。
+- **录屏**(§6.5/ADR-M4):GraphicsCaptureItem →(**IGraphicsCaptureItemInterop COM 路径**,
+ 19041 投影无 CreateForMonitorAsync)→ Direct3D11CaptureFramePool → MediaStreamSource →
+ MediaTranscoder 系统 H.264 → **MP4 直出**;停止以 null 样本收尾保证 moov 完整。
+- **悬浮球/设置/OOBE/诊断**:扇形菜单 8 动作;设置六区即时生效;6 步向导含真实环境探测;
+ 诊断 7 项聚合 + 诊断包(report.json + logs.zip)。
+- **动作总路由**:rail 按钮 / 悬浮球 / 热键 / 托盘 四入口共用 `WindowManager.Dispatch`。
+
+## Wave D 质量与性能(部分完成)
+
+| 指标(§8) | v1.2.0 基线 | 实测(Release 单文件,2560×1600@150%) | 目标 | 结论 |
+|---|---|---|---|---|
+| 空闲内存 | 预算 ≤220MB 未实测 | **私有 87.5MB / 工作集 134.6MB** | ≤80MB | 大幅优于 Electron 预算;未达 80MB,列 Wave D 优化(Avalonia 渲染线程 + WinRT 持有) |
+| 冷启动 | ≤3s 未实测 | **~3.5s**(bootstrap→AppBar 就绪,含单文件解压) | ≤1.2s | 未达;单文件解压为首帧大头,可改多文件分发或 ReadyToRun 缓解 |
+| 安装包 | 81.6MB | **14.7MB 单文件 / 10MB zip** | ≤25MB | ✅ |
+| 常驻子进程 | 1(PS 守护) | **0** | 0 | ✅ |
+| 覆盖率门禁 | 无 | 机制就绪,阈值 0(74 用例全绿) | 80% | Wave D 收官项 |
+
+## Wave B/C 新增勘误与根修(对方案)
+
+1. **§6.6/ADR-M5 勘误**:`DBT_DEVTYP_VOLUME` 不支持 `RegisterDeviceNotification` 过滤注册
+ (该 API 仅接受 DEVICEINTERFACE/HANDLE,VOLUME 一律 err=13,真机复现)。卷事件本就
+ 广播至所有顶层窗口——直接 WndProcHook 接收即正解(与 v1.2.0 一致)。
+2. **§6.5 补充**:19041 SDK 投影的 `GraphicsCaptureItem` 仅暴露 `CreateFromVisual`,
+ 监视器捕获必须走 `IGraphicsCaptureItemInterop.CreateForMonitor` COM 互操作。
+3. **file-scoped namespace 遮蔽**:项目名含 `.Windows`/`.Avalonia` 时,命名空间内引用
+ `Windows.*`/`Avalonia.*` 会被解析到自有前缀——一律 `global::` 别名(多次踩坑后固化为规范)。
+
+## 安装包(本机已产出)
+
+| 产物 | 路径 | 说明 |
+|---|---|---|
+| 单文件 | `artifacts/app/SmartSideBAR.Avalonia.exe` (14.7MB) | 自包含+部分裁剪+压缩,冒烟全绿 |
+| 绿色包 | `dist/SmartSideBAR-2.0.0-waveC-win-x64.zip` (10MB) | install.cmd/uninstall.cmd 免管理员安装 |
+| NSIS 脚本 | `installer/SmartSideBAR.nsi` | CI(choco nsis)产出标准安装器;per-user;签名 CI 注入 |
+
+发布冒烟(Trimmed 产物):AppBar 注册 ✓ / 5 热键 ✓ / 调度器 ✓ / 打印机 2 台 ✓ / 单进程 ✓。
+
+## 遗留(Wave D/E 收官清单)
+
+1. 真机人工回归:V1-V7 全场景矩阵(希沃触屏 + 多显示器)、长截图滚动页(PDF/网页)、
+ 录屏端到端产物校验(可播性/时长)、悬浮球触屏拖拽。
+2. 录屏麦克风混流(ADR-M4 增强);自动隐藏 AppBar(K6/P2);悬浮球 WS_EX_TRANSPARENT 闲时穿透。
+3. 覆盖率门禁 0→80%;内存优化(87.5→80MB 以下);冷启动(ReadyToRun / 多文件分发)。
+4. NSIS 实机打包 + 签名(CI secrets)+ v2.0-beta Release(Wave E)。
diff --git a/docs/remaining-work.md b/docs/remaining-work.md
new file mode 100644
index 0000000..37d2d70
--- /dev/null
+++ b/docs/remaining-work.md
@@ -0,0 +1,81 @@
+# SmartSideBAR(Avalonia 版)遗留工作清单
+
+> 生成于 2026-09-06,MLP 方案(优化方案_Master_MLP_v1.md)Wave A/B/C 已交付后的
+> 未完成项汇总。按优先级分组;勾选框便于逐项销账。
+> 当前基线:avalonia 分支 @ 01b0d44,安装包 `dist/SmartSideBAR-2.0.0-waveC-win-x64.zip`
+> (已签名,签名者 CN=Seewo Sidekick Team,DigiCert RFC3161 时间戳)。
+
+## P0 · 发布前必办
+
+- [ ] **真机人工回归矩阵**(docs/testing.md):V1-V8 AppBar 场景全表;
+ 6 主链路 × 100%/125%/150%/250% 缩放;希沃触屏真机(Pointer 批注/悬浮球拖拽/侧栏点击)。
+- [ ] **录屏端到端产物校验**:真实录制 ≥1 分钟 → 确认 MP4 可被希沃白板/微信读取、
+ 时长与帧率正确、StopAsync 收尾完整(moov box);多显示器场景选屏录制未实现(当前固定主屏)。
+- [ ] **长截图真机回归**:滚动页(PDF/网页/希沃课件)拼接质量、到底检测、取消路径。
+- [ ] **_NSIS 实机打包_**:本机无 makensis,`installer/SmartSideBAR.nsi` 未实测;
+ CI(choco install nsis)路径需跑通一次并人工安装验证。
+- [ ] **Explorer 重启清理验证**:dock 调试期 taskkill 强杀在 shell 中泄漏了死 hwnd 的
+ AppBar 带(dock 方块落位偏左 78px 的根因);需重启 Explorer/重启电脑确认清理后
+ 方块贴齐右缘。
+
+## P1 · 功能补全(Wave D 收官)
+
+- [ ] 录屏**麦克风混流**(ADR-M4 增强):`config.recorder.mic` 已有配置项,未接入管线。
+- [ ] 录屏**多显示器选屏**:`GraphicsCaptureItemProbe` 固定主屏,需枚举 HMONITOR + 选择 UI。
+- [ ] **自动隐藏 AppBar**(K6/P2):ABM_SETAUTOHIDEBAR 注册 + 鼠标贴边唤出。
+- [ ] 悬浮球**闲时点击穿透**(`floatball:setClickThrough` → WS_EX_TRANSPARENT)与
+ 位置持久化(拖拽结束写回 `config.floatBall.x/y`)。
+- [ ] 悬浮球**IME 徽标 / 录屏状态环**(v1.1 的 fb-ime-badge / fb-rec-ring)。
+- [ ] **提醒面板 UI**(reminder:add 等通道后端就绪,无前端界面;铃声预设选择 UI 缺)。
+- [ ] **USB/打印机/任务管理器面板**(v1.1 为侧栏内嵌面板;当前为 Toast + 独立动作)。
+- [ ] **设置窗口补全**:OOBE 角色裁剪联动设置项;外观(液态玻璃参数)编辑;
+ 热键冲突替代建议展示(HotkeyService.GetSlots 已有数据)。
+- [ ] **通知中心**(notification:show/dismiss 通道:D12 v1.2.0 空实现,当前仅 Toast,无 dismiss)。
+- [ ] 批注**触屏压力笔宽**(Pointer 压感 → 笔宽)与高亮笔类型。
+- [ ] **诊断包安装器集成**:设置界面导出后引导打开目录。
+
+## P2 · 质量工程(Wave D 门禁)
+
+- [ ] **覆盖率门禁 0 → 80%**(CI Coverage gate 阈值待提升;当前 75 用例)。
+- [ ] `dotnet format --verify-no-changes` 门禁接入 CI(§7.2)。
+- [ ] **性能优化**(§8 实测未达标项):
+ - [ ] 空闲内存 私有 87.5MB → ≤80MB(WinRT/GraphicsCapture 句柄持有审查、Avalonia 渲染线程)。
+ - [ ] 冷启动 ~3.5s → ≤1.2s(单文件解压为首帧大头:ReadyToRun / 改多文件分发 / 延迟加载)。
+- [ ] AppBar 自适应计时(V2 ≤200ms)自动化基准。
+- [ ] E2E 冒烟脚本化(SSB_SMOKE_EXIT_MS / SSB_AUTO_DOCK_MS / SSB_AUTO_UNDOCK_MS 已就绪,
+ 需接入 CI 定期跑 + 崩溃截图归档)。
+
+## P3 · 运营与演进(Wave E)
+
+- [ ] **v2.0-beta Release 流程**:打 tag → CI release job(签名走 CODESIGN_PFX_B64/
+ CODESIGN_PFX_PASS secrets——注意仓库内 pfx-password.txt 是 DPAPI 串,CI 需明文口令
+ 单独入 secret)→ NSIS 产物 → GitHub Release 草稿。
+- [ ] **旧版迁移公告**:Electron v1.2.0 → Avalonia 版切换说明(配置互认已实现,
+ `%APPDATA%\SmartSideBAR\config.json` + 旧目录自动迁移)。
+- [ ] **Electron 版归档**:FE-PARITY 验收通过后归档(附录 A 76 通道逐行勾验仍欠)。
+- [ ] 通道映射余项:`display:list/sidebarTarget`(IDisplayService 未建)、`oobe:closeAndOpenMain`
+ OOBE 完成后主窗联动、`reminder:selectSound/playTest`(文件选择 UI)、
+ `appearance:get/set/changed`(AppearanceService 未建,玻璃参数当前为静态浅色主题)。
+- [ ] MSIX 可选通道评估(ADR-M7)。
+
+## 已知技术勘误记录(实现与方案的偏差,详见 docs/mlp-wave-a-report.md)
+
+1. AppBar Reposition 锚点 rcMonitor(非方案 §5.3 的 rcWork,防占位反蚀漂移)。
+2. WndProc 钩子用 SetWindowLongPtr 子类化(Avalonia AddWndProcCallback 非公开 API)。
+3. AppBar 全部 Win32 调用包 PhysicalDpiScope(宿主线程 DPI 上下文不确定)。
+4. DBT_DEVTYP_VOLUME 不能 RegisterDeviceNotification 过滤注册(err=13);
+ USB 检测 = 全量 WM_DEVICECHANGE 广播 + 驱动器差异扫描。
+5. 19041 投影 GraphicsCaptureItem 无 CreateForMonitorAsync →
+ IGraphicsCaptureItemInterop COM 互操作。
+6. 透明窗口经外部 MoveWindow 缩放后 Avalonia 不出帧 → dock 用独立 DockWindow。
+7. Avalonia 命中测试要求可命中元素:透明画布交互挂宿主 Border(Transparent)。
+8. 项目名含 .Windows/.Avalonia 时命名空间前缀遮蔽 —— 跨框架类型一律 global:: 别名。
+
+## 环境备忘
+
+- NuGet 走 nuget.azure.cn 镜像(慢);TFM = net10.0-windows10.0.19041.0。
+- `cert/pfx-password.txt` 内容为 **DPAPI 串**(ConvertFrom-SecureString 产物),
+ 非明文口令——签名脚本需先 `ConvertTo-SecureString` 解密(Git Bash 下注意
+ signtool 参数需 `MSYS2_ARG_CONV_EXCL="*"` 防路径转换)。
+- 冒烟钩子:`SSB_SMOKE_EXIT_MS` / `SSB_AUTO_DOCK_MS` / `SSB_AUTO_UNDOCK_MS`。
+- 本机无 makensis/Inno;NSIS 产物依赖 CI。
diff --git a/docs/testing.md b/docs/testing.md
index e321580..655f511 100644
--- a/docs/testing.md
+++ b/docs/testing.md
@@ -126,3 +126,22 @@ npm run win && node scripts/bench-p28.mjs --exe dist\win-unpacked\SmartSideBAR.e
- 录屏产物为 WebM(非 MP4),README 不应宣称「转 MP4」。
- 自动更新、锁屏/电源已移除(见 `docs/project-audit-and-roadmap.md` P2-5/P2-6);快捷键槽位已实现(P2-2)。
+
+## Wave A 追加:AppBar 验收清单(方案 §5.6 V1-V8)
+
+> Avalonia 版基座已完成 AppBar 一等公民化(`docs/mlp-wave-a-report.md`)。
+> 下表为出口验收清单;自动化层见 `tests/SmartSideBAR.Windows.Tests`(状态机序列断言)。
+
+| # | 场景 | 预期 | 状态 |
+|---|---|---|---|
+| V1 | 任一应用最大化 | 工作区自动避让侧栏,侧栏完整可见 | ✅ 真机验证(ABM_NEW=True,WorkArea 扣除带宽) |
+| V2 | 任务栏移到左缘/上缘 | ABN_POSCHANGED → 自动重排(≤200ms 目标 Wave D 计时) | ✅ 单测(序列断言);真机人工项 |
+| V3 | 100%~250% 缩放像素级对齐 | 物理像素单轨,无 DIP 取整误差 | ✅ 结构性保证(PMv2 强制 + MoveWindow 直达);150% 真机验证 |
+| V4 | 双显示器 + 热插拔 | WM_DISPLAYCHANGE → 重注册 | ✅ 单测;多屏真机人工项 |
+| V5 | 全屏授课视频 | ABN_FULLSCREENAPP → 收起 rail,退出恢复 | ✅ 单测(含用户偏好恢复) |
+| V6 | 进程被强杀 | 启动自愈:Attach 前补发 ABM_REMOVE | ✅ 单测 + 实测(杀进程后 WorkArea 回收) |
+| V7 | 与第三方 AppBar 共存 | 系统自动分栏无死锁 | ⏳ 真机人工项(注册失败自动回退 Topmost 已实现) |
+| V8 | 退出应用 | WorkArea 完全恢复 | ✅ 实测(ABM_REMOVE → 对比退出前后工作区) |
+
+冒烟命令:`SSB_SMOKE_EXIT_MS=8000 src/SmartSideBAR.Avalonia/bin/Debug/net10.0-windows/SmartSideBAR.Avalonia.exe`
+(N 毫秒后自动退出;日志在 `%APPDATA%\SmartSideBAR\logs\`)
diff --git a/installer/SmartSideBAR.nsi b/installer/SmartSideBAR.nsi
new file mode 100644
index 0000000..d28766a
--- /dev/null
+++ b/installer/SmartSideBAR.nsi
@@ -0,0 +1,69 @@
+; SmartSideBAR NSIS 安装脚本 —— Wave C 发布 (方案 §7.3 release job 配套)
+; 本机无 makensis 时, 使用 dist/ 下的免工具绿色安装包 (install.cmd / uninstall.cmd);
+; CI (windows-latest + choco install nsis) 直接执行本脚本产出标准安装器。
+; 构建: makensis installer/SmartSideBAR.nsi (需先执行 dotnet publish 到 artifacts/app)
+; 签名: CI 在打包前对 exe 执行 signtool (证书从 secrets 注入, 绝不入库 §7.4)
+
+!define APP_NAME "SmartSideBAR"
+!define APP_VERSION "2.0.0"
+!define APP_PUBLISHER "Seewo Sidekick Team"
+!define APP_EXE "SmartSideBAR.exe"
+
+Name "${APP_NAME} ${APP_VERSION}"
+OutFile "..\dist\SmartSideBAR-安装包-${APP_VERSION}.exe"
+InstallDir "$LOCALAPPDATA\${APP_NAME}"
+RequestExecutionLevel user ; per-user 安装, 无需管理员 (P1-10 语义)
+SetCompressor /SOLID lzma
+Unicode true
+
+VIProductVersion "${APP_VERSION}.0"
+VIAddVersionKey /LANG=2052 "ProductName" "${APP_NAME}"
+VIAddVersionKey /LANG=2052 "FileVersion" "${APP_VERSION}"
+VIAddVersionKey /LANG=2052 "LegalCopyright" "Apache-2.0"
+
+Page directory
+Page instfiles
+UninstPage uninstConfirm
+UninstPage instfiles
+
+Section "安装"
+ SetOutPath "$INSTDIR"
+ File /oname=${APP_EXE} "..\artifacts\app\SmartSideBAR.Avalonia.exe"
+ File "..\artifacts\app\*.dll"
+
+ ; 可选信任脚本 (自签名链, 默认不执行 —— 最小信任面 §7.4)
+ File "..\cert\install-trust.cmd"
+ File "..\cert\uninstall-trust.cmd"
+ File "..\cert\SmartSideBAR-root.cer"
+
+ ; 开始菜单快捷方式
+ CreateDirectory "$SMPROGRAMS\${APP_NAME}"
+ CreateShortcut "$SMPROGRAMS\${APP_NAME}\${APP_NAME}.lnk" "$INSTDIR\${APP_EXE}"
+ CreateShortcut "$SMPROGRAMS\${APP_NAME}\卸载.lnk" "$INSTDIR\Uninstall.exe"
+
+ ; 自启动 (HKCU, asInvoker)
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${APP_NAME}" '"$INSTDIR\${APP_EXE}" /silent'
+
+ ; 卸载器
+ WriteUninstaller "$INSTDIR\Uninstall.exe"
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayName" "${APP_NAME} ${APP_VERSION}"
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "UninstallString" '"$INSTDIR\Uninstall.exe"'
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "DisplayVersion" "${APP_VERSION}"
+ WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" "Publisher" "${APP_PUBLISHER}"
+SectionEnd
+
+Section "Uninstall"
+ ; 先退应用 (AppBar 正确注销, WorkArea 恢复)
+ ExecWait 'taskkill /IM ${APP_EXE} /F'
+ Delete "$INSTDIR\${APP_EXE}"
+ Delete "$INSTDIR\*.dll"
+ Delete "$INSTDIR\Uninstall.exe"
+ Delete "$INSTDIR\install-trust.cmd"
+ Delete "$INSTDIR\uninstall-trust.cmd"
+ Delete "$INSTDIR\SmartSideBAR-root.cer"
+ RMDir "$INSTDIR"
+ Delete "$SMPROGRAMS\${APP_NAME}\*.lnk"
+ RMDir "$SMPROGRAMS\${APP_NAME}"
+ DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}"
+ DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "${APP_NAME}"
+SectionEnd
diff --git a/src/SmartSideBAR.Avalonia/App.axaml b/src/SmartSideBAR.Avalonia/App.axaml
new file mode 100644
index 0000000..1ac6f56
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/App.axaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/src/SmartSideBAR.Avalonia/App.axaml.cs b/src/SmartSideBAR.Avalonia/App.axaml.cs
new file mode 100644
index 0000000..1d22e9e
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/App.axaml.cs
@@ -0,0 +1,140 @@
+using Microsoft.Extensions.DependencyInjection;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using Avalonia.Markup.Xaml;
+using Avalonia.Threading;
+using SmartSideBAR.Core.Configuration;
+using SmartSideBAR.Core.Messaging;
+using SmartSideBAR.Core.Scheduling;
+using SmartSideBAR.Windows.AppBar;
+using SmartSideBAR.Windows.Audio;
+
+namespace SmartSideBAR.Avalonia;
+
+public partial class App : Application
+{
+ private readonly IServiceProvider? _sp;
+ private TrayIcon? _tray;
+
+ public App(IServiceProvider sp) => _sp = sp;
+
+ // 设计器/XAML 预览用
+ public App() { }
+
+ public override void Initialize() => AvaloniaXamlLoader.Load(this);
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (_sp is not null && ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var scheduler = _sp.GetRequiredService();
+ var appBar = _sp.GetRequiredService();
+ var sounds = _sp.GetRequiredService();
+ var bus = _sp.GetRequiredService();
+ var windowManager = _sp.GetRequiredService();
+
+ desktop.MainWindow = windowManager.CreateSidebar();
+
+ // 提醒: 铃声 (winmm) + 进程内通知 (Wave C Toast)
+ bus.Subscribe(d =>
+ {
+ var cfg = _sp!.GetRequiredService().Current.ReminderSound;
+ _ = sounds.PlayReminderAsync(cfg);
+ });
+
+ scheduler.Start();
+ CreateTray(desktop);
+
+ // V8: 退出前注销 AppBar / 停服务, 系统 WorkArea 完全恢复
+ desktop.ShutdownRequested += (_, _) =>
+ {
+ appBar.Detach();
+ scheduler.Stop();
+ };
+
+ ConfigureSmokeExit(desktop);
+ ConfigureAutoDock(windowManager);
+ ConfigureAutoUndock(windowManager);
+ }
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ /// 托盘 (P2-7 语义): 显示侧栏 / 打开设置 / 退出。图标运行时生成, 零资源文件。
+ private void CreateTray(IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var windowManager = _sp!.GetRequiredService();
+ var menu = new NativeMenu();
+ var show = new NativeMenuItem("显示 / 隐藏侧栏");
+ show.Click += (_, _) => windowManager.ToggleSidebar();
+ var settings = new NativeMenuItem("打开设置");
+ settings.Click += (_, _) => windowManager.OpenSettings();
+ var exit = new NativeMenuItem("退出");
+ exit.Click += (_, _) => desktop.Shutdown();
+ menu.Add(show);
+ menu.Add(settings);
+ menu.Add(new NativeMenuItemSeparator());
+ menu.Add(exit);
+
+ _tray = new TrayIcon
+ {
+ Icon = MakeIcon(),
+ ToolTipText = "SmartSideBAR",
+ Menu = menu,
+ IsVisible = true,
+ };
+ }
+
+ private static WindowIcon MakeIcon()
+ {
+ var rtb = new RenderTargetBitmap(new PixelSize(32, 32), new Vector(96, 96));
+ using (var ctx = rtb.CreateDrawingContext())
+ {
+ ctx.FillRectangle(new SolidColorBrush(Color.Parse("#2B6EE0")), new Rect(2, 2, 28, 28), 6);
+ ctx.FillRectangle(Brushes.White, new Rect(9, 14, 14, 4), 2);
+ ctx.FillRectangle(Brushes.White, new Rect(9, 21, 9, 4), 2);
+ }
+ return new WindowIcon(rtb);
+ }
+
+ /// 测试钩子: SSB_AUTO_UNDOCK_MS>0 时 N 毫秒后自动展开侧栏 (undock 回归)。
+ private void ConfigureAutoUndock(WindowManager wm)
+ {
+ if (!int.TryParse(Environment.GetEnvironmentVariable("SSB_AUTO_UNDOCK_MS"), out var ms) || ms <= 0) return;
+ var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(ms) };
+ timer.Tick += (_, _) =>
+ {
+ timer.Stop();
+ wm.Dispatch("dock"); // _docked=true → 取反为 undock
+ };
+ timer.Start();
+ }
+
+ /// 测试钩子: SSB_AUTO_DOCK_MS>0 时 N 毫秒后自动收起侧栏 (dock 形态验证)。
+ private void ConfigureAutoDock(WindowManager wm)
+ {
+ if (!int.TryParse(Environment.GetEnvironmentVariable("SSB_AUTO_DOCK_MS"), out var ms) || ms <= 0) return;
+ var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(ms) };
+ timer.Tick += (_, _) =>
+ {
+ timer.Stop();
+ wm.Dispatch("dock");
+ };
+ timer.Start();
+ }
+
+ /// 冒烟验证: SSB_SMOKE_EXIT_MS>0 时 N 毫秒后自动退出 (自动化/CI 用)。
+ private void ConfigureSmokeExit(IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (!int.TryParse(Environment.GetEnvironmentVariable("SSB_SMOKE_EXIT_MS"), out var ms) || ms <= 0) return;
+ var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(ms) };
+ timer.Tick += (_, _) =>
+ {
+ timer.Stop();
+ desktop.Shutdown();
+ };
+ timer.Start();
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Assets/floatball-icon.png b/src/SmartSideBAR.Avalonia/Assets/floatball-icon.png
new file mode 100644
index 0000000..2e79d8f
Binary files /dev/null and b/src/SmartSideBAR.Avalonia/Assets/floatball-icon.png differ
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/annotate.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/annotate.svg
new file mode 100644
index 0000000..22150f6
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/annotate.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/bell.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/bell.svg
new file mode 100644
index 0000000..d5b8fed
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/bell.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/capture.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/capture.svg
new file mode 100644
index 0000000..8d1f8a2
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/capture.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/ime.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/ime.svg
new file mode 100644
index 0000000..ce224a5
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/ime.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/link.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/link.svg
new file mode 100644
index 0000000..a720dc4
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/link.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/lock.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/lock.svg
new file mode 100644
index 0000000..de098d4
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/lock.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/longshot.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/longshot.svg
new file mode 100644
index 0000000..a55fa90
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/longshot.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/printer.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/printer.svg
new file mode 100644
index 0000000..fbe2850
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/printer.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/record.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/record.svg
new file mode 100644
index 0000000..e62ff96
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/record.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/reminder.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/reminder.svg
new file mode 100644
index 0000000..d559007
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/reminder.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/settings.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/settings.svg
new file mode 100644
index 0000000..97eb0ae
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/settings.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/sidebar.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/sidebar.svg
new file mode 100644
index 0000000..8a4dc17
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/sidebar.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/taskmgr.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/taskmgr.svg
new file mode 100644
index 0000000..978867b
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/taskmgr.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons-white/usb.svg b/src/SmartSideBAR.Avalonia/Assets/icons-white/usb.svg
new file mode 100644
index 0000000..6fd695a
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons-white/usb.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/annotate.svg b/src/SmartSideBAR.Avalonia/Assets/icons/annotate.svg
new file mode 100644
index 0000000..4850f8f
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/annotate.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/bell.svg b/src/SmartSideBAR.Avalonia/Assets/icons/bell.svg
new file mode 100644
index 0000000..22c43f9
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/bell.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/capture.svg b/src/SmartSideBAR.Avalonia/Assets/icons/capture.svg
new file mode 100644
index 0000000..56ea315
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/capture.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/ime.svg b/src/SmartSideBAR.Avalonia/Assets/icons/ime.svg
new file mode 100644
index 0000000..0a220e7
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/ime.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/link.svg b/src/SmartSideBAR.Avalonia/Assets/icons/link.svg
new file mode 100644
index 0000000..878487a
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/link.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/lock.svg b/src/SmartSideBAR.Avalonia/Assets/icons/lock.svg
new file mode 100644
index 0000000..e8cb5fa
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/lock.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/longshot.svg b/src/SmartSideBAR.Avalonia/Assets/icons/longshot.svg
new file mode 100644
index 0000000..c294eb7
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/longshot.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/printer.svg b/src/SmartSideBAR.Avalonia/Assets/icons/printer.svg
new file mode 100644
index 0000000..24664dd
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/printer.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/record.svg b/src/SmartSideBAR.Avalonia/Assets/icons/record.svg
new file mode 100644
index 0000000..df803e3
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/record.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/reminder.svg b/src/SmartSideBAR.Avalonia/Assets/icons/reminder.svg
new file mode 100644
index 0000000..49fcbda
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/reminder.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/settings.svg b/src/SmartSideBAR.Avalonia/Assets/icons/settings.svg
new file mode 100644
index 0000000..6be4237
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/settings.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/sidebar.svg b/src/SmartSideBAR.Avalonia/Assets/icons/sidebar.svg
new file mode 100644
index 0000000..25a2679
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/sidebar.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/taskmgr.svg b/src/SmartSideBAR.Avalonia/Assets/icons/taskmgr.svg
new file mode 100644
index 0000000..c99352b
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/taskmgr.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Assets/icons/usb.svg b/src/SmartSideBAR.Avalonia/Assets/icons/usb.svg
new file mode 100644
index 0000000..27731fb
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Assets/icons/usb.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs b/src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs
new file mode 100644
index 0000000..234888a
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Logging/FileLoggerProvider.cs
@@ -0,0 +1,62 @@
+// Wave A 轻量文件日志 —— 按日滚动 (Wave D 按方案换 Serilog 滚动 7d 保留)。
+// 约束: 无第三方依赖, 同步写 + 锁 (Wave A 日志量极小, 不构成瓶颈)。
+using Microsoft.Extensions.Logging;
+
+namespace SmartSideBAR.Avalonia.Logging;
+
+public sealed class FileLoggerProvider : ILoggerProvider
+{
+ private readonly object _gate = new();
+ private readonly string _logDir;
+ private DateTime _currentDay;
+ private StreamWriter? _writer;
+
+ public FileLoggerProvider(string logDir)
+ {
+ _logDir = logDir;
+ Directory.CreateDirectory(logDir);
+ }
+
+ public ILogger CreateLogger(string categoryName) => new FileLogger(this, categoryName);
+
+ private StreamWriter AcquireWriter()
+ {
+ var today = DateTime.UtcNow.Date;
+ if (_writer is null || _currentDay != today)
+ {
+ _writer?.Dispose();
+ _currentDay = today;
+ _writer = new StreamWriter(
+ Path.Combine(_logDir, $"ssb-{today:yyyyMMdd}.log"), append: true)
+ {
+ AutoFlush = true,
+ };
+ }
+ return _writer;
+ }
+
+ public void Dispose()
+ {
+ lock (_gate) _writer?.Dispose();
+ }
+
+ private sealed class FileLogger(FileLoggerProvider owner, string category) : ILogger
+ {
+ public IDisposable? BeginScope(TState state) where TState : notnull => null;
+
+ public bool IsEnabled(LogLevel logLevel) => logLevel >= LogLevel.Information;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception,
+ Func formatter)
+ {
+ if (!IsEnabled(logLevel)) return;
+ var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{logLevel}] {category}: {formatter(state, exception)}";
+ if (exception is not null) line += Environment.NewLine + exception;
+ lock (owner._gate)
+ {
+ try { owner.AcquireWriter().WriteLine(line); }
+ catch { /* 日志失败不影响业务 */ }
+ }
+ }
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Program.cs b/src/SmartSideBAR.Avalonia/Program.cs
new file mode 100644
index 0000000..07be05d
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Program.cs
@@ -0,0 +1,82 @@
+// 服务装配 (方案 §4.2): 无 IPC —— UI 与服务同进程直调类型安全接口。
+using Avalonia;
+using Avalonia.Win32;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using SmartSideBAR.Avalonia.Logging;
+using SmartSideBAR.Core.Configuration;
+using SmartSideBAR.Core.Links;
+using SmartSideBAR.Core.Messaging;
+using SmartSideBAR.Core.Policy;
+using SmartSideBAR.Core.Scheduling;
+using SmartSideBAR.Windows.AppBar;
+using SmartSideBAR.Windows.Audio;
+using SmartSideBAR.Windows.Capture;
+using SmartSideBAR.Windows.Devices;
+using SmartSideBAR.Windows.Input;
+using SmartSideBAR.Windows.Native;
+using SmartSideBAR.Windows.Recording;
+using SmartSideBAR.Windows.Shell;
+
+namespace SmartSideBAR.Avalonia;
+
+internal static class Program
+{
+ [STAThread]
+ public static int Main(string[] args)
+ {
+ var services = new ServiceCollection();
+
+ // Core 层 (100% 可单测)
+ services.AddSingleton(ConfigPaths.CreateDefault());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(TimeProvider.System);
+ services.AddSingleton();
+
+ // Windows 互操作层 (接口 + 实现成对注册, 测试可替换)
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddSingleton();
+
+ // UI 层
+ services.AddSingleton();
+
+ services.AddLogging(b =>
+ {
+ b.SetMinimumLevel(LogLevel.Information);
+ var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
+ b.AddProvider(new FileLoggerProvider(Path.Combine(appData, "SmartSideBAR", "logs")));
+ });
+
+ using var sp = services.BuildServiceProvider();
+
+ // 与 v1.2.0 main.ts bootstrap 顺序同构: 配置迁移 → 日志 → 服务 → 窗口
+ var config = sp.GetRequiredService();
+ config.Load();
+ var log = sp.GetRequiredService().CreateLogger("Bootstrap");
+ log.LogInformation(
+ "SmartSideBAR (Avalonia Wave B) 启动 — config v{Version}, 侧栏 {Side} 侧, 链接 {Links} 条, 提醒 {Reminders} 条",
+ config.Current.Version, config.Current.Display.SidebarSide,
+ config.Current.Links.Count, config.Current.Reminders.Count);
+
+ return AppBuilder.Configure(() => new App(sp))
+ .UsePlatformDetect()
+ .With(new Win32PlatformOptions())
+ .LogToTrace()
+ .StartWithClassicDesktopLifetime(args);
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj b/src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj
new file mode 100644
index 0000000..dd885cc
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/SmartSideBAR.Avalonia.csproj
@@ -0,0 +1,32 @@
+
+
+
+
+ WinExe
+ net10.0-windows10.0.19041.0
+ SmartSideBAR.Avalonia
+ true
+ app.manifest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/SmartSideBAR.Avalonia/Ui/AnnotateWindow.cs b/src/SmartSideBAR.Avalonia/Ui/AnnotateWindow.cs
new file mode 100644
index 0000000..2a8434d
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/AnnotateWindow.cs
@@ -0,0 +1,261 @@
+// 屏幕批注窗 (§6.3): Avalonia 统一 Pointer 事件 —— 鼠标/触摸/笔天然同一事件流 (D1 根修);
+// 撤销栈即时可观察 (D3); 导出双模式: 仅笔迹 (透明 PNG) / 含背景合成 (P1-8)。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using Avalonia.Media.Imaging;
+using SmartSideBAR.Windows.AppBar;
+using SmartSideBAR.Windows.Native;
+using AloLayout = global::Avalonia.Layout;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+/// 笔迹画布: 全类型指针接受, 渲染即状态。
+public sealed class InkCanvas : Control
+{
+ public sealed class Stroke
+ {
+ public List Points { get; } = [];
+ public Color Color { get; init; } = Colors.DodgerBlue;
+ public double Width { get; init; } = 4;
+ }
+
+ private readonly List _strokes = [];
+ private Stroke? _current;
+ private Color _color = Colors.DodgerBlue;
+ private double _width = 4;
+
+ public event EventHandler? StrokesChanged;
+
+ public void SetColor(Color c) => _color = c;
+ public void SetWidth(double w) => _width = w;
+
+ public int Count => _strokes.Count;
+
+ public void Undo()
+ {
+ if (_strokes.Count == 0) return;
+ _strokes.RemoveAt(_strokes.Count - 1);
+ InvalidateVisual();
+ StrokesChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ public void Clear()
+ {
+ _strokes.Clear();
+ InvalidateVisual();
+ StrokesChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ /// 导出仅笔迹 (透明底)。width/height 为物理像素, DIP 笔迹按 scale 放大。
+ public Bitmap ExportStrokesOnly(int width, int height, double scale)
+ {
+ var rtb = new RenderTargetBitmap(new PixelSize(width, height), new Vector(96 * scale, 96 * scale));
+ using (var ctx = rtb.CreateDrawingContext())
+ {
+ using (ctx.PushTransform(Matrix.CreateScale(scale, scale)))
+ {
+ DrawAll(ctx, 1.0);
+ }
+ }
+ return rtb;
+ }
+
+ internal void DrawAll(DrawingContext ctx, double opacity)
+ {
+ using var _ = ctx.PushOpacity(opacity);
+ foreach (var stroke in _strokes)
+ {
+ if (stroke.Points.Count == 1)
+ {
+ var p = stroke.Points[0];
+ ctx.DrawEllipse(new SolidColorBrush(stroke.Color), null, p, stroke.Width / 2, stroke.Width / 2);
+ continue;
+ }
+ var geometry = new StreamGeometry();
+ using (var g = geometry.Open())
+ {
+ g.BeginFigure(stroke.Points[0], isFilled: false);
+ g.LineTo(stroke.Points[0]);
+ for (var i = 1; i < stroke.Points.Count; i++) g.LineTo(stroke.Points[i]);
+ g.EndFigure(false);
+ }
+ ctx.DrawGeometry(null, new Pen(new SolidColorBrush(stroke.Color), stroke.Width, lineCap: PenLineCap.Round, lineJoin: PenLineJoin.Round), geometry);
+ }
+ if (_current is { } cur && cur.Points.Count > 0)
+ {
+ // 进行中笔迹
+ for (var i = 1; i < cur.Points.Count; i++)
+ {
+ ctx.DrawLine(new Pen(new SolidColorBrush(cur.Color), cur.Width, lineCap: PenLineCap.Round), cur.Points[i - 1], cur.Points[i]);
+ }
+ }
+ }
+
+ public override void Render(DrawingContext context) => DrawAll(context, 1.0);
+
+ // ---- 由宿主 Border (Background=Transparent, 保证可命中) 驱动的笔画 API ----
+
+ public void BeginStroke(Point p, IPointer pointer)
+ {
+ _current = new Stroke { Color = _color, Width = _width };
+ _current.Points.Add(p);
+ pointer.Capture(this);
+ }
+
+ public void ExtendStroke(Point p)
+ {
+ if (_current is null) return;
+ _current.Points.Add(p);
+ InvalidateVisual();
+ }
+
+ public void EndStroke()
+ {
+ if (_current is null) return;
+ if (_current.Points.Count > 0) _strokes.Add(_current);
+ _current = null;
+ InvalidateVisual();
+ StrokesChanged?.Invoke(this, EventArgs.Empty);
+ }
+}
+
+/// 批注主窗: 全屏透明置顶; 导出 StrokesOnly | WithBackground。
+public sealed class AnnotateWindow : Window
+{
+ private readonly InkCanvas _ink = new();
+ private readonly string _dir;
+ private readonly string _format;
+ private readonly byte[]? _backgroundPng; // 打开时的全屏底图 (WithBackground 用)
+
+ public AnnotateWindow(byte[]? backgroundPng, string dir, string format)
+ {
+ _backgroundPng = backgroundPng;
+ _dir = dir;
+ _format = format;
+ }
+
+ protected override void OnOpened(EventArgs e)
+ {
+ Topmost = true;
+ WindowState = WindowState.FullScreen;
+ SystemDecorations = SystemDecorations.None;
+ ShowInTaskbar = false;
+ Background = Brushes.Transparent;
+ TransparencyLevelHint = [];
+
+ var toolbar = BuildToolbar();
+ var inkHost = new Border { Background = Brushes.Transparent }; // Transparent 笔刷 = 可命中
+ inkHost.PointerPressed += (_, e) =>
+ {
+ var p = e.GetCurrentPoint(inkHost);
+ if (!p.Properties.IsLeftButtonPressed) return;
+ _ink.BeginStroke(p.Position, e.Pointer);
+ e.Handled = true; // Pointer 统一: 触屏/笔/鼠标 (D1 根修)
+ };
+ inkHost.PointerMoved += (_, e) =>
+ {
+ if (e.Pointer.Captured is null) return;
+ _ink.ExtendStroke(e.GetCurrentPoint(inkHost).Position);
+ e.Handled = true;
+ };
+ inkHost.PointerReleased += (_, e) =>
+ {
+ _ink.EndStroke();
+ e.Handled = true;
+ };
+ Content = new Panel
+ {
+ Children = { inkHost, _ink, toolbar },
+ };
+ KeyDown += (_, args) =>
+ {
+ if (args.Key == Key.Escape) Close();
+ };
+ base.OnOpened(e);
+ }
+
+ private Control BuildToolbar()
+ {
+ Button ColorBtn(string hex, double width = 26)
+ {
+ var btn = new Button
+ {
+ Width = width,
+ Height = 26,
+ Background = new SolidColorBrush(Color.Parse(hex)),
+ CornerRadius = new CornerRadius(13),
+ Tag = hex,
+ };
+ btn.Click += (_, _) => _ink.SetColor(Color.Parse(hex));
+ return btn;
+ }
+ Button ActionBtn(string label, Action act)
+ {
+ var b = new Button { Content = label, Padding = new Thickness(10, 4) };
+ b.Click += (_, _) => act();
+ return b;
+ }
+ var panel = new StackPanel
+ {
+ Orientation = AloLayout.Orientation.Horizontal,
+ Spacing = 8,
+ HorizontalAlignment = AloLayout.HorizontalAlignment.Center,
+ VerticalAlignment = AloLayout.VerticalAlignment.Top,
+ Margin = new Thickness(0, 10, 0, 0),
+ };
+ foreach (var c in new[] { "#2B6EE0", "#E53935", "#111111", "#FFFFFF", "#FBC02D" }) panel.Children.Add(ColorBtn(c));
+ panel.Children.Add(ActionBtn("细", () => _ink.SetWidth(2)));
+ panel.Children.Add(ActionBtn("中", () => _ink.SetWidth(5)));
+ panel.Children.Add(ActionBtn("粗", () => _ink.SetWidth(10)));
+ panel.Children.Add(ActionBtn("撤销", () => _ink.Undo()));
+ panel.Children.Add(ActionBtn("清空", _ink.Clear));
+ panel.Children.Add(ActionBtn("保存(仅笔迹)", () => Save(withBackground: false)));
+ panel.Children.Add(ActionBtn("保存(含背景)", () => Save(withBackground: true)));
+ panel.Children.Add(ActionBtn("退出", Close));
+ return new Border
+ {
+ Background = new SolidColorBrush(Color.FromArgb(230, 20, 26, 34)),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(10, 6),
+ Child = panel,
+ };
+ }
+
+ private void Save(bool withBackground)
+ {
+ try
+ {
+ var path = Path.Combine(_dir, $"annotate_{DateTime.Now:yyyyMMdd_HHmmss}.png");
+ Directory.CreateDirectory(_dir);
+ var scale = Win32Display.GetScaling(TryGetPlatformHandle()?.Handle ?? 0);
+ var w = (int)(Bounds.Width * scale);
+ var h = (int)(Bounds.Height * scale);
+
+ if (withBackground && _backgroundPng is not null)
+ {
+ using var bg = new Bitmap(new MemoryStream(_backgroundPng));
+ using var strokes = _ink.ExportStrokesOnly(w, h, scale);
+ using var composite = new RenderTargetBitmap(new PixelSize(w, h), new Vector(96 * scale, 96 * scale));
+ using (var ctx = composite.CreateDrawingContext())
+ {
+ ctx.DrawImage(bg, new Rect(0, 0, w, h));
+ ctx.DrawImage(strokes, new Rect(0, 0, w, h));
+ }
+ composite.Save(path);
+ }
+ else
+ {
+ using var strokes = _ink.ExportStrokesOnly(w, h, scale);
+ strokes.Save(path);
+ }
+ ToastService.Instance.Show($"批注已保存: {path}", "success");
+ Close();
+ }
+ catch (Exception ex)
+ {
+ ToastService.Instance.Show($"批注保存失败: {ex.Message}", "error");
+ }
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/DockWindow.cs b/src/SmartSideBAR.Avalonia/Ui/DockWindow.cs
new file mode 100644
index 0000000..6ab1280
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/DockWindow.cs
@@ -0,0 +1,57 @@
+// v1.1 dock 收起形态: 工作区底角 52×52 玻璃小方块 (独立常驻窗口 ——
+// 勘误: 对透明窗口做外部 MoveWindow 缩放后 Avalonia 不再出帧, 故 dock 不复用侧栏窗口)。
+// 点击 → 展开侧栏 (undock)。AppBar 在 dock 期间注销 (WorkArea 完整归还, v1.1 同语义)。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using SmartSideBAR.Avalonia.Views;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class DockWindow : Window
+{
+ private readonly Action _undock;
+
+ public DockWindow(Action undock)
+ {
+ _undock = undock;
+ Width = 52;
+ Height = 52;
+ SystemDecorations = SystemDecorations.None;
+ ShowInTaskbar = false;
+ ShowActivated = false;
+ Background = Brushes.Transparent;
+ TransparencyLevelHint = [ ];
+
+ var btn = new Button
+ {
+ Tag = "svg:sidebar",
+ Width = 44,
+ Height = 44,
+ CornerRadius = new CornerRadius(10),
+ Background = Brushes.Transparent,
+ Padding = new Thickness(0),
+ HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
+ VerticalAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
+ };
+ btn.PointerReleased += (_, _) => _undock();
+ var root = new Border
+ {
+ CornerRadius = new CornerRadius(12),
+ BoxShadow = BoxShadows.Parse("0 6 20 #26000000"),
+ Child = btn,
+ };
+ root.Background = new LinearGradientBrush
+ {
+ StartPoint = new RelativePoint(0.3, 0, RelativeUnit.Relative),
+ EndPoint = new RelativePoint(0.7, 1, RelativeUnit.Relative),
+ GradientStops =
+ {
+ new GradientStop(Color.Parse("#8AFAFCFF"), 0),
+ new GradientStop(Color.Parse("#7AFFFFFF"), 1),
+ },
+ };
+ Content = root;
+ Opened += (_, _) => SidebarWindow.LoadSvgIcons(this);
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/FloatBallWindow.cs b/src/SmartSideBAR.Avalonia/Ui/FloatBallWindow.cs
new file mode 100644
index 0000000..41d8acc
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/FloatBallWindow.cs
@@ -0,0 +1,211 @@
+// 悬浮球 (§6.14, 对齐 v1.1): floatball-icon 图元圆球 + 拖拽吸附 + 点击展开玻璃扇形菜单
+// (白色 SVG 图标) + 双击开合侧栏。布局算法 = Core.FloatBallLayout (v1.1 逐函数移植)。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using SmartSideBAR.Core.Configuration;
+using SmartSideBAR.Core.FloatBall;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class FloatBallWindow : Window
+{
+ private readonly string[] _actions;
+ private readonly Action _dispatch;
+ private readonly Action _toggleSidebar;
+ private readonly int _size;
+ private readonly int _snapThreshold;
+
+ private readonly Border _orb;
+ private readonly Canvas _root = new();
+ private RectLike _workPx; // 工作区 (物理像素)
+ private bool _dragging;
+ private bool _moved;
+ private bool _expanded;
+ private Point _pressPos;
+ private PixelPoint _pressWindowPos;
+ private DateTime _lastTap = DateTime.MinValue;
+
+ public FloatBallWindow(FloatBallConfig cfg, RectLike workPx, Action dispatch, Action toggleSidebar)
+ {
+ _actions = [.. cfg.Actions];
+ _dispatch = dispatch;
+ _toggleSidebar = toggleSidebar;
+ _size = cfg.Size;
+ _snapThreshold = cfg.SnapThreshold;
+ _workPx = workPx;
+
+ Width = _size;
+ Height = _size;
+ SystemDecorations = SystemDecorations.None;
+ ShowInTaskbar = false;
+ Topmost = true;
+ Background = Brushes.Transparent;
+ TransparencyLevelHint = [];
+ ShowActivated = false;
+
+ _orb = BuildOrb();
+ _root.Children.Add(_orb);
+ Content = _root;
+
+ var pos = FloatBallLayout.ResolveBallPosition((cfg.X, cfg.Y),
+ new RectLike(0, 0, workPx.Width, workPx.Height), _size, "right");
+ Position = new PixelPoint(pos.X, pos.Y);
+
+ PointerPressed += OnPress;
+ PointerMoved += OnMove;
+ PointerReleased += OnRelease;
+ DoubleTapped += (_, _) =>
+ {
+ _toggleSidebar();
+ if (_expanded) CollapseFan(); // 双击的第一击可能已展开, 一并收起
+ };
+ }
+
+ private Border BuildOrb()
+ {
+ var orb = new Border
+ {
+ Width = _size,
+ Height = _size,
+ CornerRadius = new CornerRadius(_size / 2.0),
+ BoxShadow = BoxShadows.Parse("0 6 18 #40000000"),
+ Child = new Image
+ {
+ Source = new global::Avalonia.Media.Imaging.Bitmap(
+ global::Avalonia.Platform.AssetLoader.Open(
+ new Uri("avares://SmartSideBAR.Avalonia/Assets/floatball-icon.png"))),
+ Stretch = Stretch.UniformToFill,
+ },
+ ClipToBounds = true,
+ };
+ orb.CornerRadius = new CornerRadius(_size / 2.0);
+ return orb;
+ }
+
+ // ---- 拖拽 / 点击 ----
+
+ private void OnPress(object? s, PointerPressedEventArgs e)
+ {
+ var p = e.GetCurrentPoint(this);
+ if (!p.Properties.IsLeftButtonPressed) return;
+ _dragging = true;
+ _moved = false;
+ _pressPos = p.Position;
+ _pressWindowPos = Position;
+ e.Pointer.Capture(this);
+ }
+
+ private void OnMove(object? s, PointerEventArgs e)
+ {
+ if (!_dragging) return;
+ var p = e.GetCurrentPoint(this).Position;
+ var dx = (int)(p.X - _pressPos.X);
+ var dy = (int)(p.Y - _pressPos.Y);
+ if (!_moved && Math.Abs(dx) + Math.Abs(dy) > 6) _moved = true;
+ if (_moved && !_expanded)
+ {
+ Position = new PixelPoint(_pressWindowPos.X + dx, _pressWindowPos.Y + dy);
+ }
+ }
+
+ private void OnRelease(object? s, PointerReleasedEventArgs e)
+ {
+ if (!_dragging) return;
+ _dragging = false;
+ if (_moved && !_expanded)
+ {
+ // 吸附 (v1.1 SnapToEdges 语义, 物理像素)
+ var snapped = FloatBallLayout.SnapToEdges(
+ (Position.X, Position.Y), (_size, _size), _workPx, _snapThreshold);
+ Position = new PixelPoint(snapped.X, snapped.Y);
+ return;
+ }
+ // 双击的第一击: 250ms 内第二击由 DoubleTapped 处理
+ var sinceLast = DateTime.UtcNow - _lastTap;
+ _lastTap = DateTime.UtcNow;
+ if (sinceLast.TotalMilliseconds < 260) return;
+ ToggleFan();
+ }
+
+ // ---- 扇形菜单 ----
+
+ private void ToggleFan() { if (_expanded) CollapseFan(); else ExpandFan(); }
+
+ private void ExpandFan()
+ {
+ var count = Math.Max(1, _actions.Length);
+ var radius = FloatBallLayout.FanRadius(count);
+ var panel = radius * 2 + 72;
+ var (ballX, ballY) = (Position.X, Position.Y);
+ var centerX = ballX + _size / 2;
+ var centerY = ballY + _size / 2;
+
+ // 方向: 远离最近边缘 (v1.1 computeFanLayout dir 语义)
+ var dirX = centerX - _workPx.X >= _workPx.X + _workPx.Width - centerX ? -1 : 1;
+ var dirY = centerY - _workPx.Y >= _workPx.Y + _workPx.Height - centerY ? -1 : 1;
+
+
+ Width = panel;
+ Height = panel;
+ var winX = Math.Clamp(centerX - panel / 2, _workPx.X, _workPx.X + _workPx.Width - panel);
+ var winY = Math.Clamp(centerY - panel / 2, _workPx.Y, _workPx.Y + _workPx.Height - panel);
+ Position = new PixelPoint(winX, winY);
+
+ _root.Children.Clear();
+ _root.Children.Add(_orb);
+ Canvas.SetLeft(_orb, panel / 2.0 - _size / 2.0);
+ Canvas.SetTop(_orb, panel / 2.0 - _size / 2.0);
+
+ for (var i = 0; i < count; i++)
+ {
+ var action = _actions[i];
+ var (ox, oy) = FloatBallLayout.FanItemOffset(i, count, radius, (dirX, dirY));
+ var item = new Button
+ {
+ Width = 46,
+ Height = 46,
+ Tag = "svgw:" + action,
+ CornerRadius = new CornerRadius(23),
+ Background = new SolidColorBrush(Color.FromArgb(215, 24, 32, 44)),
+ Padding = new Thickness(0),
+ HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center,
+ VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center,
+
+ };
+ item.Click += (_, _) =>
+ {
+ CollapseFan();
+ _dispatch(action);
+ };
+ Canvas.SetLeft(item, panel / 2.0 + ox - 23);
+ Canvas.SetTop(item, panel / 2.0 + oy - 23);
+ _root.Children.Add(item);
+ }
+ _expanded = true;
+ Views.SidebarWindow.LoadSvgIcons(this);
+ }
+
+ private void CollapseFan()
+ {
+ _expanded = false;
+ Width = _size;
+ Height = _size;
+ _root.Children.Clear();
+ _root.Children.Add(_orb);
+ }
+
+ private static string ActionTitle(string action) => action switch
+ {
+ "capture" => "区域截图",
+ "annotate" => "屏幕批注",
+ "longshot" => "长截图",
+ "record" => "录屏",
+ "ime" => "输入法",
+ "taskmgr" => "任务管理器",
+ "sidebar" => "侧边栏",
+ "settings" => "设置",
+ _ => action,
+ };
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/OobeWindow.cs b/src/SmartSideBAR.Avalonia/Ui/OobeWindow.cs
new file mode 100644
index 0000000..7c998c6
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/OobeWindow.cs
@@ -0,0 +1,169 @@
+// OOBE 向导 (§6.11): 6 步流程 + 角色裁剪 + 环境真实探测 (P1-12/P2-3 语义); OobeState 字段级合并 (E2 根修)。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using SmartSideBAR.Core.Configuration;
+using SmartSideBAR.Windows.Devices;
+using SmartSideBAR.Windows.Input;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class OobeWindow : Window
+{
+ private readonly ConfigService _config;
+ private readonly IImeService _ime;
+ private readonly IPrinterMonitor _printer;
+ private int _step;
+
+ private readonly StackPanel _body = new() { Spacing = 12 };
+ private readonly TextBlock _title = new() { FontSize = 22, FontWeight = FontWeight.Bold };
+ private readonly TextBlock _envText = new() { TextWrapping = TextWrapping.Wrap, FontSize = 13 };
+ private readonly CheckBox _prefIme = new() { Content = "输入法切换", IsChecked = true };
+ private readonly CheckBox _prefUsb = new() { Content = "U 盘监控", IsChecked = true };
+ private readonly CheckBox _prefShot = new() { Content = "截图/批注", IsChecked = true };
+ private readonly CheckBox _prefRecorder = new() { Content = "录屏", IsChecked = false };
+ private readonly CheckBox _prefPrinter = new() { Content = "打印机状态", IsChecked = true };
+ private string _role = "teacher";
+
+ public OobeWindow(ConfigService config, IImeService ime, IPrinterMonitor printer)
+ {
+ _config = config;
+ _ime = ime;
+ _printer = printer;
+ Width = 560;
+ Height = 480;
+ WindowStartupLocation = WindowStartupLocation.CenterScreen;
+ Title = "SmartSideBAR 初始化向导";
+ RenderStep();
+ }
+
+ private void RenderStep()
+ {
+ _body.Children.Clear();
+ var next = new Button { Content = _step >= 5 ? "完成" : "下一步", Padding = new Thickness(20, 8), HorizontalAlignment = HorizontalAlignment.Right };
+ next.Click += (_, _) => Advance();
+ var back = new Button { Content = "上一步", Padding = new Thickness(20, 8) };
+ back.Click += (_, _) => { if (_step > 0) { _step--; RenderStep(); } };
+ var skip = new Button { Content = "跳过向导" };
+ skip.Click += (_, _) => Complete(skipped: true);
+
+ var buttons = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10, HorizontalAlignment = HorizontalAlignment.Right };
+ if (_step > 0) buttons.Children.Add(back);
+ buttons.Children.Add(next);
+
+ _body.Children.Add(new TextBlock { Text = $"第 {_step + 1} / 6 步", Foreground = Brushes.Gray, FontSize = 12 });
+ switch (_step)
+ {
+ case 0:
+ _title.Text = "欢迎使用 SmartSideBAR";
+ _body.Children.Add(_title);
+ _body.Children.Add(new TextBlock
+ {
+ Text = "教学大屏常驻助手: 侧栏快捷操作 · 输入法切换 · U 盘提醒 · 截图批注 · 录屏。\n\n侧栏将停靠屏幕右缘, 任何窗口最大化都不会遮挡它。",
+ TextWrapping = TextWrapping.Wrap,
+ });
+ break;
+ case 1:
+ _title.Text = "你的角色";
+ _body.Children.Add(_title);
+ var teacher = new RadioButton { Content = "教师 —— 常用教学功能 (推荐)", GroupName = "role", IsChecked = _role == "teacher" };
+ var admin = new RadioButton { Content = "管理员 —— 全部功能 + 策略下发", GroupName = "role", IsChecked = _role == "admin" };
+ teacher.IsCheckedChanged += (_, _) => _role = "teacher";
+ admin.IsCheckedChanged += (_, _) => _role = "admin";
+ _body.Children.Add(teacher);
+ _body.Children.Add(admin);
+ break;
+ case 2:
+ _title.Text = "选择要启用的功能";
+ _body.Children.Add(_title);
+ _body.Children.Add(_prefIme);
+ _body.Children.Add(_prefUsb);
+ _body.Children.Add(_prefShot);
+ _body.Children.Add(_prefRecorder);
+ _body.Children.Add(_prefPrinter);
+ break;
+ case 3:
+ _title.Text = "环境检测";
+ _body.Children.Add(_title);
+ _envText.Text = "检测中…";
+ _body.Children.Add(_envText);
+ Task.Run(() =>
+ {
+ var printers = _printer.Query();
+ var imeState = _ime.GetState();
+ global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ var screen = Screens.Primary?.Bounds ?? new PixelRect(0, 0, 1920, 1080);
+ _envText.Text = $"屏幕: {screen.Width}x{screen.Height} (DIP)\n" +
+ $"输入法: {imeState.Locale} (中文={imeState.IsChinese})\n" +
+ $"打印机: {printers.Count} 台\n" +
+ $"录屏: {(global::Windows.Graphics.Capture.GraphicsCaptureSession.IsSupported() ? "可用" : "不可用")}";
+ });
+ });
+ break;
+ case 4:
+ _title.Text = "快捷键";
+ _body.Children.Add(_title);
+ _body.Children.Add(new TextBlock
+ {
+ Text = "Ctrl+Shift+A 区域截图\nCtrl+Shift+D 屏幕批注\nCtrl+Shift+L 长截图\nAlt+Q 悬浮球\n\n可在设置的快捷键页修改; 与其他软件冲突时系统会明确告知并给出替代建议。",
+ TextWrapping = TextWrapping.Wrap,
+ });
+ break;
+ case 5:
+ _title.Text = "一切就绪";
+ _body.Children.Add(_title);
+ _body.Children.Add(new TextBlock
+ {
+ Text = "侧栏已停靠屏幕右缘; 悬浮球可在屏幕边缘拖拽。托盘图标提供设置与退出入口。",
+ TextWrapping = TextWrapping.Wrap,
+ });
+ break;
+ }
+ _body.Children.Add(buttons);
+
+ var outer = new ScrollViewer { Content = new StackPanel { Margin = new Thickness(24), Children = { _body, skip } } };
+ Content = outer;
+ }
+
+ private void Advance()
+ {
+ if (_step >= 5) { Complete(skipped: false); return; }
+ _step++;
+ RenderStep();
+ }
+
+ /// E2 根修: OobeState 字段级合并 —— 仅写本次向导涉及的字段, 保留其他 (record with)。
+ private void Complete(bool skipped)
+ {
+ var current = _config.Current.Oobe;
+ var prefs = new OobePrefs
+ {
+ Ime = _prefIme.IsChecked == true,
+ Usb = _prefUsb.IsChecked == true,
+ Shot = _prefShot.IsChecked == true,
+ Recorder = _prefRecorder.IsChecked == true,
+ Printer = _prefPrinter.IsChecked == true,
+ };
+ var state = current with
+ {
+ Completed = !skipped,
+ CompletedAt = DateTime.Now.ToString("o"),
+ Skipped = skipped,
+ Role = _role,
+ LastStepIndex = _step,
+ Prefs = prefs,
+ };
+ _ = _config.Set("oobe", state);
+
+ // 角色裁剪 → disabledModules (P2-3 语义)
+ if (!skipped && _role == "teacher")
+ {
+ var disabled = new List();
+ if (prefs.Recorder) { /* keep recorder on */ } else disabled.Add("recorder");
+ _ = _config.Set("policy.disabledModules", disabled);
+ }
+ Close();
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/OverlayWindow.cs b/src/SmartSideBAR.Avalonia/Ui/OverlayWindow.cs
new file mode 100644
index 0000000..703145e
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/OverlayWindow.cs
@@ -0,0 +1,136 @@
+// 区域截图覆盖层 (§6.2): 全屏透明置顶 → 拖拽选框 (Pointer 统一鼠标/触屏) → BitBlt 物理裁剪。
+// Esc 取消 / Enter 或松手确认; 坐标 DIP↔物理仅在此边界换算一次 (K4)。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media;
+using SmartSideBAR.Windows.AppBar;
+using SmartSideBAR.Windows.Capture;
+using SmartSideBAR.Windows.Native;
+using AloLayout = global::Avalonia.Layout;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class OverlayWindow : Window
+{
+ private readonly ICaptureApi _capture;
+ private readonly string _dir;
+ private readonly string _format;
+ private Point _start;
+ private Border? _selection;
+ private bool _dragging;
+ private bool _done;
+
+ public OverlayWindow(ICaptureApi capture, string dir, string format)
+ {
+ _capture = capture;
+ _dir = dir;
+ _format = format;
+ }
+
+ protected override void OnOpened(EventArgs e)
+ {
+ Topmost = true;
+ WindowState = WindowState.FullScreen;
+ SystemDecorations = SystemDecorations.None;
+ ShowInTaskbar = false;
+
+ _selection = new Border
+ {
+ BorderBrush = new SolidColorBrush(Color.Parse("#2B6EE0")),
+ BorderThickness = new Thickness(2),
+ Background = new SolidColorBrush(Color.FromArgb(40, 43, 110, 224)),
+ IsVisible = false,
+ };
+ var canvas = new Canvas { Children = { _selection } };
+ var hint = new TextBlock
+ {
+ Text = "拖拽选择截图区域 · Enter 确认 · Esc 取消",
+ Foreground = Brushes.White,
+ FontSize = 15,
+ HorizontalAlignment = AloLayout.HorizontalAlignment.Center,
+ VerticalAlignment = AloLayout.VerticalAlignment.Top,
+ Margin = new Thickness(0, 24, 0, 0),
+ };
+ Content = new Panel
+ {
+ Background = new SolidColorBrush(Color.FromArgb(60, 0, 0, 0)),
+ Children = { canvas, hint },
+ };
+ KeyDown += (_, args) =>
+ {
+ if (args.Key == Key.Escape) Close();
+ else if (args.Key == Key.Enter) FinishSelection();
+ };
+ base.OnOpened(e);
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ var p = e.GetCurrentPoint(this);
+ if (!p.Properties.IsLeftButtonPressed) return;
+ _dragging = true;
+ _start = p.Position;
+ _selection!.IsVisible = true;
+ Canvas.SetLeft(_selection!, _start.X);
+ Canvas.SetTop(_selection!, _start.Y);
+ _selection!.Width = 0;
+ _selection!.Height = 0;
+ e.Pointer.Capture(this);
+ e.Handled = true;
+ }
+
+ protected override void OnPointerMoved(PointerEventArgs e)
+ {
+ if (!_dragging) return;
+ var p = e.GetCurrentPoint(this).Position;
+ var x = Math.Min(_start.X, p.X);
+ var y = Math.Min(_start.Y, p.Y);
+ _selection!.Width = Math.Abs(p.X - _start.X);
+ _selection!.Height = Math.Abs(p.Y - _start.Y);
+ Canvas.SetLeft(_selection!, x);
+ Canvas.SetTop(_selection!, y);
+ e.Handled = true;
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ if (!_dragging) return;
+ _dragging = false;
+ FinishSelection();
+ e.Handled = true;
+ }
+
+ /// 选框 → 物理像素 → BitBlt 裁剪 → 落盘 → 通知。
+ private void FinishSelection()
+ {
+ if (_done || _selection is not { } sel || sel.Width < 8 || sel.Height < 8)
+ {
+ Close();
+ return;
+ }
+ _done = true;
+ var left = Canvas.GetLeft(sel);
+ var top = Canvas.GetTop(sel);
+ var scaling = Win32Display.GetScaling(TryGetPlatformHandle()?.Handle ?? 0);
+ var region = new AppBarRect(
+ (int)Math.Round(left * scaling),
+ (int)Math.Round(top * scaling),
+ (int)Math.Round(sel.Width * scaling),
+ (int)Math.Round(sel.Height * scaling));
+ Close();
+
+ Task.Run(() =>
+ {
+ var bmp = _capture.CaptureRegion(region);
+ if (bmp is null)
+ {
+ ToastService.Instance.Show("截图失败", "error");
+ return;
+ }
+ var path = _capture.SaveImage(bmp, _dir, _format);
+ bmp.Dispose();
+ ToastService.Instance.Show($"截图已保存: {path}", "success");
+ });
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/SettingsWindow.cs b/src/SmartSideBAR.Avalonia/Ui/SettingsWindow.cs
new file mode 100644
index 0000000..e228106
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/SettingsWindow.cs
@@ -0,0 +1,152 @@
+// 设置窗 (§6.11): 唯一表单语义 (P1-6) —— 保存经 ConfigService.Set 白名单路径, 自动广播 ConfigChanged。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using SmartSideBAR.Core.Configuration;
+using SmartSideBAR.Windows.Devices;
+using SmartSideBAR.Windows.Input;
+using SmartSideBAR.Windows.Shell;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class SettingsWindow : Window
+{
+ private readonly ConfigService _config;
+ private readonly IShellService _shell;
+ private readonly IDiagnosticsService _diag;
+
+ private readonly ComboBox _side = new() { ItemsSource = new[] { "left", "right" }, SelectedIndex = 1 };
+ private readonly CheckBox _autoLaunch = new() { Content = "开机自启动" };
+ private readonly ComboBox _format = new() { ItemsSource = new[] { "PNG", "JPG" }, SelectedIndex = 0 };
+ private readonly TextBox _captureDir = new() { Watermark = "{Pictures}/Sidekick" };
+ private readonly TextBox _recorderDir = new() { Watermark = "{Videos}/Sidekick" };
+ private readonly NumericUpDown _fps = new() { Value = 15, Minimum = 5, Maximum = 60 };
+ private readonly CheckBox _mic = new() { Content = "录制系统麦克风 (Wave D)" };
+ private readonly CheckBox _usbEnabled = new() { Content = "启用 U 盘监控" };
+ private readonly NumericUpDown _printerSec = new() { Value = 10, Minimum = 3, Maximum = 300 };
+ private readonly TextBox _hkCapture = new();
+ private readonly TextBox _hkAnnotate = new();
+ private readonly TextBox _hkLongshot = new();
+ private readonly TextBox _hkFloatBall = new();
+ private readonly TextBox _hkIme = new();
+ private readonly TextBlock _diagResult = new() { TextWrapping = TextWrapping.Wrap, FontSize = 12 };
+
+ public SettingsWindow(ConfigService config, IShellService shell, HotkeyService hotkeys, IDiagnosticsService diag)
+ {
+ _config = config;
+ _shell = shell;
+ _diag = diag;
+ Title = "SmartSideBAR 设置";
+ Width = 560;
+ Height = 640;
+ WindowStartupLocation = WindowStartupLocation.CenterScreen;
+ LoadFrom(config.Current);
+ Content = BuildUi();
+ }
+
+ private void LoadFrom(SidekickConfig c)
+ {
+ _side.SelectedIndex = c.Display.SidebarSide == SidebarSide.Left ? 0 : 1;
+ _autoLaunch.IsChecked = _shell.GetAutoLaunch();
+ _format.SelectedIndex = c.Capture.Format.Equals("jpg", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
+ _captureDir.Text = c.Capture.Dir;
+ _recorderDir.Text = c.Recorder.Dir;
+ _fps.Value = c.Recorder.Fps;
+ _mic.IsChecked = c.Recorder.Mic;
+ _usbEnabled.IsChecked = c.Usb.Enabled;
+ _printerSec.Value = c.Printer.PollIntervalSec;
+ _hkCapture.Text = c.Capture.Hotkey;
+ _hkAnnotate.Text = c.Capture.AnnotateHotkey;
+ _hkLongshot.Text = c.Capture.LongshotHotkey;
+ _hkFloatBall.Text = c.FloatBall.Hotkey;
+ _hkIme.Text = "Ctrl+Shift+I";
+ }
+
+ private Control BuildUi()
+ {
+ static StackPanel RowOf(string label, Control control)
+ {
+ var sp = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 10, Margin = new Thickness(0, 4) };
+ sp.Children.Add(new TextBlock { Text = label, Width = 120, VerticalAlignment = VerticalAlignment.Center });
+ sp.Children.Add(control);
+ return sp;
+ }
+
+ var save = new Button { Content = "保存", Padding = new Thickness(24, 8), HorizontalAlignment = HorizontalAlignment.Right };
+ save.Click += (_, _) => Save();
+
+ var runDiag = new Button { Content = "运行诊断" };
+ runDiag.Click += (_, _) =>
+ {
+ _diagResult.Text = "巡检中…";
+ Task.Run(() => _diag.RunFullCheck()).ContinueWith(t =>
+ {
+ global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
+ {
+ _diagResult.Text = string.Join('\n', t.Result.Select(kv => $"{kv.Key}: {kv.Value}"));
+ });
+ });
+ };
+ var exportDiag = new Button { Content = "导出诊断包", Margin = new Thickness(8, 0, 0, 0) };
+ exportDiag.Click += (_, _) =>
+ {
+ var path = _diag.ExportPack(Path.GetTempPath());
+ ToastService.Instance.Show($"诊断包: {path}", "success");
+ };
+
+ var scroll = new ScrollViewer
+ {
+ Margin = new Thickness(16),
+ Content = new StackPanel { Spacing = 6 },
+ };
+ var root = (StackPanel)scroll.Content!;
+ root.Children.Add(new TextBlock { Text = "常规", FontWeight = FontWeight.Bold, FontSize = 15, Margin = new Thickness(0, 8, 0, 4) });
+ root.Children.Add(RowOf("侧栏位置", _side));
+ root.Children.Add(RowOf("", _autoLaunch));
+ root.Children.Add(new TextBlock { Text = "截图", FontWeight = FontWeight.Bold, FontSize = 15, Margin = new Thickness(0, 12, 0, 4) });
+ root.Children.Add(RowOf("格式", _format));
+ root.Children.Add(RowOf("保存目录", _captureDir));
+ root.Children.Add(new TextBlock { Text = "录屏 (MP4)", FontWeight = FontWeight.Bold, FontSize = 15, Margin = new Thickness(0, 12, 0, 4) });
+ root.Children.Add(RowOf("帧率", _fps));
+ root.Children.Add(RowOf("", _mic));
+ root.Children.Add(RowOf("保存目录", _recorderDir));
+ root.Children.Add(new TextBlock { Text = "设备", FontWeight = FontWeight.Bold, FontSize = 15, Margin = new Thickness(0, 12, 0, 4) });
+ root.Children.Add(RowOf("", _usbEnabled));
+ root.Children.Add(RowOf("打印机轮询(秒)", _printerSec));
+ root.Children.Add(new TextBlock { Text = "快捷键 (Ctrl/Alt/Shift/Win + 键)", FontWeight = FontWeight.Bold, FontSize = 15, Margin = new Thickness(0, 12, 0, 4) });
+ root.Children.Add(RowOf("区域截图", _hkCapture));
+ root.Children.Add(RowOf("屏幕批注", _hkAnnotate));
+ root.Children.Add(RowOf("长截图", _hkLongshot));
+ root.Children.Add(RowOf("悬浮球", _hkFloatBall));
+ root.Children.Add(RowOf("输入法", _hkIme));
+ root.Children.Add(new TextBlock { Text = "诊断", FontWeight = FontWeight.Bold, FontSize = 15, Margin = new Thickness(0, 12, 0, 4) });
+ var diagRow = new StackPanel { Orientation = Orientation.Horizontal };
+ diagRow.Children.Add(runDiag);
+ diagRow.Children.Add(exportDiag);
+ root.Children.Add(diagRow);
+ root.Children.Add(_diagResult);
+ root.Children.Add(save);
+
+ return scroll;
+ }
+
+ private void Save()
+ {
+ var c = _config.Current;
+ _ = _config.Set("display.sidebarSide", _side.SelectedIndex == 0 ? "left" : "right");
+ _ = _config.Set("capture.format", (string?)_format.SelectedItem ?? "PNG");
+ _ = _config.Set("capture.dir", string.IsNullOrWhiteSpace(_captureDir.Text) ? c.Capture.Dir : _captureDir.Text.Trim());
+ _ = _config.Set("recorder.dir", string.IsNullOrWhiteSpace(_recorderDir.Text) ? c.Recorder.Dir : _recorderDir.Text.Trim());
+ _ = _config.Set("recorder.fps", (int)(_fps.Value ?? 15));
+ _ = _config.Set("usb.enabled", _usbEnabled.IsChecked == true);
+ _ = _config.Set("printer.pollIntervalSec", (int)(_printerSec.Value ?? 10));
+ _ = _config.Set("capture.hotkey", _hkCapture.Text?.Trim());
+ _ = _config.Set("capture.annotateHotkey", _hkAnnotate.Text?.Trim());
+ _ = _config.Set("capture.longshotHotkey", _hkLongshot.Text?.Trim());
+ _ = _config.Set("floatBall.hotkey", _hkFloatBall.Text?.Trim());
+ _shell.SetAutoLaunch(_autoLaunch.IsChecked == true);
+ ToastService.Instance.Show("设置已保存", "success");
+ Close();
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/ToastService.cs b/src/SmartSideBAR.Avalonia/Ui/ToastService.cs
new file mode 100644
index 0000000..e6a748d
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/ToastService.cs
@@ -0,0 +1,77 @@
+// 轻量进程内 Toast (右下角, 3.5s 自动消失) —— 替代 Electron renderer 通知。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Threading;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class ToastService
+{
+ public static ToastService Instance { get; } = new();
+
+ public void Show(string message, string tone = "info")
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ try
+ {
+ var color = tone switch
+ {
+ "success" => "#2E7D32",
+ "warn" => "#B26A00",
+ "error" => "#C62828",
+ _ => "#243041",
+ };
+ var text = new TextBlock
+ {
+ Text = message,
+ Foreground = Brushes.White,
+ TextWrapping = TextWrapping.Wrap,
+ MaxWidth = 360,
+ FontSize = 13,
+ };
+ var border = new Border
+ {
+ Background = new SolidColorBrush(Color.Parse(color)),
+ CornerRadius = new CornerRadius(10),
+ Padding = new Thickness(14, 10),
+ Child = text,
+ Opacity = 0.96,
+ };
+ var win = new Window
+ {
+ SystemDecorations = SystemDecorations.None,
+ ShowInTaskbar = false,
+ TransparencyLevelHint = [],
+ Background = Brushes.Transparent,
+ SizeToContent = SizeToContent.WidthAndHeight,
+ ShowActivated = false,
+ Topmost = true,
+ Content = border,
+ };
+ win.Show();
+ // 右下角 (Show 后取屏幕信息; DIP 边界 + 物理像素位置的混合由 Avalonia 折算)
+ try
+ {
+ var screen = win.Screens.Primary ?? win.Screens.All[0];
+ win.Position = new PixelPoint(
+ screen.Bounds.Right - 420,
+ screen.Bounds.Bottom - 140);
+ }
+ catch { /* 定位尽力而为 */ }
+ var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(3500) };
+ timer.Tick += (_, _) =>
+ {
+ timer.Stop();
+ win.Close();
+ };
+ timer.Start();
+ }
+ catch
+ {
+ // 通知失败不影响业务
+ }
+ });
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/Ui/WindowPickerWindow.cs b/src/SmartSideBAR.Avalonia/Ui/WindowPickerWindow.cs
new file mode 100644
index 0000000..ffa62f4
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/Ui/WindowPickerWindow.cs
@@ -0,0 +1,49 @@
+// 长截图目标窗口选择器 (longshot:selectWindow 落点)。
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Layout;
+using Avalonia.Media;
+using SmartSideBAR.Windows.Capture;
+
+namespace SmartSideBAR.Avalonia.Ui;
+
+public sealed class WindowPickerWindow(IReadOnlyList targets) : Window
+{
+ public LongshotTarget? Selected { get; private set; }
+
+ public WindowPickerWindow() : this([]) { }
+
+ protected override void OnOpened(EventArgs e)
+ {
+ Title = "选择要长截图的窗口";
+ Width = 460;
+ Height = 520;
+ Topmost = true;
+ WindowStartupLocation = WindowStartupLocation.CenterScreen;
+
+ var list = new ListBox { Margin = new Thickness(12) };
+ foreach (var t in targets) list.Items.Add(new TextBlock { Text = t.Title, Margin = new Thickness(4, 2) });
+
+ var start = new Button
+ {
+ Content = "开始长截图 (3 秒倒计时)",
+ Padding = new Thickness(16, 8),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ };
+ start.Click += (_, _) =>
+ {
+ if (list.SelectedIndex >= 0)
+ {
+ Selected = targets[list.SelectedIndex];
+ Close();
+ }
+ };
+ Content = new DockPanel
+ {
+ Margin = new Thickness(8),
+ Children = { start, list },
+ };
+ DockPanel.SetDock(start, Dock.Bottom);
+ base.OnOpened(e);
+ }
+}
diff --git a/src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs b/src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs
new file mode 100644
index 0000000..9ff18e1
--- /dev/null
+++ b/src/SmartSideBAR.Avalonia/ViewModels/RelayCommand.cs
@@ -0,0 +1,18 @@
+// 轻量命令 (无第三方依赖; Wave B 如引入 CommunityToolkit 可替换)
+using System.Collections;
+using System.Windows.Input;
+
+namespace SmartSideBAR.Avalonia.ViewModels;
+
+public sealed class RelayCommand(Action