Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,10 @@ jobs:
with:
args: --target ${{ matrix.target }}
projectPath: desktop
certificatePassword: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
certificate: ${{ secrets.APPLE_CERTIFICATE }}
appleId: ${{ secrets.APPLE_ID }}
appleIdPassword: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}

- name: Debug - list bundle output
run: |
Expand Down Expand Up @@ -189,6 +193,12 @@ jobs:
hdiutil detach aarch64-mount -quiet
hdiutil detach x86_64-mount -quiet

# Re-sign the universal binary if signing identity is available
if [ -n "${{ secrets.APPLE_CERTIFICATE }}" ]; then
echo "Re-signing universal binary..."
codesign --force --deep --sign "${{ secrets.APPLE_SIGNING_IDENTITY }}" "Universal-$APP_NAME"
fi

# Create universal DMG
hdiutil create -volname "PinWall" \
-srcfolder "Universal-$APP_NAME" \
Expand Down
5 changes: 3 additions & 2 deletions desktop/RELEASE_QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ Current local note:

## Desktop Behavior

- [ ] Clicking blank desktop/window area sends PinWall back behind normal desktop interactions.
- [ ] Floating buttons and cards remain interactive when the app is summoned.
- [ ] Default/bottom state: the main window stays on the desktop layer, and blank areas click through to desktop icons or lower windows.
- [ ] Summoned/top state: clicking a blank area is captured by PinWall and sends the main window back to the default/bottom state.
- [ ] Summoned/top state: clicking cards, floating buttons, widgets, stack panels, or modals does not send the main window back.
- [ ] `Cmd+Shift+Space` toggles the main window state.
- [ ] `Cmd+Shift+A` arranges cards.
- [ ] `Cmd+Shift+B` opens the breathing guide.
Expand Down
16 changes: 9 additions & 7 deletions desktop/TEST_CASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
| # | 测试项 | 操作步骤 | 预期结果 | 状态 |
|---|--------|----------|----------|------|
| 1.1 | 首次启动 | 双击 PinWall.app 启动 | 应用正常启动,主窗口最大化铺满屏幕,透明背景,无系统标题栏 | ☐ |
| 1.2 | 默认桌面底层模式 | 启动后观察主窗口行为 | 主窗口置于桌面最底层(always on bottom),鼠标可穿透空白区域点击桌面其他应用 | ☐ |
| 1.2 | 默认桌面底层模式 | 启动后观察主窗口行为 | 主窗口置于桌面最底层(always on bottom),停留在桌面上一层;点击主窗口空白区域时,鼠标事件穿透到桌面或下层应用,PinWall 不获得焦点、不拦截点击 | ☐ |
| 1.3 | 系统托盘图标 | 查看 macOS 菜单栏 | 出现 PinWall 托盘图标 | ☐ |
| 1.4 | 托盘菜单 — 打开 | 点击托盘 → "打开 PinWall" | 主窗口被唤醒到前台(always on top),可交互 | ☐ |
| 1.4 | 托盘菜单 — 打开 | 点击托盘 → "打开 PinWall" | 主窗口被唤醒到前台(always on top),整窗恢复可命中,卡片和浮动按钮可交互 | ☐ |
| 1.5 | 托盘菜单 — 设置 | 点击托盘 → "设置" | 设置窗口弹出,有系统标题栏 | ☐ |
| 1.6 | 托盘菜单 — 退出 | 点击托盘 → "退出" | 应用完全退出 | ☐ |
| 1.7 | 关闭窗口行为 | 点击主窗口的关闭按钮(如果有) | 应用退出(而非隐藏) | ☐ |
Expand All @@ -22,10 +22,12 @@

| # | 测试项 | 操作步骤 | 预期结果 | 状态 |
|---|--------|----------|----------|------|
| 2.1 | 唤起主窗口 | 按 `Cmd+Shift+Space` | 主窗口从底层切换到前台(可交互),可看到便签和按钮 | ☐ |
| 2.2 | 退回底层 | 再次按 `Cmd+Shift+Space` | 主窗口回到底层(鼠标穿透,不可交互) | ☐ |
| 2.3 | 空白区域点击退回 | 唤起后点击窗口空白区域(非卡片/按钮区域) | 主窗口退回底层模式 | ☐ |
| 2.4 | 重复切换稳定性 | 快速连续按 `Cmd+Shift+Space` 10 次 | 窗口状态正确切换,无崩溃或卡顿 | ☐ |
| 2.1 | 唤起主窗口 | 按 `Cmd+Shift+Space` 或托盘打开主窗口 | 主窗口从底层切换到前台(always on top),空白区域不穿透,卡片、Widget、浮动按钮、弹窗均可正常交互 | ☐ |
| 2.2 | 退回底层 | 前台状态下再次按 `Cmd+Shift+Space` | 主窗口回到底层(always on bottom),空白区域点击穿透到桌面或下层应用 | ☐ |
| 2.3 | 前台空白区域点击退回 | 主窗口被唤起到前台后,点击窗口空白区域(非卡片/按钮/Widget/弹窗区域) | 本次点击由 PinWall 接收,主窗口立即消失到桌面底层模式;不会把这次点击继续传给桌面 | ☐ |
| 2.4 | 底层空白区域点击穿透 | 主窗口处于底层模式时,点击没有卡片、按钮或 Widget 的空白区域 | 点击穿透到桌面或下层应用,可选中桌面文件、打开右键菜单或操作下层窗口;PinWall 保持底层模式 | ☐ |
| 2.5 | 交互元素不触发退回 | 主窗口被唤起到前台后,点击卡片、浮动按钮、Widget、卡片堆叠或弹窗 | 对应 PinWall 元素正常响应,主窗口保持前台,不退回底层 | ☐ |
| 2.6 | 重复切换稳定性 | 快速连续按 `Cmd+Shift+Space` 10 次 | 窗口状态正确切换,无崩溃或卡顿;最终状态下的空白点击行为符合前台/底层规则 | ☐ |

---

Expand Down Expand Up @@ -141,7 +143,7 @@
| 10.1 | 按钮可见性 | 在唤起模式下观察右下角 | 显示 "+" 和 "⚙️" 两个浮动按钮 | ☐ |
| 10.2 | 悬停效果 | 鼠标悬停在按钮上 | 按钮放大 1.15 倍,阴影增强 | ☐ |
| 10.3 | 点击效果 | 点击按钮 | 按钮缩小 0.95 倍(按压反馈) | ☐ |
| 10.4 | 底层模式隐藏 | 退回桌面底层模式 | 浮动按钮不可点击(鼠标穿透) | ☐ |
| 10.4 | 底层模式穿透 | 退回桌面底层模式后点击浮动按钮所在区域 | 鼠标事件穿透到桌面或下层应用,浮动按钮不响应;再次唤起主窗口后按钮恢复可点击 | ☐ |

---

Expand Down
225 changes: 224 additions & 1 deletion desktop/bug.md
Original file line number Diff line number Diff line change
@@ -1 +1,224 @@
1. 设置页面不太好看,有些地方不太合理
1. 设置页面不太好看,有些地方不太合理

版本更新规划:

## 目标

- 在设置页面增加「版本更新」模块,展示当前版本、检测更新状态、最新版本信息、发布时间、更新说明。
- 应用启动后支持静默检测;设置页支持手动检测。
- 检测到新版本后,明确提示用户更新;用户点击后,完成下载、校验、安装,并引导重启或自动重启完成升级。
- 更新失败时给出清晰错误提示和降级方案,例如打开 Release 下载页。
- 尽量抽离成可复用的更新能力,后续新 Tauri App 直接接入。

## 方案选型

- 底层更新引擎采用 Tauri 官方 updater 能力,不重复造轮子。
- 当前项目不建议一开始就自研「纯 Rust 更新插件」替代官方 updater,因为真正和系统安装、签名校验、平台差异打交道的部分本来就属于 Tauri 官方 updater 的职责。
- 更合理的可复用方向是抽一层「更新接入套件」:
- 前端复用:更新状态管理、轮询策略、弹窗文案、下载进度、失败回退。
- 配置复用:版本源地址、更新通道、是否启动自动检测、忽略版本策略。
- CI 复用:发布时自动生成更新元数据、签名文件、Release 资产上传。
- 也就是说,底层继续用官方 updater,项目内再封装成一个统一的 `updater-kit`,后续新 App 只需要配置品牌信息和发布地址即可。

## 用户侧完整流程

### 1. 应用启动

- 启动后延迟几秒执行一次后台检测,避免影响首屏体验。
- 若用户关闭了自动检测,则只保留设置页手动检测。
- 若本次没有新版本,只更新「最近检查时间」和状态文案,不打扰用户。

### 2. 设置页展示

- 在设置页基础分组下新增「版本更新」卡片。
- 展示内容建议包含:
- 当前版本
- 更新通道(正式版,后续可扩展 beta)
- 自动检测更新开关
- 最近一次检测时间
- 当前检测状态:未检测 / 检测中 / 已是最新 / 发现新版本 / 下载中 / 安装中 / 更新失败
- 手动按钮:`检查更新`
- 当发现新版本后,卡片内出现醒目的新版本信息和 `立即更新` 按钮。

### 3. 发现新版本后的交互

- 弹出更新提示层,至少包含:
- 新版本号
- 发布时间
- 更新说明
- 操作按钮:`立即更新`、`稍后再说`
- 可选增加:
- `跳过此版本`
- `查看完整发布说明`
- 如果用户选择稍后,则本次会话不再重复打扰。
- 如果用户选择跳过此版本,则记录被忽略版本,直到下一个更高版本再重新提示。

### 4. 下载与安装

- 用户点击 `立即更新` 后,进入下载态,显示进度、下载速度或已下载大小。
- 下载完成后进入安装态,安装期间禁用重复点击,避免并发更新。
- 安装完成后分平台处理:
- 能自动重启则直接重启进入新版本。
- 若平台或场景不适合自动重启,则提示用户「更新已安装,点击重启生效」。
- 若安装失败,弹出失败原因,并提供 `打开下载页手动安装`。

## 工程架构规划

### A. 前端层

- 新增更新服务层,统一管理:
- `checkForUpdates`
- `downloadAndInstall`
- `dismissVersion`
- `skipVersion`
- `getUpdateState`
- 新增更新状态模型,避免把更新逻辑直接塞进 `Settings.tsx` 或 `SettingsPanel.tsx`。
- 推荐将更新逻辑封装为:
- `src/services/updater/`:底层 API 调用
- `src/hooks/useAppUpdater.ts`:页面状态编排
- `src/components/update/`:更新提示卡片、更新弹窗、下载进度 UI
- 设置页只负责展示和触发,不直接持有复杂更新细节。

### B. 本地配置层

- 在现有 settings 中增加更新配置字段,建议包含:
- `autoCheckUpdates`
- `updateChannel`
- `skippedVersion`
- `lastUpdateCheckAt`
- `lastUpdatePromptedVersion`
- 默认建议:
- `autoCheckUpdates = true`
- `updateChannel = stable`
- 需要做好向后兼容:老用户升级后自动补默认值。

### C. Tauri 原生层

- 接入 updater 插件初始化。
- 在 Tauri 配置中补齐 updater 所需配置:
- 公钥
- 更新元数据地址
- 平台安装策略
- 同时补齐 capability / permission 配置,确保前端可以正常调用更新能力。

### D. 发布链路

- 这是本需求最关键的一层,因为现在项目已有 GitHub Releases,但还没有完整的「应用内更新」发布产物链路。
- 现有发布流程目前偏向生成并上传 DMG,后续需要扩展为两类产物并存:
- 给用户手动下载的安装包
- 给应用内更新使用的 updater 产物和更新清单
- 发布流程需要新增:
- 生成 updater 签名密钥
- 将私钥和密码放入 GitHub Secrets
- 构建时生成 updater 对应的签名文件和更新清单
- 将更新清单发布到固定可访问地址
- 建议保留现有 DMG 发布,作为失败兜底和手动安装入口。

## 可复用抽离方案

### 推荐抽法

- 第一阶段先在 `desktop` 内部做稳定封装,不急着拆独立仓库。
- 等第一版在 PinWall 跑通后,再抽成一个可复用模块,例如:
- `packages/tauri-updater-kit`
- 或者项目模板中的 `shared/updater`
- 该模块应提供:
- 通用状态机
- 通用 Hook
- 通用弹窗和进度组件
- 通用配置类型
- GitHub Release 工作流模板
- 文档化接入步骤
- 每个新 App 只需要注入:
- 应用名
- 当前版本来源
- 更新通道配置
- Release 地址
- 品牌化文案

### 不建议的抽法

- 不建议把「发布工作流 + updater 配置 + UI 提示 + 业务设置存储」一次性强耦合成一个黑盒插件。
- 因为不同 App 的:
- 发布平台
- 安装包格式
- 更新文案
- 是否强更
- 是否支持 beta 通道
都会不一样。
- 更适合做成「底层统一,接入可配置,UI 可替换」的半插件化方案。

## 页面交互建议

- 入口位置:优先放在设置页 `基础` 分组中,因为版本、启动、自启动、快捷键都属于应用级设置。
- 卡片建议分三态:
- 默认态:显示当前版本 + 检查更新按钮
- 有更新态:高亮显示 `发现新版本 vX.Y.Z`
- 更新中:显示下载/安装进度,按钮禁用
- 文案建议清晰直接:
- `当前已是最新版本`
- `发现新版本,建议更新`
- `更新包下载中`
- `更新安装中,请勿关闭应用`
- `更新失败,可前往下载页手动安装`

## 异常与边界场景

- 无网络:检测失败,但不影响正常使用。
- Release 资产缺失:提示服务器配置异常,并记录日志。
- 签名校验失败:禁止安装,提示安全校验失败。
- 用户跳过当前版本:该版本不再弹窗,直到更高版本出现。
- 用户正在使用关键窗口:更新提示可先提示下载,安装前再二次确认,避免突然重启影响使用。
- 后台托盘应用场景:更新成功后要考虑主窗口、设置窗口、通知窗口和托盘状态的平滑退出与恢复。

## 测试与验收规划

### 单元与前端层

- 验证更新状态机切换是否正确:
- 空闲 -> 检测中 -> 已是最新
- 空闲 -> 检测中 -> 发现更新
- 发现更新 -> 下载中 -> 安装中 -> 待重启
- 任意阶段 -> 失败
- 验证忽略版本、最近检查时间、自动检测开关的持久化逻辑。

### 手动验收

- 无更新时,设置页正确显示最新状态。
- 有更新时,设置页和弹窗都能提示。
- 点击更新后,能完成下载、安装、重启。
- 更新失败时,不会损坏当前版本。
- 跳过版本后,不会重复提示同一版本。
- 从旧版本直接升到新版本时,原有用户配置不丢失。

### 发布链路验收

- 使用测试版本 tag 跑一遍 GitHub Actions。
- 验证更新清单地址可访问。
- 验证签名正确。
- 验证一台旧版本机器能真实更新到新版本。

## 分阶段落地计划

### P1. 基础接入

- 接入 updater 底层能力。
- 打通 GitHub Release 到应用内更新清单的链路。
- 设置页增加「检查更新」和「立即更新」最小可用能力。

### P2. 体验完善

- 增加自动检测、忽略版本、更新说明弹窗、下载进度、失败回退。
- 优化更新时的窗口关闭与重启体验。

### P3. 可复用抽象

- 将 updater 服务、Hook、UI 组件、配置类型、CI 模板整理成 `updater-kit`。
- 输出一份新项目接入文档,做到新 Tauri App 可快速复用。

## 我建议的最终落地原则

- 先以 PinWall 为首个落地样板,把完整链路跑通。
- 底层依赖官方 updater,减少平台兼容风险。
- 复用重点放在「接入层、状态层、UI 层、CI 模板层」,而不是重复造底层更新轮子。
- 保留 GitHub Releases 手动下载入口,作为更新失败时的兜底方案。
1 change: 1 addition & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub fn run() {
commands::quit_app,
window_layer::send_to_background,
window_layer::summon_main,
window_layer::is_main_summoned,
window_layer::set_cursor_passthrough,
background::import_background_images,
background::delete_background_image_file,
Expand Down
15 changes: 13 additions & 2 deletions desktop/src-tauri/src/window_layer.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use tauri::Manager;
use tauri::{Emitter, Manager};

pub struct MainLayerState(pub std::sync::Mutex<bool>);

Expand All @@ -9,11 +9,12 @@ pub fn set_main_default_layer<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.set_always_on_top(false);
let _ = window.set_always_on_bottom(true);
let _ = window.set_ignore_cursor_events(false);
let _ = window.set_ignore_cursor_events(true);
let _ = window.set_visible_on_all_workspaces(true);
let _ = window.set_shadow(false);
let _ = window.show();
}
let _ = app.emit("main-layer-changed", false);
}

pub fn summon_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
Expand All @@ -30,6 +31,7 @@ pub fn summon_main_window<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
let _ = window.unminimize();
let _ = window.set_focus();
}
let _ = app.emit("main-layer-changed", true);
}

pub fn toggle_main_layer<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
Expand Down Expand Up @@ -57,6 +59,15 @@ pub fn summon_main(app: tauri::AppHandle) {
summon_main_window(&app);
}

#[tauri::command]
pub fn is_main_summoned(app: tauri::AppHandle) -> bool {
app.state::<MainLayerState>()
.0
.lock()
.map(|v| *v)
.unwrap_or(false)
}

#[tauri::command]
pub fn set_cursor_passthrough(window: tauri::WebviewWindow, ignore: bool) {
let _ = window.set_ignore_cursor_events(ignore);
Expand Down
10 changes: 8 additions & 2 deletions desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,18 @@
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
],
"macOS": {
"signingIdentity": "Apple Development: YOUR_NAME (YOUR_TEAM_ID)",
"entitlements": "tauri.entitlements",
"exceptionDomain": "",
"hardenedRuntime": true
}
},
"plugins": {
"updater": {
"endpoints": [
"https://github.com/rain9/pinwall/releases/download/latest/{{target}}-update.json"
"https://github.com/you-want/PinWall/releases/download/latest/{{target}}-update.json"
],
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IGZpbGU6c3ByaW5nLXNpZ25hdHVyZS5zaWcK"
}
Expand Down
14 changes: 14 additions & 0 deletions desktop/src-tauri/tauri.entitlements
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<false/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
Loading
Loading