From 6838a0cc4048fe77ee9fc8793a5febabf6aad061 Mon Sep 17 00:00:00 2001
From: rain9 <15911122312@163.com>
Date: Tue, 30 Jun 2026 08:45:14 +0800
Subject: [PATCH 1/2] fix: add macOS code signing and notarization support
---
.github/workflows/release.yml | 10 ++++++++++
desktop/src-tauri/tauri.conf.json | 12 ++++++++++--
desktop/src-tauri/tauri.entitlements | 14 ++++++++++++++
3 files changed, 34 insertions(+), 2 deletions(-)
create mode 100644 desktop/src-tauri/tauri.entitlements
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a8b9242..923d773 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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: |
@@ -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" \
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index 5dd7d55..778eace 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -68,12 +68,20 @@
"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,
+ "gatekeeperAssess": false,
+ "notarize": 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"
}
diff --git a/desktop/src-tauri/tauri.entitlements b/desktop/src-tauri/tauri.entitlements
new file mode 100644
index 0000000..194e124
--- /dev/null
+++ b/desktop/src-tauri/tauri.entitlements
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.network.client
+
+ com.apple.security.network.server
+
+ com.apple.security.files.user-selected.read-write
+
+
+
\ No newline at end of file
From 09062c822b4275d62eee576ea25b47d84ca1412f Mon Sep 17 00:00:00 2001
From: rain9 <15911122312@163.com>
Date: Wed, 1 Jul 2026 09:29:16 +0800
Subject: [PATCH 2/2] fix: click event
---
desktop/RELEASE_QA.md | 5 +-
desktop/TEST_CASES.md | 16 +-
desktop/bug.md | 225 +++++++++++++++++++-
desktop/src-tauri/src/lib.rs | 1 +
desktop/src-tauri/src/window_layer.rs | 15 +-
desktop/src-tauri/tauri.conf.json | 4 +-
desktop/src/hooks/useDesktopClickThrough.ts | 48 +++++
desktop/src/pages/Wall.tsx | 18 +-
desktop/src/test-setup.ts | 8 +-
9 files changed, 309 insertions(+), 31 deletions(-)
create mode 100644 desktop/src/hooks/useDesktopClickThrough.ts
diff --git a/desktop/RELEASE_QA.md b/desktop/RELEASE_QA.md
index 6624fd3..fff2e06 100644
--- a/desktop/RELEASE_QA.md
+++ b/desktop/RELEASE_QA.md
@@ -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.
diff --git a/desktop/TEST_CASES.md b/desktop/TEST_CASES.md
index d1c2e6e..bad0a43 100644
--- a/desktop/TEST_CASES.md
+++ b/desktop/TEST_CASES.md
@@ -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 | 关闭窗口行为 | 点击主窗口的关闭按钮(如果有) | 应用退出(而非隐藏) | ☐ |
@@ -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 次 | 窗口状态正确切换,无崩溃或卡顿;最终状态下的空白点击行为符合前台/底层规则 | ☐ |
---
@@ -141,7 +143,7 @@
| 10.1 | 按钮可见性 | 在唤起模式下观察右下角 | 显示 "+" 和 "⚙️" 两个浮动按钮 | ☐ |
| 10.2 | 悬停效果 | 鼠标悬停在按钮上 | 按钮放大 1.15 倍,阴影增强 | ☐ |
| 10.3 | 点击效果 | 点击按钮 | 按钮缩小 0.95 倍(按压反馈) | ☐ |
-| 10.4 | 底层模式隐藏 | 退回桌面底层模式 | 浮动按钮不可点击(鼠标穿透) | ☐ |
+| 10.4 | 底层模式穿透 | 退回桌面底层模式后点击浮动按钮所在区域 | 鼠标事件穿透到桌面或下层应用,浮动按钮不响应;再次唤起主窗口后按钮恢复可点击 | ☐ |
---
diff --git a/desktop/bug.md b/desktop/bug.md
index 35fbdcf..f3b5170 100644
--- a/desktop/bug.md
+++ b/desktop/bug.md
@@ -1 +1,224 @@
-1. 设置页面不太好看,有些地方不太合理
\ No newline at end of file
+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 手动下载入口,作为更新失败时的兜底方案。
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 469bbf5..928e4c8 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -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,
diff --git a/desktop/src-tauri/src/window_layer.rs b/desktop/src-tauri/src/window_layer.rs
index 5bbc41d..f0d97cd 100644
--- a/desktop/src-tauri/src/window_layer.rs
+++ b/desktop/src-tauri/src/window_layer.rs
@@ -1,4 +1,4 @@
-use tauri::Manager;
+use tauri::{Emitter, Manager};
pub struct MainLayerState(pub std::sync::Mutex);
@@ -9,11 +9,12 @@ pub fn set_main_default_layer(app: &tauri::AppHandle) {
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(app: &tauri::AppHandle) {
@@ -30,6 +31,7 @@ pub fn summon_main_window(app: &tauri::AppHandle) {
let _ = window.unminimize();
let _ = window.set_focus();
}
+ let _ = app.emit("main-layer-changed", true);
}
pub fn toggle_main_layer(app: &tauri::AppHandle) {
@@ -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::()
+ .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);
diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json
index 778eace..5807b76 100644
--- a/desktop/src-tauri/tauri.conf.json
+++ b/desktop/src-tauri/tauri.conf.json
@@ -73,9 +73,7 @@
"signingIdentity": "Apple Development: YOUR_NAME (YOUR_TEAM_ID)",
"entitlements": "tauri.entitlements",
"exceptionDomain": "",
- "hardenedRuntime": true,
- "gatekeeperAssess": false,
- "notarize": true
+ "hardenedRuntime": true
}
},
"plugins": {
diff --git a/desktop/src/hooks/useDesktopClickThrough.ts b/desktop/src/hooks/useDesktopClickThrough.ts
new file mode 100644
index 0000000..5de8276
--- /dev/null
+++ b/desktop/src/hooks/useDesktopClickThrough.ts
@@ -0,0 +1,48 @@
+import { useEffect } from "react";
+import { invoke } from "@tauri-apps/api/core";
+import { listen } from "@tauri-apps/api/event";
+import { getCurrentWindow } from "@tauri-apps/api/window";
+
+export function useDesktopClickThrough(enabled: boolean) {
+ useEffect(() => {
+ if (!enabled) return;
+
+ const currentWindow = getCurrentWindow();
+ let isSummoned = false;
+ let unlisten: (() => void) | null = null;
+
+ const syncCursorPassthrough = async (summoned: boolean) => {
+ isSummoned = summoned;
+ await currentWindow.setIgnoreCursorEvents(!summoned);
+ };
+
+ invoke("is_main_summoned")
+ .then((summoned) => syncCursorPassthrough(summoned))
+ .catch(() => syncCursorPassthrough(false));
+
+ listen("main-layer-changed", (event) => {
+ void syncCursorPassthrough(event.payload);
+ }).then((fn) => {
+ unlisten = fn;
+ });
+
+ const handleMouseDown = (event: MouseEvent) => {
+ if (!isSummoned || event.button !== 0) return;
+
+ const target = event.target as HTMLElement | null;
+ if (target?.closest("[data-interactive]")) return;
+
+ event.preventDefault();
+ event.stopPropagation();
+ void invoke("send_to_background");
+ };
+
+ document.addEventListener("mousedown", handleMouseDown, true);
+
+ return () => {
+ unlisten?.();
+ document.removeEventListener("mousedown", handleMouseDown, true);
+ void currentWindow.setIgnoreCursorEvents(false);
+ };
+ }, [enabled]);
+}
diff --git a/desktop/src/pages/Wall.tsx b/desktop/src/pages/Wall.tsx
index 35d5172..fb67ec2 100644
--- a/desktop/src/pages/Wall.tsx
+++ b/desktop/src/pages/Wall.tsx
@@ -1,5 +1,4 @@
import { useState, useEffect, useCallback, useRef } from "react";
-import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { webviewWindow } from "@tauri-apps/api";
@@ -27,6 +26,7 @@ import { useRestReminder } from "../hooks/useRestReminder";
import { useOffWorkReminder } from "../hooks/useOffWorkReminder";
import { useEyeCareReminder } from "../hooks/useEyeCareReminder";
import { useMoodCheckin } from "../hooks/useMoodCheckin";
+import { useDesktopClickThrough } from "../hooks/useDesktopClickThrough";
import type { CardType } from "../types";
import { useWidgetStore } from "../stores/widgetStore";
@@ -37,6 +37,7 @@ type NewCardState =
function Wall() {
const { t } = useI18n();
const [settings, setSettings] = useState(null);
+ const hasSettings = !!settings;
const [newCardModal, setNewCardModal] = useState({ open: false });
const [showBreathing, setShowBreathing] = useState(false);
const newCardPositionRef = useRef({ x: 0, y: 0 });
@@ -70,6 +71,7 @@ function Wall() {
useOffWorkReminder();
useEyeCareReminder();
useMoodCheckin();
+ useDesktopClickThrough(hasSettings);
const { results: quotaResults, loading: quotaLoading, refresh: quotaRefresh } =
useQuotaMonitor(settings?.quotaMonitor);
@@ -108,19 +110,6 @@ function Wall() {
});
}, [syncWidgets]);
- useEffect(() => {
- const handleMouseDown = async (e: MouseEvent) => {
- if (e.button !== 0) return;
- const el = document.elementFromPoint(e.clientX, e.clientY);
- const isBlankArea = !el || !el.closest("[data-interactive]");
- if (isBlankArea) {
- await invoke('send_to_background');
- }
- };
- document.addEventListener('mousedown', handleMouseDown);
- return () => document.removeEventListener('mousedown', handleMouseDown);
- }, []);
-
const openNewCardModal = useCallback(() => {
const centerX = window.innerWidth / 2 - 190;
const centerY = window.innerHeight / 2 - 160;
@@ -175,7 +164,6 @@ function Wall() {
setNewCardModal({ open: false });
}, []);
- const hasSettings = !!settings;
const quotaConfig = settings?.quotaMonitor;
const showQuotaCard = !!(quotaConfig?.enabled && quotaConfig.models.length > 0);
const showWeatherCard = !!settings?.weatherCareEnabled;
diff --git a/desktop/src/test-setup.ts b/desktop/src/test-setup.ts
index 9bea861..8b60255 100644
--- a/desktop/src/test-setup.ts
+++ b/desktop/src/test-setup.ts
@@ -14,7 +14,13 @@ vi.mock('@tauri-apps/api', () => ({
}));
vi.mock('@tauri-apps/api/core', () => ({
- invoke: vi.fn(),
+ invoke: vi.fn(() => Promise.resolve(false)),
+}));
+
+vi.mock('@tauri-apps/api/window', () => ({
+ getCurrentWindow: vi.fn(() => ({
+ setIgnoreCursorEvents: vi.fn(() => Promise.resolve()),
+ })),
}));
vi.mock('@tauri-apps/plugin-dialog', () => ({