diff --git a/.env.shell b/.env.shell
new file mode 100644
index 0000000..e296220
--- /dev/null
+++ b/.env.shell
@@ -0,0 +1,10 @@
+# Windows 外壳构建模式 (npm run build:shell / vite build --mode shell)
+# 外壳内前端跑在 tauri://localhost, 没有同源后端, API 必须指向远端 Express。
+# 与 android 打包脚本 (scripts/wsl/build-apk.sh) 注入的值保持一致;
+# 进程环境变量优先级高于本文件, android 流程不受影响。
+#
+# 0.2.1 起正式域名: https://iboard.honor3.com (NPM 反代 -> baihu:3001, 受信证书)
+# PowerSync 同步端点默认同域 (NPM 把 /sync 反代到同步服务); 若改用独立子域,
+# 打包时用 VITE_POWERSYNC_URL=https://sync.example.com 覆盖即可。
+VITE_API_BASE=https://iboard.honor3.com
+VITE_POWERSYNC_URL=https://iboard.honor3.com
diff --git a/.esbuild-tmp/build2.log b/.esbuild-tmp/build2.log
new file mode 100644
index 0000000..10db7d1
Binary files /dev/null and b/.esbuild-tmp/build2.log differ
diff --git a/.gitea/workflows/android-build.yml b/.gitea/workflows/android-build.yml
index 2571cc5..4340858 100644
--- a/.gitea/workflows/android-build.yml
+++ b/.gitea/workflows/android-build.yml
@@ -23,7 +23,7 @@ jobs:
env:
# 固定工具链版本, 便于 actions/cache 命中
# Android 后端地址 (bake 进 APK); 与 scripts/wsl/build-apk.sh 保持一致
- VITE_API_BASE: http://bh.vps.honor3.com:3001
+ VITE_API_BASE: https://iboard.honor3.com
steps:
- name: Checkout
diff --git a/.gitea/workflows/docker-build.yml b/.gitea/workflows/docker-build.yml
index 0990807..a68b2eb 100644
--- a/.gitea/workflows/docker-build.yml
+++ b/.gitea/workflows/docker-build.yml
@@ -30,7 +30,7 @@ jobs:
# 关键: 显式固定 Android/OTA 后端地址, 覆盖 runner 环境里可能存在的
# 其他 VITE_API_BASE 值。否则 OTA 包里的前端可能连错后端, 导致
# "OTA 更新后登录失败/进不去内部"。与 android-build.yml 保持一致。
- VITE_API_BASE: http://bh.vps.honor3.com:3001
+ VITE_API_BASE: https://iboard.honor3.com
# OTA bundle 要求的最低原生二进制版本。
# 与 package.json 的应用版本是**两个概念**: 前端 OTA 不碰原生层,
# 现有 APK 仍是 0.1.0, 所以这里固定 0.1.0 (否则旧 APK 会被判定为不兼容而收不到更新)。
diff --git a/docs/V0.2/任务模块/视图切换移入项目栏-设计稿.html b/docs/V0.2/任务模块/视图切换移入项目栏-设计稿.html
new file mode 100644
index 0000000..75f5697
--- /dev/null
+++ b/docs/V0.2/任务模块/视图切换移入项目栏-设计稿.html
@@ -0,0 +1,108 @@
+
+
+
+
+
+设计稿 · 视图切换移入项目栏 (T-VW-02/03 调整)
+
+
+
+
+
+
状态 1 · 列表视图(默认)
+
切换段控放在项目名右侧, 与名称同一行
+
+
我的看板项目
+
+
已归档
+
管理
+
+ 新增
+
+
「项目管理」按钮文案收窄为「管理」, 与「已归档」同为次级样式, 突出右侧主操作
+
+
梳理 V0.2 归档需求
+
OTA 发布说明样式修复
+
看板列滚动条位置调整
+
+
+ 规格
+ · 分段控件高 26px, 选中项白底浮起(复用现有 .seg 体系收窄版)
+ · 项目名 flex:1 + 单行省略, 最短保留 4 字符
+ · 段控始终紧贴名称右侧, 距离 8px
+
+
+
+
+
状态 2 · 看板视图
+
点「看板」后段控选中项滑换, 其余布局不变
+
+
我的看板项目
+
+
已归档
+
管理
+
+ 新增
+
+
+
+ 交互
+ · 点按即切换并持久化(设备端记忆, T-VW-02/03 不变)
+ · 切换不引发项目栏高度变化, 下方视图平滑重排
+ · 长按/拖动手势不受影响(段控独立控件, 不在卡片上)
+
+
+
+
+
状态 3 · 长项目名
+
名称被压缩时单行省略, 段控与按钮永不换行/收缩
+
+
经济数据整理与策略研究-2026年第一季度重点项目
+
+
已归档
+
管理
+
+ 新增
+
+
+
+ 宽度预算 (375px)
+ · 左右内边距 24 + 段控 90 + 已归档 48 + 管理 42 + 新增 54 + 间距 32 ≈ 290
+ · 名称可用 ≈ 85px(约 6 个汉字)起, 不足部分省略
+ · 若仍嫌拥挤, 备选:「已归档」「管理」收进 ⋯ 溢出菜单(二期可选, 本稿未采用)
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index 7d94b7b..b040951 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3228,6 +3228,7 @@
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-13.0.3.tgz",
"integrity": "sha512-RbOBxmLBG8uvFUc15X9+9SFemKcQ0WBuISBVkpuiaUB2qblC8UWlHEjdWVoZ8AdhSwmoEgsiXKfopX0CQxaACQ==",
+ "hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
diff --git a/package.json b/package.json
index f9245ac..924b717 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "iboard",
- "version": "0.2.0",
+ "version": "0.2.1",
"type": "module",
"scripts": {
"tauri": "tauri",
@@ -8,6 +8,9 @@
"dev:server": "node --watch --experimental-strip-types --no-warnings server/index.js",
"dev": "concurrently -n web,api -c blue,green \"npm run dev:web\" \"npm run dev:server\"",
"build": "vite build",
+ "build:shell": "vite build --mode shell",
+ "build:win": "node scripts/build-windows.mjs",
+ "build:win:debug": "node scripts/build-windows.mjs --debug",
"build:ota": "node scripts/build-ota.mjs",
"preview": "vite preview",
"start": "node --experimental-strip-types --no-warnings server/index.js",
@@ -55,8 +58,8 @@
"ws": "^8.21.2"
},
"devDependencies": {
- "@tauri-apps/api": "^2.0.0",
- "@tauri-apps/cli": "^2.0.0",
+ "@tauri-apps/api": "^2.12.4",
+ "@tauri-apps/cli": "^2.12.4",
"@vitejs/plugin-react": "^4.3.1",
"cheerio": "^1.2.0",
"concurrently": "^9.0.0",
diff --git a/public/dock.html b/public/dock.html
new file mode 100644
index 0000000..31277d8
--- /dev/null
+++ b/public/dock.html
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/dock.js b/public/dock.js
new file mode 100644
index 0000000..57b6a5a
--- /dev/null
+++ b/public/dock.js
@@ -0,0 +1,65 @@
+// 悬浮球交互 (W-SNP-04/05/12/13/14):
+// mouseenter/leave → hover 延时恢复 (Rust 侧计时 150ms)
+// 单击 (位移 ≤4px) → 立即恢复, 触屏点按等效
+// 拖动 → dock_drag_move 增量移动, 结束时按最近边停靠/换边/拖出
+const { invoke } = window.__TAURI__.core
+const ball = document.getElementById('ball')
+
+let dragging = false
+let moved = 0
+let lastX = 0
+let lastY = 0
+
+ball.addEventListener('mouseenter', () => invoke('dock_hover_start'))
+ball.addEventListener('mouseleave', () => invoke('dock_hover_end'))
+ball.addEventListener('contextmenu', (e) => e.preventDefault())
+
+ball.addEventListener('pointerdown', (e) => {
+ dragging = true
+ moved = 0
+ lastX = e.clientX
+ lastY = e.clientY
+ ball.setPointerCapture(e.pointerId)
+ invoke('dock_drag_start')
+})
+
+ball.addEventListener('pointermove', (e) => {
+ if (!dragging) return
+ const dx = e.clientX - lastX
+ const dy = e.clientY - lastY
+ if (dx === 0 && dy === 0) return
+ lastX = e.clientX
+ lastY = e.clientY
+ moved += Math.abs(dx) + Math.abs(dy)
+ if (moved > 4) {
+ // clientX 为逻辑像素, 增量按 DPI 缩放换算成物理像素 (Rust 注入 __SCALE)
+ const s = window.__SCALE || 1
+ invoke('dock_drag_move', { dx: Math.round(dx * s), dy: Math.round(dy * s) })
+ }
+})
+
+ball.addEventListener('pointerup', (e) => {
+ if (!dragging) return
+ dragging = false
+ try { ball.releasePointerCapture(e.pointerId) } catch { /* 已释放 */ }
+ if (moved <= 4) {
+ invoke('dock_click')
+ } else {
+ invoke('dock_drag_end')
+ }
+})
+
+ball.addEventListener('pointercancel', () => {
+ dragging = false
+ invoke('dock_drag_end')
+})
+
+// Rust 在停靠/换边/主题变化时调用
+window.applyDock = (edge, theme) => {
+ document.body.className = 'dock-' + edge + ' theme-' + theme
+}
+window.applyTheme = (theme) => {
+ const edge = [...document.body.classList].find((c) => c.startsWith('dock-')) || 'dock-left'
+ document.body.className = edge + ' theme-' + theme
+}
+window.__SCALE = 1
diff --git a/scripts/build-windows.mjs b/scripts/build-windows.mjs
new file mode 100644
index 0000000..2ece216
--- /dev/null
+++ b/scripts/build-windows.mjs
@@ -0,0 +1,152 @@
+#!/usr/bin/env node
+// Windows 外壳一键构建 (开发用): 安装包 (NSIS) + 便携版 (portable zip)
+//
+// 用法:
+// npm run build:win # 完整流程
+// node scripts/build-windows.mjs --skip-check # 跳过版本一致性校验
+// node scripts/build-windows.mjs --debug # debug 构建 (快速验证, 产物带 -debug 后缀)
+//
+// 流程:
+// 1) 版本一致性校验 (scripts/ci/check-version.mjs, 外壳三件套)
+// 2) 前端构建 (build:shell, 注入 VITE_API_BASE 指向远端后端)
+// 3) tauri build --bundles nsis → 编译一次 release exe + NSIS 安装包
+// 4) 复用同一个 exe 组装便携版目录并压成 zip (不二次编译)
+//
+// 产物 (artifacts/windows/):
+// iBoard__x64-setup.exe 安装包 (当前用户安装, 免管理员)
+// iBoard__x64-portable.zip 便携版 (解压即用)
+// portable/ 便携版解压目录 (iBoard.exe + README.txt)
+//
+// 说明: 便携版不内置 WebView2 —— 目标机需 Win10 19041+ / Win11 (系统自带 WebView2 Runtime)。
+import { spawnSync } from 'node:child_process'
+import fs from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const args = process.argv.slice(2)
+const skipCheck = args.includes('--skip-check')
+const isDebug = args.includes('--debug')
+const profile = isDebug ? 'debug' : 'release'
+
+const conf = JSON.parse(fs.readFileSync(path.join(root, 'src-tauri', 'tauri.conf.json'), 'utf8'))
+const version = String(conf.version || '0.0.0')
+const product = conf.productName || 'iBoard'
+const suffix = isDebug ? '-debug' : ''
+
+const artifactsDir = path.join(root, 'artifacts', 'windows')
+const targetDir = path.join(root, 'src-tauri', 'target', profile)
+
+function log(msg) {
+ console.log(`\x1b[36m[build-win]\x1b[0m ${msg}`)
+}
+function fail(msg) {
+ console.error(`\x1b[31m[build-win] 失败: ${msg}\x1b[0m`)
+ process.exit(1)
+}
+function run(cmd, cmdArgs, opts = {}) {
+ log(`$ ${cmd} ${cmdArgs.join(' ')}`)
+ const res = spawnSync(cmd, cmdArgs, {
+ cwd: root,
+ stdio: 'inherit',
+ shell: process.platform === 'win32',
+ ...opts,
+ })
+ if (res.status !== 0) fail(`${cmd} ${cmdArgs.join(' ')} 退出码 ${res.status}`)
+}
+
+// ---------- 0. 前置检查 ----------
+if (process.platform !== 'win32') fail('本脚本仅支持 Windows')
+if (!fs.existsSync(path.join(root, 'node_modules'))) fail('缺少 node_modules, 请先运行 npm ci')
+
+log(`项目 ${product} 外壳版本 ${version} (${profile})`)
+
+// ---------- 1. 版本一致性 ----------
+if (!skipCheck) {
+ log('校验版本一致性 (外壳 tauri.conf.json / Cargo.toml / Cargo.lock)')
+ run('node', ['scripts/ci/check-version.mjs'], {
+ env: { ...process.env, BRANCH: process.env.BRANCH || '' },
+ })
+} else {
+ log('跳过版本一致性校验 (--skip-check)')
+}
+
+// ---------- 2. 前端构建 ----------
+log('构建前端 (build:shell, VITE_API_BASE 指向远端后端)')
+run('npm', ['run', 'build:shell'])
+
+// ---------- 3. 安装包 (NSIS) ----------
+const tauriArgs = ['tauri', 'build']
+if (isDebug) tauriArgs.push('--debug')
+// 只出 NSIS: 避免 bundle.targets="all" 在 Windows 上拉 WiX 打 MSI
+tauriArgs.push('--bundles', 'nsis')
+log('编译外壳并生成 NSIS 安装包')
+run('npx', tauriArgs)
+
+// ---------- 4. 便携版 ----------
+const exePath = path.join(targetDir, `iboard${suffix}.exe`)
+if (!fs.existsSync(exePath)) fail(`未找到构建产物 ${exePath}`)
+
+fs.rmSync(artifactsDir, { recursive: true, force: true })
+fs.mkdirSync(artifactsDir, { recursive: true })
+
+const portableDir = path.join(artifactsDir, 'portable')
+fs.mkdirSync(portableDir, { recursive: true })
+const portableExe = path.join(portableDir, `${product}.exe`)
+fs.copyFileSync(exePath, portableExe)
+
+const readme = `${product} ${version} 便携版 (Windows x64)
+=================================================
+
+免安装: 双击 ${product}.exe 即可运行。
+
+系统要求
+ - Windows 10 19041+ 或 Windows 11 (x64)
+ - WebView2 Runtime: Win10/11 系统自带; 若提示缺失, 请安装
+ https://developer.microsoft.com/microsoft-edge/webview2/
+
+使用说明
+ - 关闭窗口 = 隐藏到托盘, 不会退出; 退出请用托盘右键菜单「退出」。
+ - 窗口贴屏幕边缘会自动收起为半圆悬浮球, 鼠标停留或单击唤回。
+ - 托盘菜单可开关「开机启动」「边缘吸附」。
+ - 配置保存在 %APPDATA%\\com.iboard.app\\shell-config.json (删除即恢复默认)。
+
+注意: 便携版不写注册表、不装服务; 但若开启了「开机启动」, 会在
+ HKCU\\...\\Run 写入自启动项, 关闭开关即可移除。
+`
+fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8')
+
+const zipName = `${product}_${version}_x64-portable${suffix}.zip`
+const zipPath = path.join(artifactsDir, zipName)
+log('打包便携版 zip')
+const psZip = `Compress-Archive -Path '${portableDir}\\*' -DestinationPath '${zipPath}' -Force`
+const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', psZip], {
+ cwd: root,
+ stdio: 'inherit',
+})
+if (res.status !== 0) fail('压缩便携版失败 (Compress-Archive)')
+
+// ---------- 5. 汇总 ----------
+const setupExe = path.join(
+ targetDir,
+ 'bundle',
+ 'nsis',
+ `${product}_${version}_x64-setup${suffix}.exe`,
+)
+const outputs = []
+if (fs.existsSync(setupExe)) {
+ const dest = path.join(artifactsDir, path.basename(setupExe))
+ fs.copyFileSync(setupExe, dest)
+ outputs.push(['安装包 (NSIS)', dest])
+} else {
+ outputs.push(['安装包 (NSIS)', `未找到: ${setupExe}`])
+}
+outputs.push(['便携版 (zip)', zipPath])
+outputs.push(['便携版 (目录)', `${portableDir} (${fs.readdirSync(portableDir).length} 个文件)`])
+
+log('构建完成:')
+for (const [label, p] of outputs) {
+ const size = fs.existsSync(p) ? `${(fs.statSync(p).size / 1024 / 1024).toFixed(1)} MB` : ''
+ console.log(` ${label.padEnd(14)} ${p}${size ? ' (' + size + ')' : ''}`)
+}
+process.exit(0)
diff --git a/scripts/gen-tray-icons.mjs b/scripts/gen-tray-icons.mjs
new file mode 100644
index 0000000..d492a0c
--- /dev/null
+++ b/scripts/gen-tray-icons.mjs
@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+// 生成托盘深浅色单色图标 (W-THE-01):
+// src-tauri/icons/tray-light.png 深色字形 —— 浅色任务栏用
+// src-tauri/icons/tray-dark.png 白色字形 —— 深色任务栏用
+//
+// 做法: 不能只用 alpha 通道做剪影 —— 源图标是「深色圆角方块 + 白色 i」,
+// 整个方块都是不透明的, 只取 alpha 会得到一块纯色方块, i 字母细节全丢。
+// 这里按**亮度**映射, 保留字母结构:
+// 深色字形: alpha = (255-L)/255 → 方块保留深色, 白色 i 镂空透出任务栏
+// 浅色字形: alpha = max(L,40)/255 → i 白色高亮, 方块留一点淡淡衬底
+// 产物已提交进仓库, 仅图标变更时需要重跑: node scripts/gen-tray-icons.mjs
+import sharp from 'sharp'
+import { fileURLToPath } from 'node:url'
+import path from 'node:path'
+
+const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
+const src = path.join(root, 'src-tauri', 'icons', '128x128.png')
+const outDir = path.join(root, 'src-tauri', 'icons')
+
+const targets = [
+ { file: 'tray-light.png', rgb: [31, 31, 31], mode: 'dark-glyph' }, // 深色字形 (浅色任务栏)
+ { file: 'tray-dark.png', rgb: [250, 250, 250], mode: 'light-glyph' }, // 白色字形 (深色任务栏)
+]
+
+for (const t of targets) {
+ const { data, info } = await sharp(src)
+ .resize(32, 32, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
+ .ensureAlpha()
+ .raw()
+ .toBuffer({ resolveWithObject: true })
+ const out = Buffer.alloc(info.width * info.height * 4)
+ for (let i = 0; i < info.width * info.height; i++) {
+ const r = data[i * 4]
+ const g = data[i * 4 + 1]
+ const b = data[i * 4 + 2]
+ const a = data[i * 4 + 3]
+ const lumi = 0.2126 * r + 0.7152 * g + 0.0722 * b
+ const alpha =
+ t.mode === 'dark-glyph' ? (a * (255 - lumi)) / 255 : (a * Math.max(lumi, 40)) / 255
+ out[i * 4] = t.rgb[0]
+ out[i * 4 + 1] = t.rgb[1]
+ out[i * 4 + 2] = t.rgb[2]
+ out[i * 4 + 3] = Math.round(alpha)
+ }
+ await sharp(out, { raw: { width: info.width, height: info.height, channels: 4 } })
+ .png()
+ .toFile(path.join(outDir, t.file))
+ console.log('generated', t.file)
+}
diff --git a/scripts/wsl/build-apk.sh b/scripts/wsl/build-apk.sh
index b723dae..12cc790 100644
--- a/scripts/wsl/build-apk.sh
+++ b/scripts/wsl/build-apk.sh
@@ -1,143 +1,143 @@
-#!/usr/bin/env bash
-# iBoard Android APK 打包
-# 用法: bash scripts/wsl/build-apk.sh [debug|release]
-# release 模式自动构建 4 个 ABI 并出 universal APK
-set -euo pipefail
-
-WIN_PROJECT='/mnt/d/workbench/iboard'
-cd "$WIN_PROJECT"
-
-VARIANT="${1:-debug}"
-echo "构建变体: $VARIANT"
-
-# 环境变量
-export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
-export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/27.2.12479018}"
-# 关键: 把 NDK toolchain bin 加进 PATH, .cargo/config.toml 里
-# linker = "aarch64-linux-android24-clang" 这种短名才能被解析到
-NDK_TOOLCHAIN_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
-if [ ! -d "$NDK_TOOLCHAIN_BIN" ]; then
- echo "ERROR: NDK toolchain bin 不存在: $NDK_TOOLCHAIN_BIN"
- echo "请确认 ANDROID_NDK_HOME 正确指向 NDK 27.2.x 目录"
- exit 1
-fi
-export PATH="$NDK_TOOLCHAIN_BIN:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH"
-
-# 0. 首次需要 android 工程
-cd src-tauri
-if [ ! -d "gen/android" ]; then
- echo "=== 0. 初始化 Android 工程 (一次性) ==="
- npx tauri android init
-fi
-cd "$WIN_PROJECT"
-
-# Android 后端地址, 写到前端 bundle 里 (覆盖默认的 /api 相对路径)
-export VITE_API_BASE="${VITE_API_BASE:-http://bh.vps.honor3.com:3001}"
-echo "VITE_API_BASE=$VITE_API_BASE"
-
-# 1. 先构建前端, 让 Tauri android 工程能拿到 dist/
-echo "=== 1. 构建前端 ==="
-npm run build
-
-# 1.5 拷贝 dist/ 到 Android 工程 assets/, 绕过 Tauri CLI 时这步要做
-# Tauri CLI 默认在 android build 前会干这事, 我们走直 gradle 要自己补上
-ANDROID_ASSETS="$WIN_PROJECT/src-tauri/gen/android/app/src/main/assets"
-mkdir -p "$ANDROID_ASSETS"
-# 清空旧的 (避免上次的残余)
-find "$ANDROID_ASSETS" -mindepth 1 -maxdepth 1 ! -name dexopt ! -name tauri.conf.json -exec rm -rf {} +
-cp -r dist/. "$ANDROID_ASSETS/"
-echo "前端 bundle 已拷贝到 $ANDROID_ASSETS"
-ls -la "$ANDROID_ASSETS" | head -10
-
-# 2. 走不同路径
-cd src-tauri
-
-# 全部 4 个 Android 架构的 rustBuild 任务 (Tauri 2 默认会编 4 个)
-RUST_BUILD_TASKS=(
- ":app:rustBuildArm64Release"
- ":app:rustBuildArmRelease"
- ":app:rustBuildX86_64Release"
- ":app:rustBuildX86Release"
-)
-SKIP_ARGS=()
-for t in "${RUST_BUILD_TASKS[@]}"; do SKIP_ARGS+=(-x "${t#:}"); done
-
-# 4 个 Android 目标的 cargo target triple
-ANDROID_TARGETS=(
- "aarch64-linux-android"
- "armv7-linux-androideabi"
- "x86_64-linux-android"
- "i686-linux-android"
-)
-
-if [ "$VARIANT" = "release" ]; then
- echo "[release] 手工 cargo 编 lib (4 个 ABI), gradle 只跑 packaging (跳过 rustBuild* 避免 --release 重复 bug)"
-
- for target in "${ANDROID_TARGETS[@]}"; do
- echo " [cargo] build lib release for $target ..."
- cargo build \
- --target "$target" \
- --features custom-protocol \
- --lib \
- --release
- done
-
- echo " [gradle] assembleRelease -x rustBuild* ..."
- cd gen/android
- ./gradlew --no-daemon assembleRelease "${SKIP_ARGS[@]}"
-
- APK_DIR="$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/universal/release"
- # Gradle 配了 signingConfig 就输出 app-universal-release.apk,
- # 否则输出 app-universal-release-unsigned.apk, 按文件名判断是否已签名
- SIGNED_APK="$APK_DIR/app-universal-release.apk"
- UNSIGNED_APK="$APK_DIR/app-universal-release-unsigned.apk"
-
- if [ -f "$SIGNED_APK" ]; then
- APK_PATH="$SIGNED_APK"
- APK_STATUS="已签名"
- elif [ -f "$UNSIGNED_APK" ]; then
- APK_PATH="$UNSIGNED_APK"
- APK_STATUS="未签名"
- else
- APK_PATH=""
- APK_STATUS="未找到"
- fi
-
- echo
- echo "=== 产物 ($APK_STATUS) ==="
- if [ -n "$APK_PATH" ]; then
- ls -lh "$APK_PATH"
- else
- ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)"
- fi
-
- if [ "$APK_STATUS" = "已签名" ]; then
- echo
- echo "=== 安装到设备 ==="
- echo " adb install -r $APK_PATH"
- elif [ "$APK_STATUS" = "未签名" ]; then
- echo
- echo "=== Release 签名 (按需) ==="
- echo "1. 生成 keystore (一次性):"
- echo " keytool -genkey -v -keystore ~/iboard-release.keystore -alias iboard -keyalg RSA -keysize 2048 -validity 10000"
- echo "2. 在 ~/.gradle/gradle.properties 写:"
- echo " IBOARD_UPLOAD_STORE_FILE=/home/$(whoami)/iboard-release.keystore"
- echo " IBOARD_UPLOAD_KEY_ALIAS=iboard"
- echo " IBOARD_UPLOAD_STORE_PASSWORD=..."
- echo " IBOARD_UPLOAD_KEY_PASSWORD=..."
- echo "3. 重跑本脚本 (Gradle 读 gradle.properties 里的 IBOARD_UPLOAD_* 自动用 releaseSigning)"
- echo
- echo "=== 临时用 debug keystore 手动签 (仅测试) ==="
- echo " \$ANDROID_HOME/build-tools/*/apksigner sign --ks ~/.android/debug.keystore \"
- echo " --ks-pass pass:android --key-pass pass:android \"
- echo " $APK_PATH"
- fi
-else
- echo "[debug] Tauri CLI debug 路径 (不传 --release 不会触发 bug), 直接走"
- npx tauri android build --apk
-
- APK_DIR="$WIN_PROJECT/src-tauri/target/debug/bundle/android/app/build/outputs/apk/debug"
- echo
- echo "=== 产物 ==="
- ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)"
-fi
+#!/usr/bin/env bash
+# iBoard Android APK 打包
+# 用法: bash scripts/wsl/build-apk.sh [debug|release]
+# release 模式自动构建 4 个 ABI 并出 universal APK
+set -euo pipefail
+
+WIN_PROJECT='/mnt/d/workbench/iboard'
+cd "$WIN_PROJECT"
+
+VARIANT="${1:-debug}"
+echo "构建变体: $VARIANT"
+
+# 环境变量
+export ANDROID_HOME="${ANDROID_HOME:-$HOME/android-sdk}"
+export ANDROID_NDK_HOME="${ANDROID_NDK_HOME:-$ANDROID_HOME/ndk/27.2.12479018}"
+# 关键: 把 NDK toolchain bin 加进 PATH, .cargo/config.toml 里
+# linker = "aarch64-linux-android24-clang" 这种短名才能被解析到
+NDK_TOOLCHAIN_BIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
+if [ ! -d "$NDK_TOOLCHAIN_BIN" ]; then
+ echo "ERROR: NDK toolchain bin 不存在: $NDK_TOOLCHAIN_BIN"
+ echo "请确认 ANDROID_NDK_HOME 正确指向 NDK 27.2.x 目录"
+ exit 1
+fi
+export PATH="$NDK_TOOLCHAIN_BIN:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$HOME/.cargo/bin:$PATH"
+
+# 0. 首次需要 android 工程
+cd src-tauri
+if [ ! -d "gen/android" ]; then
+ echo "=== 0. 初始化 Android 工程 (一次性) ==="
+ npx tauri android init
+fi
+cd "$WIN_PROJECT"
+
+# Android 后端地址, 写到前端 bundle 里 (覆盖默认的 /api 相对路径)
+export VITE_API_BASE="${VITE_API_BASE:-https://iboard.honor3.com}"
+echo "VITE_API_BASE=$VITE_API_BASE"
+
+# 1. 先构建前端, 让 Tauri android 工程能拿到 dist/
+echo "=== 1. 构建前端 ==="
+npm run build
+
+# 1.5 拷贝 dist/ 到 Android 工程 assets/, 绕过 Tauri CLI 时这步要做
+# Tauri CLI 默认在 android build 前会干这事, 我们走直 gradle 要自己补上
+ANDROID_ASSETS="$WIN_PROJECT/src-tauri/gen/android/app/src/main/assets"
+mkdir -p "$ANDROID_ASSETS"
+# 清空旧的 (避免上次的残余)
+find "$ANDROID_ASSETS" -mindepth 1 -maxdepth 1 ! -name dexopt ! -name tauri.conf.json -exec rm -rf {} +
+cp -r dist/. "$ANDROID_ASSETS/"
+echo "前端 bundle 已拷贝到 $ANDROID_ASSETS"
+ls -la "$ANDROID_ASSETS" | head -10
+
+# 2. 走不同路径
+cd src-tauri
+
+# 全部 4 个 Android 架构的 rustBuild 任务 (Tauri 2 默认会编 4 个)
+RUST_BUILD_TASKS=(
+ ":app:rustBuildArm64Release"
+ ":app:rustBuildArmRelease"
+ ":app:rustBuildX86_64Release"
+ ":app:rustBuildX86Release"
+)
+SKIP_ARGS=()
+for t in "${RUST_BUILD_TASKS[@]}"; do SKIP_ARGS+=(-x "${t#:}"); done
+
+# 4 个 Android 目标的 cargo target triple
+ANDROID_TARGETS=(
+ "aarch64-linux-android"
+ "armv7-linux-androideabi"
+ "x86_64-linux-android"
+ "i686-linux-android"
+)
+
+if [ "$VARIANT" = "release" ]; then
+ echo "[release] 手工 cargo 编 lib (4 个 ABI), gradle 只跑 packaging (跳过 rustBuild* 避免 --release 重复 bug)"
+
+ for target in "${ANDROID_TARGETS[@]}"; do
+ echo " [cargo] build lib release for $target ..."
+ cargo build \
+ --target "$target" \
+ --features custom-protocol \
+ --lib \
+ --release
+ done
+
+ echo " [gradle] assembleRelease -x rustBuild* ..."
+ cd gen/android
+ ./gradlew --no-daemon assembleRelease "${SKIP_ARGS[@]}"
+
+ APK_DIR="$WIN_PROJECT/src-tauri/gen/android/app/build/outputs/apk/universal/release"
+ # Gradle 配了 signingConfig 就输出 app-universal-release.apk,
+ # 否则输出 app-universal-release-unsigned.apk, 按文件名判断是否已签名
+ SIGNED_APK="$APK_DIR/app-universal-release.apk"
+ UNSIGNED_APK="$APK_DIR/app-universal-release-unsigned.apk"
+
+ if [ -f "$SIGNED_APK" ]; then
+ APK_PATH="$SIGNED_APK"
+ APK_STATUS="已签名"
+ elif [ -f "$UNSIGNED_APK" ]; then
+ APK_PATH="$UNSIGNED_APK"
+ APK_STATUS="未签名"
+ else
+ APK_PATH=""
+ APK_STATUS="未找到"
+ fi
+
+ echo
+ echo "=== 产物 ($APK_STATUS) ==="
+ if [ -n "$APK_PATH" ]; then
+ ls -lh "$APK_PATH"
+ else
+ ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)"
+ fi
+
+ if [ "$APK_STATUS" = "已签名" ]; then
+ echo
+ echo "=== 安装到设备 ==="
+ echo " adb install -r $APK_PATH"
+ elif [ "$APK_STATUS" = "未签名" ]; then
+ echo
+ echo "=== Release 签名 (按需) ==="
+ echo "1. 生成 keystore (一次性):"
+ echo " keytool -genkey -v -keystore ~/iboard-release.keystore -alias iboard -keyalg RSA -keysize 2048 -validity 10000"
+ echo "2. 在 ~/.gradle/gradle.properties 写:"
+ echo " IBOARD_UPLOAD_STORE_FILE=/home/$(whoami)/iboard-release.keystore"
+ echo " IBOARD_UPLOAD_KEY_ALIAS=iboard"
+ echo " IBOARD_UPLOAD_STORE_PASSWORD=..."
+ echo " IBOARD_UPLOAD_KEY_PASSWORD=..."
+ echo "3. 重跑本脚本 (Gradle 读 gradle.properties 里的 IBOARD_UPLOAD_* 自动用 releaseSigning)"
+ echo
+ echo "=== 临时用 debug keystore 手动签 (仅测试) ==="
+ echo " \$ANDROID_HOME/build-tools/*/apksigner sign --ks ~/.android/debug.keystore \"
+ echo " --ks-pass pass:android --key-pass pass:android \"
+ echo " $APK_PATH"
+ fi
+else
+ echo "[debug] Tauri CLI debug 路径 (不传 --release 不会触发 bug), 直接走"
+ npx tauri android build --apk
+
+ APK_DIR="$WIN_PROJECT/src-tauri/target/debug/bundle/android/app/build/outputs/apk/debug"
+ echo
+ echo "=== 产物 ==="
+ ls -lh "$APK_DIR"/*.apk 2>/dev/null || echo "(未找到 APK)"
+fi
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 536cfbb..1f6ca45 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -19,15 +19,15 @@ dependencies = [
[[package]]
name = "alloc-no-stdlib"
-version = "2.0.4"
+version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+checksum = "c2fb6cfd47bf496ff64095c20eaba0c201404ee38714d4142fcfa1dc334fcc7a"
[[package]]
name = "alloc-stdlib"
-version = "0.2.4"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195"
+checksum = "0b5c1865780388bfa186411ab5f247819487fc4864c6e9c3106611fa347586e1"
dependencies = [
"alloc-no-stdlib",
]
@@ -47,6 +47,137 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+[[package]]
+name = "async-broadcast"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand",
+ "futures-lite",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "async-io"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc"
+dependencies = [
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite",
+ "parking",
+ "polling",
+ "rustix",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311"
+dependencies = [
+ "event-listener",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-process"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75"
+dependencies = [
+ "async-channel",
+ "async-io",
+ "async-lock",
+ "async-signal",
+ "async-task",
+ "blocking",
+ "cfg-if",
+ "event-listener",
+ "futures-lite",
+ "rustix",
+]
+
+[[package]]
+name = "async-recursion"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f8abc12baad266b1c8cec146854c195b5864b4221d4b2ca7296a7ae82d9e451"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
+[[package]]
+name = "async-signal"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485"
+dependencies = [
+ "async-io",
+ "async-lock",
+ "atomic-waker",
+ "cfg-if",
+ "futures-core",
+ "futures-io",
+ "rustix",
+ "signal-hook-registry",
+ "slab",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+]
+
[[package]]
name = "atk"
version = "0.18.2"
@@ -76,6 +207,20 @@ version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+[[package]]
+name = "auto-launch"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17918dba7ecf78b9a14507ec9f984e7977d13fad87dc86d61038980a45d128cf"
+dependencies = [
+ "dirs 6.0.0",
+ "os_info",
+ "smappservice-rs",
+ "thiserror 2.0.19",
+ "windows-registry",
+ "windows-result",
+]
+
[[package]]
name = "autocfg"
version = "1.5.1"
@@ -94,6 +239,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+[[package]]
+name = "base64"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
+
[[package]]
name = "bit-set"
version = "0.8.0"
@@ -143,10 +294,23 @@ dependencies = [
]
[[package]]
-name = "brotli"
-version = "8.0.4"
+name = "blocking"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3"
+checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa"
+dependencies = [
+ "async-channel",
+ "async-task",
+ "futures-io",
+ "futures-lite",
+ "piper",
+]
+
+[[package]]
+name = "brotli"
+version = "9.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6737ce8b81460bf9407909f73ff50bf13f7cb8577e1e76715f57ed0eba58ba44"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -155,9 +319,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
-version = "5.0.3"
+version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583"
+checksum = "34c6783f42c21c952ad7112b354f0ba2f10760e9decea5b1b60523d6e270faaf"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@@ -190,6 +354,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+[[package]]
+name = "byteorder-lite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
+
[[package]]
name = "bytes"
version = "1.12.1"
@@ -258,12 +428,13 @@ dependencies = [
[[package]]
name = "cargo_toml"
-version = "0.22.3"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77"
+checksum = "82f4b26e751e711a5302649417f2da046dce6391b2ea30a4820f37462314f0b9"
dependencies = [
+ "semver",
"serde",
- "toml 0.9.12+spec-1.1.0",
+ "toml 1.1.4+spec-1.1.0",
]
[[package]]
@@ -284,13 +455,13 @@ checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cfb"
-version = "0.7.3"
+version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f"
+checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
dependencies = [
- "byteorder",
"fnv",
"uuid",
+ "web-time",
]
[[package]]
@@ -335,7 +506,7 @@ dependencies = [
"iana-time-zone",
"num-traits",
"serde",
- "windows-link 0.2.1",
+ "windows-link",
]
[[package]]
@@ -348,6 +519,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
[[package]]
name = "cookie"
version = "0.18.1"
@@ -462,9 +642,9 @@ dependencies = [
[[package]]
name = "cssparser"
-version = "0.36.0"
+version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2"
+checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98"
dependencies = [
"cssparser-macros",
"dtoa-short",
@@ -475,29 +655,19 @@ dependencies = [
[[package]]
name = "cssparser-macros"
-version = "0.6.1"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
+checksum = "d045de693cb712d0b22c6a64be5b953f67b3ce00ab5ad3dd5d8b441886ab8e1a"
dependencies = [
"quote",
- "syn 2.0.119",
+ "syn 3.0.3",
]
[[package]]
name = "ctor"
-version = "0.8.0"
+version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98"
-dependencies = [
- "ctor-proc-macro",
- "dtor",
-]
-
-[[package]]
-name = "ctor-proc-macro"
-version = "0.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
+checksum = "914a755b7c2d4af2bdcff7ce1739e2db9a1b81a9b07123d8015786ae03c0980d"
[[package]]
name = "darling"
@@ -593,6 +763,15 @@ dependencies = [
"dirs-sys",
]
+[[package]]
+name = "dirs"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f"
+dependencies = [
+ "dirs-sys",
+]
+
[[package]]
name = "dirs-sys"
version = "0.5.0"
@@ -653,9 +832,9 @@ dependencies = [
[[package]]
name = "dom_query"
-version = "0.27.0"
+version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89"
+checksum = "fac5fca71e65e94cc718a6e2af65d6e0f9c6027751c2aa562fbb5087fda639bc"
dependencies = [
"bit-set",
"cssparser",
@@ -690,21 +869,6 @@ dependencies = [
"dtoa",
]
-[[package]]
-name = "dtor"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4"
-dependencies = [
- "dtor-proc-macro",
-]
-
-[[package]]
-name = "dtor-proc-macro"
-version = "0.0.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5"
-
[[package]]
name = "dunce"
version = "1.0.5"
@@ -746,6 +910,33 @@ dependencies = [
"cfg-if",
]
+[[package]]
+name = "endi"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099"
+
+[[package]]
+name = "enumflags2"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef"
+dependencies = [
+ "enumflags2_derive",
+ "serde",
+]
+
+[[package]]
+name = "enumflags2_derive"
+version = "0.7.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -773,6 +964,26 @@ dependencies = [
"windows-sys 0.61.2",
]
+[[package]]
+name = "event-listener"
+version = "5.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
+dependencies = [
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
+dependencies = [
+ "event-listener",
+ "pin-project-lite",
+]
+
[[package]]
name = "fastrand"
version = "2.5.0"
@@ -919,6 +1130,19 @@ version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
+[[package]]
+name = "futures-lite"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad"
+dependencies = [
+ "fastrand",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
[[package]]
name = "futures-macro"
version = "0.3.33"
@@ -1297,6 +1521,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+[[package]]
+name = "hermit-abi"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284"
+
[[package]]
name = "hex"
version = "0.4.3"
@@ -1305,9 +1535,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "html5ever"
-version = "0.38.0"
+version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2"
+checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8"
dependencies = [
"log",
"markup5ever",
@@ -1442,7 +1672,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
- "windows-core 0.62.2",
+ "windows-core",
]
[[package]]
@@ -1456,23 +1686,26 @@ dependencies = [
[[package]]
name = "iboard"
-version = "0.2.0"
+version = "0.2.1"
dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-build",
+ "tauri-plugin-autostart",
"tauri-plugin-hotswap",
+ "tauri-plugin-single-instance",
+ "windows-sys 0.60.2",
]
[[package]]
name = "ico"
-version = "0.5.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
+checksum = "d75e94d66081e4d8dcd098bd8dc5254800fea14a26389b298410158d834de167"
dependencies = [
"byteorder",
- "png 0.17.16",
+ "png",
]
[[package]]
@@ -1584,6 +1817,19 @@ dependencies = [
"icu_properties",
]
+[[package]]
+name = "image"
+version = "0.25.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
+dependencies = [
+ "bytemuck",
+ "byteorder-lite",
+ "moxcms",
+ "num-traits",
+ "png",
+]
+
[[package]]
name = "indexmap"
version = "1.9.3"
@@ -1609,9 +1855,9 @@ dependencies = [
[[package]]
name = "infer"
-version = "0.19.0"
+version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7"
+checksum = "f4200d433cbd5178df7797c9c2e75b348b728e39631cf14520d1e2fc424201f4"
dependencies = [
"cfb",
]
@@ -1708,21 +1954,21 @@ dependencies = [
[[package]]
name = "json-patch"
-version = "3.0.1"
+version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08"
+checksum = "7421438de105a0827e44fadd05377727847d717c80ce29a229f85fd04c427b72"
dependencies = [
"jsonptr",
"serde",
"serde_json",
- "thiserror 1.0.69",
+ "thiserror 2.0.19",
]
[[package]]
name = "jsonptr"
-version = "0.6.3"
+version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70"
+checksum = "a5a3cc660ba5d72bce0b3bb295bf20847ccbb40fd423f3f05b61273672e561fe"
dependencies = [
"serde",
"serde_json",
@@ -1730,13 +1976,12 @@ dependencies = [
[[package]]
name = "keyboard-types"
-version = "0.7.0"
+version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
+checksum = "0fbe853b403ae61a04233030ae8a79d94975281ed9770a1f9e246732b534b28d"
dependencies = [
"bitflags 2.13.1",
"serde",
- "unicode-segmentation",
]
[[package]]
@@ -1832,9 +2077,9 @@ checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "markup5ever"
-version = "0.38.0"
+version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862"
+checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de"
dependencies = [
"log",
"tendril",
@@ -1890,10 +2135,20 @@ dependencies = [
]
[[package]]
-name = "muda"
-version = "0.19.3"
+name = "moxcms"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878"
+checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
+dependencies = [
+ "num-traits",
+ "pxfm",
+]
+
+[[package]]
+name = "muda"
+version = "0.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cca139e57da4383727f189e43e66e84fa372347027fc996f9f1a007ec0a37996"
dependencies = [
"crossbeam-channel",
"dpi",
@@ -1904,7 +2159,7 @@ dependencies = [
"objc2-core-foundation",
"objc2-foundation",
"once_cell",
- "png 0.18.1",
+ "png",
"serde",
"thiserror 2.0.19",
"windows-sys 0.61.2",
@@ -1942,6 +2197,12 @@ dependencies = [
"thiserror 1.0.69",
]
+[[package]]
+name = "ndk-context"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
+
[[package]]
name = "ndk-sys"
version = "0.6.0+11769913"
@@ -1957,6 +2218,18 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+[[package]]
+name = "nix"
+version = "0.31.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
+dependencies = [
+ "bitflags 2.13.1",
+ "cfg-if",
+ "cfg_aliases",
+ "libc",
+]
+
[[package]]
name = "num-conv"
version = "0.2.2"
@@ -2015,6 +2288,7 @@ dependencies = [
"objc2",
"objc2-core-foundation",
"objc2-foundation",
+ "objc2-quartz-core",
]
[[package]]
@@ -2117,6 +2391,7 @@ checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
dependencies = [
"bitflags 2.13.1",
"block2",
+ "libc",
"objc2",
"objc2-core-foundation",
]
@@ -2144,6 +2419,30 @@ dependencies = [
"objc2-foundation",
]
+[[package]]
+name = "objc2-security"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a"
+dependencies = [
+ "bitflags 2.13.1",
+ "objc2",
+ "objc2-core-foundation",
+]
+
+[[package]]
+name = "objc2-service-management"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b213642d6959cc6023ceb1217aa595eaaf09b8094ce95127c103cab611fe65e8"
+dependencies = [
+ "block2",
+ "objc2",
+ "objc2-core-foundation",
+ "objc2-foundation",
+ "objc2-security",
+]
+
[[package]]
name = "objc2-ui-kit"
version = "0.3.2"
@@ -2244,6 +2543,31 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+[[package]]
+name = "ordered-stream"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "os_info"
+version = "3.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf20a545b305cf1da722b236b5155c9bb35f1d5ceb28c048bd96ca842f41b5b"
+dependencies = [
+ "android_system_properties",
+ "log",
+ "nix",
+ "objc2",
+ "objc2-foundation",
+ "objc2-ui-kit",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "pango"
version = "0.18.3"
@@ -2269,6 +2593,12 @@ dependencies = [
"system-deps",
]
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
[[package]]
name = "parking_lot"
version = "0.12.5"
@@ -2289,7 +2619,7 @@ dependencies = [
"libc",
"redox_syscall",
"smallvec",
- "windows-link 0.2.1",
+ "windows-link",
]
[[package]]
@@ -2357,6 +2687,17 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+[[package]]
+name = "piper"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1"
+dependencies = [
+ "atomic-waker",
+ "fastrand",
+ "futures-io",
+]
+
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -2376,19 +2717,6 @@ dependencies = [
"time",
]
-[[package]]
-name = "png"
-version = "0.17.16"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
-dependencies = [
- "bitflags 1.3.2",
- "crc32fast",
- "fdeflate",
- "flate2",
- "miniz_oxide",
-]
-
[[package]]
name = "png"
version = "0.18.1"
@@ -2402,6 +2730,20 @@ dependencies = [
"miniz_oxide",
]
+[[package]]
+name = "polling"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi",
+ "pin-project-lite",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "potential_utf"
version = "0.1.5"
@@ -2485,6 +2827,12 @@ dependencies = [
"unicode-ident",
]
+[[package]]
+name = "pxfm"
+version = "0.1.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
+
[[package]]
name = "quick-xml"
version = "0.41.0"
@@ -2942,9 +3290,9 @@ dependencies = [
[[package]]
name = "selectors"
-version = "0.36.1"
+version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c"
+checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d"
dependencies = [
"bitflags 2.13.1",
"cssparser",
@@ -3156,6 +3504,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
+dependencies = [
+ "errno",
+ "libc",
+]
+
[[package]]
name = "simd-adler32"
version = "0.3.10"
@@ -3180,6 +3538,18 @@ version = "1.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
+[[package]]
+name = "smappservice-rs"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52703b97a53101cf5d4580e0737aaa634ce1fdcfc123c16b2a9658e358013488"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+ "objc2-service-management",
+ "thiserror 2.0.19",
+]
+
[[package]]
name = "socket2"
version = "0.6.5"
@@ -3244,6 +3614,12 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+[[package]]
+name = "static_vcruntime"
+version = "3.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
+
[[package]]
name = "string_cache"
version = "0.9.0"
@@ -3282,9 +3658,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "swift-rs"
-version = "1.0.7"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7"
+checksum = "cefd87076fd385308ee4aff597256c902f1fbb4d031de1558efc12f8ffefadd9"
dependencies = [
"base64 0.21.7",
"serde",
@@ -3379,9 +3755,9 @@ dependencies = [
[[package]]
name = "tao"
-version = "0.35.3"
+version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9"
+checksum = "d5777b06fe6435126ae8d816eba269e050e1e10d302cf5461e9c49eccabc3548"
dependencies = [
"bitflags 2.13.1",
"block2",
@@ -3399,12 +3775,12 @@ dependencies = [
"libc",
"log",
"ndk",
+ "ndk-context",
"ndk-sys",
"objc2",
"objc2-app-kit",
"objc2-foundation",
"objc2-ui-kit",
- "once_cell",
"parking_lot",
"percent-encoding",
"raw-window-handle",
@@ -3412,7 +3788,7 @@ dependencies = [
"unicode-segmentation",
"url",
"windows",
- "windows-core 0.61.2",
+ "windows-core",
"windows-version",
"x11-dl",
]
@@ -3447,14 +3823,14 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
-version = "2.11.5"
+version = "2.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "667b20e2726d572dea2de7370da16e188eb06008faf9a92fab7cdc46791190b5"
+checksum = "5fb0244332cfb9cee7e2a568bf5da29a36ed8fdbd717f6f47a66b6b7b358c072"
dependencies = [
"anyhow",
"bytes",
"cookie",
- "dirs",
+ "dirs 7.0.0",
"dunce",
"embed_plist",
"getrandom 0.3.4",
@@ -3462,6 +3838,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "image",
"jni",
"libc",
"log",
@@ -3498,13 +3875,13 @@ dependencies = [
[[package]]
name = "tauri-build"
-version = "2.6.3"
+version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632"
+checksum = "adddb0e648752aaac5da9c3c3c6a68e1e87cab55e3b757b18a9441ef8c3ac614"
dependencies = [
"anyhow",
"cargo_toml",
- "dirs",
+ "dirs 7.0.0",
"glob",
"heck 0.5.0",
"json-patch",
@@ -3512,6 +3889,8 @@ dependencies = [
"semver",
"serde",
"serde_json",
+ "static_vcruntime",
+ "strsim",
"tauri-utils",
"tauri-winres",
"walkdir",
@@ -3519,16 +3898,16 @@ dependencies = [
[[package]]
name = "tauri-codegen"
-version = "2.6.3"
+version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5"
+checksum = "fb37631cf71dab5e8ab55e1bdd701e18a52653d90b78e71d0e5598b1e0a61316"
dependencies = [
- "base64 0.22.1",
+ "base64 0.23.1",
"brotli",
"ico",
"json-patch",
"plist",
- "png 0.17.16",
+ "png",
"proc-macro2",
"quote",
"semver",
@@ -3546,9 +3925,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
-version = "2.6.3"
+version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45"
+checksum = "0cc38db58854c10fb5f91a887ed8bf82d3239bb92b5d8db86eda09e96d9963d1"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -3560,9 +3939,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
-version = "2.6.3"
+version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020"
+checksum = "67fb21a77e8f94e8beb3325e583102488659365af0a3948b7ea244c5b779c291"
dependencies = [
"anyhow",
"glob",
@@ -3574,6 +3953,20 @@ dependencies = [
"walkdir",
]
+[[package]]
+name = "tauri-plugin-autostart"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bce56ceb0a99b8ab5eebc977ba9ffc62391b862648cb9ab76fc8bc7231b8f7e2"
+dependencies = [
+ "auto-launch",
+ "serde",
+ "serde_json",
+ "tauri",
+ "tauri-plugin",
+ "thiserror 2.0.19",
+]
+
[[package]]
name = "tauri-plugin-hotswap"
version = "0.0.4"
@@ -3581,7 +3974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3550b0b6727104637a12c6c1c5270714d4a8d935de3b2b71a5fe5a111d7ffc5"
dependencies = [
"base64 0.22.1",
- "dirs",
+ "dirs 6.0.0",
"flate2",
"log",
"minisign-verify",
@@ -3598,10 +3991,26 @@ dependencies = [
]
[[package]]
-name = "tauri-runtime"
-version = "2.11.3"
+name = "tauri-plugin-single-instance"
+version = "2.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8"
+checksum = "561cd99484561bc39c954768e7b41a66cebbe1ac3d9d081c34361f740c489e2c"
+dependencies = [
+ "serde",
+ "serde_json",
+ "tauri",
+ "thiserror 2.0.19",
+ "tokio",
+ "tracing",
+ "windows-sys 0.61.2",
+ "zbus",
+]
+
+[[package]]
+name = "tauri-runtime"
+version = "2.12.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c55766d99cea69055904d1735b93c2049302b2e09effd462120deb5581d78e9"
dependencies = [
"cookie",
"dpi",
@@ -3624,9 +4033,9 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
-version = "2.11.4"
+version = "2.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e6fac707727b7a2f48e4ded90976324267371073edbb415ffb73bb0458d203f"
+checksum = "e1aa9d8b7a120f879f44fdc1053f7b7dbf416bf0bbc6a72b714dec46b782b52f"
dependencies = [
"gtk",
"http",
@@ -3634,7 +4043,6 @@ dependencies = [
"log",
"objc2",
"objc2-app-kit",
- "once_cell",
"percent-encoding",
"raw-window-handle",
"softbuffer",
@@ -3650,9 +4058,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
-version = "2.9.3"
+version = "2.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887"
+checksum = "362ab21cd4cc713f2a02b1a9b5688198b1541cfb6907c4281e9190e0f0ef4aa3"
dependencies = [
"anyhow",
"brotli",
@@ -3874,21 +4282,6 @@ dependencies = [
"toml_edit 0.20.2",
]
-[[package]]
-name = "toml"
-version = "0.9.12+spec-1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
-dependencies = [
- "indexmap 2.14.0",
- "serde_core",
- "serde_spanned 1.1.1",
- "toml_datetime 0.7.5+spec-1.1.0",
- "toml_parser",
- "toml_writer",
- "winnow 0.7.15",
-]
-
[[package]]
name = "toml"
version = "1.1.4+spec-1.1.0"
@@ -3913,15 +4306,6 @@ dependencies = [
"serde",
]
-[[package]]
-name = "toml_datetime"
-version = "0.7.5+spec-1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
-dependencies = [
- "serde_core",
-]
-
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
@@ -4034,9 +4418,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
+ "tracing-attributes",
"tracing-core",
]
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "tracing-core"
version = "0.1.36"
@@ -4048,12 +4444,12 @@ dependencies = [
[[package]]
name = "tray-icon"
-version = "0.24.2"
+version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "045979e3f037cd18ad1cb2a419dfda133c5c29c9f3453370079f2255d46c257e"
+checksum = "b2b9c52859a94554803ccd4a24b98f74148ebc73b90676d783f3490b1bff9d72"
dependencies = [
"crossbeam-channel",
- "dirs",
+ "dirs 7.0.0",
"libappindicator",
"muda",
"objc2",
@@ -4062,7 +4458,7 @@ dependencies = [
"objc2-core-graphics",
"objc2-foundation",
"once_cell",
- "png 0.18.1",
+ "png",
"serde",
"thiserror 2.0.19",
"windows-sys 0.61.2",
@@ -4087,44 +4483,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
-name = "unic-char-property"
-version = "0.9.0"
+name = "uds_windows"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
- "unic-char-range",
-]
-
-[[package]]
-name = "unic-char-range"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
-
-[[package]]
-name = "unic-common"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
-
-[[package]]
-name = "unic-ucd-ident"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987"
-dependencies = [
- "unic-char-property",
- "unic-char-range",
- "unic-ucd-version",
-]
-
-[[package]]
-name = "unic-ucd-version"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
-dependencies = [
- "unic-common",
+ "memoffset",
+ "tempfile",
+ "windows-sys 0.61.2",
]
[[package]]
@@ -4160,13 +4526,13 @@ dependencies = [
[[package]]
name = "urlpattern"
-version = "0.3.0"
+version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d"
+checksum = "df16f50ef4cc145211879a3867ba757076b25dfee812040dcb0658bd9ae7904b"
dependencies = [
+ "icu_properties",
"regex",
"serde",
- "unic-ucd-ident",
"url",
]
@@ -4428,16 +4794,14 @@ dependencies = [
[[package]]
name = "webview2-com"
-version = "0.38.2"
+version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a"
+checksum = "3f89fca7a704cee10dcb3654c1dbb8941d1783132f1917358af75bec37a7d7e6"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
- "windows-core 0.61.2",
- "windows-implement",
- "windows-interface",
+ "windows-core",
]
[[package]]
@@ -4453,13 +4817,13 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
-version = "0.38.2"
+version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c"
+checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb"
dependencies = [
"thiserror 2.0.19",
"windows",
- "windows-core 0.61.2",
+ "windows-core",
]
[[package]]
@@ -4495,52 +4859,39 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "window-vibrancy"
-version = "0.6.0"
+version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
+checksum = "111e51caca442cafd9bab396628ac4d814880eaea4e63dfd76a12e9f342b5580"
dependencies = [
"objc2",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation",
+ "objc2-quartz-core",
"raw-window-handle",
- "windows-sys 0.59.0",
+ "windows-sys 0.61.2",
"windows-version",
]
[[package]]
name = "windows"
-version = "0.61.3"
+version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
- "windows-core 0.61.2",
+ "windows-core",
"windows-future",
- "windows-link 0.1.3",
"windows-numerics",
]
[[package]]
name = "windows-collections"
-version = "0.2.0"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
- "windows-core 0.61.2",
-]
-
-[[package]]
-name = "windows-core"
-version = "0.61.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
-dependencies = [
- "windows-implement",
- "windows-interface",
- "windows-link 0.1.3",
- "windows-result 0.3.4",
- "windows-strings 0.4.2",
+ "windows-core",
]
[[package]]
@@ -4551,19 +4902,19 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
- "windows-link 0.2.1",
- "windows-result 0.4.1",
- "windows-strings 0.5.1",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
]
[[package]]
name = "windows-future"
-version = "0.2.1"
+version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
- "windows-core 0.61.2",
- "windows-link 0.1.3",
+ "windows-core",
+ "windows-link",
"windows-threading",
]
@@ -4589,12 +4940,6 @@ dependencies = [
"syn 2.0.119",
]
-[[package]]
-name = "windows-link"
-version = "0.1.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
-
[[package]]
name = "windows-link"
version = "0.2.1"
@@ -4603,12 +4948,12 @@ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
-version = "0.2.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
- "windows-core 0.61.2",
- "windows-link 0.1.3",
+ "windows-core",
+ "windows-link",
]
[[package]]
@@ -4617,18 +4962,9 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
- "windows-link 0.2.1",
- "windows-result 0.4.1",
- "windows-strings 0.5.1",
-]
-
-[[package]]
-name = "windows-result"
-version = "0.3.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
-dependencies = [
- "windows-link 0.1.3",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
]
[[package]]
@@ -4637,16 +4973,7 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
- "windows-link 0.2.1",
-]
-
-[[package]]
-name = "windows-strings"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
-dependencies = [
- "windows-link 0.1.3",
+ "windows-link",
]
[[package]]
@@ -4655,7 +4982,7 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
- "windows-link 0.2.1",
+ "windows-link",
]
[[package]]
@@ -4685,13 +5012,22 @@ dependencies = [
"windows-targets 0.52.6",
]
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
- "windows-link 0.2.1",
+ "windows-link",
]
[[package]]
@@ -4718,7 +5054,7 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
@@ -4726,12 +5062,29 @@ dependencies = [
]
[[package]]
-name = "windows-threading"
-version = "0.1.0"
+name = "windows-targets"
+version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
- "windows-link 0.1.3",
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
]
[[package]]
@@ -4740,7 +5093,7 @@ version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631"
dependencies = [
- "windows-link 0.2.1",
+ "windows-link",
]
[[package]]
@@ -4755,6 +5108,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -4767,6 +5126,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -4779,12 +5144,24 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -4797,6 +5174,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -4809,6 +5192,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -4821,6 +5210,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -4833,6 +5228,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
[[package]]
name = "winnow"
version = "0.5.40"
@@ -4842,12 +5243,6 @@ dependencies = [
"memchr",
]
-[[package]]
-name = "winnow"
-version = "0.7.15"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
-
[[package]]
name = "winnow"
version = "1.0.4"
@@ -4881,15 +5276,15 @@ checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
[[package]]
name = "wry"
-version = "0.55.1"
+version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514"
+checksum = "759c50ddad0ef9913bb5496cb9d509a50433a3b78cb0238d61105526589cf0e9"
dependencies = [
- "base64 0.22.1",
+ "base64 0.23.1",
"block2",
"cookie",
"crossbeam-channel",
- "dirs",
+ "dirs 7.0.0",
"dom_query",
"dpi",
"dunce",
@@ -4906,7 +5301,6 @@ dependencies = [
"objc2-foundation",
"objc2-ui-kit",
"objc2-web-kit",
- "once_cell",
"percent-encoding",
"raw-window-handle",
"sha2",
@@ -4918,7 +5312,7 @@ dependencies = [
"webkit2gtk-sys",
"webview2-com",
"windows",
- "windows-core 0.61.2",
+ "windows-core",
"windows-version",
"x11-dl",
]
@@ -4977,6 +5371,76 @@ dependencies = [
"synstructure",
]
+[[package]]
+name = "zbus"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5db4be7c075cb421e4b7ee645541604239bd243ba7c357511f4ff3a74b555907"
+dependencies = [
+ "async-broadcast",
+ "async-executor",
+ "async-io",
+ "async-lock",
+ "async-process",
+ "async-recursion",
+ "async-task",
+ "async-trait",
+ "blocking",
+ "enumflags2",
+ "event-listener",
+ "futures-core",
+ "futures-lite",
+ "hex",
+ "libc",
+ "ordered-stream",
+ "rustix",
+ "serde",
+ "serde_repr",
+ "tracing",
+ "uds_windows",
+ "uuid",
+ "windows-sys 0.61.2",
+ "winnow 1.0.4",
+ "zbus_macros",
+ "zbus_names",
+ "zvariant",
+]
+
+[[package]]
+name = "zbus_macros"
+version = "5.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2990635d09ade6df1868f72f8cac69a876a90981e8bd3c40b1be413f8dc88f40"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+ "zbus_names",
+ "zvariant",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zbus_names"
+version = "4.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812a12be4ead7a59e18ea8f047d08add0066b73bc62fc1467066a2472ae088dd"
+dependencies = [
+ "serde",
+ "winnow 1.0.4",
+ "zvariant",
+]
+
+[[package]]
+name = "zcheapstr"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1afec51604565183aeb5c54c20aeab286120d4e4460f7f76e3e8bb8c0d99473"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "zerofrom"
version = "0.1.8"
@@ -5042,3 +5506,44 @@ name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
+
+[[package]]
+name = "zvariant"
+version = "5.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80bf6d6cd6a4b601acb64cfb8284cdc643fcfa75ef26ae3c46ec8aa4e8a1fbb7"
+dependencies = [
+ "endi",
+ "enumflags2",
+ "serde",
+ "winnow 1.0.4",
+ "zcheapstr",
+ "zvariant_derive",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_derive"
+version = "5.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef2672456ba1cb54b5eaec136e63135b3c7495fb938e53b28a62e633496d486d"
+dependencies = [
+ "proc-macro-crate 3.5.0",
+ "proc-macro2",
+ "quote",
+ "syn 3.0.3",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "4.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bad0294361a320b694a328460dc73add56c306150f5cb6bfafc44446120008a3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 3.0.3",
+ "winnow 1.0.4",
+]
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index f19b1ee..b274c56 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "iboard"
-version = "0.2.0"
+version = "0.2.1"
description = "iBoard - 经济数据与策略"
authors = ["iBoard Team"]
edition = "2021"
@@ -27,6 +27,20 @@ tauri-plugin-hotswap = "0.0.4"
[target."cfg(target_os = \"android\")".dependencies]
tauri = { version = "2", features = [] }
+# Windows 外壳能力 (W-M1~M4): 托盘 / 自启动 / 单实例 / Win32 辅助
+# 仅加在 Windows 目标上, Android 构建不受影响 (W-NF-05)
+[target."cfg(windows)".dependencies]
+tauri = { version = "2", features = ["tray-icon", "image-png"] }
+tauri-plugin-autostart = "2"
+tauri-plugin-single-instance = "2"
+windows-sys = { version = "0.60", features = [
+ "Win32_Foundation",
+ "Win32_Graphics_Gdi",
+ "Win32_System_Registry",
+ "Win32_UI_Shell",
+ "Win32_UI_WindowsAndMessaging",
+] }
+
[features]
# Tauri 的"自定义协议"模式让打包后的 webview 能从 tauri:// 加载资源
# 用于生产构建, dev 模式不需要
diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png
index 145eb8a..f3c5bcf 100644
Binary files a/src-tauri/icons/128x128.png and b/src-tauri/icons/128x128.png differ
diff --git a/src-tauri/icons/128x128@2x.png b/src-tauri/icons/128x128@2x.png
index 95a986b..f89781c 100644
Binary files a/src-tauri/icons/128x128@2x.png and b/src-tauri/icons/128x128@2x.png differ
diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png
index 1bc47b2..5da1844 100644
Binary files a/src-tauri/icons/32x32.png and b/src-tauri/icons/32x32.png differ
diff --git a/src-tauri/icons/64x64.png b/src-tauri/icons/64x64.png
new file mode 100644
index 0000000..0dde31b
Binary files /dev/null and b/src-tauri/icons/64x64.png differ
diff --git a/src-tauri/icons/Square107x107Logo.png b/src-tauri/icons/Square107x107Logo.png
new file mode 100644
index 0000000..3354325
Binary files /dev/null and b/src-tauri/icons/Square107x107Logo.png differ
diff --git a/src-tauri/icons/Square142x142Logo.png b/src-tauri/icons/Square142x142Logo.png
new file mode 100644
index 0000000..079f462
Binary files /dev/null and b/src-tauri/icons/Square142x142Logo.png differ
diff --git a/src-tauri/icons/Square150x150Logo.png b/src-tauri/icons/Square150x150Logo.png
new file mode 100644
index 0000000..e93f9b8
Binary files /dev/null and b/src-tauri/icons/Square150x150Logo.png differ
diff --git a/src-tauri/icons/Square284x284Logo.png b/src-tauri/icons/Square284x284Logo.png
new file mode 100644
index 0000000..c03af83
Binary files /dev/null and b/src-tauri/icons/Square284x284Logo.png differ
diff --git a/src-tauri/icons/Square30x30Logo.png b/src-tauri/icons/Square30x30Logo.png
new file mode 100644
index 0000000..f328746
Binary files /dev/null and b/src-tauri/icons/Square30x30Logo.png differ
diff --git a/src-tauri/icons/Square310x310Logo.png b/src-tauri/icons/Square310x310Logo.png
new file mode 100644
index 0000000..c529095
Binary files /dev/null and b/src-tauri/icons/Square310x310Logo.png differ
diff --git a/src-tauri/icons/Square44x44Logo.png b/src-tauri/icons/Square44x44Logo.png
new file mode 100644
index 0000000..813fe01
Binary files /dev/null and b/src-tauri/icons/Square44x44Logo.png differ
diff --git a/src-tauri/icons/Square71x71Logo.png b/src-tauri/icons/Square71x71Logo.png
new file mode 100644
index 0000000..6e20681
Binary files /dev/null and b/src-tauri/icons/Square71x71Logo.png differ
diff --git a/src-tauri/icons/Square89x89Logo.png b/src-tauri/icons/Square89x89Logo.png
new file mode 100644
index 0000000..5e7d527
Binary files /dev/null and b/src-tauri/icons/Square89x89Logo.png differ
diff --git a/src-tauri/icons/StoreLogo.png b/src-tauri/icons/StoreLogo.png
new file mode 100644
index 0000000..df7d3c7
Binary files /dev/null and b/src-tauri/icons/StoreLogo.png differ
diff --git a/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..2ffbf24
--- /dev/null
+++ b/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..b147d29
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..1645beb
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..eb46e90
Binary files /dev/null and b/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..8372b96
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..cbfdf38
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..16101ec
Binary files /dev/null and b/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..3a3ccea
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..e78c690
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..9fbf717
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..943bcb2
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..3f72e4f
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a54aed6
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..63ba0d8
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..d6422bb
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..0d0186a
Binary files /dev/null and b/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/src-tauri/icons/android/values/ic_launcher_background.xml b/src-tauri/icons/android/values/ic_launcher_background.xml
new file mode 100644
index 0000000..ea9c223
--- /dev/null
+++ b/src-tauri/icons/android/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #fff
+
\ No newline at end of file
diff --git a/src-tauri/icons/icon.icns b/src-tauri/icons/icon.icns
index 067233f..aecff57 100644
Binary files a/src-tauri/icons/icon.icns and b/src-tauri/icons/icon.icns differ
diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico
index 95a986b..c9dd1dc 100644
Binary files a/src-tauri/icons/icon.ico and b/src-tauri/icons/icon.ico differ
diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png
index 067233f..8e57d01 100644
Binary files a/src-tauri/icons/icon.png and b/src-tauri/icons/icon.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@1x.png b/src-tauri/icons/ios/AppIcon-20x20@1x.png
new file mode 100644
index 0000000..d09ccd8
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
new file mode 100644
index 0000000..811c4d4
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@2x.png b/src-tauri/icons/ios/AppIcon-20x20@2x.png
new file mode 100644
index 0000000..811c4d4
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-20x20@3x.png b/src-tauri/icons/ios/AppIcon-20x20@3x.png
new file mode 100644
index 0000000..c1790e8
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-20x20@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@1x.png b/src-tauri/icons/ios/AppIcon-29x29@1x.png
new file mode 100644
index 0000000..a6d8d0c
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
new file mode 100644
index 0000000..abaf338
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@2x.png b/src-tauri/icons/ios/AppIcon-29x29@2x.png
new file mode 100644
index 0000000..abaf338
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-29x29@3x.png b/src-tauri/icons/ios/AppIcon-29x29@3x.png
new file mode 100644
index 0000000..e89c378
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-29x29@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@1x.png b/src-tauri/icons/ios/AppIcon-40x40@1x.png
new file mode 100644
index 0000000..811c4d4
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
new file mode 100644
index 0000000..2f8bbb1
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@2x.png b/src-tauri/icons/ios/AppIcon-40x40@2x.png
new file mode 100644
index 0000000..2f8bbb1
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-40x40@3x.png b/src-tauri/icons/ios/AppIcon-40x40@3x.png
new file mode 100644
index 0000000..752956f
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-40x40@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-512@2x.png b/src-tauri/icons/ios/AppIcon-512@2x.png
new file mode 100644
index 0000000..bc42423
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-512@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-60x60@2x.png b/src-tauri/icons/ios/AppIcon-60x60@2x.png
new file mode 100644
index 0000000..752956f
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-60x60@3x.png b/src-tauri/icons/ios/AppIcon-60x60@3x.png
new file mode 100644
index 0000000..47d80fd
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-60x60@3x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-76x76@1x.png b/src-tauri/icons/ios/AppIcon-76x76@1x.png
new file mode 100644
index 0000000..333da4b
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@1x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-76x76@2x.png b/src-tauri/icons/ios/AppIcon-76x76@2x.png
new file mode 100644
index 0000000..81c7717
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-76x76@2x.png differ
diff --git a/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
new file mode 100644
index 0000000..c63e868
Binary files /dev/null and b/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ
diff --git a/src-tauri/icons/tray-dark.png b/src-tauri/icons/tray-dark.png
new file mode 100644
index 0000000..a8842c4
Binary files /dev/null and b/src-tauri/icons/tray-dark.png differ
diff --git a/src-tauri/icons/tray-light.png b/src-tauri/icons/tray-light.png
new file mode 100644
index 0000000..0ead4c9
Binary files /dev/null and b/src-tauri/icons/tray-light.png differ
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 4e99313..bc40ab1 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -1,9 +1,14 @@
// 跨平台入口: 桌面从这里走, Android 走 mobile_entry_point 宏
// 启动流程:
-// 1) 初始化 tauri-plugin-hotswap (前端 OTA 升级插件)
+// 1) Windows 外壳能力注册 (单实例 / 自启动 / 托盘 / 吸附悬浮球, 见 src/shell)
+// 2) 初始化 tauri-plugin-hotswap (前端 OTA 升级插件)
// - 必须在 build 之前调用, 插件会替换 tauri::generate_context! 的 asset provider,
// 后续可被 tauri::Builder 消费
-// 2) 业务初始化
+// 3) 业务初始化
+
+#[cfg(all(desktop, target_os = "windows"))]
+mod shell;
+
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
// 生成 context, 交给 hotswap 包装 (替换 Assets provider, 让 tauri://localhost 资源
@@ -12,8 +17,94 @@ pub fn run() {
let (hotswap, context) = tauri_plugin_hotswap::init(context)
.expect("failed to initialize tauri-plugin-hotswap");
- tauri::Builder::default()
+ let mut builder = tauri::Builder::default()
+ .invoke_handler(tauri::generate_handler![
+ dock_hover_start,
+ dock_hover_end,
+ dock_click,
+ dock_drag_start,
+ dock_drag_move,
+ dock_drag_end,
+ ])
+ .on_window_event(on_window_event);
+
+ #[cfg(all(desktop, target_os = "windows"))]
+ {
+ builder = builder
+ // 单实例必须最先注册 (W-TRY-07): 二次启动唤回已有实例
+ .plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
+ shell::activate_primary(app);
+ }))
+ .plugin(tauri_plugin_autostart::init(
+ tauri_plugin_autostart::MacosLauncher::LaunchAgent,
+ Some(vec!["--autostart"]),
+ ))
+ .setup(|app| shell::setup(app));
+ }
+
+ builder
.plugin(hotswap)
.run(context)
.expect("error while running iBoard");
}
+
+fn on_window_event(window: &tauri::Window, event: &tauri::WindowEvent) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::on_window_event(window, event);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ {
+ let _ = (window, event);
+ }
+}
+
+// —— 悬浮球 IPC (dock.html → Rust); 非 Windows 桌面平台为 no-op ——
+
+#[tauri::command]
+fn dock_hover_start(app: tauri::AppHandle) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::dock_hover_start(&app);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ let _ = app;
+}
+
+#[tauri::command]
+fn dock_hover_end(app: tauri::AppHandle) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::dock_hover_end(&app);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ let _ = app;
+}
+
+#[tauri::command]
+fn dock_click(app: tauri::AppHandle) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::dock_click(&app);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ let _ = app;
+}
+
+#[tauri::command]
+fn dock_drag_start(app: tauri::AppHandle) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::dock_drag_start(&app);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ let _ = app;
+}
+
+#[tauri::command]
+fn dock_drag_move(app: tauri::AppHandle, dx: i32, dy: i32) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::dock_drag_move(&app, dx, dy);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ {
+ let _ = (app, dx, dy);
+ }
+}
+
+#[tauri::command]
+fn dock_drag_end(app: tauri::AppHandle) {
+ #[cfg(all(desktop, target_os = "windows"))]
+ shell::dock_drag_end(&app);
+ #[cfg(not(all(desktop, target_os = "windows")))]
+ let _ = app;
+}
diff --git a/src-tauri/src/shell/config.rs b/src-tauri/src/shell/config.rs
new file mode 100644
index 0000000..ff8e9d8
--- /dev/null
+++ b/src-tauri/src/shell/config.rs
@@ -0,0 +1,160 @@
+// 澶栧3璁剧疆鎸佷箙鍖?(W-CFG-01~03): %APPDATA%\com.iboard.app\shell-config.json
+// - 涓庡墠绔处鍙?鏈嶅姟绔棤鍏? 绂荤嚎鍙敤;
+// - 瀛楁缂哄け/鎹熷潖鏃跺洖閫€榛樿鍊?(serde(default) + 瀹芥澗鍔犺浇);
+// - 鍐欏叆閲囩敤 涓存椂鏂囦欢 + 鏇挎崲 鐨勫師瀛愯涔夈€?
+use serde::{Deserialize, Serialize};
+use std::path::Path;
+
+pub const FILE_NAME: &str = "shell-config.json";
+
+/// 璁捐鍩哄噯灏哄 (绉诲姩绔竷灞€鍙e緞): 绐楀彛姣斾緥閿佹涓?405:720
+pub const DESIGN_WIDTH: f64 = 405.0;
+pub const DESIGN_HEIGHT: f64 = 720.0;
+/// 窗口宽度可配置范围 (逻辑像素); 高度由比例推导, 不接受单独配置
+pub const WIDTH_MIN: f64 = 242.0;
+pub const WIDTH_MAX: f64 = 620.0;
+
+/// 鎸夐攣瀹氭瘮渚嬬敱瀹藉害鎺ㄥ楂樺害 (鍞竴灏哄鍏ュ彛)
+pub fn height_for_width(width: f64) -> f64 {
+ let w = width.clamp(WIDTH_MIN, WIDTH_MAX);
+ (w * DESIGN_HEIGHT / DESIGN_WIDTH).round()
+}
+
+/// 鍚搁檮杈?(W-SNP-01: 鍥涜竟)
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
+#[serde(rename_all = "lowercase")]
+pub enum Edge {
+ #[default]
+ Left,
+ Right,
+ Top,
+ Bottom,
+}
+
+impl Edge {
+ pub fn css(&self) -> &'static str {
+ match self {
+ Edge::Left => "left",
+ Edge::Right => "right",
+ Edge::Top => "top",
+ Edge::Bottom => "bottom",
+ }
+ }
+}
+
+/// 涓荤獥鍙h蹇?(W-WIN-07): 灏哄涓洪€昏緫鍍忕礌 (DPI 绋冲畾), 浣嶇疆涓虹墿鐞嗗儚绱?(鍔犺浇鏃舵牎楠屽す鍙?
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct WindowCfg {
+ pub width: f64,
+ pub height: f64,
+ pub x: Option,
+ pub y: Option,
+}
+
+impl Default for WindowCfg {
+ fn default() -> Self {
+ // 323x574 = 405:720 姣斾緥 (0.797x of 405x720, 2026-10-26 鏀惧ぇ 12%)
+ Self { width: 323.0, height: height_for_width(323.0), x: None, y: None }
+ }
+}
+
+/// 鎮诞鐞冨仠闈?(W-SNP-02/12): 杈?+ 娌胯竟浣嶇疆 (鎮诞鐞冧腑蹇? 鐗╃悊鍍忕礌)
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct DockCfg {
+ pub edge: Edge,
+ pub offset: i32,
+}
+
+impl Default for DockCfg {
+ fn default() -> Self {
+ Self { edge: Edge::Left, offset: 360 }
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(default)]
+pub struct ShellConfig {
+ /// 鍚搁檮鎬诲紑鍏?(W-SNP-09, 榛樿寮€鍚?
+ pub snap_enabled: bool,
+ pub window: WindowCfg,
+ pub dock: DockCfg,
+ /// 绐楀彛瀹藉害 (閫昏緫鍍忕礌) 鈥斺€?**鍞竴灏哄閰嶇疆椤?*銆? /// 楂樺害鎸?405:720 閿佸畾姣斾緥鑷姩鎺ㄥ, 鏃犻渶涔熸棤娉曞崟鐙厤缃?
+ /// 鐣欑┖鏃跺洖钀藉埌 window.width銆? #[serde(default)]
+ pub window_width: Option,
+ /// 閰嶇疆缁撴瀯鐗堟湰: 0 = 鏃х増 (绐楀彛灏哄鎸?405x720 鍙e緞淇濆瓨),
+ /// 1 = 鏁翠綋缂╁皬 30% (288 鍙e緞), 2 = 鏀惧ぇ 12% (323 鍙e緞) + 鍗曞€煎搴﹂厤缃€? /// load 鏃跺仛涓€娆℃€ц縼绉汇€? /// 娉ㄦ剰蹇呴』鐢ㄥ瓧娈电骇 serde(default) (u8 鈫?0): 瀹瑰櫒绾?default 浼氬彇
+ /// ShellConfig::default() 鐨?2, 鏃ч厤缃枃浠跺氨姘歌繙涓嶈縼绉讳簡銆? #[serde(default)]
+ pub cfg_version: u8,
+}
+
+impl Default for ShellConfig {
+ fn default() -> Self {
+ Self {
+ snap_enabled: true,
+ window: WindowCfg::default(),
+ dock: DockCfg::default(),
+ window_width: Some(WindowCfg::default().width),
+ cfg_version: 2,
+ }
+ }
+}
+
+/// 杩佺Щ鍓嶇殑绐楀彛瀹藉害 (鏃у彛寰?, 渚?load 浣跨敤
+impl WindowCfg {
+ /// 搴旂敤閿佸畾姣斾緥: 瀹藉害澶瑰彇鍒板悎娉曡寖鍥? 楂樺害鐢卞搴︽帹瀵?(蹇界暐瀛樼洏楂樺害)
+ pub(crate) fn normalize(&mut self) {
+ self.width = self.width.clamp(WIDTH_MIN, WIDTH_MAX);
+ self.height = height_for_width(self.width);
+ }
+}
+
+impl ShellConfig {
+ pub fn load(dir: &Path) -> ShellConfig {
+ let path = dir.join(FILE_NAME);
+ let mut cfg = match std::fs::read_to_string(&path) {
+ Ok(text) => match serde_json::from_str::(&text) {
+ Ok(cfg) => cfg,
+ Err(_) => ShellConfig::default(), // 鍐欏潖 JSON 鈫?榛樿鍊?(W-CFG-03)
+ },
+ Err(_) => ShellConfig::default(),
+ };
+ // 一次性迁移
+ if cfg.cfg_version < 1 {
+ // v0 (405 鍙e緞) 鈫?v1: 鏁翠綋缂╁皬 30%
+ cfg.window.width = (cfg.window.width * 0.7).max(216.0);
+ cfg.window.height = (cfg.window.height * 0.7).max(384.0);
+ cfg.cfg_version = 1;
+ }
+ if cfg.cfg_version < 2 {
+ // v1 (288 口径) -> v2: 放大 12% (323 口径), 引入单值宽度配置
+ cfg.window.width = (cfg.window.width * 1.12).max(WIDTH_MIN);
+ cfg.window.height = height_for_width(cfg.window.width);
+ cfg.cfg_version = 2;
+ }
+ // 鍗曞€煎搴︿紭鍏? 楂樺害涓€寰嬫寜閿佸畾姣斾緥鎺ㄥ (蹇界暐瀛樼洏楂樺害)
+ if let Some(w) = cfg.window_width {
+ cfg.window.width = w;
+ }
+ cfg.window.normalize();
+ cfg.window_width = Some(cfg.window.width);
+ cfg.save(dir);
+ cfg
+ }
+
+ pub fn save(&self, dir: &Path) {
+ let path = dir.join(FILE_NAME);
+ let tmp = dir.join(format!("{FILE_NAME}.tmp"));
+ let text = match serde_json::to_string_pretty(self) {
+ Ok(t) => t,
+ Err(_) => return,
+ };
+ if std::fs::write(&tmp, text).is_ok() {
+ // Windows 上 rename 不能覆盖已存在文件, 先移除
+ let _ = std::fs::remove_file(&path);
+ let _ = std::fs::rename(&tmp, &path);
+ }
+ // 鍐欏け璐ヤ繚鎸侀潤榛?(W-CFG-04): 杩愯涓殑澶栧3涓嶅彈褰卞搷
+ }
+}
diff --git a/src-tauri/src/shell/mod.rs b/src-tauri/src/shell/mod.rs
new file mode 100644
index 0000000..9fc3736
--- /dev/null
+++ b/src-tauri/src/shell/mod.rs
@@ -0,0 +1,960 @@
+//! Windows 外壳 (需求文档 §3): 托盘常驻 / 开机自启动 / 四边吸附悬浮球 / 主题跟随
+//!
+//! 仅在 `desktop + windows` 下编译 (lib.rs cfg 门控); Android 构建不含本模块。
+//! 状态机要点:
+//! - 收起判定在「松开鼠标」后静默 150ms 进行 (W-SNP-01, 评审决策 #1);
+//! - 悬浮球 hover 停留 ≥150ms 或单击即恢复 (W-SNP-04/05, 决策 #2);
+//! - 悬浮球可沿边拖动、拖到其他边换边、拖离边缘 ≥32px 拖出主窗口 (W-SNP-12~14);
+//! - 无边框全屏之上保持显示, 独占全屏自动避让 (W-SNP-15/16);
+//! - 托盘与悬浮球深浅色跟随系统 (W-THE)。
+
+mod config;
+mod win;
+
+use config::{DockCfg, Edge, ShellConfig};
+use std::path::PathBuf;
+use std::sync::Mutex;
+use std::time::{Duration, Instant};
+use tauri::menu::{CheckMenuItem, Menu, MenuEvent, MenuItem};
+use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
+use tauri::{AppHandle, Manager, PhysicalPosition, PhysicalSize, WebviewUrl, WebviewWindowBuilder, Wry};
+
+const MAIN: &str = "main";
+const DOCK: &str = "dock";
+const TRAY_ID: &str = "iboard-tray";
+
+/// 悬浮球尺寸 (逻辑像素, 附录 A): 垂直边 = 28×56, 水平边 = 56×28
+const HANDLE_SHORT: f64 = 28.0;
+const HANDLE_LONG: f64 = 56.0;
+/// 吸附触发阈值 (W-SNP-01)
+const SNAP_PX: f64 = 8.0;
+/// 解除吸附阈值 (W-SNP-06/14)
+const UNDOCK_PX: f64 = 32.0;
+/// 碰触恢复延时 (W-SNP-04)
+const RESTORE_DELAY: Duration = Duration::from_millis(150);
+/// 吸附态展开后, 鼠标移开窗口多久自动收起 (auto-hide)
+const AUTOHIDE_DELAY: Duration = Duration::from_millis(400);
+/// 移动静默判定窗口 (松手判定, 决策 #1)
+const MOVE_QUIET: Duration = Duration::from_millis(150);
+/// 程序性移动后的吸附抑制 (避免 set_position 触发立即收起)
+const MOVE_SUPPRESS: Duration = Duration::from_millis(400);
+/// 尺寸比例锁容差 (逻辑像素)
+const RATIO_TOL: f64 = 2.0;
+
+type CheckItem = CheckMenuItem;
+
+struct Shared {
+ data_dir: PathBuf,
+ cfg: ShellConfig,
+ /// 主窗口处于吸附态 (含贴边可见的“已恢复”状态)
+ docked: bool,
+ /// 悬浮球当前可见 (true = 收起态)
+ dock_visible: bool,
+ /// 因独占全屏而隐藏 (W-SNP-16)
+ fs_hidden: bool,
+ /// 悬浮球拖动中 (JS pointer 事件驱动)
+ dragging: bool,
+ hover_since: Option,
+ last_main_move: Option,
+ /// 最近一次移动的 (最近边, 距边缘距离), 供解除吸附的静默判定
+ last_move_eval: Option<(Edge, f64)>,
+ /// 是否已离开边缘条带 (拖离过程中置位; 只有 armed 才允许再次触碰收起,
+ /// 否则恢复到贴边位置后的第一次移动事件会立即误收)
+ edge_armed: bool,
+ /// 上一次移动事件时的窗口矩形 (越线判定用)
+ last_main_rect: Option,
+ /// 吸附态展开后, 光标移出窗口的起始时刻 (auto-hide 计时)
+ mouse_outside_since: Option,
+ /// 展开后光标是否进过窗口 (悬浮球唤回=true; 托盘唤回时光标在远处=false,
+ /// 未武装则永不自动收起, 否则托盘唤回会立刻缩回去)
+ mouse_inside_since_restore: bool,
+ ignore_until: Option,
+ theme_dark: bool,
+ menu_autostart: Option,
+ menu_snap: Option,
+ /// 主窗口在吸附态下的恢复位置 (贴边, 物理像素)
+ docked_rect: Option<(PhysicalPosition, PhysicalSize)>,
+}
+
+type State = Mutex;
+
+fn state<'a>(app: &'a AppHandle) -> Option<&'a State> {
+ app.try_state::().map(|m| m.inner())
+}
+
+// ============================================================================
+// 生命周期
+// ============================================================================
+
+pub fn setup(app: &mut tauri::App) -> Result<(), Box> {
+ let silent = std::env::args().any(|a| a == "--autostart"); // W-AUT-04 静默启动
+ let data_dir = app.path().app_data_dir()?;
+ let _ = std::fs::create_dir_all(&data_dir);
+ let cfg = ShellConfig::load(&data_dir);
+
+ let main = app.get_webview_window(MAIN).expect("main window missing");
+ apply_saved_window(&main, &cfg);
+
+ // 内容缩放跟随窗口宽度: 布局视口始终等于 405 设计宽, 窗口变大/变小只是整体等比缩放.
+ // 仅主窗口 —— 悬浮球窗口自绘尺寸不缩放; Android 端不经过本模块, 不受影响。
+ let _ = main.set_zoom(cfg.window.width / config::DESIGN_WIDTH);
+
+ // 悬浮球窗口 (W-SNP-02): 透明 / 置顶 / 无任务栏条目, 初始隐藏
+ WebviewWindowBuilder::new(app, DOCK, WebviewUrl::App("dock.html".into()))
+ .title("iBoard Dock")
+ .decorations(false)
+ .transparent(true)
+ .shadow(false)
+ .resizable(false)
+ .always_on_top(true)
+ .skip_taskbar(true)
+ .focused(false)
+ .visible(false)
+ .build()?;
+
+ let dark = win::system_prefers_dark();
+ let (menu_autostart, menu_snap) = build_tray(app, &cfg, dark)?;
+
+ let state = Mutex::new(Shared {
+ data_dir,
+ cfg,
+ docked: false,
+ dock_visible: false,
+ fs_hidden: false,
+ dragging: false,
+ hover_since: None,
+ last_main_move: None,
+ last_move_eval: None,
+ edge_armed: true,
+ last_main_rect: None,
+ mouse_outside_since: None,
+ mouse_inside_since_restore: false,
+ ignore_until: None,
+ theme_dark: dark,
+ menu_autostart: Some(menu_autostart),
+ menu_snap: Some(menu_snap),
+ docked_rect: None,
+ });
+ app.manage(state);
+
+ let handle = app.handle().clone();
+ std::thread::spawn(move || checker_loop(handle));
+
+ if !silent {
+ let _ = main.show();
+ let _ = main.set_focus();
+ }
+ Ok(())
+}
+
+fn build_tray(
+ app: &tauri::App,
+ cfg: &ShellConfig,
+ dark: bool,
+) -> Result<(CheckItem, CheckItem), Box> {
+ use tauri_plugin_autostart::ManagerExt;
+ let autostart_on = app.autolaunch().is_enabled().unwrap_or(false); // W-AUT-03 真实状态
+
+ let show = MenuItem::with_id(app, "show", "显示主窗口", true, None::<&str>)?;
+ let autostart = CheckMenuItem::with_id(app, "autostart", "开机启动", true, autostart_on, None::<&str>)?;
+ let snap = CheckMenuItem::with_id(app, "snap", "边缘吸附", true, cfg.snap_enabled, None::<&str>)?;
+ let quit = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
+ let menu = Menu::with_items(app, &[&show, &autostart, &snap, &quit])?;
+
+ TrayIconBuilder::with_id(TRAY_ID)
+ .menu(&menu)
+ .show_menu_on_left_click(false) // 左键留给唤回 (W-TRY-03), 右键出菜单 (W-TRY-05)
+ .icon(tray_icon_image(dark)?)
+ .tooltip("iBoard")
+ .on_menu_event(on_menu_event)
+ .on_tray_icon_event(on_tray_event)
+ .build(app)?;
+ Ok((autostart, snap))
+}
+
+fn checker_loop(app: AppHandle) {
+ let mut tick: u64 = 0;
+ loop {
+ std::thread::sleep(Duration::from_millis(80));
+ tick += 1;
+ let now = Instant::now();
+ let action = {
+ let Some(st) = state(&app) else { continue };
+ let mut s = st.lock().expect("shell state poisoned");
+ decide(&mut s, now)
+ };
+ match action {
+ Action::Restore => restore_main(&app),
+ Action::Undock => undock(&app, None),
+ Action::None => {}
+ }
+ autohide_check(&app);
+ if tick % 6 == 0 {
+ fullscreen_check(&app); // ~480ms
+ }
+ if tick % 12 == 0 {
+ theme_check(&app); // ~1s
+ }
+ }
+}
+
+enum Action {
+ None,
+ Restore,
+ Undock,
+}
+
+fn decide(s: &mut Shared, now: Instant) -> Action {
+ // 1) 移动静默判定仅用于「解除吸附」(W-SNP-05/06); 收起改为触碰即收起
+ // (见 record_main_move, 修订决策 #1)
+ if let Some(t) = s.last_main_move {
+ if now.duration_since(t) >= MOVE_QUIET && s.ignore_until.map_or(true, |u| now >= u) {
+ s.last_main_move = None;
+ if s.cfg.snap_enabled {
+ if let Some((_edge, dist)) = s.last_move_eval {
+ if s.docked && !s.dock_visible && s.edge_armed && dist >= UNDOCK_PX {
+ return Action::Undock; // 拖离 → 解除吸附 (W-SNP-05)
+ }
+ }
+ }
+ }
+ }
+ // 2) 悬浮球碰触延时恢复 (W-SNP-04)
+ if s.dock_visible && !s.dragging {
+ if let Some(t0) = s.hover_since {
+ if now.duration_since(t0) >= RESTORE_DELAY {
+ s.hover_since = None;
+ return Action::Restore;
+ }
+ }
+ }
+ Action::None
+}
+
+// ============================================================================
+// 事件入口 (lib.rs 转发)
+// ============================================================================
+
+pub fn on_window_event(window: &tauri::Window, event: &tauri::WindowEvent) {
+ if window.label() != MAIN {
+ return;
+ }
+ match event {
+ WindowEvent::CloseRequested { api, .. } => {
+ // W-TRY-02: 关闭到托盘, 进程不退出
+ api.prevent_close();
+ save_window_cfg(window.app_handle());
+ let _ = window.hide();
+ }
+ WindowEvent::Resized(size) => ratio_lock(window, *size),
+ WindowEvent::Moved(pos) => record_main_move(window, *pos),
+ _ => {}
+ }
+}
+
+use tauri::WindowEvent;
+
+fn record_main_move(window: &tauri::Window, pos: PhysicalPosition) {
+ let app = window.app_handle();
+ let Some(st) = state(app) else { return };
+ // 程序性移动 (恢复/解除吸附的 set_position) 期间不记录,
+ // 否则抑制期一过会把"恢复到贴边位置"误判成用户再次贴边 (W-SNP-04)
+ if st
+ .lock()
+ .expect("shell state poisoned")
+ .ignore_until
+ .map_or(false, |u| Instant::now() < u)
+ {
+ return;
+ }
+ let Some(wa) = window
+ .hwnd()
+ .ok()
+ .map(|h| win::workarea_for_hwnd(h.0))
+ .flatten()
+ else {
+ return;
+ };
+ let Ok(size) = window.outer_size() else { return };
+ let rect = rect_of(pos, size);
+ let (near_edge, dist) = nearest_edge(&rect, &wa);
+ let prev_rect = st.lock().expect("shell state poisoned").last_main_rect;
+ let touching = edge_touching(prev_rect.as_ref(), &rect, &wa);
+
+ let do_collapse = {
+ let mut s = st.lock().expect("shell state poisoned");
+ // 触碰即收起 (修订决策 #1): 拖动中窗口边缘一进入边缘条带就收起,
+ // 不做静默等待 —— 天然免疫 Win11 Aero Snap 抢跑与不可见边框过冲。
+ // 「入带 + 越线」双条件: 停泊在边缘以外 (如压住任务栏区) 的窗口不受影响。
+ match touching {
+ Some(edge) => {
+ let go = s.cfg.snap_enabled && !s.dock_visible && s.edge_armed;
+ if go {
+ s.edge_armed = false;
+ s.last_main_move = None;
+ s.last_main_rect = None;
+ }
+ go.then_some(edge)
+ }
+ None => {
+ s.edge_armed = true; // 离开边缘条带 → 重新武装
+ s.last_main_move = Some(Instant::now());
+ s.last_move_eval = Some((near_edge, dist));
+ s.last_main_rect = Some(rect);
+ None
+ }
+ }
+ };
+ if let Some(edge) = do_collapse {
+ collapse(app, edge);
+ }
+}
+
+/// 9:16 比例锁 (W-WIN-02/03): 宽度为驱动, 高度 = 宽 × 16/9, 上限 = 可工作区高 × 95%
+fn ratio_lock(window: &tauri::Window, actual: PhysicalSize) {
+ if window.is_minimized().unwrap_or(true) || actual.width == 0 || actual.height == 0 {
+ return;
+ }
+ let Ok(scale) = window.scale_factor() else { return };
+ let wa_h: i32 = window
+ .hwnd()
+ .ok()
+ .and_then(|h| win::workarea_for_hwnd(h.0))
+ .map(|wa| wa.height())
+ .or_else(|| {
+ window
+ .current_monitor()
+ .ok()
+ .flatten()
+ .map(|m| m.size().height as i32)
+ })
+ .unwrap_or(1080);
+ let max_h = (wa_h as f64 * 0.95) as u32;
+ let min_w = (216.0 * scale).round() as u32;
+ let mut w = actual.width;
+ let mut h = w * 16 / 9;
+ if h > max_h {
+ h = max_h;
+ w = h * 9 / 16;
+ }
+ if w < min_w {
+ w = min_w;
+ h = w * 16 / 9;
+ if h > max_h {
+ h = max_h;
+ w = h * 9 / 16;
+ }
+ }
+ let tol = (RATIO_TOL * scale).ceil() as i64;
+ if (actual.width as i64 - w as i64).abs() > tol || (actual.height as i64 - h as i64).abs() > tol {
+ let _ = window.set_size(PhysicalSize::new(w, h));
+ }
+}
+
+// ============================================================================
+// 吸附 / 恢复 / 解除
+// ============================================================================
+
+/// 收起 (W-SNP-01/02): 主窗口贴边隐藏, 悬浮球出现在边缘。
+/// edge 由 record_main_move 的触碰判定给出, 不在此重判。
+fn collapse(app: &AppHandle, edge: Edge) {
+ let Some(main) = app.get_webview_window(MAIN) else { return };
+ let Ok(hwnd) = main.hwnd() else { return };
+ let Some(wa) = win::workarea_for_hwnd(hwnd.0) else { return };
+ let Ok(scale) = main.scale_factor() else { return };
+ let (Ok(pos), Ok(size)) = (main.outer_position(), main.outer_size()) else { return };
+ let rect = rect_of(pos, size);
+ let docked_pos = flush_pos(&rect, edge, &wa);
+ let offset = clamp_offset(
+ along_center(&rect, edge),
+ edge,
+ &wa,
+ HANDLE_LONG,
+ scale,
+ );
+ {
+ let Some(st) = state(app) else { return };
+ let mut s = st.lock().expect("shell state poisoned");
+ s.docked = true;
+ s.mouse_outside_since = None;
+ s.cfg.dock = DockCfg { edge, offset };
+ s.docked_rect = Some((docked_pos, size));
+ let cfg = s.cfg.clone();
+ let dir = s.data_dir.clone();
+ drop(s);
+ cfg.save(&dir);
+ }
+ let _ = main.set_position(PhysicalPosition::new(docked_pos.x, docked_pos.y));
+ let _ = main.hide();
+ place_handle(app, edge, offset, &wa, scale);
+}
+
+/// 悬浮球定位 + 显示 (W-SNP-02/03)
+fn place_handle(app: &AppHandle, edge: Edge, offset: i32, wa: &win::WorkArea, scale: f64) {
+ let Some(dock) = app.get_webview_window(DOCK) else { return };
+ let (lw, lh) = match edge {
+ Edge::Left | Edge::Right => (HANDLE_SHORT, HANDLE_LONG),
+ Edge::Top | Edge::Bottom => (HANDLE_LONG, HANDLE_SHORT),
+ };
+ let w = (lw * scale).round() as i32;
+ let h = (lh * scale).round() as i32;
+ let (x, y) = match edge {
+ Edge::Left => (wa.left, wa.clamp_y(offset - h / 2, h)),
+ Edge::Right => (wa.right - w, wa.clamp_y(offset - h / 2, h)),
+ Edge::Top => (wa.clamp_x(offset - w / 2, w), wa.top),
+ Edge::Bottom => (wa.clamp_x(offset - w / 2, w), wa.bottom - h),
+ };
+ let dark = state(app)
+ .map(|s| s.lock().expect("shell state poisoned").theme_dark)
+ .unwrap_or(false);
+ let _ = dock.set_size(PhysicalSize::new(w as u32, h as u32));
+ let _ = dock.set_position(PhysicalPosition::new(x, y));
+ let _ = dock.eval(&format!(
+ "window.__SCALE = {scale}; window.applyDock('{}', '{}')",
+ edge.css(),
+ if dark { "dark" } else { "light" }
+ ));
+ let _ = dock.show();
+ if let Some(st) = state(app) {
+ st.lock().expect("shell state poisoned").dock_visible = true;
+ }
+}
+
+/// 唤回主窗口 (W-SNP-04/05, W-EX-01): 恢复到吸附位置, 悬浮球消失
+fn restore_main(app: &AppHandle) {
+ let Some(st) = state(app) else { return };
+ let docked_rect = {
+ let mut s = st.lock().expect("shell state poisoned");
+ if !s.docked {
+ return;
+ }
+ s.dock_visible = false;
+ s.hover_since = None;
+ s.edge_armed = false; // 恢复位置贴边, 需先拖离才允许再次触碰收起
+ s.last_main_rect = None;
+ s.mouse_outside_since = None;
+ // auto-hide 武装: 恢复时光标是否已在窗口区域内
+ // (悬浮球唤回 = 在; 托盘/单实例唤回 = 光标在远处, 不武装, 否则立刻缩回)
+ s.mouse_inside_since_restore = win::cursor_pos().map_or(false, |(cx, cy)| {
+ s.docked_rect.map_or(false, |(p, sz)| {
+ cx >= p.x
+ && cx <= p.x + sz.width as i32
+ && cy >= p.y
+ && cy <= p.y + sz.height as i32
+ })
+ });
+ s.ignore_until = Some(Instant::now() + MOVE_SUPPRESS);
+ s.docked_rect
+ };
+ let Some((pos, _size)) = docked_rect else { return };
+ let Some(main) = app.get_webview_window(MAIN) else { return };
+ let Some(dock) = app.get_webview_window(DOCK) else { return };
+ let _ = dock.hide();
+ if let Ok(hwnd) = main.hwnd() {
+ if let Some(wa) = win::workarea_for_hwnd(hwnd.0) {
+ if let Ok(size) = main.outer_size() {
+ let x = wa.clamp_x(pos.x, size.width as i32);
+ let y = wa.clamp_y(pos.y, size.height as i32);
+ let _ = main.set_position(PhysicalPosition::new(x, y));
+ }
+ }
+ }
+ let _ = main.unminimize();
+ let _ = main.show();
+ let _ = main.set_focus();
+}
+
+/// 解除吸附 (W-SNP-05/14): drop = 拖出时的释放点 (悬浮球中心, 物理像素)
+fn undock(app: &AppHandle, drop: Option<(i32, i32)>) {
+ let Some(st) = state(app) else { return };
+ {
+ let mut s = st.lock().expect("shell state poisoned");
+ s.docked = false;
+ s.dock_visible = false;
+ s.hover_since = None;
+ s.edge_armed = true; // 自由窗口, 触碰边缘可再次收起
+ s.last_main_rect = None;
+ s.ignore_until = Some(Instant::now() + MOVE_SUPPRESS);
+ }
+ if let Some(dock) = app.get_webview_window(DOCK) {
+ let _ = dock.hide();
+ }
+ let Some(main) = app.get_webview_window(MAIN) else { return };
+ if let Some((cx, cy)) = drop {
+ // 主窗口以释放点为中心, 完整夹取进可工作区 (W-SNP-14)
+ let Ok(size) = main.outer_size() else { return };
+ if let Ok(hwnd) = main.hwnd() {
+ if let Some(wa) = win::workarea_for_hwnd(hwnd.0) {
+ let x = wa.clamp_x(cx - size.width as i32 / 2, size.width as i32);
+ let y = wa.clamp_y(cy - size.height as i32 / 2, size.height as i32);
+ let _ = main.set_position(PhysicalPosition::new(x, y));
+ }
+ }
+ let _ = main.show();
+ let _ = main.set_focus();
+ }
+}
+
+// ============================================================================
+// 托盘 / 菜单 / 退出
+// ============================================================================
+
+fn on_menu_event(app: &AppHandle, event: MenuEvent) {
+ match event.id().as_ref() {
+ "show" => activate_primary(app),
+ "quit" => quit(app),
+ "autostart" => toggle_autostart(app),
+ "snap" => toggle_snap(app),
+ _ => {}
+ }
+}
+
+fn on_tray_event(tray: &tauri::tray::TrayIcon, event: TrayIconEvent) {
+ match event {
+ // 单击与双击均唤回主窗口, 幂等 (W-TRY-03/04)
+ TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } => {
+ activate_primary(tray.app_handle());
+ }
+ TrayIconEvent::DoubleClick { button: MouseButton::Left, .. } => {
+ activate_primary(tray.app_handle());
+ }
+ _ => {}
+ }
+}
+
+/// 唤回入口统一处理: 收起态 → 恢复到吸附位置 (W-EX-01); 否则前置主窗口
+pub fn activate_primary(app: &AppHandle) {
+ let docked = state(app)
+ .map(|s| s.lock().expect("shell state poisoned").docked)
+ .unwrap_or(false);
+ if docked {
+ restore_main(app);
+ return;
+ }
+ if let Some(main) = app.get_webview_window(MAIN) {
+ let _ = main.unminimize();
+ let _ = main.show();
+ let _ = main.set_focus();
+ }
+}
+
+fn toggle_autostart(app: &AppHandle) {
+ use tauri_plugin_autostart::ManagerExt;
+ let on = app.autolaunch().is_enabled().unwrap_or(false);
+ let result = if on {
+ app.autolaunch().disable()
+ } else {
+ app.autolaunch().enable()
+ };
+ // 写入失败时如实回显 (W-EX-06), 不静默假成功
+ let new_on = if on { false } else { result.is_ok() };
+ if let Some(st) = state(app) {
+ let s = st.lock().expect("shell state poisoned");
+ if let Some(item) = &s.menu_autostart {
+ let _ = item.set_checked(new_on);
+ }
+ }
+}
+
+fn toggle_snap(app: &AppHandle) {
+ let Some(st) = state(app) else { return };
+ let (on, cfg, dir) = {
+ let mut s = st.lock().expect("shell state poisoned");
+ s.cfg.snap_enabled = !s.cfg.snap_enabled;
+ if let Some(item) = &s.menu_snap {
+ let _ = item.set_checked(s.cfg.snap_enabled);
+ }
+ (s.cfg.snap_enabled, s.cfg.clone(), s.data_dir.clone())
+ };
+ let _ = on;
+ cfg.save(&dir);
+}
+
+/// 唯一正常退出入口 (W-TRY-06 / W-EX-02): 先收 UI, 再保存配置, 最后退出
+pub fn quit(app: &AppHandle) {
+ if let Some(dock) = app.get_webview_window(DOCK) {
+ let _ = dock.hide();
+ }
+ if let Some(main) = app.get_webview_window(MAIN) {
+ save_window_cfg(app);
+ let _ = main.hide();
+ }
+ if let Some(st) = state(app) {
+ let s = st.lock().expect("shell state poisoned");
+ s.cfg.save(&s.data_dir);
+ }
+ let _ = app.remove_tray_by_id(TRAY_ID);
+ app.exit(0);
+}
+
+// ============================================================================
+// 悬浮球 IPC (dock.html → Rust)
+// ============================================================================
+
+pub fn dock_hover_start(app: &AppHandle) {
+ if let Some(st) = state(app) {
+ st.lock().expect("shell state poisoned").hover_since = Some(Instant::now());
+ }
+}
+
+pub fn dock_hover_end(app: &AppHandle) {
+ if let Some(st) = state(app) {
+ st.lock().expect("shell state poisoned").hover_since = None;
+ }
+}
+
+/// 单击/触屏点按 → 立即恢复 (W-SNP-05)
+pub fn dock_click(app: &AppHandle) {
+ let docked = state(app)
+ .map(|s| s.lock().expect("shell state poisoned").docked)
+ .unwrap_or(false);
+ if docked {
+ restore_main(app);
+ }
+}
+
+pub fn dock_drag_start(app: &AppHandle) {
+ if let Some(st) = state(app) {
+ let mut s = st.lock().expect("shell state poisoned");
+ s.dragging = true;
+ s.hover_since = None; // 拖动取消未决的 hover 恢复
+ }
+}
+
+pub fn dock_drag_move(app: &AppHandle, dx: i32, dy: i32) {
+ let Some(dock) = app.get_webview_window(DOCK) else { return };
+ if let Ok(pos) = dock.outer_position() {
+ let _ = dock.set_position(PhysicalPosition::new(pos.x + dx, pos.y + dy));
+ }
+}
+
+/// 拖动结束: ≤8px 停靠该边 (含换边 W-SNP-13); 8~32px 吸附到最近边; ≥32px 拖出恢复 (W-SNP-14)
+pub fn dock_drag_end(app: &AppHandle) {
+ {
+ let Some(st) = state(app) else { return };
+ st.lock().expect("shell state poisoned").dragging = false;
+ }
+ let Some(dock) = app.get_webview_window(DOCK) else { return };
+ let (Ok(pos), Ok(size)) = (dock.outer_position(), dock.outer_size()) else { return };
+ let Ok(hwnd) = dock.hwnd() else { return };
+ let Some(wa) = win::workarea_for_hwnd(hwnd.0) else { return };
+ let Ok(scale) = dock.scale_factor() else { return };
+ let rect = rect_of(pos, size);
+ let (edge, dist) = nearest_edge(&rect, &wa);
+ if dist >= UNDOCK_PX {
+ let center = (rect.left + rect.width() / 2, rect.top + rect.height() / 2);
+ undock(app, Some(center));
+ return;
+ }
+ // 停靠 (含 8~32px 死区: 吸附到最近边, 悬浮球不悬空)
+ let offset = clamp_offset(along_center(&rect, edge), edge, &wa, HANDLE_LONG, scale);
+ {
+ let Some(st) = state(app) else { return };
+ let mut s = st.lock().expect("shell state poisoned");
+ s.docked = true;
+ s.mouse_outside_since = None;
+ s.cfg.dock = DockCfg { edge, offset };
+ // 主窗口恢复位置改为新边的贴边矩形 (W-SNP-13: 下次恢复出现在新边缘)
+ let main_size = s
+ .docked_rect
+ .map(|(_, sz)| sz)
+ .or_else(|| app.get_webview_window(MAIN).and_then(|m| m.outer_size().ok()));
+ if let Some(msize) = main_size {
+ let (x, y) = main_flush_from_offset(edge, offset, msize, &wa);
+ s.docked_rect = Some((PhysicalPosition::new(x, y), msize));
+ }
+ let cfg = s.cfg.clone();
+ let dir = s.data_dir.clone();
+ drop(s);
+ cfg.save(&dir);
+ }
+ place_handle(app, edge, offset, &wa, scale);
+}
+
+// ============================================================================
+// 全屏避让 / 主题跟随
+// ============================================================================
+
+/// 独占全屏 → 隐藏悬浮球; 退出 → 原位恢复 (W-SNP-16)。无边框全屏不受影响 (W-SNP-15)。
+fn fullscreen_check(app: &AppHandle) {
+ let exclusive = win::is_exclusive_fullscreen();
+ let Some(st) = state(app) else { return };
+ let (transition, was_visible) = {
+ let mut s = st.lock().expect("shell state poisoned");
+ let mut t = None;
+ if exclusive && s.dock_visible && !s.fs_hidden {
+ s.fs_hidden = true;
+ t = Some(false);
+ } else if !exclusive && s.fs_hidden {
+ s.fs_hidden = false;
+ t = Some(true);
+ }
+ (t, s.dock_visible)
+ };
+ match transition {
+ Some(false) => {
+ if let Some(d) = app.get_webview_window(DOCK) {
+ let _ = d.hide();
+ }
+ }
+ Some(true) => {
+ if was_visible {
+ if let Some(d) = app.get_webview_window(DOCK) {
+ let _ = d.show();
+ }
+ }
+ }
+ None => {}
+ }
+}
+
+/// 吸附态已展开 (悬浮球唤回) 时, 光标移开窗口 → 自动收起 (auto-hide 停靠交互)。
+/// 仅当光标在展开后进过窗口才武装 —— 托盘/单实例唤回时光标在远处, 不缩回。
+fn autohide_check(app: &AppHandle) {
+ let now = Instant::now();
+ let armed = {
+ let Some(st) = state(app) else { return };
+ let s = st.lock().expect("shell state poisoned");
+ if !s.docked || s.dock_visible || s.dragging {
+ return;
+ }
+ if s.ignore_until.map_or(false, |u| now < u) {
+ return;
+ }
+ // 窗口正在移动 (拖拽中) 时不判定, 避免拖拽途中误收
+ if s.last_main_move.map_or(false, |t| now.duration_since(t) < MOVE_QUIET * 2) {
+ return;
+ }
+ s.mouse_inside_since_restore
+ };
+ if !armed {
+ return;
+ }
+ let Some(main) = app.get_webview_window(MAIN) else { return };
+ let (Ok(pos), Ok(size)) = (main.outer_position(), main.outer_size()) else { return };
+ let Some((cx, cy)) = win::cursor_pos() else { return };
+ let inside = cx >= pos.x
+ && cx <= pos.x + size.width as i32
+ && cy >= pos.y
+ && cy <= pos.y + size.height as i32;
+ let outside_since = {
+ let Some(st) = state(app) else { return };
+ let mut s = st.lock().expect("shell state poisoned");
+ if inside {
+ s.mouse_outside_since = None;
+ None
+ } else {
+ Some(*s.mouse_outside_since.get_or_insert(now))
+ }
+ };
+ if let Some(t) = outside_since {
+ if now.duration_since(t) >= AUTOHIDE_DELAY {
+ let edge = {
+ let Some(st) = state(app) else { return };
+ st.lock().expect("shell state poisoned").cfg.dock.edge
+ };
+ collapse(app, edge);
+ }
+ }
+}
+
+/// 深浅色跟随系统 (W-THE): 托盘图标 + 悬浮球外观
+fn theme_check(app: &AppHandle) {
+ let dark = win::system_prefers_dark();
+ let changed = {
+ let Some(st) = state(app) else { return };
+ let mut s = st.lock().expect("shell state poisoned");
+ if s.theme_dark == dark {
+ false
+ } else {
+ s.theme_dark = dark;
+ true
+ }
+ };
+ if !changed {
+ return;
+ }
+ if let Ok(icon) = tray_icon_image(dark) {
+ if let Some(tray) = app.tray_by_id(TRAY_ID) {
+ let _ = tray.set_icon(Some(icon));
+ }
+ }
+ if let Some(d) = app.get_webview_window(DOCK) {
+ let _ = d.eval(&format!("window.applyTheme('{}')", if dark { "dark" } else { "light" }));
+ }
+}
+
+// ============================================================================
+// 配置持久化
+// ============================================================================
+
+/// 启动时应用记忆的窗口尺寸/位置 (W-WIN-07)
+/// 尺寸只取宽度, 高度按 405:720 锁定比例推导 (见 config::height_for_width)
+fn apply_saved_window(main: &tauri::WebviewWindow, cfg: &ShellConfig) {
+ let Ok(scale) = main.scale_factor() else { return };
+ let w = cfg.window.width.clamp(config::WIDTH_MIN, config::WIDTH_MAX);
+ let h = config::height_for_width(w);
+ let _ = main.set_size(tauri::LogicalSize::new(w, h));
+ if let (Some(x), Some(y)) = (cfg.window.x, cfg.window.y) {
+ let pw = (w * scale).round() as i32;
+ let ph = (h * scale).round() as i32;
+ if let Some(wa) = win::workarea_for_point(x + pw / 2, y + ph / 2) {
+ let nx = wa.clamp_x(x, pw);
+ let ny = wa.clamp_y(y, ph);
+ let _ = main.set_position(PhysicalPosition::new(nx, ny));
+ }
+ // 无效位置 → 保持配置默认居中 (W-WIN-07)
+ }
+}
+
+/// 关闭/退出时保存窗口尺寸与位置 (W-WIN-07)
+fn save_window_cfg(app: &AppHandle) {
+ let Some(main) = app.get_webview_window(MAIN) else { return };
+ let Some(st) = state(app) else { return };
+ let Ok(scale) = main.scale_factor() else { return };
+ let (Ok(size), Ok(pos)) = (main.outer_size(), main.outer_position()) else { return };
+ let cfg = {
+ let mut s = st.lock().expect("shell state poisoned");
+ // 只记宽度; 高度按锁定比例推导, 保证下次启动比例不变
+ s.cfg.window.width = size.width as f64 / scale;
+ s.cfg.window.normalize();
+ s.cfg.window_width = Some(s.cfg.window.width);
+ s.cfg.window.x = Some(pos.x);
+ s.cfg.window.y = Some(pos.y);
+ s.cfg.clone()
+ };
+ let dir = st.lock().expect("shell state poisoned").data_dir.clone();
+ cfg.save(&dir);
+}
+
+// ============================================================================
+// 几何辅助
+// ============================================================================
+
+#[derive(Clone, Copy)]
+struct Rect {
+ left: i32,
+ top: i32,
+ right: i32,
+ bottom: i32,
+}
+
+impl Rect {
+ fn width(&self) -> i32 {
+ self.right - self.left
+ }
+ fn height(&self) -> i32 {
+ self.bottom - self.top
+ }
+}
+
+fn rect_of(pos: PhysicalPosition, size: PhysicalSize) -> Rect {
+ Rect {
+ left: pos.x,
+ top: pos.y,
+ right: pos.x + size.width as i32,
+ bottom: pos.y + size.height as i32,
+ }
+}
+
+/// 最近边与距离 (窗口边缘 ↔ 可工作区边缘)
+fn nearest_edge(rect: &Rect, wa: &win::WorkArea) -> (Edge, f64) {
+ let candidates = [
+ (Edge::Left, (rect.left - wa.left).abs()),
+ (Edge::Right, (wa.right - rect.right).abs()),
+ (Edge::Top, (rect.top - wa.top).abs()),
+ (Edge::Bottom, (wa.bottom - rect.bottom).abs()),
+ ];
+ let (edge, dist) = candidates
+ .into_iter()
+ .min_by_key(|(_, d)| *d)
+ .unwrap_or((Edge::Left, i32::MAX));
+ (edge, dist as f64)
+}
+
+/// 触碰判定 (W-SNP-01, 修订决策 #1):
+/// ① 入带 —— 当前窗口边缘落在边缘条带 [edge, edge+SNAP] 内;
+/// ② 越线 —— 上一样本在带外, 当前样本已到线或越过线 (快速拖动跨过条带的采样间隙)。
+/// 对不可见边框 (视觉贴边时 rect 已越界 7~9px) 与过冲免疫;
+/// 停泊在边缘以外 (如窗口压住任务栏区, rect.bottom > wa.bottom) 不算触碰。
+fn edge_touching(prev: Option<&Rect>, cur: &Rect, wa: &win::WorkArea) -> Option {
+ let s = SNAP_PX as i32;
+ let in_band = |a: i32, lo: i32, hi: i32| a >= lo && a <= hi;
+ let crossed = |p: i32, c: i32, line: i32, outward: bool| {
+ if outward {
+ // Bottom: 带在 [line-8, line], "带外"指 p < line-8
+ p < line - s && c >= line - s
+ } else {
+ // Left/Right/Top: 带在 [line, line+8], "带外"指 p > line+8
+ p > line + s && c <= line + s
+ }
+ };
+ let hit =
+ |p: Option, c: i32, lo: i32, hi: i32, line: i32, outward: bool| match p {
+ Some(p) => in_band(c, lo, hi) || crossed(p, c, line, outward),
+ None => in_band(c, lo, hi),
+ };
+ if hit(prev.map(|r| r.left), cur.left, wa.left, wa.left + s, wa.left, false) {
+ Some(Edge::Left)
+ } else if hit(prev.map(|r| r.right), cur.right, wa.right - s, wa.right, wa.right, true) {
+ Some(Edge::Right)
+ } else if hit(prev.map(|r| r.top), cur.top, wa.top, wa.top + s, wa.top, false) {
+ Some(Edge::Top)
+ } else if hit(prev.map(|r| r.bottom), cur.bottom, wa.bottom - s, wa.bottom, wa.bottom, true) {
+ Some(Edge::Bottom)
+ } else {
+ None
+ }
+}
+
+/// 贴边位置: 保持沿边坐标, 垂直于边的坐标贴到边缘
+fn flush_pos(rect: &Rect, edge: Edge, wa: &win::WorkArea) -> PhysicalPosition {
+ match edge {
+ Edge::Left => PhysicalPosition::new(wa.left, wa.clamp_y(rect.top, rect.height())),
+ Edge::Right => PhysicalPosition::new(wa.right - rect.width(), wa.clamp_y(rect.top, rect.height())),
+ Edge::Top => PhysicalPosition::new(wa.clamp_x(rect.left, rect.width()), wa.top),
+ Edge::Bottom => PhysicalPosition::new(wa.clamp_x(rect.left, rect.width()), wa.bottom - rect.height()),
+ }
+}
+
+/// 主窗口在给定边/沿边偏移下的贴边位置
+fn main_flush_from_offset(
+ edge: Edge,
+ offset: i32,
+ size: PhysicalSize,
+ wa: &win::WorkArea,
+) -> (i32, i32) {
+ let w = size.width as i32;
+ let h = size.height as i32;
+ match edge {
+ Edge::Left => (wa.left, wa.clamp_y(offset - h / 2, h)),
+ Edge::Right => (wa.right - w, wa.clamp_y(offset - h / 2, h)),
+ Edge::Top => (wa.clamp_x(offset - w / 2, w), wa.top),
+ Edge::Bottom => (wa.clamp_x(offset - w / 2, w), wa.bottom - h),
+ }
+}
+
+/// 沿边中心 (Left/Right → 垂直中心; Top/Bottom → 水平中心)
+fn along_center(rect: &Rect, edge: Edge) -> i32 {
+ match edge {
+ Edge::Left | Edge::Right => rect.top + rect.height() / 2,
+ Edge::Top | Edge::Bottom => rect.left + rect.width() / 2,
+ }
+}
+
+/// 悬浮球沿边位置夹取 (完整可见, W-SNP-02)
+fn clamp_offset(offset: i32, edge: Edge, wa: &win::WorkArea, handle_long: f64, scale: f64) -> i32 {
+ let half = (handle_long * scale / 2.0).round() as i32;
+ match edge {
+ Edge::Left | Edge::Right => offset.clamp(wa.top + half, wa.bottom - half),
+ Edge::Top | Edge::Bottom => offset.clamp(wa.left + half, wa.right - half),
+ }
+}
+
+fn tray_icon_image(dark: bool) -> tauri::Result> {
+ if dark {
+ tauri::image::Image::from_bytes(include_bytes!("../../icons/tray-dark.png"))
+ } else {
+ tauri::image::Image::from_bytes(include_bytes!("../../icons/tray-light.png"))
+ }
+}
diff --git a/src-tauri/src/shell/win.rs b/src-tauri/src/shell/win.rs
new file mode 100644
index 0000000..1bc1506
--- /dev/null
+++ b/src-tauri/src/shell/win.rs
@@ -0,0 +1,117 @@
+//! Win32 辅助: 可工作区 / 独占全屏检测 / 系统深浅色 (W-SNP、W-THE)
+#![cfg(windows)]
+
+use windows_sys::Win32::{
+ Foundation::{HWND, POINT, RECT},
+ Graphics::Gdi::{
+ GetMonitorInfoW, MonitorFromPoint, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTONEAREST,
+ },
+ System::Registry::{RegGetValueW, HKEY_CURRENT_USER, RRF_RT_REG_DWORD},
+ UI::Shell::{SHQueryUserNotificationState, QUNS_RUNNING_D3D_FULL_SCREEN},
+ UI::WindowsAndMessaging::GetCursorPos,
+};
+
+/// 当前光标位置 (物理像素)
+pub fn cursor_pos() -> Option<(i32, i32)> {
+ let mut pt = POINT { x: 0, y: 0 };
+ unsafe {
+ if GetCursorPos(&mut pt) == 0 {
+ return None;
+ }
+ }
+ Some((pt.x, pt.y))
+}
+
+/// 显示器可工作区 (物理像素, 已扣除任务栏, W-EX-05)
+#[derive(Debug, Clone, Copy)]
+pub struct WorkArea {
+ pub left: i32,
+ pub top: i32,
+ pub right: i32,
+ pub bottom: i32,
+}
+
+impl WorkArea {
+ pub fn height(&self) -> i32 {
+ self.bottom - self.top
+ }
+ pub fn clamp_x(&self, x: i32, w: i32) -> i32 {
+ x.clamp(self.left, (self.right - w).max(self.left))
+ }
+ pub fn clamp_y(&self, y: i32, h: i32) -> i32 {
+ y.clamp(self.top, (self.bottom - h).max(self.top))
+ }
+}
+
+fn utf16(s: &str) -> Vec {
+ s.encode_utf16().chain(std::iter::once(0)).collect()
+}
+
+/// 窗口所在显示器的可工作区 (hwnd: tauri `hwnd().0`, 即裸指针)
+pub fn workarea_for_hwnd(hwnd: *mut std::ffi::c_void) -> Option {
+ unsafe {
+ let hmon = MonitorFromWindow(hwnd as HWND, MONITOR_DEFAULTTONEAREST);
+ if hmon.is_null() {
+ return None;
+ }
+ let mut info: MONITORINFO = std::mem::zeroed();
+ info.cbSize = std::mem::size_of::() as u32;
+ if GetMonitorInfoW(hmon, &mut info) == 0 {
+ return None;
+ }
+ let RECT { left, top, right, bottom } = info.rcWork;
+ Some(WorkArea { left, top, right, bottom })
+ }
+}
+
+/// 前台是否为独占全屏 (D3D FSE, W-SNP-16)。
+/// 无边框全屏 / 全屏优化不在此列 (悬浮球保持可见, W-SNP-15)。
+pub fn is_exclusive_fullscreen() -> bool {
+ unsafe {
+ let mut state: i32 = 0;
+ if SHQueryUserNotificationState(&mut state) < 0 {
+ return false; // 判定失败 → 默认非全屏 (W-EX-11)
+ }
+ state == QUNS_RUNNING_D3D_FULL_SCREEN
+ }
+}
+
+/// 系统「应用模式」是否为深色 (W-THE-01, AppsUseLightTheme == 0)
+pub fn system_prefers_dark() -> bool {
+ unsafe {
+ let subkey = utf16("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize");
+ let value = utf16("AppsUseLightTheme");
+ let mut val: u32 = 1; // 缺省按浅色
+ let mut size = std::mem::size_of::() as u32;
+ let status = RegGetValueW(
+ HKEY_CURRENT_USER,
+ subkey.as_ptr(),
+ value.as_ptr(),
+ RRF_RT_REG_DWORD,
+ std::ptr::null_mut(),
+ (&mut val as *mut u32).cast(),
+ &mut size,
+ );
+ if status != 0 {
+ return false; // 读不到按浅色 (W-EX-11 口径)
+ }
+ val == 0
+ }
+}
+
+/// 物理坐标所在显示器的可工作区 (启动位置校验, W-WIN-07)
+pub fn workarea_for_point(x: i32, y: i32) -> Option {
+ unsafe {
+ let hmon = MonitorFromPoint(POINT { x, y }, MONITOR_DEFAULTTONEAREST);
+ if hmon.is_null() {
+ return None;
+ }
+ let mut info: MONITORINFO = std::mem::zeroed();
+ info.cbSize = std::mem::size_of::() as u32;
+ if GetMonitorInfoW(hmon, &mut info) == 0 {
+ return None;
+ }
+ let RECT { left, top, right, bottom } = info.rcWork;
+ Some(WorkArea { left, top, right, bottom })
+ }
+}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 7b82045..33d518a 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,36 +1,38 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "iBoard",
- "version": "0.2.0",
+ "version": "0.2.1",
"identifier": "com.iboard.app",
"build": {
"beforeDevCommand": "npm run dev",
- "beforeBuildCommand": "npm run build",
+ "beforeBuildCommand": "npm run build:shell",
"devUrl": "http://localhost:5173",
"frontendDist": "../dist"
},
"plugins": {
"hotswap": {
- "endpoint": "http://bh.vps.honor3.com:3001/api/ota/check/{{current_sequence}}",
+ "endpoint": "https://iboard.honor3.com/api/ota/check/{{current_sequence}}",
"pubkey": "RWQQ4/wfHjJiC/O6NmA7eIQYUNRpCsz5wLLsLqJXgHmsUraAavUWx0HA",
"require_https": false,
"headers": {}
}
},
"app": {
+ "withGlobalTauri": true,
"windows": [
{
"title": "iBoard",
- "width": 420,
- "height": 820,
- "minWidth": 360,
- "minHeight": 640,
+ "width": 323,
+ "height": 574,
+ "minWidth": 242,
+ "minHeight": 430,
"resizable": true,
+ "visible": false,
"fullscreen": false
}
],
"security": {
- "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' data: blob: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost http://bh.vps.honor3.com:3001 https://bh.vps.honor3.com:3001; style-src 'self' 'unsafe-inline'; script-src 'self'"
+ "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' data: blob: asset: http://asset.localhost; connect-src 'self' ipc: http://ipc.localhost https://iboard.honor3.com http://bh.vps.honor3.com:3001 https://bh.vps.honor3.com:3001; style-src 'self' 'unsafe-inline'; script-src 'self'"
}
},
"bundle": {
@@ -43,6 +45,13 @@
"icons/icon.icns",
"icons/icon.ico"
],
+ "windows": {
+ "nsis": {
+ "installMode": "currentUser",
+ "languages": ["SimpChinese", "English"],
+ "displayLanguageSelector": false
+ }
+ },
"android": {
"minSdkVersion": 24
}
diff --git a/src/main.jsx b/src/main.jsx
index 8f87dfd..5905f44 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -21,6 +21,16 @@ initOta()
function calibrateSafeTop() {
const root = document.documentElement
+ // 校准只对 Android 有意义 (状态栏/刘海让位)。桌面 (Windows WebView2 / 桌面浏览器)
+ // 没有状态栏: env() 本来就是 0; 而「screen.height - visualViewport.height」在桌面
+ // 竖屏窗口下会把标题栏+边框高度 (~99px) 误判成状态栏, 让标题栏下多出一条大空隙
+ // (2026-10-26 Windows 外壳实测)。非 Android 一律 0。
+ const isAndroid = /android/i.test(navigator.userAgent)
+ if (!isAndroid) {
+ root.style.setProperty('--safe-top', '0px')
+ return
+ }
+
// env() 的真实值必须用一个临时元素测量, 不能直接读 --safe-top ——
// 该变量是 max(env(...), 0px) 这样的表达式, parseFloat 会得到 NaN.
const probe = document.createElement('div')
@@ -37,7 +47,7 @@ function calibrateSafeTop() {
const viewH = window.visualViewport?.height || window.innerHeight || 0
const screenW = window.screen?.width || 0
const viewW = window.visualViewport?.width || window.innerWidth || 0
- // 只在"纵向且高度差合理"时采用, 避免横屏/桌面窗口误判
+ // 只在"纵向且高度差合理"时采用, 避免横屏误判
const isPortrait = viewW <= viewH
const diff = screenH - viewH
const measured = isPortrait && diff > 0 && diff < 120 ? diff : 0
@@ -45,7 +55,7 @@ function calibrateSafeTop() {
// 取 env() 与实测值的较大者; 两者都失效时给 24px 保底
// (多数 Android 状态栏在 24~28px, 宁可多一点也不能压字)
let top = Math.max(envTop, measured)
- if (top < 4) top = 24 // env 与实测都没拿到 → 用保守兜底, 保证不遮挡
+ if (top < 4) top = 24
root.style.setProperty('--safe-top', top + 'px')
}