76 Commits

Author SHA1 Message Date
ayangweb
d9fb19aa2f docs: 更新 QQ 群二维码 (#810) 2026-01-22 11:01:34 +08:00
ayangweb
ef17ba9ebc fix: 修复 JSON 格式错误导致模型不可用的问题 (#790) 2026-01-10 17:51:44 +08:00
ayangweb
d69669bd3a feat: 设置窗口尺寸最大值为 500 (#778) 2026-01-08 17:00:08 +08:00
ayangweb
2fc6ec9d2f fix: 修复切换模型时报 undefined 的问题 (#775) 2026-01-06 22:45:11 +08:00
ayangweb
50803eb2ce perf: 移除 Windows 下拖拽到屏幕边缘的放大效果 (#771) 2026-01-01 17:09:27 +08:00
ayangweb
57892f3ab3 feat: 新增「猫咪设置 > 窗口设置 > 窗口位置」配置项 (#764) 2026-01-01 15:44:07 +08:00
ayang
b59ec09354 v0.9.0 2025-12-24 17:53:23 +08:00
ayangweb
49b5d9954c feat(i18n): 改进英语翻译 (#762) 2025-12-24 17:52:05 +08:00
Suh
f1fac05975 feat: 新增葡萄牙语翻译 (#744)
Co-authored-by: ayang <473033518@qq.com>
2025-12-24 17:26:19 +08:00
BiggerRain
6554508c30 feat: 补全遗漏的国际化内容 (#742) 2025-12-09 14:21:18 +08:00
Thexvoilone
7e7c1aded0 feat: 新增「猫咪设置 > 窗口设置 > 鼠标移入隐藏」配置项 (#727)
Co-authored-by: ayang <473033518@qq.com>
2025-12-01 20:27:24 +08:00
ayangweb
1e24c04da2 docs: update README.md (#729) 2025-11-26 12:21:29 +08:00
ayang
392963f6fd v0.8.2 2025-11-24 21:27:23 +08:00
ayangweb
29623577ff feat: 新增「猫咪设置 > 模型设置 > 按键自动释放延迟」配置项 (#725) 2025-11-24 21:11:42 +08:00
ayangweb
a75c2d6a74 docs: 更新贡献者展示链接 (#701) 2025-11-06 18:44:00 +08:00
ayangweb
af76a58fba docs: 更新贡献者展示链接 (#695) 2025-11-05 10:24:46 +08:00
ayangweb
cd0066d2fa docs: 添加微信赞赏码 (#688) 2025-10-25 16:41:12 +08:00
ayangweb
4b717363c1 feat: 将应用归类为「娱乐」类别 (#668) 2025-10-09 09:46:40 +08:00
ayang
c486a32372 v0.8.1 2025-09-22 22:41:44 +08:00
ayangweb
ec4150d68b fix: 修复配置项在重启后丢失的问题 (#656) 2025-09-22 22:40:40 +08:00
ayangweb
3aa24f5b75 feat: 补全遗漏的国际化内容 (#655) 2025-09-22 21:08:51 +08:00
ayang
5c0de3afb6 v0.8.0 2025-09-22 09:19:27 +08:00
ayangweb
bbdb63ff6d refactor: 优化国际化后出现的一些问题 (#651) 2025-09-21 10:58:01 +08:00
CuongLM
c163f52fd0 feat: 新增英语翻译 (#649) 2025-09-18 23:11:10 +08:00
ayangweb
ff4d590f4c refactor: 完善中文翻译 (#648) 2025-09-18 21:44:30 +08:00
CuongLM
e4084aabb5 feat: 新增越南语翻译 (#647) 2025-09-18 21:31:00 +08:00
CuongLM
9abd640197 feat: 为 Windows 安装器新增英文和越南语语言选项 (#646) 2025-09-18 08:56:08 +08:00
ayangweb
54872012b1 feat: 新增「通用设置 > 外观设置 > 语言」配置项 (#645) 2025-09-17 23:33:42 +08:00
ayangweb
0eac8e489a feat: 新增「猫咪设置 > 窗口设置 > 窗口圆角」配置项 (#644) 2025-09-17 11:22:43 +08:00
Michaelwucoc
7a997a4bfd fix: 更正偏好设置窗口中的文字错误 (#624) 2025-08-18 11:25:32 +08:00
ayang
9aeee8d105 v0.7.1 2025-07-29 20:03:08 +08:00
ayangweb
4c0a30d621 docs: update README.md (#596) 2025-07-28 17:33:25 +08:00
ayangweb
c33703e368 fix: 修复版本号显示不正确的问题 (#595) 2025-07-28 16:55:10 +08:00
ayangweb
36aaf1067e fix: 修复了部分模型图片显示异常的问题 (#593) 2025-07-28 16:49:10 +08:00
ayang
ce3b9bc544 v0.7.0 2025-07-27 17:21:24 +08:00
ayangweb
aa3b09f171 ci: add libudev-dev to linux build (#590) 2025-07-27 17:20:58 +08:00
ayangweb
38adb1801e refactor: improve mouse button check (#589) 2025-07-26 21:24:46 +08:00
ayangweb
7e5fb0dbe1 chore: upgrade gilrs dependency version (#588) 2025-07-24 18:31:00 +08:00
ayangweb
52c0c714f3 feat: 支持按住 Shift 并鼠标右键拖动调整窗口大小 (#585) 2025-07-24 18:23:13 +08:00
ayangweb
ed06f39976 chore: change gilrs dependency source (#586) 2025-07-24 18:22:45 +08:00
ayangweb
07767324fb feat: 新增「模型管理 > 浮动按钮 > 转换模型」选项 (#584) 2025-07-24 10:23:05 +08:00
ayangweb
55aea542df feat: 新增「通用设置 > 外观设置 > 主题模式」配置项 (#583) 2025-07-23 21:16:07 +08:00
ayangweb
db542c3fe1 chore: change rdev dependency source (#577) 2025-07-22 17:00:23 +08:00
ayangweb
344669a85c feat: 更新 macOS 上的托盘图标 (#576) 2025-07-22 15:58:09 +08:00
ayangweb
39e4052b1b fix: 修复 Windows 按键未正确记录的问题 (#575) 2025-07-22 15:19:36 +08:00
ayangweb
61d0bd28ad refactor: 优化了获取鼠标所在显示器的逻辑 (#572) 2025-07-21 21:58:15 +08:00
ayangweb
13bf0a8376 refactor: remove support for wayland for now (#570) 2025-07-21 20:34:31 +08:00
ayangweb
93137883e6 feat: 支持游戏手柄模式 (#562) 2025-07-21 14:50:42 +08:00
伊墨
1e844d0ab9 feat: 新增「通用设置 > 应用设置 > 显示任务栏图标」配置项 (#564) 2025-07-19 21:10:30 +08:00
ayangweb
c50b197fb3 refactor: 优化部分 Rust 代码实现 (#558) 2025-07-15 14:58:19 +08:00
伊墨
8bb06f6cc8 perf: 降低鼠标移动处理的 CPU 消耗 (#458) 2025-07-15 14:39:39 +08:00
Panda527
b6ad482851 feat: 支持 Linux Wayland 环境 (#554) 2025-07-15 14:38:41 +08:00
ayangweb
7d541a486e refactor: 优化模型尺寸的处理逻辑 (#552) 2025-07-11 19:29:49 +08:00
ayangweb
496852630b fix: 修复了部分模型键位图错位的问题 (#550) 2025-07-11 18:25:06 +08:00
ayangweb
54b9b9f34f fix: 修复了部分场景下模型重叠的问题 (#551) 2025-07-11 17:19:57 +08:00
ayang
e666ae5399 v0.6.2 2025-07-11 11:45:48 +08:00
ayangweb
d56f79676e fix: 修复 macOS 26 上无法使用的问题 (#548) 2025-07-11 11:36:49 +08:00
ayangweb
b75562a6fc docs: update the homebrew installation command (#547) 2025-07-11 09:52:31 +08:00
ayangweb
c6af9827ac docs: update README.md (#516) 2025-06-19 18:20:55 +08:00
ayang
a1806e87bd v0.6.1 2025-06-19 14:30:25 +08:00
20savage
83eb9581db refactor: optimized the retrieval of the first model card element (#514) 2025-06-19 14:28:30 +08:00
ayangweb
beeecb617f fix: 修复找不到内置模型路径的问题 (#511) 2025-06-17 19:35:53 +08:00
20savage
7df7aad81a feat: 导入模型区域高度与首个模型卡片保持一致 (#508) 2025-06-17 18:39:31 +08:00
ayangweb
7f3c103ef6 fix: 修复模型删除按钮缺失的问题 (#504) 2025-06-17 10:13:31 +08:00
ayang
1dda390450 v0.6.0 2025-06-16 20:37:57 +08:00
ayangweb
0a412dc827 feat: 使用瀑布流布局展示模型列表 (#502) 2025-06-16 20:37:28 +08:00
ayangweb
d15376e9cb feat: 支持点击模型卡片切换模型 (#501) 2025-06-16 14:56:40 +08:00
ayangweb
af895ce839 refactor: temporarily allow deprecated imports to suppress warnings (#500) 2025-06-16 14:52:36 +08:00
ayangweb
fa418e0d63 chore: support UpgradeLink as the auto-update provider (#496) 2025-06-14 23:17:38 +08:00
ayangweb
368480fd3d fix: 修复了部分模型无法适配的问题 (#494) 2025-06-13 23:42:11 +08:00
Michaelwucoc
9e478eae34 docs: updated preview images for different platforms (#493) 2025-06-13 23:17:57 +08:00
Michaelwucoc
03793b0cc8 docs: 更新 QQ 群二维码 (#492) 2025-06-13 23:10:43 +08:00
ayangweb
b8ce3e4a39 fix: 修复托盘菜单状态无法实时更新的问题 (#478) 2025-06-11 23:22:25 +08:00
ayangweb
2a190efa15 feat: 将窗口尺寸滑块替换为数字输入框 (#477) 2025-06-11 22:59:08 +08:00
ypa y yhm
73bbd6a4fb docs: update “window” to “windows” (#471) 2025-06-11 13:41:38 +08:00
伊墨
3d1206cb2d chore: set up VSCode to use workspace TypeScript SDK (#456) 2025-06-07 19:36:25 +08:00
100 changed files with 2932 additions and 595 deletions

View File

@@ -24,7 +24,7 @@ brew tap ayangweb/BongoCat
2. 安装:
```bash
brew install bongo-cat
brew install --no-quarantine bongo-cat
```
3. 更新:

View File

@@ -1,4 +1,4 @@
name: Release CI
name: BongoCat Release
on:
push:
@@ -72,7 +72,7 @@ jobs:
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libudev-dev patchelf xdg-utils
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
@@ -88,7 +88,7 @@ jobs:
node-version: 20
cache: pnpm
- name: Install app dependencies and build web
- name: Install front-end dependencies
run: pnpm install --frozen-lockfile
- name: Build the app

20
.github/workflows/upgradelink.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
name: Upload Release to UpgradeLink
on:
release:
types: [published]
workflow_dispatch:
jobs:
upgradeLink-upload:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Send a request to UpgradeLink
uses: toolsetlink/upgradelink-action@v5
with:
source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json'
access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }}
github-token: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -48,5 +48,12 @@
"scss",
"pcss",
"postcss"
]
],
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "./node_modules/typescript/lib",
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.displayLanguage": "zh-CN"
}

137
Cargo.lock generated
View File

@@ -450,9 +450,10 @@ dependencies = [
[[package]]
name = "bongo-cat"
version = "0.5.0"
version = "0.9.0"
dependencies = [
"fs_extra",
"gilrs",
"rdev",
"serde",
"serde_json",
@@ -465,6 +466,7 @@ dependencies = [
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-locale",
"tauri-plugin-log",
"tauri-plugin-macos-permissions",
"tauri-plugin-opener",
@@ -971,6 +973,18 @@ dependencies = [
"syn 2.0.101",
]
[[package]]
name = "current_locale"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0be8ddcccda8be68d8e31a421ceea7c79857404daa052434ae30ce2f402cd10"
dependencies = [
"libc",
"objc",
"objc-foundation",
"winapi",
]
[[package]]
name = "darling"
version = "0.20.11"
@@ -1774,6 +1788,39 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "gilrs"
version = "0.11.0"
source = "git+https://github.com/ayangweb/gilrs#f6cecd62943ebbaf3905318e5c71130c9cc4d516"
dependencies = [
"fnv",
"gilrs-core",
"log",
"uuid",
"vec_map",
]
[[package]]
name = "gilrs-core"
version = "0.6.4"
source = "git+https://github.com/ayangweb/gilrs#f6cecd62943ebbaf3905318e5c71130c9cc4d516"
dependencies = [
"core-foundation 0.10.1",
"inotify 0.11.0",
"io-kit-sys",
"js-sys",
"libc",
"libudev-sys",
"log",
"nix 0.29.0",
"rusty-xinput",
"uuid",
"vec_map",
"wasm-bindgen",
"web-sys",
"winapi",
]
[[package]]
name = "gimli"
version = "0.31.1"
@@ -2313,6 +2360,17 @@ dependencies = [
"libc",
]
[[package]]
name = "inotify"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
dependencies = [
"bitflags 2.9.1",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify-sys"
version = "0.1.5"
@@ -2322,6 +2380,16 @@ dependencies = [
"libc",
]
[[package]]
name = "io-kit-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
dependencies = [
"core-foundation-sys",
"mach2",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -2542,6 +2610,16 @@ dependencies = [
"redox_syscall",
]
[[package]]
name = "libudev-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -2591,6 +2669,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "macos-accessibility-client"
version = "0.0.1"
@@ -2753,6 +2840,18 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.9.1",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]]
name = "nix"
version = "0.30.1"
@@ -3780,7 +3879,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
[[package]]
name = "rdev"
version = "0.5.0-2"
source = "git+https://github.com/ayangweb/rdev#a8d5724f4533a69958280aa4df6bf6bca042d064"
source = "git+https://github.com/kunkunsh/rdev#cb9a29e19668a52e4e67d8a0ca6739c1807f8d3f"
dependencies = [
"cocoa 0.24.1",
"core-foundation 0.9.4",
@@ -3789,7 +3888,7 @@ dependencies = [
"dispatch",
"enum-map",
"epoll",
"inotify",
"inotify 0.10.2",
"lazy_static",
"libc",
"log",
@@ -4086,6 +4185,17 @@ version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
[[package]]
name = "rusty-xinput"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3335c2b62e1e48dd927f6c8941705386e3697fa944aabcb10431bea7ee47ef3"
dependencies = [
"lazy_static",
"log",
"winapi",
]
[[package]]
name = "ryu"
version = "1.0.20"
@@ -4921,6 +5031,19 @@ dependencies = [
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-locale"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5686c1fe8fb748c45ba0b516db42f78dd6fde91b16bc5248de438eff8d2e6abd"
dependencies = [
"current_locale",
"serde",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-log"
version = "2.4.0"
@@ -5736,6 +5859,12 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -6685,7 +6814,7 @@ dependencies = [
"futures-core",
"futures-lite",
"hex",
"nix",
"nix 0.30.1",
"ordered-stream",
"serde",
"serde_repr",

136
README.md
View File

@@ -2,58 +2,39 @@
<div align="center">
<div>
<a href="https://github.com/ayangweb/BongoCat/releases">
<img
alt="Windows"
src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg=="
/>
</a>
<a href="https://github.com/ayangweb/BongoCat/releases">
<img
alt="MacOS"
src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white"
/>
</a>
<a href="https://github.com/ayangweb/BongoCat/releases">
<img
alt="Linux"
src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white"
/>
</a>
<a href="https://github.com/ayangweb/BongoCat/releases"><img alt="Windows" src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg==" /></a>
<a href="https://github.com/ayangweb/BongoCat/releases"><img alt="MacOS" src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white" /></a>
<a href="https://github.com/ayangweb/BongoCat/releases"><img alt="Linux" src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white" /></a>
</div>
<p>
<a href="./LICENSE">
<img
src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square"
/>
</a>
<a href="https://github.com/ayangweb/BongoCat/releases/latest">
<img
src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square"
/>
</a>
<a href="https://github.com/ayangweb/BongoCat/releases">
<img
src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square"
/>
</a>
<a href="./LICENSE"><img src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square" /></a>
<a href="https://github.com/ayangweb/BongoCat/releases/latest"><img src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square"/></a>
<a href="https://github.com/ayangweb/BongoCat/releases"><img src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square"/></a>
</p>
<p>
<a href="https://trendshift.io/developers/8507" target="_blank"><img src="https://trendshift.io/api/badge/developers/8507" alt="ayangweb | Trendshift" width="250" height="55" /></a>
<a href="https://trendshift.io/repositories/14605" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14605" alt="ayangweb%2FBongoCat | Trendshift" width="250" height="55" /></a>
<a href="https://hellogithub.com/repository/7d23863fd4be47b39e816193ded385c9" target="_blank">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
<img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
<img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" width="250" height="55" />
</picture>
</a>
</p>
</div>
| macOS | Window | Linux(x11) |
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| ![macOS](https://github.com/user-attachments/assets/e932b022-1472-4bbd-87ef-4a8ea374890a) | ![Windows](https://github.com/user-attachments/assets/79a4652e-0d14-412d-a274-4ccdd825d7c6) | ![Linux](https://github.com/user-attachments/assets/fd069c12-d12d-423b-b792-98b5926a7f09) |
| macOS | Windows | Linux(x11) |
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| ![macOS](https://i0.hdslb.com/bfs/openplatform/dff276b96d49c5d6c431b74b531aab72191b3d87.png) | ![Windows](https://i0.hdslb.com/bfs/openplatform/a4149b753856ee7f401989da902cf3b5ad35b39e.png) | ![Linux](https://i0.hdslb.com/bfs/openplatform/3b49f961819d3ff63b2b80251c1cc13c27e986b0.png) |
## 赞助商
<a href="https://www.toolsetlink.com">
<img height="54" alt="UpgradeLink" src="https://github.com/user-attachments/assets/6b84fb0f-3f1d-44b5-9932-2298bc999d8d" />
</a>
## 开发背景
@@ -66,16 +47,22 @@
- [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1)
- [GitHub Releases](https://github.com/ayangweb/BongoCat/releases)
> 不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。
不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。
## 功能介绍
- 适配 macOS、Windows 和 Linux(x11)。
- 根据键盘鼠标操作,同步移动鼠标或敲击键盘
- 根据键盘鼠标或手柄的操作,同步对应的动作
- 支持导入自定义模型,自由打造专属猫咪形象。
- 完全开源,代码公开透明,绝不收集任何用户数据。
- 支持离线运行,无需联网,保护用户隐私。
## 模型转换
如果你想将 Bongo-Cat-Mver 应用中的模型转换为兼容 BongoCat 的格式,可以使用以下工具:
🔗 [在线转换](https://bongocat.vteamer.cc)
## 更多模型
你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:
@@ -84,20 +71,73 @@
## 社区交流
<a href="https://qm.qq.com/q/AS3gNv2Vzy">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/6428b6ff-0b39-4e16-a750-bad6f6b376e9" />
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/e50883e2-0be7-4eea-9879-e725fa1dffd2" />
<img alt="QQ Group" src="https://github.com/user-attachments/assets/e50883e2-0be7-4eea-9879-e725fa1dffd2" height="250" />
</picture>
</a>
<table>
<thead>
<tr>
<th>QQ 群 1</th>
<th>QQ 群 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<a href="https://qm.qq.com/q/AS3gNv2Vzy">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://i0.hdslb.com/bfs/openplatform/8ecdc4982ab01b59d7731fcca3ec26631a274560.png"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png"
/>
<img
alt="QQ Group 1"
src="https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png"
height="250"
/>
</picture>
</a>
</td>
<td>
<a href="https://qm.qq.com/q/TmltLAod2O">
<picture>
<source
media="(prefers-color-scheme: dark)"
srcset="https://i0.hdslb.com/bfs/openplatform/473c522487ff33e0f32b15466aeb0734f17161c8.png"
/>
<source
media="(prefers-color-scheme: light)"
srcset="https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png"
/>
<img
alt="QQ Group 2"
src="https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png"
height="250"
/>
</picture>
</a>
</td>
</tr>
</tbody>
</table>
## 赞赏
每一份认可都值得被珍视!赞赏随缘,心意无价,谢谢你的支持 ❤️
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/e7438bff14cdfb6bfd0feacbb482f99ea4093294.png" />
<source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png" />
<img alt="微信赞赏码" src="https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png" height="250" />
</picture>
## 贡献指南
感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。
<a href="https://github.com/ayangweb/BongoCat/graphs/contributors">
<img src="https://contrib.rocks/image?repo=ayangweb/BongoCat" />
<a href="https://openomy.com/ayangweb/BongoCat" target="_blank" style="display: block; width: 100%;" align="center">
<img src="https://openomy.com/svg?repo=ayangweb/BongoCat&chart=bubble" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>
## 历史星标

View File

@@ -1,7 +1,7 @@
{
"name": "bongo-cat",
"type": "module",
"version": "0.5.0",
"version": "0.9.0",
"private": true,
"author": {
"name": "ayangweb",
@@ -39,14 +39,18 @@
"dayjs": "^1.11.13",
"es-toolkit": "^1.38.0",
"is-url": "^1.2.4",
"json5": "^2.2.3",
"nanoid": "^5.1.5",
"pinia": "^3.0.3",
"pixi-live2d-display": "^0.4.0",
"pixi.js": "^6.5.10",
"tauri-plugin-locale-api": "^2.0.1",
"tauri-plugin-macos-permissions-api": "^2.3.0",
"vue": "^3.5.16",
"vue-i18n": "^11.1.12",
"vue-markdown-render": "^2.2.1",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"vue3-masonry-css": "^1.0.7"
},
"devDependencies": {
"@antfu/eslint-config": "^4.13.3",

73
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
is-url:
specifier: ^1.2.4
version: 1.2.4
json5:
specifier: ^2.2.3
version: 2.2.3
nanoid:
specifier: ^5.1.5
version: 5.1.5
@@ -74,18 +77,27 @@ importers:
pixi.js:
specifier: ^6.5.10
version: 6.5.10
tauri-plugin-locale-api:
specifier: ^2.0.1
version: 2.0.1
tauri-plugin-macos-permissions-api:
specifier: ^2.3.0
version: 2.3.0
vue:
specifier: ^3.5.16
version: 3.5.16(typescript@5.6.3)
vue-i18n:
specifier: ^11.1.12
version: 11.1.12(vue@3.5.16(typescript@5.6.3))
vue-markdown-render:
specifier: ^2.2.1
version: 2.2.1(vue@3.5.16(typescript@5.6.3))
vue-router:
specifier: ^4.5.1
version: 4.5.1(vue@3.5.16(typescript@5.6.3))
vue3-masonry-css:
specifier: ^1.0.7
version: 1.0.7(vue@3.5.16(typescript@5.6.3))
devDependencies:
'@antfu/eslint-config':
specifier: ^4.13.3
@@ -730,6 +742,18 @@ packages:
'@types/node':
optional: true
'@intlify/core-base@11.1.12':
resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==}
engines: {node: '>= 16'}
'@intlify/message-compiler@11.1.12':
resolution: {integrity: sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==}
engines: {node: '>= 16'}
'@intlify/shared@11.1.12':
resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==}
engines: {node: '>= 16'}
'@jridgewell/gen-mapping@0.3.8':
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
engines: {node: '>=6.0.0'}
@@ -3206,6 +3230,11 @@ packages:
json-stable-stringify-without-jsonify@1.0.1:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
json5@2.2.3:
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
engines: {node: '>=6'}
hasBin: true
jsonc-eslint-parser@2.4.0:
resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -4408,6 +4437,9 @@ packages:
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
engines: {node: '>=6'}
tauri-plugin-locale-api@2.0.1:
resolution: {integrity: sha512-kiNcbP6ZKUpPbFseePrR9eb8kTRw8tSJ0ZBpQk4mDyGcbK3xGu0/dy7Cx8lBfC/p2MlmnyO4uRVIwta7FoV6eQ==}
tauri-plugin-macos-permissions-api@2.3.0:
resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==}
@@ -4649,6 +4681,12 @@ packages:
peerDependencies:
vue: ^3.4.37
vue-i18n@11.1.12:
resolution: {integrity: sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==}
engines: {node: '>= 16'}
peerDependencies:
vue: ^3.0.0
vue-markdown-render@2.2.1:
resolution: {integrity: sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==}
peerDependencies:
@@ -4665,6 +4703,12 @@ packages:
peerDependencies:
vue: ^3.0.0
vue3-masonry-css@1.0.7:
resolution: {integrity: sha512-uPbeBKBU8W57m2QnHQ9Yvr1qqtS9P3zuMn+ji/iTNNgpfB+bQhXeG8+UVh9ue4AjHplZjhezIHAGBqISr1y6kg==}
engines: {node: '>=18.18.0', npm: '>=9.0.0'}
peerDependencies:
vue: ^3.4.38
vue@3.5.16:
resolution: {integrity: sha512-rjOV2ecxMd5SiAmof2xzh2WxntRcigkX/He4YFJ6WdRvVUrbt6DxC1Iujh10XLl8xCDRDtGKMeO3D+pRQ1PP9w==}
peerDependencies:
@@ -5345,6 +5389,18 @@ snapshots:
optionalDependencies:
'@types/node': 22.15.29
'@intlify/core-base@11.1.12':
dependencies:
'@intlify/message-compiler': 11.1.12
'@intlify/shared': 11.1.12
'@intlify/message-compiler@11.1.12':
dependencies:
'@intlify/shared': 11.1.12
source-map-js: 1.2.1
'@intlify/shared@11.1.12': {}
'@jridgewell/gen-mapping@0.3.8':
dependencies:
'@jridgewell/set-array': 1.2.1
@@ -7983,6 +8039,8 @@ snapshots:
json-stable-stringify-without-jsonify@1.0.1: {}
json5@2.2.3: {}
jsonc-eslint-parser@2.4.0:
dependencies:
acorn: 8.14.1
@@ -9428,6 +9486,10 @@ snapshots:
tapable@2.2.2: {}
tauri-plugin-locale-api@2.0.1:
dependencies:
'@tauri-apps/api': 2.5.0
tauri-plugin-macos-permissions-api@2.3.0:
dependencies:
'@tauri-apps/api': 2.5.0
@@ -9701,6 +9763,13 @@ snapshots:
dependencies:
vue: 3.5.16(typescript@5.6.3)
vue-i18n@11.1.12(vue@3.5.16(typescript@5.6.3)):
dependencies:
'@intlify/core-base': 11.1.12
'@intlify/shared': 11.1.12
'@vue/devtools-api': 6.6.4
vue: 3.5.16(typescript@5.6.3)
vue-markdown-render@2.2.1(vue@3.5.16(typescript@5.6.3)):
dependencies:
markdown-it: 13.0.2
@@ -9716,6 +9785,10 @@ snapshots:
is-plain-object: 3.0.1
vue: 3.5.16(typescript@5.6.3)
vue3-masonry-css@1.0.7(vue@3.5.16(typescript@5.6.3)):
dependencies:
vue: 3.5.16(typescript@5.6.3)
vue@3.5.16(typescript@5.6.3):
dependencies:
'@vue/compiler-dom': 3.5.16

View File

@@ -1,6 +1,6 @@
[package]
name = "bongo-cat"
version = "0.5.0"
version = "0.9.0"
description = "A Tauri App"
authors = [ "ayangweb" ]
edition = "2024"
@@ -22,7 +22,6 @@ tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-pr
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tauri-plugin-custom-window.workspace = true
rdev = { git = "https://github.com/ayangweb/rdev" }
tauri-plugin-os = "2"
tauri-plugin-process = "2"
tauri-plugin-opener = "2"
@@ -38,6 +37,9 @@ tauri-plugin-fs = "2"
fs_extra = "1"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-global-shortcut = "2"
tauri-plugin-locale = "2"
rdev = { git = "https://github.com/kunkunsh/rdev" }
gilrs = { git = "https://github.com/ayangweb/gilrs", default-features = false, features = ["xinput"] }
[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel.workspace = true

View File

@@ -0,0 +1,69 @@
{
"Version": 3,
"FileReferences": {
"Moc": "demomodel3.moc3",
"Textures": [
"demomodel3.1024/texture_00.png",
"demomodel3.1024/texture_01.png",
"demomodel3.1024/texture_02.png"
],
"DisplayInfo": "demomodel3.cdi3.json",
"Expressions": [
{
"Name": "live2d_expression0.exp3.json",
"File": "live2d_expression0.exp3.json"
},
{
"Name": "live2d_expression1.exp3.json",
"File": "live2d_expression1.exp3.json"
},
{
"Name": "live2d_expression2.exp3.json",
"File": "live2d_expression2.exp3.json"
}
],
"Motions": {
"CAT_motion": [
{
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0
}
],
"CAT_motion_lock": [
{
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0
}
]
}
},
"Groups": [
{
"Target": "Parameter",
"Name": "LipSync",
"Ids": []
},
{
"Target": "Parameter",
"Name": "EyeBlink",
"Ids": [
"ParamEyeLOpen",
"ParamEyeROpen"
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -0,0 +1,289 @@
{
"Version": 3,
"Parameters": [
{
"Id": "ParamAngleX",
"GroupId": "",
"Name": "角度 X"
},
{
"Id": "ParamAngleY",
"GroupId": "",
"Name": "角度 Y"
},
{
"Id": "CatParamLeftHandDown",
"GroupId": "",
"Name": "左手按下"
},
{
"Id": "CatParamRightHandDown",
"GroupId": "",
"Name": "右手按下"
},
{
"Id": "CatParamStickLeftDown",
"GroupId": "",
"Name": "左摇杆点亮"
},
{
"Id": "CatParamStickRightDown",
"GroupId": "",
"Name": "右摇杆点亮"
},
{
"Id": "CatParamStickShowLeftHand",
"GroupId": "",
"Name": "显示摇杆左手"
},
{
"Id": "CatParamStickShowRightHand",
"GroupId": "",
"Name": "显示摇杆右手"
},
{
"Id": "CatParamStickLX",
"GroupId": "",
"Name": "左摇杆X"
},
{
"Id": "CatParamStickLY",
"GroupId": "",
"Name": "左摇杆Y"
},
{
"Id": "CatParamStickRX",
"GroupId": "",
"Name": "右摇杆X"
},
{
"Id": "CatParamStickRY",
"GroupId": "",
"Name": "右摇杆Y"
},
{
"Id": "ParamAngleZ",
"GroupId": "",
"Name": "角度 Z"
},
{
"Id": "ParamEyeLOpen",
"GroupId": "",
"Name": "左眼 开闭"
},
{
"Id": "ParamEyeLSmile",
"GroupId": "",
"Name": "左眼 微笑"
},
{
"Id": "ParamEyeROpen",
"GroupId": "",
"Name": "右眼"
},
{
"Id": "ParamEyeRSmile",
"GroupId": "",
"Name": "右眼 微笑"
},
{
"Id": "Param3",
"GroupId": "",
"Name": "挥手"
},
{
"Id": "Param",
"GroupId": "ParamGroup",
"Name": "开启闪电"
},
{
"Id": "Param2",
"GroupId": "ParamGroup",
"Name": "闪电划过"
},
{
"Id": "Param4",
"GroupId": "ParamGroup2",
"Name": "表情:thuglife"
},
{
"Id": "Param5",
"GroupId": "ParamGroup2",
"Name": "表情:升天"
},
{
"Id": "ParamEyeBallX",
"GroupId": "",
"Name": "眼球 X"
},
{
"Id": "ParamEyeBallY",
"GroupId": "",
"Name": "眼球 Y"
},
{
"Id": "ParamBrowLY",
"GroupId": "",
"Name": "左眉上下"
},
{
"Id": "ParamBrowRY",
"GroupId": "",
"Name": "右眉 上下"
},
{
"Id": "ParamBrowLX",
"GroupId": "",
"Name": "左眉 左右"
},
{
"Id": "ParamBrowRX",
"GroupId": "",
"Name": "右眉 左右"
},
{
"Id": "ParamBrowLAngle",
"GroupId": "",
"Name": "左眉 角度"
},
{
"Id": "ParamBrowRAngle",
"GroupId": "",
"Name": "右眉 角度"
},
{
"Id": "ParamBrowLForm",
"GroupId": "",
"Name": "左眉 変形"
},
{
"Id": "ParamBrowRForm",
"GroupId": "",
"Name": "右眉 変形"
},
{
"Id": "ParamMouthForm",
"GroupId": "",
"Name": "嘴部 变形"
},
{
"Id": "ParamMouthOpenY",
"GroupId": "",
"Name": "嘴巴 张开和闭合"
},
{
"Id": "ParamCheek",
"GroupId": "",
"Name": "脸颊"
},
{
"Id": "ParamBodyAngleX",
"GroupId": "",
"Name": "身体旋转 X"
},
{
"Id": "ParamBodyAngleY",
"GroupId": "",
"Name": "身体旋转 Y"
},
{
"Id": "ParamBodyAngleZ",
"GroupId": "",
"Name": "身体旋转 Z"
},
{
"Id": "ParamBreath",
"GroupId": "",
"Name": "呼吸"
},
{
"Id": "ParamHairFront",
"GroupId": "",
"Name": "摇动 前发"
},
{
"Id": "ParamHairSide",
"GroupId": "",
"Name": "摇动 侧发"
},
{
"Id": "ParamHairBack",
"GroupId": "",
"Name": "摇动 后发"
}
],
"ParameterGroups": [
{
"Id": "ParamGroup",
"GroupId": "",
"Name": "闪电"
},
{
"Id": "ParamGroup2",
"GroupId": "",
"Name": "表情"
}
],
"Parts": [
{
"Id": "Part12",
"Name": "右手"
},
{
"Id": "Part9",
"Name": "左手"
},
{
"Id": "Part11",
"Name": "demomodel.psd(未找到对应图层)"
},
{
"Id": "Part7",
"Name": "demomodel.psd(未找到对应图层)"
},
{
"Id": "Part3",
"Name": "demomodel.psd(未找到对应图层)"
},
{
"Id": "Part2",
"Name": "demomodel.psd(未找到对应图层)"
},
{
"Id": "Part",
"Name": "demomodel.psd(未找到对应图层)"
},
{
"Id": "Part10",
"Name": "天使环"
},
{
"Id": "Part5",
"Name": "demomodel.psd(未找到对应图层)"
},
{
"Id": "PartSketch0",
"Name": "[ 参考图 ]"
},
{
"Id": "rightstick",
"Name": "rightstick"
},
{
"Id": "leftstick",
"Name": "leftstick"
},
{
"Id": "Part8",
"Name": "thug life"
},
{
"Id": "Part6",
"Name": "闪电"
},
{
"Id": "Part4",
"Name": "闪电"
}
]
}

Binary file not shown.

View File

@@ -0,0 +1,15 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": 0.321,
"Blend": "Multiply"
},
{
"Id": "ParamEyeROpen",
"Value": 0.313,
"Blend": "Multiply"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"Type": "Live2D Expression",
"Parameters": [
{
"Id": "ParamEyeLOpen",
"Value": -1,
"Blend": "Add"
}
]
}

View File

@@ -0,0 +1,4 @@
{
"Type": "Live2D Expression",
"Parameters": []
}

View File

@@ -0,0 +1,11 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.8,
"Parameters": [
{
"Id": "Param4",
"Value": 1,
"Blend": "Add"
}
]
}

View File

@@ -0,0 +1,11 @@
{
"Type": "Live2D Expression",
"FadeInTime": 0.5,
"Parameters": [
{
"Id": "Param5",
"Value": 1,
"Blend": "Add"
}
]
}

Binary file not shown.

View File

@@ -0,0 +1,76 @@
{
"Version": 3,
"Meta": {
"Duration": 1.633,
"Fps": 30.0,
"Loop": true,
"AreBeziersRestricted": false,
"CurveCount": 2,
"TotalSegmentCount": 8,
"TotalPointCount": 20,
"UserDataCount": 0,
"TotalUserDataSize": 0
},
"Curves": [
{
"Target": "Parameter",
"Id": "Param",
"Segments": [
0,
0,
1,
0.033,
0,
0.067,
1,
0.1,
1,
1,
0.411,
1,
0.722,
1,
1.033,
1,
1,
1.189,
1,
1.344,
0,
1.5,
0,
0,
1.633,
0
]
},
{
"Target": "Parameter",
"Id": "Param2",
"Segments": [
0,
0,
0,
0.067,
0,
1,
0.1,
0,
0.133,
0.142,
0.167,
0.2,
1,
0.489,
0.764,
0.811,
1,
1.133,
1,
0,
1.633,
1
]
}
]
}

View File

@@ -0,0 +1,77 @@
{
"Version": 3,
"Meta": {
"Duration": 2.333,
"Fps": 30.0,
"Loop": true,
"AreBeziersRestricted": true,
"CurveCount": 2,
"TotalSegmentCount": 7,
"TotalPointCount": 21,
"UserDataCount": 0,
"TotalUserDataSize": 0
},
"Curves": [
{
"Target": "Parameter",
"Id": "CatParamLeftHandDown",
"Segments": [
0,
0,
0,
2.333,
0
]
},
{
"Target": "Parameter",
"Id": "Param3",
"Segments": [
0,
0,
1,
0.133,
0,
0.267,
30,
0.4,
30,
1,
0.522,
30,
0.644,
0,
0.767,
0,
1,
0.9,
0,
1.033,
30,
1.167,
30,
1,
1.3,
30,
1.433,
0,
1.567,
0,
1,
1.7,
0,
1.833,
30,
1.967,
30,
1,
2.089,
30,
2.211,
0,
2.333,
0
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

24
src-tauri/assets/models/keyboard/cat.model3.json Executable file → Normal file
View File

@@ -10,30 +10,40 @@
"DisplayInfo": "demomodel2.cdi3.json",
"Expressions": [
{
"Name": "默认喵",
"Name": "live2d_expression0.exp3.json",
"File": "live2d_expression0.exp3.json"
},
{
"Name": "社会喵",
"File": "live2d_expression1.exp3.json",
"Description": "喵喵我叼根小烟耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
"Name": "live2d_expression1.exp3.json",
"File": "live2d_expression1.exp3.json"
},
{
"Name": "天使喵",
"Name": "live2d_expression2.exp3.json",
"File": "live2d_expression2.exp3.json"
}
],
"Motions": {
"CAT_motion": [
{
"Name": "雷霆喵",
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"Name": "摇摆喵",
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0
}
],
"CAT_motion_lock": [
{
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0

25
src-tauri/assets/models/standard/cat.model3.json Executable file → Normal file
View File

@@ -10,31 +10,40 @@
"DisplayInfo": "demomodel.cdi3.json",
"Expressions": [
{
"Name": "默认喵",
"Name": "live2d_expression0.exp3.json",
"File": "live2d_expression0.exp3.json"
},
{
"Name": "社会喵",
"File": "live2d_expression1.exp3.json",
"Description": "喵喵我叼根小烟耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
"Name": "live2d_expression1.exp3.json",
"File": "live2d_expression1.exp3.json"
},
{
"Name": "天使喵",
"Name": "live2d_expression2.exp3.json",
"File": "live2d_expression2.exp3.json"
}
],
"Motions": {
"CAT_motion": [
{
"Name": "雷霆喵",
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"Name": "摇摆喵",
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0
}
],
"CAT_motion_lock": [
{
"File": "live2d_motion1.motion3.json",
"Sound": "live2d_motion1.flac",
"FadeInTime": 0,
"FadeOutTime": 0
},
{
"File": "live2d_motion2.motion3.json",
"FadeInTime": 0,
"FadeOutTime": 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -14,6 +14,8 @@
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-set-decorations",
"core:window:allow-set-position",
"core:window:allow-set-theme",
"core:window:allow-set-title",
"custom-window:default",
"os:default",
"process:default",
@@ -45,6 +47,7 @@
"clipboard-manager:allow-write-text",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
"global-shortcut:allow-unregister",
"locale:default"
]
}

View File

@@ -2,12 +2,10 @@ use rdev::{Event, EventType, listen};
use serde::Serialize;
use serde_json::{Value, json};
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{AppHandle, Emitter};
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
use tauri::{AppHandle, Emitter, Runtime, command};
#[derive(Debug, Clone, Serialize)]
pub enum DeviceKind {
pub enum DeviceEventKind {
MousePress,
MouseRelease,
MouseMove,
@@ -17,56 +15,49 @@ pub enum DeviceKind {
#[derive(Debug, Clone, Serialize)]
pub struct DeviceEvent {
kind: DeviceKind,
kind: DeviceEventKind,
value: Value,
}
pub fn start_listening(app_handle: AppHandle) {
if IS_RUNNING.load(Ordering::SeqCst) {
return;
static IS_LISTENING: AtomicBool = AtomicBool::new(false);
#[command]
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
if IS_LISTENING.load(Ordering::SeqCst) {
return Ok(());
}
IS_RUNNING.store(true, Ordering::SeqCst);
IS_LISTENING.store(true, Ordering::SeqCst);
let callback = move |event: Event| {
let device = match event.event_type {
let device_event = match event.event_type {
EventType::ButtonPress(button) => DeviceEvent {
kind: DeviceKind::MousePress,
kind: DeviceEventKind::MousePress,
value: json!(format!("{:?}", button)),
},
EventType::ButtonRelease(button) => DeviceEvent {
kind: DeviceKind::MouseRelease,
kind: DeviceEventKind::MouseRelease,
value: json!(format!("{:?}", button)),
},
EventType::MouseMove { x, y } => DeviceEvent {
kind: DeviceKind::MouseMove,
kind: DeviceEventKind::MouseMove,
value: json!({ "x": x, "y": y }),
},
EventType::KeyPress(key) => DeviceEvent {
kind: DeviceKind::KeyboardPress,
kind: DeviceEventKind::KeyboardPress,
value: json!(format!("{:?}", key)),
},
EventType::KeyRelease(key) => DeviceEvent {
kind: DeviceKind::KeyboardRelease,
kind: DeviceEventKind::KeyboardRelease,
value: json!(format!("{:?}", key)),
},
_ => return,
};
if let Err(e) = app_handle.emit("device-changed", device) {
eprintln!("Failed to emit event: {:?}", e);
}
let _ = app_handle.emit("device-changed", device_event);
};
#[cfg(target_os = "macos")]
if let Err(e) = listen(callback) {
eprintln!("Device listening error: {:?}", e);
}
listen(callback).map_err(|err| format!("Failed to listen device: {:?}", err))?;
#[cfg(not(target_os = "macos"))]
std::thread::spawn(move || {
if let Err(e) = listen(callback) {
eprintln!("Device listening error: {:?}", e);
}
});
Ok(())
}

View File

@@ -0,0 +1,61 @@
use gilrs::{EventType, Gilrs};
use serde::Serialize;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{AppHandle, Emitter, Runtime, command};
static IS_LISTENING: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Clone, Serialize)]
pub enum GamepadEventKind {
ButtonChanged,
AxisChanged,
}
#[derive(Debug, Clone, Serialize)]
pub struct GamepadEvent {
kind: GamepadEventKind,
name: String,
value: f32,
}
#[command]
pub async fn start_gamepad_listing<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
if IS_LISTENING.load(Ordering::SeqCst) {
return Ok(());
}
IS_LISTENING.store(true, Ordering::SeqCst);
let mut gilrs = Gilrs::new().map_err(|err| err.to_string())?;
while IS_LISTENING.load(Ordering::SeqCst) {
while let Some(event) = gilrs.next_event() {
let gamepad_event = match event.event {
EventType::ButtonChanged(button, value, ..) => GamepadEvent {
kind: GamepadEventKind::ButtonChanged,
name: format!("{:?}", button),
value,
},
EventType::AxisChanged(axis, value, ..) => GamepadEvent {
kind: GamepadEventKind::AxisChanged,
name: format!("{:?}", axis),
value,
},
_ => continue,
};
let _ = app_handle.emit("gamepad-changed", gamepad_event);
}
}
Ok(())
}
#[command]
pub async fn stop_gamepad_listing() {
if !IS_LISTENING.load(Ordering::SeqCst) {
return;
}
IS_LISTENING.store(false, Ordering::SeqCst);
}

View File

@@ -1,3 +1,4 @@
pub mod device;
pub mod gamepad;
pub mod prevent_default;
pub mod setup;

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};
use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
use tauri_plugin_custom_window::MAIN_WINDOW_LABEL;
@@ -56,7 +57,7 @@ pub fn platform(
"window_did_resize" => {
window_move_event();
if let Ok(size) = main_window.inner_size() {
if let Ok(size) = main_window.outer_size() {
let _ = main_window.emit_to(target, WINDOW_RESIZED_EVENT, size);
}
}

View File

@@ -3,20 +3,14 @@ use tauri::{AppHandle, WebviewWindow};
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "linux")]
mod linux;
#[cfg(not(target_os = "macos"))]
pub mod common;
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(target_os = "windows")]
pub use windows::*;
#[cfg(target_os = "linux")]
pub use linux::*;
#[cfg(not(target_os = "macos"))]
pub use common::*;
pub fn default(
app_handle: &AppHandle,

View File

@@ -1,8 +0,0 @@
use tauri::{AppHandle, WebviewWindow};
pub fn platform(
_app_handle: &AppHandle,
_main_window: WebviewWindow,
_preference_window: WebviewWindow,
) {
}

View File

@@ -1,7 +1,11 @@
mod core;
mod utils;
use core::{device, prevent_default, setup};
use core::{
device::start_device_listening,
gamepad::{start_gamepad_listing, stop_gamepad_listing},
prevent_default, setup,
};
use tauri::{Manager, WindowEvent, generate_handler};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_custom_window::{
@@ -21,11 +25,14 @@ pub fn run() {
setup::default(&app_handle, main_window.clone(), preference_window.clone());
device::start_listening(app_handle.clone());
Ok(())
})
.invoke_handler(generate_handler![copy_dir])
.invoke_handler(generate_handler![
copy_dir,
start_device_listening,
start_gamepad_listing,
stop_gamepad_listing
])
.plugin(tauri_plugin_custom_window::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
@@ -38,7 +45,12 @@ pub fn run() {
show_preference_window(app_handle);
},
))
.plugin(tauri_plugin_log::Builder::new().build())
.plugin(
tauri_plugin_log::Builder::new()
.timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
.filter(|metadata| !metadata.target().contains("gilrs"))
.build(),
)
.plugin(tauri_plugin_autostart::init(
MacosLauncher::LaunchAgent,
None,
@@ -48,6 +60,7 @@ pub fn run() {
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.plugin(tauri_plugin_locale::init())
.on_window_event(|window, event| match event {
WindowEvent::CloseRequested { api, .. } => {
let _ = window.hide();

View File

@@ -1,4 +1,9 @@
const COMMANDS: &[&str] = &["show_window", "hide_window", "set_always_on_top"];
const COMMANDS: &[&str] = &[
"show_window",
"hide_window",
"set_always_on_top",
"set_taskbar_visibility",
];
fn main() {
tauri_plugin::Builder::new(COMMANDS).build();

View File

@@ -2,4 +2,4 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-show-window", "allow-hide-window", "allow-set-always-on-top"]
permissions = ["allow-show-window", "allow-hide-window", "allow-set-always-on-top", "allow-set-taskbar-visibility"]

View File

@@ -19,3 +19,8 @@ pub async fn set_always_on_top<R: Runtime>(
) {
shared_set_always_on_top(&app_handle, &window, always_on_top);
}
#[command]
pub async fn set_taskbar_visibility<R: Runtime>(window: WebviewWindow<R>, visible: bool) {
let _ = window.set_skip_taskbar(!visible);
}

View File

@@ -1,3 +1,4 @@
#![allow(deprecated)]
use super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window};
use crate::MAIN_WINDOW_LABEL;
use tauri::{AppHandle, Runtime, WebviewWindow, command};
@@ -73,3 +74,8 @@ pub fn set_macos_panel<R: Runtime>(
});
}
}
#[command]
pub async fn set_taskbar_visibility<R: Runtime>(app_handle: AppHandle<R>, visible: bool) {
let _ = app_handle.set_dock_visibility(visible);
}

View File

@@ -7,13 +7,13 @@ pub static PREFERENCE_WINDOW_LABEL: &str = "preference";
mod macos;
#[cfg(not(target_os = "macos"))]
mod not_macos;
mod common;
#[cfg(target_os = "macos")]
pub use macos::*;
#[cfg(not(target_os = "macos"))]
pub use not_macos::*;
pub use common::*;
pub fn is_main_window<R: Runtime>(window: &WebviewWindow<R>) -> bool {
window.label() == MAIN_WINDOW_LABEL

View File

@@ -12,7 +12,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.invoke_handler(generate_handler![
commands::show_window,
commands::hide_window,
commands::set_always_on_top
commands::set_always_on_top,
commands::set_taskbar_visibility,
])
.build()
}

View File

@@ -21,11 +21,11 @@
"transparent": true,
"decorations": false,
"acceptFirstMouse": true,
"skipTaskbar": true
"skipTaskbar": true,
"maximizable": false
},
{
"label": "preference",
"title": "偏好设置",
"url": "index.html/#/preference",
"visible": false,
"titleBarStyle": "Overlay",
@@ -48,7 +48,7 @@
},
"bundle": {
"active": true,
"category": "Game",
"category": "Entertainment",
"createUpdaterArtifacts": true,
"targets": ["nsis", "dmg", "app", "appimage", "deb", "rpm"],
"shortDescription": "BongoCat",
@@ -63,8 +63,10 @@
},
"plugins": {
"updater": {
"dangerousInsecureTransportProtocol": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRjJFMzE3MjEwMUZEMTAKUldRUS9RRWhGK1B5NmdkemhKcUFrVjZBQXlzdExpakdWVEJDeU9XckVsbzV2cFIycVJOempWa2UK",
"endpoints": [
"http://api.upgrade.toolsetlink.com/v1/tauri/upgrade?tauriKey=KtGlsZUVXmWfjkRKCuqpfw&versionName={{current_version}}&target={{target}}&arch={{arch}}&appointVersionName=&devModelKey=&devKey=",
"https://gh-proxy.com/github.com/ayangweb/BongoCat/releases/latest/download/latest.json"
]
},

View File

@@ -1,6 +1,6 @@
{
"identifier": "com.ayangweb.BongoCat",
"bundle": {
"resources": ["assets/tray.png", "assets/models"]
"resources": ["assets/tray-mac.png", "assets/models"]
}
}

View File

@@ -4,8 +4,14 @@
"windows": {
"digestAlgorithm": "sha256",
"nsis": {
"languages": ["SimpChinese"],
"installMode": "both"
"languages": [
"English",
"Vietnamese",
"SimpChinese",
"PortugueseBR"
],
"installMode": "both",
"displayLanguageSelector": true
}
}
}

View File

@@ -3,17 +3,18 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { error } from '@tauri-apps/plugin-log'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useEventListener } from '@vueuse/core'
import { ConfigProvider } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { ConfigProvider, theme } from 'ant-design-vue'
import { isString } from 'es-toolkit'
import isURL from 'is-url'
import { onMounted } from 'vue'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { useTauriListen } from './composables/useTauriListen'
import { useThemeVars } from './composables/useThemeVars'
import { useWindowState } from './composables/useWindowState'
import { LISTEN_KEY } from './constants'
import { LANGUAGE, LISTEN_KEY } from './constants'
import { getAntdLocale } from './locales/index.ts'
import { hideWindow, showWindow } from './plugins/window'
import { useAppStore } from './stores/app'
import { useCatStore } from './stores/cat'
@@ -29,18 +30,26 @@ const generalStore = useGeneralStore()
const shortcutStore = useShortcutStore()
const appWindow = getCurrentWebviewWindow()
const { isRestored, restoreState } = useWindowState()
const { darkAlgorithm, defaultAlgorithm } = theme
const { locale } = useI18n()
onMounted(async () => {
generateColorVars()
await appStore.$tauri.start()
await appStore.init()
await modelStore.$tauri.start()
await modelStore.init()
await catStore.$tauri.start()
catStore.init()
await generalStore.$tauri.start()
await generalStore.init()
await shortcutStore.$tauri.start()
catStore.visible = true
await restoreState()
})
restoreState()
watch(() => generalStore.appearance.language, (value) => {
locale.value = value ?? LANGUAGE.EN_US
})
useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
@@ -79,7 +88,12 @@ useEventListener('click', (event) => {
</script>
<template>
<ConfigProvider :locale="zhCN">
<ConfigProvider
:locale="getAntdLocale(generalStore.appearance.language)"
:theme="{
algorithm: generalStore.appearance.isDark ? darkAlgorithm : defaultAlgorithm,
}"
>
<RouterView v-if="isRestored" />
</ConfigProvider>
</template>

View File

@@ -3,6 +3,10 @@ html {
color-scheme: light;
body {
--uno: transition-opacity-300;
}
&.dark {
color-scheme: dark;
}

View File

@@ -18,12 +18,15 @@ const hasDescription = computed(() => {
<template>
<Flex
:align="vertical ? void 0 : 'center'"
class="b b-color-2 rounded-lg b-solid bg-white p-4"
gap="middle"
class="b b-color-2 rounded-lg b-solid bg-color-3 p-4"
:gap="vertical ? 'middle' : 'large'"
justify="space-between"
:vertical="vertical"
>
<Flex align="center">
<Flex
align="center"
class="flex-1"
>
<Flex vertical>
<div class="text-sm font-medium">
{{ title }}

View File

@@ -110,7 +110,7 @@ function handleKeyUp(event: KeyboardEvent) {
@mouseover="isHovering = true"
>
<span v-if="pressedKeys.length === 0">
{{ isFocusing ? '按下录制快捷键' : '点击录制快捷键' }}
{{ isFocusing ? $t('components.proShortcut.hints.pressRecordShortcut') : $t('components.proShortcut.hints.clickRecordShortcut') }}
</span>
<span class="text-primary font-bold">

View File

@@ -8,10 +8,11 @@ import { Flex, message, Modal } from 'ant-design-vue'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { computed, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import VueMarkdown from 'vue-markdown-render'
import { useTauriListen } from '@/composables/useTauriListen'
import { GITHUB_LINK, LISTEN_KEY } from '@/constants'
import { GITHUB_LINK, LISTEN_KEY, UPGRADE_LINK_ACCESS_KEY } from '@/constants'
import { showWindow } from '@/plugins/window'
import { useGeneralStore } from '@/stores/general'
@@ -32,10 +33,11 @@ const state = reactive<State>({
downloadProgress: 0,
})
const MESSAGE_KEY = 'updatable'
const { t } = useI18n()
const { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24)
watch(() => generalStore.autoCheckUpdate, (value) => {
watch(() => generalStore.update.autoCheck, (value) => {
pause()
if (!value) return
@@ -51,7 +53,7 @@ useTauriListen<boolean>(LISTEN_KEY.UPDATE_APP, () => {
message.loading({
key: MESSAGE_KEY,
duration: 0,
content: '正在检查更新...',
content: t('components.updateApp.hints.checkingUpdates'),
})
})
@@ -67,7 +69,12 @@ const downloadProgress = computed(() => {
async function checkUpdate(visibleMessage = false) {
try {
const update = await check()
const update = await check({
timeout: 5000,
headers: {
'X-AccessKey': UPGRADE_LINK_ACCESS_KEY,
},
})
if (update) {
const { version, currentVersion, body = '', date, downloadAndInstall } = update
@@ -86,7 +93,7 @@ async function checkUpdate(visibleMessage = false) {
message.destroy(MESSAGE_KEY)
} else if (visibleMessage) {
message.success({ key: MESSAGE_KEY, content: '当前已是最新版本🎉' })
message.success({ key: MESSAGE_KEY, content: t('components.updateApp.hints.alreadyLatest') })
}
} catch (error) {
if (!visibleMessage) return
@@ -122,7 +129,10 @@ async function handleOk() {
} catch (error) {
message.error(String(error))
} finally {
state.downloading = false
Object.assign(state, {
downloading: false,
downloadProgress: 0,
})
}
}
</script>
@@ -130,15 +140,15 @@ async function handleOk() {
<template>
<Modal
v-model:open="state.open"
cancel-text="稍后更新"
:cancel-text="$t('components.updateApp.buttons.updateLater')"
centered
:closable="false"
:mask-closable="false"
title="发现新版本🥳"
:title="$t('components.updateApp.title')"
@ok="handleOk"
>
<template #okText>
{{ state.downloading ? downloadProgress : "立即更新" }}
{{ state.downloading ? downloadProgress : $t('components.updateApp.buttons.updateNow') }}
</template>
<Flex
@@ -147,7 +157,7 @@ async function handleOk() {
vertical
>
<Flex align="center">
<span>更新版本</span>
<span>{{ $t('components.updateApp.labels.updateVersion') }}</span>
<span>
<span>{{ state.update?.currentVersion }} 👉 </span>
<a
@@ -159,12 +169,12 @@ async function handleOk() {
</Flex>
<Flex align="center">
<span>更新时间</span>
<span>{{ $t('components.updateApp.labels.updateTime') }}</span>
<span>{{ state.update?.date }}</span>
</Flex>
<Flex vertical>
<span>更新日志</span>
<span>{{ $t('components.updateApp.labels.changelog') }}</span>
<VueMarkdown
class="update-note max-h-40 overflow-auto"

View File

@@ -1,17 +1,15 @@
import type { Ref } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { cursorPosition } from '@tauri-apps/api/window'
import { readDir } from '@tauri-apps/plugin-fs'
import { uniq } from 'es-toolkit'
import { reactive, ref, watch } from 'vue'
import { LISTEN_KEY } from '../constants'
import { INVOKE_KEY, LISTEN_KEY } from '../constants'
import { useModel } from './useModel'
import { useTauriListen } from './useTauriListen'
import { useCatStore } from '@/stores/cat'
import { useModelStore } from '@/stores/model'
import { isImage } from '@/utils/is'
import { join } from '@/utils/path'
import { inBetween } from '@/utils/is'
import { isWindows } from '@/utils/platform'
interface MouseButtonEvent {
@@ -19,14 +17,14 @@ interface MouseButtonEvent {
value: string
}
interface MouseMoveValue {
export interface CursorPoint {
x: number
y: number
}
interface MouseMoveEvent {
kind: 'MouseMove'
value: MouseMoveValue
value: CursorPoint
}
interface KeyboardEvent {
@@ -37,102 +35,64 @@ interface KeyboardEvent {
type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent
export function useDevice() {
const supportLeftKeys = ref<string[]>([])
const supportRightKeys = ref<string[]>([])
const pressedMouses = ref<string[]>([])
const mousePosition = reactive<MouseMoveValue>({ x: 0, y: 0 })
const pressedLeftKeys = ref<string[]>([])
const pressedRightKeys = ref<string[]>([])
const catStore = useCatStore()
const modelStore = useModelStore()
const releaseTimers = new Map<string, NodeJS.Timeout>()
const catStore = useCatStore()
const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()
watch(() => modelStore.currentModel, async (model) => {
if (!model) return
const keySides = [
{
side: 'left',
supportKeys: supportLeftKeys,
pressedKeys: pressedLeftKeys,
},
{
side: 'right',
supportKeys: supportRightKeys,
pressedKeys: pressedRightKeys,
},
]
for await (const item of keySides) {
const { side, supportKeys, pressedKeys } = item
try {
const files = await readDir(join(model.path, 'resources', `${side}-keys`))
const imageFiles = files.filter(file => isImage(file.name))
supportKeys.value = imageFiles.map((item) => {
return item.name.split('.')[0]
})
pressedKeys.value = pressedKeys.value.filter((key) => {
return supportKeys.value.includes(key)
})
} catch {
supportKeys.value = []
pressedKeys.value = []
}
}
}, { deep: true, immediate: true })
const handlePress = (array: Ref<string[]>, value?: string) => {
if (!value) return
if (catStore.singleMode) {
array.value = [value]
} else {
array.value = uniq(array.value.concat(value))
}
}
const handleRelease = (array: Ref<string[]>, value?: string) => {
if (!value) return
array.value = array.value.filter(item => item !== value)
const startListening = () => {
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
}
const getSupportedKey = (key: string) => {
for (const side of ['left', 'right']) {
let nextKey = key
let nextKey = key
const supportKeys = side === 'left' ? supportLeftKeys.value : supportRightKeys.value
const unsupportedKey = !modelStore.supportKeys[nextKey]
const unsupportedKeys = !supportKeys.includes(key)
if (key.startsWith('F') && unsupportedKey) {
nextKey = key.replace(/F(\d+)/, 'Fn')
}
if (key.startsWith('F') && unsupportedKeys) {
nextKey = key.replace(/F(\d+)/, 'Fn')
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
if (key.startsWith(item) && unsupportedKey) {
const regex = new RegExp(`^(${item}).*`)
nextKey = key.replace(regex, '$1')
}
}
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
if (key.startsWith(item) && unsupportedKeys) {
const regex = new RegExp(`^(${item}).*`)
nextKey = key.replace(regex, '$1')
}
return nextKey
}
const handleCursorMove = async () => {
const cursorPoint = await cursorPosition()
handleMouseMove(cursorPoint)
if (catStore.window.hideOnHover) {
const appWindow = getCurrentWebviewWindow()
const position = await appWindow.outerPosition()
const { width, height } = await appWindow.innerSize()
const isInWindow = inBetween(cursorPoint.x, position.x, position.x + width)
&& inBetween(cursorPoint.y, position.y, position.y + height)
document.body.style.setProperty('opacity', isInWindow ? '0' : 'unset')
if (!catStore.window.passThrough) {
appWindow.setIgnoreCursorEvents(isInWindow)
}
if (!supportKeys.includes(nextKey)) continue
return nextKey
}
}
const handleScheduleRelease = (keys: Ref<string[]>, key: string, delay = 500) => {
const handleAutoRelease = (key: string, delay = 100) => {
handlePress(key)
if (releaseTimers.has(key)) {
clearTimeout(releaseTimers.get(key))
}
const timer = setTimeout(() => {
handleRelease(keys, key)
handleRelease(key)
releaseTimers.delete(key)
}, delay)
@@ -148,41 +108,34 @@ export function useDevice() {
if (!nextValue) return
const isLeftSide = supportLeftKeys.value.includes(nextValue)
const pressedKeys = isLeftSide ? pressedLeftKeys : pressedRightKeys
if (nextValue === 'CapsLock') {
handlePress(pressedKeys, nextValue)
return handleScheduleRelease(pressedKeys, nextValue, 100)
return handleAutoRelease(nextValue)
}
if (kind === 'KeyboardPress') {
if (isWindows) {
handleScheduleRelease(pressedKeys, nextValue)
const delay = catStore.model.autoReleaseDelay * 1000
return handleAutoRelease(nextValue, delay)
}
return handlePress(pressedKeys, nextValue)
return handlePress(nextValue)
}
return handleRelease(pressedKeys, nextValue)
return handleRelease(nextValue)
}
switch (kind) {
case 'MousePress':
return handlePress(pressedMouses, value)
return handleMouseChange(value)
case 'MouseRelease':
return handleRelease(pressedMouses, value)
return handleMouseChange(value, false)
case 'MouseMove':
return Object.assign(mousePosition, value)
return handleCursorMove()
}
})
return {
pressedMouses,
mousePosition,
pressedLeftKeys,
pressedRightKeys,
startListening,
}
}

View File

@@ -0,0 +1,104 @@
import type { LiteralUnion } from 'ant-design-vue/es/_util/type'
import { invoke } from '@tauri-apps/api/core'
import { computed, reactive, watch } from 'vue'
import { useModel } from './useModel'
import { useTauriListen } from './useTauriListen'
import { INVOKE_KEY, LISTEN_KEY } from '@/constants'
import { useModelStore } from '@/stores/model'
import live2d from '@/utils/live2d'
type GamepadEventName = LiteralUnion<'LeftStickX' | 'LeftStickY' | 'RightStickX' | 'RightStickY' | 'LeftThumb' | 'RightThumb'>
interface GamepadEvent {
kind: 'ButtonChanged' | 'AxisChanged'
name: GamepadEventName
value: number
}
interface StickState {
x: number
y: number
moved: boolean
pressed: boolean
}
interface Sticks {
left: StickState
right: StickState
}
const INITIAL_STICK_STATE: StickState = { x: 0, y: 0, moved: false, pressed: false }
export function useGamepad() {
const { currentModel } = useModelStore()
const { handlePress, handleRelease, handleAxisChange } = useModel()
const sticks = reactive<Sticks>({
left: { ...INITIAL_STICK_STATE },
right: { ...INITIAL_STICK_STATE },
})
const stickActive = computed(() => ({
left: sticks.left.moved || sticks.left.pressed,
right: sticks.right.moved || sticks.right.pressed,
}))
watch(() => currentModel?.mode, (mode) => {
if (mode === 'gamepad') {
return invoke(INVOKE_KEY.START_GAMEPAD_LISTING)
}
invoke(INVOKE_KEY.STOP_GAMEPAD_LISTING)
}, { immediate: true })
watch(sticks.left, ({ x, y, moved, pressed }) => {
sticks.left.moved = x !== 0 || y !== 0
live2d.setParameterValue('CatParamStickShowLeftHand', moved || pressed)
}, { deep: true })
watch(sticks.right, ({ x, y, moved, pressed }) => {
sticks.right.moved = x !== 0 || y !== 0
live2d.setParameterValue('CatParamStickShowRightHand', moved || pressed)
}, { deep: true })
useTauriListen<GamepadEvent>(LISTEN_KEY.GAMEPAD_CHANGED, ({ payload }) => {
const { name, value } = payload
switch (name) {
case 'LeftStickX':
sticks.left.x = value
return handleAxisChange('CatParamStickLX', value)
case 'LeftStickY':
sticks.left.y = value
return handleAxisChange('CatParamStickLY', value)
case 'RightStickX':
sticks.right.x = value
return handleAxisChange('CatParamStickRX', value)
case 'RightStickY':
sticks.right.y = value
return handleAxisChange('CatParamStickRY', value)
case 'LeftThumb':
sticks.left.pressed = value !== 0
return live2d.setParameterValue('CatParamStickLeftDown', value !== 0)
case 'RightThumb':
sticks.right.pressed = value !== 0
return live2d.setParameterValue('CatParamStickRightDown', value !== 0)
default:
return value > 0 ? handlePress(name) : handleRelease(name)
}
})
return {
stickActive,
}
}

View File

@@ -1,53 +1,46 @@
import { convertFileSrc } from '@tauri-apps/api/core'
import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'
import { resolveResource } from '@tauri-apps/api/path'
import type { PhysicalPosition } from '@tauri-apps/api/dpi'
import { LogicalSize } from '@tauri-apps/api/dpi'
import { resolveResource, sep } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { message } from 'ant-design-vue'
import { isNil, round } from 'es-toolkit'
import { computed, watch } from 'vue'
import { nth } from 'es-toolkit/compat'
import { ref } from 'vue'
import live2d from '../utils/live2d'
import { getCursorMonitor } from '../utils/monitor'
import { useCatStore } from '@/stores/cat'
import { useModelStore } from '@/stores/model'
import { getImageSize } from '@/utils/dom'
import { join } from '@/utils/path'
import { getCursorMonitor } from '@/utils/monitor'
const appWindow = getCurrentWebviewWindow()
export interface ModelSize {
width: number
height: number
}
export function useModel() {
const modelStore = useModelStore()
const catStore = useCatStore()
const backgroundImage = computed(() => {
return convertFileSrc(join(modelStore.currentModel!.path, 'resources', 'background.png'))
})
watch(() => modelStore.currentModel, handleLoad, { deep: true, immediate: true })
watch(() => catStore.scale, async () => {
const { width, height } = await getImageSize(backgroundImage.value)
appWindow.setSize(
new PhysicalSize({
width: round(width * (catStore.scale / 100)),
height: round(height * (catStore.scale / 100)),
}),
)
}, { immediate: true })
const modelSize = ref<ModelSize>()
async function handleLoad() {
try {
const { path } = modelStore.currentModel!
if (!modelStore.currentModel) return
const { path } = modelStore.currentModel
await resolveResource(path)
const data = await live2d.load(path)
const { width, height, ...rest } = await live2d.load(path)
modelSize.value = { width, height }
handleResize()
Object.assign(modelStore, data)
Object.assign(modelStore, rest)
} catch (error) {
message.error(String(error))
}
@@ -58,13 +51,11 @@ export function useModel() {
}
async function handleResize() {
if (!live2d.model) return
if (!modelSize.value) return
const { innerWidth, innerHeight } = window
live2d.resizeModel(modelSize.value)
const { width, height } = await getImageSize(backgroundImage.value)
live2d.model?.scale.set(innerWidth / width)
const { width, height } = modelSize.value
if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) {
await appWindow.setSize(
@@ -77,26 +68,54 @@ export function useModel() {
const size = await appWindow.size()
catStore.scale = round((size.width / width) * 100)
catStore.window.scale = round((size.width / width) * 100)
}
function handleKeyDown(side: 'left' | 'right', pressed: boolean) {
const id = side === 'left' ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
const handlePress = (key: string) => {
const path = modelStore.supportKeys[key]
const { min, max } = live2d.getParameterRange(id)
if (!path) return
live2d.setParameterValue(id, pressed ? max : min)
if (catStore.model.single) {
const dirName = nth(path.split(sep()), -2)!
const filterKeys = Object.entries(modelStore.pressedKeys).filter(([, value]) => {
return value.includes(dirName)
})
for (const [key] of filterKeys) {
handleRelease(key)
}
}
modelStore.pressedKeys[key] = path
}
async function handleMouseMove() {
const monitor = await getCursorMonitor()
const handleRelease = (key: string) => {
delete modelStore.pressedKeys[key]
}
function handleKeyChange(isLeft = true, pressed = true) {
const id = isLeft ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
live2d.setParameterValue(id, pressed)
}
function handleMouseChange(key: string, pressed = true) {
const id = key === 'Left' ? 'ParamMouseLeftDown' : 'ParamMouseRightDown'
live2d.setParameterValue(id, pressed)
}
async function handleMouseMove(cursorPoint: PhysicalPosition) {
const monitor = await getCursorMonitor(cursorPoint)
if (!monitor) return
const { size, position, cursorPosition } = monitor
const { size, position } = monitor
const xRatio = (cursorPosition.x - position.x) / size.width
const yRatio = (cursorPosition.y - position.y) / size.height
const xRatio = (cursorPoint.x - position.x) / size.width
const yRatio = (cursorPoint.y - position.y) / size.height
for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) {
const { min, max } = live2d.getParameterRange(id)
@@ -108,7 +127,7 @@ export function useModel() {
const ratio = isXAxis ? xRatio : yRatio
let value = max - (ratio * (max - min))
if (isXAxis && catStore.mouseMirror) {
if (isXAxis && catStore.model.mouseMirror) {
value *= -1
}
@@ -116,26 +135,22 @@ export function useModel() {
}
}
function handleMouseDown(value: string[]) {
const params = {
ParamMouseLeftDown: value.includes('Left'),
ParamMouseRightDown: value.includes('Right'),
}
async function handleAxisChange(id: string, value: number) {
const { min, max } = live2d.getParameterRange(id)
for (const [id, pressed] of Object.entries(params)) {
const { min, max } = live2d.getParameterRange(id)
live2d.setParameterValue(id, pressed ? max : min)
}
live2d.setParameterValue(id, Math.max(min, value * max))
}
return {
backgroundImage,
modelSize,
handlePress,
handleRelease,
handleLoad,
handleDestroy,
handleResize,
handleKeyDown,
handleKeyChange,
handleMouseChange,
handleMouseMove,
handleMouseDown,
handleAxisChange,
}
}

View File

@@ -1,5 +1,6 @@
import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
import { range } from 'es-toolkit'
import { useI18n } from 'vue-i18n'
import { showWindow } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
@@ -7,23 +8,24 @@ import { isMac } from '@/utils/platform'
export function useSharedMenu() {
const catStore = useCatStore()
const { t } = useI18n()
const getScaleMenuItems = async () => {
const options = range(50, 151, 25)
const items = options.map((item) => {
return CheckMenuItem.new({
text: item === 100 ? '默认' : `${item}%`,
checked: catStore.scale === item,
text: `${item}%`,
checked: catStore.window.scale === item,
action: () => {
catStore.scale = item
catStore.window.scale = item
},
})
})
if (!options.includes(catStore.scale)) {
if (!options.includes(catStore.window.scale)) {
items.unshift(CheckMenuItem.new({
text: `${catStore.scale}%`,
text: `${catStore.window.scale}%`,
checked: true,
enabled: false,
}))
@@ -38,16 +40,16 @@ export function useSharedMenu() {
const items = options.map((item) => {
return CheckMenuItem.new({
text: `${item}%`,
checked: catStore.opacity === item,
checked: catStore.window.opacity === item,
action: () => {
catStore.opacity = item
catStore.window.opacity = item
},
})
})
if (!options.includes(catStore.opacity)) {
if (!options.includes(catStore.window.opacity)) {
items.unshift(CheckMenuItem.new({
text: `${catStore.opacity}%`,
text: `${catStore.window.opacity}%`,
checked: true,
enabled: false,
}))
@@ -59,30 +61,30 @@ export function useSharedMenu() {
const getSharedMenu = async () => {
return await Promise.all([
MenuItem.new({
text: '偏好设置...',
text: t('composables.useSharedMenu.labels.preference'),
accelerator: isMac ? 'Cmd+,' : '',
action: () => showWindow('preference'),
}),
MenuItem.new({
text: catStore.visible ? '隐藏猫咪' : '显示猫咪',
text: catStore.window.visible ? t('composables.useSharedMenu.labels.hideCat') : t('composables.useSharedMenu.labels.showCat'),
action: () => {
catStore.visible = !catStore.visible
catStore.window.visible = !catStore.window.visible
},
}),
PredefinedMenuItem.new({ item: 'Separator' }),
CheckMenuItem.new({
text: '窗口穿透',
checked: catStore.penetrable,
text: t('composables.useSharedMenu.labels.passThrough'),
checked: catStore.window.passThrough,
action: () => {
catStore.penetrable = !catStore.penetrable
catStore.window.passThrough = !catStore.window.passThrough
},
}),
Submenu.new({
text: '窗口尺寸',
text: t('composables.useSharedMenu.labels.windowSize'),
items: await getScaleMenuItems(),
}),
Submenu.new({
text: '不透明度',
text: t('composables.useSharedMenu.labels.opacity'),
items: await getOpacityMenuItems(),
}),
])

View File

@@ -8,7 +8,7 @@ import {
} from '@tauri-apps/plugin-global-shortcut'
import { ref, watch } from 'vue'
export function useTauriKeyPress(shortcut: Ref<string, string>, callback: ShortcutHandler) {
export function useTauriShortcut(shortcut: Ref<string, string>, callback: ShortcutHandler) {
const oldShortcut = ref(shortcut.value)
watch(shortcut, async (value) => {

View File

@@ -8,6 +8,8 @@ import { TrayIcon } from '@tauri-apps/api/tray'
import { openUrl } from '@tauri-apps/plugin-opener'
import { exit, relaunch } from '@tauri-apps/plugin-process'
import { watchDebounced } from '@vueuse/core'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { GITHUB_LINK, LISTEN_KEY } from '../constants'
import { showWindow } from '../plugins/window'
@@ -16,16 +18,23 @@ import { isMac } from '../utils/platform'
import { useSharedMenu } from './useSharedMenu'
import { useCatStore } from '@/stores/cat'
import { useGeneralStore } from '@/stores/general'
const TRAY_ID = 'BONGO_CAT_TRAY'
export function useTray() {
const catStore = useCatStore()
const generalStore = useGeneralStore()
const { getSharedMenu } = useSharedMenu()
const { t } = useI18n()
watchDebounced(() => catStore, () => {
watch([() => catStore.window.visible, () => catStore.window.passThrough, () => generalStore.appearance.language], () => {
updateTrayMenu()
}, { deep: true, debounce: 500 })
})
watchDebounced([() => catStore.window.scale, () => catStore.window.opacity], () => {
updateTrayMenu()
}, { debounce: 200 })
const createTray = async () => {
const tray = await getTrayById()
@@ -37,14 +46,15 @@ export function useTray() {
const menu = await getTrayMenu()
const icon = await resolveResource('assets/tray.png')
const path = isMac ? 'assets/tray-mac.png' : 'assets/tray.png'
const icon = await resolveResource(path)
const options: TrayIconOptions = {
menu,
icon,
id: TRAY_ID,
tooltip: `${appName} v${appVersion}`,
iconAsTemplate: false,
iconAsTemplate: true,
menuOnLeftClick: true,
}
@@ -62,7 +72,7 @@ export function useTray() {
...await getSharedMenu(),
PredefinedMenuItem.new({ item: 'Separator' }),
MenuItem.new({
text: '检查更新',
text: t('composables.useTray.checkUpdate'),
action: () => {
showWindow()
@@ -70,20 +80,20 @@ export function useTray() {
},
}),
MenuItem.new({
text: '开源地址',
text: t('composables.useTray.openSource'),
action: () => openUrl(GITHUB_LINK),
}),
PredefinedMenuItem.new({ item: 'Separator' }),
MenuItem.new({
text: `版本 ${appVersion}`,
text: `v${appVersion}`,
enabled: false,
}),
MenuItem.new({
text: '重启应用',
text: t('composables.useTray.restartApp'),
action: relaunch,
}),
MenuItem.new({
text: '退出应用',
text: t('composables.useTray.quitApp'),
accelerator: isMac ? 'Cmd+Q' : '',
action: () => exit(0),
}),

View File

@@ -0,0 +1,47 @@
import { PhysicalPosition } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { onMounted, ref, watch } from 'vue'
import { useCatStore } from '@/stores/cat'
import { getCursorMonitor } from '@/utils/monitor'
const appWindow = getCurrentWebviewWindow()
export function useWindowPosition() {
const catStore = useCatStore()
const isMounted = ref(false)
const setWindowPosition = async () => {
const monitor = await getCursorMonitor()
if (!monitor) return
const windowSize = await appWindow.outerSize()
switch (catStore.window.position) {
case 'topLeft':
return appWindow.setPosition(new PhysicalPosition(0, 0))
case 'topRight':
return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, 0))
case 'bottomLeft':
return appWindow.setPosition(new PhysicalPosition(0, monitor.size.height - windowSize.height))
default:
return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, monitor.size.height - windowSize.height))
}
}
onMounted(async () => {
await setWindowPosition()
isMounted.value = true
appWindow.onScaleChanged(setWindowPosition)
})
watch(() => catStore.window.position, setWindowPosition)
return {
isMounted,
setWindowPosition,
}
}

View File

@@ -1,12 +1,25 @@
export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'
export const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'
export const LISTEN_KEY = {
SHOW_WINDOW: 'show-window',
HIDE_WINDOW: 'hide-window',
DEVICE_CHANGED: 'device-changed',
UPDATE_APP: 'update-app',
GAMEPAD_CHANGED: 'gamepad-changed',
}
export const INVOKE_KEY = {
COPY_DIR: 'copy_dir',
START_DEVICE_LISTENING: 'start_device_listening',
START_GAMEPAD_LISTING: 'start_gamepad_listing',
STOP_GAMEPAD_LISTING: 'stop_gamepad_listing',
}
export const LANGUAGE = {
ZH_CN: 'zh-CN',
EN_US: 'en-US',
VI_VN: 'vi-VN',
PT_BR: 'pt-BR',
} as const

182
src/locales/en-US.json Normal file
View File

@@ -0,0 +1,182 @@
{
"pages": {
"main": {
"hints": {
"redrawing": "Redrawing..."
}
},
"preference": {
"title": "Preferences",
"cat": {
"title": "Cat",
"labels": {
"modelSettings": "Model Settings",
"mirrorMode": "Mirror Mode",
"singleMode": "Single Key Mode",
"mouseMirror": "Mouse Mirror",
"windowSettings": "Window Settings",
"passThrough": "Pass Through",
"alwaysOnTop": "Always on Top",
"windowSize": "Window Size",
"windowRadius": "Window Radius",
"opacity": "Opacity",
"autoReleaseDelay": "Auto Release Delay",
"hideOnHover": "Hide on Hover",
"position": "Window Position"
},
"hints": {
"mirrorMode": "When enabled, the model will be mirrored horizontally.",
"singleMode": "When enabled, only the last pressed key is displayed for each hand.",
"mouseMirror": "When enabled, the mouse will mirror the hand movement.",
"passThrough": "When enabled, clicks pass through the window without affecting it.",
"alwaysOnTop": "When enabled, the window stays above all other windows.",
"windowSize": "Move mouse to window edge, or hold Shift and right-drag to resize.",
"autoReleaseDelay": "On Windows, some system keys cannot capture release events and will auto-release after timeout.",
"hideOnHover": "When enabled, the window hides when mouse hovers over it.",
"position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes."
},
"options": {
"topLeft": "Top Left",
"topRight": "Top Right",
"bottomLeft": "Bottom Left",
"bottomRight": "Bottom Right"
}
},
"general": {
"title": "General",
"labels": {
"appSettings": "Application Settings",
"launchOnStartup": "Launch on Startup",
"showTaskbarIcon": "Show Taskbar Icon",
"appearanceSettings": "Appearance Settings",
"themeMode": "Theme Mode",
"language": "Language",
"updateSettings": "Update Settings",
"autoCheckUpdate": "Auto Check for Updates",
"permissionsSettings": "Permissions Settings",
"inputMonitoringPermission": "Input Monitoring Permission"
},
"options": {
"auto": "System",
"lightMode": "Light",
"darkMode": "Dark"
},
"hints": {
"showTaskbarIcon": "When enabled, the window can be captured via OBS Studio.",
"inputMonitoringPermission": "Enable input monitoring to receive keyboard and mouse events from the system.",
"inputMonitoringPermissionGuide": "If the permission is already enabled, select it and click the \"-\" button to remove it, then manually add it again and restart the app."
},
"status": {
"authorized": "Authorized",
"authorize": "Go to Enable"
},
"buttons": {
"openNow": "Open Now",
"openLater": "Open Later"
}
},
"model": {
"title": "Model",
"labels": {
"deleteModel": "Delete Model"
},
"hints": {
"deleteSuccess": "Deleted Successfully",
"deleteModel": "Are you sure you want to delete this model?",
"importSuccess": "Imported Successfully",
"clickOrDragToImport": "Click or drag here to import"
},
"tooltips": {
"createModel": "Create Model",
"convertModel": "Convert Model",
"moreModels": "More Models"
}
},
"shortcut": {
"title": "Shortcuts",
"labels": {
"toggleCat": "Toggle Cat",
"togglePreferences": "Toggle Preferences",
"mirrorMode": "Mirror Mode",
"passThrough": "Pass Through",
"alwaysOnTop": "Always on Top"
},
"hints": {
"toggleCat": "Toggle the visibility of the cat window.",
"togglePreferences": "Toggle the visibility of the preferences window.",
"mirrorMode": "Toggle the cat's mirror mode.",
"passThrough": "Toggle whether the cat window is pass-through.",
"alwaysOnTop": "Toggle whether the cat window stays on top."
}
},
"about": {
"title": "About",
"labels": {
"aboutApp": "About App",
"appLog": "App Logs",
"appInfo": "App Info",
"openSource": "Open Source"
},
"hints": {
"appInfo": "Copy app information and provide it to bug issue.",
"copySuccess": "Copied Successfully"
},
"buttons": {
"checkUpdate": "Check for Updates",
"copy": "Copy",
"feedbackIssues": "Feedback Issues",
"viewLog": "View Logs"
}
}
}
},
"components": {
"proShortcut": {
"hints": {
"pressRecordShortcut": "Press to record shortcut",
"clickRecordShortcut": "Click to record shortcut"
}
},
"updateApp": {
"title": "New Version Found 🥳",
"labels": {
"updateVersion": "Update Version: ",
"updateTime": "Update Time: ",
"changelog": "Changelog: "
},
"hints": {
"checkingUpdates": "Checking for updates...",
"alreadyLatest": "Already on the latest version 🎉"
},
"buttons": {
"updateNow": "Update Now",
"updateLater": "Update Later"
}
}
},
"composables": {
"useSharedMenu": {
"labels": {
"preference": "Preferences...",
"hideCat": "Hide Cat",
"showCat": "Show Cat",
"passThrough": "Pass Through",
"windowSize": "Window Size",
"opacity": "Opacity"
}
},
"useTray": {
"checkUpdate": "Check for Updates",
"openSource": "Open Source",
"restartApp": "Restart App",
"quitApp": "Quit App"
}
},
"utils": {
"live2d": {
"hints": {
"notFound": "Model master configuration file not found, please ensure the model files are complete."
}
}
}
}

38
src/locales/index.ts Normal file
View File

@@ -0,0 +1,38 @@
import type { Language } from '@/stores/general'
import type { Locale as AntdLocale } from 'ant-design-vue/es/locale'
import antdEnUS from 'ant-design-vue/locale/en_US'
import antdPtBR from 'ant-design-vue/locale/pt_BR'
import antdViVN from 'ant-design-vue/locale/vi_VN'
import antdZhCN from 'ant-design-vue/locale/zh_CN'
import { createI18n } from 'vue-i18n'
import enUS from './en-US.json'
import ptBR from './pt-BR.json'
import viVN from './vi-VN.json'
import zhCN from './zh-CN.json'
import { LANGUAGE } from '@/constants'
export const i18n = createI18n({
legacy: false,
locale: LANGUAGE.EN_US,
fallbackLocale: LANGUAGE.EN_US,
messages: {
[LANGUAGE.ZH_CN]: zhCN,
[LANGUAGE.EN_US]: enUS,
[LANGUAGE.VI_VN]: viVN,
[LANGUAGE.PT_BR]: ptBR,
},
})
export function getAntdLocale(language: Language = LANGUAGE.EN_US) {
const antdLanguage: Record<Language, AntdLocale> = {
[LANGUAGE.ZH_CN]: antdZhCN,
[LANGUAGE.EN_US]: antdEnUS,
[LANGUAGE.VI_VN]: antdViVN,
[LANGUAGE.PT_BR]: antdPtBR,
}
return antdLanguage[language]
}

182
src/locales/pt-BR.json Normal file
View File

@@ -0,0 +1,182 @@
{
"pages": {
"main": {
"hints": {
"redrawing": "Redimensionando..."
}
},
"preference": {
"title": "Preferências",
"cat": {
"title": "Gato",
"labels": {
"modelSettings": "Configurações do Modelo",
"mirrorMode": "Modo Espelho",
"singleMode": "Mostrar Apenas Última Tecla",
"mouseMirror": "Espelho do Mouse",
"windowSettings": "Configurações da Janela",
"passThrough": "Janela Transparente",
"alwaysOnTop": "Sempre no Topo",
"windowSize": "Tamanho da Janela",
"windowRadius": "Raio da Janela",
"opacity": "Opacidade",
"autoReleaseDelay": "Atraso de Liberação Automática",
"hideOnHover": "Ocultar ao Passar o Mouse",
"position": "Posição da Janela"
},
"hints": {
"mirrorMode": "Quando ativado, o modelo será invertido horizontalmente.",
"singleMode": "Quando ativado, apenas a última tecla pressionada em cada mão é exibida (evita mostrar múltiplas mãos ao pressionar várias teclas ao mesmo tempo).",
"mouseMirror": "Quando ativado, o mouse espelhará o movimento da mão.",
"passThrough": "Quando ativado, a janela não afetará operações em outros aplicativos.",
"alwaysOnTop": "Quando ativado, a janela sempre ficará acima de outros aplicativos.",
"windowSize": "Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.",
"autoReleaseDelay": "Devido ao Windows não capturar eventos de liberação de certas teclas de nível do sistema, elas serão automaticamente tratadas como liberadas após um tempo limite.",
"hideOnHover": "Quando ativado, a janela será ocultada quando o mouse passar sobre ela.",
"position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado."
},
"options": {
"topLeft": "Canto Superior Esquerdo",
"topRight": "Canto Superior Direito",
"bottomLeft": "Canto Inferior Esquerdo",
"bottomRight": "Canto Inferior Direito"
}
},
"general": {
"title": "Geral",
"labels": {
"appSettings": "Configurações do aplicativo",
"launchOnStartup": "Iniciar na inicialização",
"showTaskbarIcon": "Mostrar ícone na barra de tarefas",
"appearanceSettings": "Configurações de aparência",
"themeMode": "Tema",
"language": "Idiomas",
"updateSettings": "Configurações de atualização",
"autoCheckUpdate": "Verificar atualizações automaticamente",
"permissionsSettings": "Configurações de Permissões",
"inputMonitoringPermission": "Permissão de Monitoramento de Entrada"
},
"options": {
"auto": "Sistema",
"lightMode": "Claro",
"darkMode": "Escuro"
},
"hints": {
"showTaskbarIcon": "Uma vez ativado, você pode capturar a janela via OBS Studio.",
"inputMonitoringPermission": "Ative a permissão de monitoramento de entrada para receber eventos de teclado e mouse do sistema para responder às suas ações.",
"inputMonitoringPermissionGuide": "Se a permissão já estiver ativada, primeiro selecione-a e clique no botão \"-\" para removê-la. Em seguida, adicione-a novamente manualmente e reinicie o aplicativo para garantir que a permissão entre em vigor."
},
"status": {
"authorized": "Autorizado",
"authorize": "Ir para Ativar"
},
"buttons": {
"openNow": "Abrir Agora",
"openLater": "Abrir Mais Tarde"
}
},
"model": {
"title": "Modelo",
"labels": {
"deleteModel": "Excluir modelo"
},
"hints": {
"deleteSuccess": "Excluído com sucesso",
"deleteModel": "Tem certeza de que deseja excluir este modelo?",
"importSuccess": "Importação bem-sucedida",
"clickOrDragToImport": "Clique ou arraste para importar"
},
"tooltips": {
"createModel": "Criar modelo",
"convertModel": "Converter modelo",
"moreModels": "Mais modelos"
}
},
"shortcut": {
"title": "Atalhos",
"labels": {
"toggleCat": "Mostrar/Ocultar Gato",
"togglePreferences": "Abrir Preferências",
"mirrorMode": "Modo Espelho",
"passThrough": "Janela Transparente",
"alwaysOnTop": "Sempre no Topo"
},
"hints": {
"toggleCat": "Alternar a visibilidade da janela do gato.",
"togglePreferences": "Alternar a visibilidade da janela de preferências.",
"mirrorMode": "Alternar o modo espelho do gato.",
"passThrough": "Alternar se a janela do gato é clicável.",
"alwaysOnTop": "Alternar se a janela do gato permanece no topo."
}
},
"about": {
"title": "Sobre",
"labels": {
"aboutApp": "Sobre o Aplicativo",
"appLog": "Logs do Aplicativo",
"appInfo": "Informações do Aplicativo",
"openSource": "Código Aberto"
},
"hints": {
"appInfo": "Copiar informações do aplicativo para incluir em relatórios de bugs.",
"copySuccess": "Copiado com sucesso"
},
"buttons": {
"checkUpdate": "Verificar atualizações",
"copy": "Copiar",
"feedbackIssues": "Reportar Problema",
"viewLog": "Ver Logs"
}
}
}
},
"components": {
"proShortcut": {
"hints": {
"pressRecordShortcut": "Pressione as teclas para gravar atalho",
"clickRecordShortcut": "Clique para gravar atalho"
}
},
"updateApp": {
"title": "Nova versão encontrada 🥳",
"labels": {
"updateVersion": "Versão: ",
"updateTime": "Hora da atualização: ",
"changelog": "Registro de alterações: "
},
"hints": {
"checkingUpdates": "Verificando atualizações...",
"alreadyLatest": "Você já está na versão mais recente 🎉"
},
"buttons": {
"updateNow": "Atualizar Agora",
"updateLater": "Atualizar mais tarde"
}
}
},
"composables": {
"useSharedMenu": {
"labels": {
"preference": "Preferências...",
"hideCat": "Ocultar Gato",
"showCat": "Mostrar Gato",
"passThrough": "Janela Transparente",
"windowSize": "Tamanho da Janela",
"opacity": "Opacidade"
}
},
"useTray": {
"checkUpdate": "Verificar atualizações",
"openSource": "Código Fonte",
"restartApp": "Reiniciar",
"quitApp": "Sair"
}
},
"utils": {
"live2d": {
"hints": {
"notFound": "Arquivo de configuração principal do modelo não encontrado. Verifique se os arquivos do modelo estão completos."
}
}
}
}

182
src/locales/vi-VN.json Normal file
View File

@@ -0,0 +1,182 @@
{
"pages": {
"main": {
"hints": {
"redrawing": "Đang đổi kích thước..."
}
},
"preference": {
"title": "Tùy chỉnh",
"cat": {
"title": "Mèo",
"labels": {
"modelSettings": "Cài đặt Mô hình",
"mirrorMode": "Chế độ gương",
"singleMode": "Chỉ hiển thị phím cuối cùng",
"mouseMirror": "Phản chiếu chuột",
"windowSettings": "Cài đặt Cửa sổ",
"passThrough": "Click xuyên",
"alwaysOnTop": "Luôn trên cùng",
"windowSize": "Kích thước",
"windowRadius": "Độ bo tròn cửa sổ",
"opacity": "Độ mờ",
"autoReleaseDelay": "Độ trễ tự động nhả phím",
"hideOnHover": "Ẩn khi di chuột",
"position": "Vị trí cửa sổ"
},
"hints": {
"mirrorMode": "Bật để lật ngang mô hình.",
"singleMode": "Khi bật, mỗi tay mèo chỉ hiển thị phím vừa nhấn cuối cùng (tránh hiện nhiều tay khi nhấn nhiều phím cùng lúc).",
"mouseMirror": "Khi bật, chuột của mô hình sẽ phản chiếu theo chuyển động chuột thực tế.",
"passThrough": "Bật để cửa sổ không ảnh hưởng đến thao tác trên ứng dụng khác.",
"alwaysOnTop": "Bật để cửa sổ luôn nằm trên ứng dụng khác.",
"windowSize": "Di chuyển chuột đến mép cửa sổ hoặc giữ Shift và kéo chuột phải để thay đổi kích thước.",
"autoReleaseDelay": "Do Windows không bắt được sự kiện nhả của một số phím hệ thống, các phím đó sẽ được tự động xem như đã nhả sau khi hết thời gian chờ.",
"hideOnHover": "Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào.",
"position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi."
},
"options": {
"topLeft": "Góc trên cùng bên trái",
"topRight": "Góc trên cùng bên phải",
"bottomLeft": "Góc dưới cùng bên trái",
"bottomRight": "Góc dưới cùng bên phải"
}
},
"general": {
"title": "Chung",
"labels": {
"appSettings": "Cài đặt ứng dụng",
"launchOnStartup": "Khởi động cùng hệ thống",
"showTaskbarIcon": "Hiện biểu tượng trên thanh tác vụ (icon taskbar)",
"appearanceSettings": "Cài đặt giao diện",
"themeMode": "Giao diện",
"language": "Ngôn ngữ",
"updateSettings": "Cài đặt cập nhật",
"autoCheckUpdate": "Tự động kiểm tra cập nhật",
"permissionsSettings": "Cài đặt quyền",
"inputMonitoringPermission": "Quyền giám sát đầu vào"
},
"options": {
"auto": "Theo hệ thống",
"lightMode": "Sáng",
"darkMode": "Tối"
},
"hints": {
"showTaskbarIcon": "Bật để có thể quay cửa sổ qua OBS.",
"inputMonitoringPermission": "Bật quyền giám sát để nhận sự kiện bàn phím và chuột từ hệ thống nhằm phản hồi thao tác của bạn.",
"inputMonitoringPermissionGuide": "Nếu quyền đã được bật, hãy chọn nó và nhấn nút \"-\" để xóa. Sau đó thêm lại thủ công và khởi động lại ứng dụng để đảm bảo quyền được áp dụng."
},
"status": {
"authorized": "Đã cấp quyền",
"authorize": "Đi đến Bật"
},
"buttons": {
"openNow": "Mở ngay",
"openLater": "Mở sau"
}
},
"model": {
"title": "Mô hình",
"labels": {
"deleteModel": "Xóa mô hình"
},
"hints": {
"deleteSuccess": "Xóa thành công",
"deleteModel": "Bạn chắc muốn xóa mô hình này?",
"importSuccess": "Nhập thành công",
"clickOrDragToImport": "Nhấp hoặc kéo tệp vào đây"
},
"tooltips": {
"createModel": "Tạo mô hình",
"convertModel": "Chuyển đổi mô hình",
"moreModels": "Khám phá mô hình khác"
}
},
"shortcut": {
"title": "Phím tắt",
"labels": {
"toggleCat": "Ẩn/Hiện Mèo",
"togglePreferences": "Mở Tùy chỉnh",
"mirrorMode": "Chế độ gương",
"passThrough": "Click xuyên",
"alwaysOnTop": "Luôn trên cùng"
},
"hints": {
"toggleCat": "Bật/Tắt cửa sổ mèo.",
"togglePreferences": "Bật/Tắt cửa sổ tùy chỉnh.",
"mirrorMode": "Bật/Tắt chế độ gương.",
"passThrough": "Bật/Tắt cho phép click xuyên cửa sổ mèo.",
"alwaysOnTop": "Bật/Tắt luôn giữ cửa sổ mèo trên cùng."
}
},
"about": {
"title": "Giới thiệu",
"labels": {
"aboutApp": "Thông tin ứng dụng",
"appLog": "Nhật ký ứng dụng",
"appInfo": "Thông tin ứng dụng",
"openSource": "Mã nguồn"
},
"hints": {
"appInfo": "Sao chép thông tin để gửi bug.",
"copySuccess": "Đã sao chép"
},
"buttons": {
"checkUpdate": "Kiểm tra cập nhật",
"copy": "Sao chép",
"feedbackIssues": "Báo lỗi",
"viewLog": "Xem nhật ký"
}
}
}
},
"components": {
"proShortcut": {
"hints": {
"pressRecordShortcut": "Nhấn phím/tổ hợp phím để ghi",
"clickRecordShortcut": "Click để ghi phím tắt"
}
},
"updateApp": {
"title": "Đã tìm thấy phiên bản mới 🥳",
"labels": {
"updateVersion": "Phiên bản: ",
"updateTime": "Thời gian cập nhật: ",
"changelog": "Nhật ký thay đổi: "
},
"hints": {
"checkingUpdates": "Đang kiểm tra cập nhật...",
"alreadyLatest": "Bạn đang dùng phiên bản mới nhất 🎉"
},
"buttons": {
"updateNow": "Cập nhật ngay",
"updateLater": "Để sau"
}
}
},
"composables": {
"useSharedMenu": {
"labels": {
"preference": "Tùy chỉnh...",
"hideCat": "Ẩn Mèo",
"showCat": "Hiện Mèo",
"passThrough": "Click xuyên",
"windowSize": "Kích thước",
"opacity": "Độ mờ"
}
},
"useTray": {
"checkUpdate": "Kiểm tra cập nhật",
"openSource": "Mã nguồn",
"restartApp": "Khởi động lại",
"quitApp": "Thoát"
}
},
"utils": {
"live2d": {
"hints": {
"notFound": "Không tìm thấy tệp cấu hình chính của mô hình, vui lòng xác nhận các tệp mô hình có đầy đủ không."
}
}
}
}

182
src/locales/zh-CN.json Normal file
View File

@@ -0,0 +1,182 @@
{
"pages": {
"main": {
"hints": {
"redrawing": "重绘中..."
}
},
"preference": {
"title": "偏好设置",
"cat": {
"title": "猫咪设置",
"labels": {
"modelSettings": "模型设置",
"mirrorMode": "镜像模式",
"singleMode": "单键模式",
"mouseMirror": "鼠标镜像",
"windowSettings": "窗口设置",
"passThrough": "窗口穿透",
"alwaysOnTop": "窗口置顶",
"windowSize": "窗口尺寸",
"windowRadius": "窗口圆角",
"opacity": "不透明度",
"autoReleaseDelay": "按键自动释放延迟",
"hideOnHover": "鼠标移入隐藏",
"position": "窗口位置"
},
"hints": {
"mirrorMode": "启用后,模型将水平镜像翻转。",
"singleMode": "启用后,每只手只显示最后按下的一个按键。",
"mouseMirror": "启用后,鼠标将镜像跟随手部移动。",
"passThrough": "启用后,窗口不影响对其他应用程序的操作。",
"alwaysOnTop": "启用后,窗口始终显示在其他应用程序上方。",
"windowSize": "将鼠标移至窗口边缘,或按住 Shift 并右键拖动,也可以调整窗口大小。",
"autoReleaseDelay": "由于 Windows 下部分系统级按键无法捕获释放事件,超时后将自动视为已释放。",
"hideOnHover": "启用后,鼠标悬停在窗口上时,窗口会隐藏。",
"position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。"
},
"options": {
"topLeft": "左上角",
"topRight": "右上角",
"bottomLeft": "左下角",
"bottomRight": "右下角"
}
},
"general": {
"title": "通用设置",
"labels": {
"appSettings": "应用设置",
"launchOnStartup": "开机自启动",
"showTaskbarIcon": "显示任务栏图标",
"appearanceSettings": "外观设置",
"themeMode": "主题模式",
"language": "语言",
"updateSettings": "更新设置",
"autoCheckUpdate": "自动检查更新",
"permissionsSettings": "权限设置",
"inputMonitoringPermission": "输入监控权限"
},
"options": {
"auto": "跟随系统",
"lightMode": "亮色模式",
"darkMode": "暗色模式"
},
"hints": {
"showTaskbarIcon": "启用后,即可通过 OBS Studio 捕获窗口。",
"inputMonitoringPermission": "开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。",
"inputMonitoringPermissionGuide": "如果权限已开启,请先选中并点击“-”按钮将其删除,然后重新手动添加,最后重启应用以确保权限生效。"
},
"status": {
"authorized": "已授权",
"authorize": "去授权"
},
"buttons": {
"openNow": "前往开启",
"openLater": "稍后开启"
}
},
"model": {
"title": "模型管理",
"labels": {
"deleteModel": "删除模型"
},
"hints": {
"deleteSuccess": "删除成功",
"deleteModel": "你确定要删除此模型吗?",
"importSuccess": "导入成功",
"clickOrDragToImport": "点击或拖动至此区域导入"
},
"tooltips": {
"createModel": "制作模型",
"convertModel": "转换模型",
"moreModels": "更多模型"
}
},
"shortcut": {
"title": "快捷键",
"labels": {
"toggleCat": "打开猫咪",
"togglePreferences": "打开偏好设置",
"mirrorMode": "镜像模式",
"passThrough": "窗口穿透",
"alwaysOnTop": "窗口置顶"
},
"hints": {
"toggleCat": "切换猫咪窗口的显示与隐藏。",
"togglePreferences": "切换偏好设置窗口的显示与隐藏。",
"mirrorMode": "切换猫咪的镜像模式。",
"passThrough": "切换猫咪窗口是否可穿透。",
"alwaysOnTop": "切换猫咪窗口是否置顶。"
}
},
"about": {
"title": "关于",
"labels": {
"aboutApp": "关于软件",
"appLog": "软件日志",
"appInfo": "软件信息",
"openSource": "开源地址"
},
"hints": {
"appInfo": "复制软件信息并提供给 Bug Issue。",
"copySuccess": "复制成功"
},
"buttons": {
"checkUpdate": "检查更新",
"copy": "复制",
"feedbackIssues": "反馈问题",
"viewLog": "查看日志"
}
}
}
},
"components": {
"proShortcut": {
"hints": {
"pressRecordShortcut": "按下录制快捷键",
"clickRecordShortcut": "点击录制快捷键"
}
},
"updateApp": {
"title": "发现新版本🥳",
"labels": {
"updateVersion": "更新版本:",
"updateTime": "更新时间:",
"changelog": "更新日志:"
},
"hints": {
"checkingUpdates": "正在检查更新...",
"alreadyLatest": "当前已是最新版本🎉"
},
"buttons": {
"updateNow": "立即更新",
"updateLater": "稍后更新"
}
}
},
"composables": {
"useSharedMenu": {
"labels": {
"preference": "偏好设置...",
"hideCat": "隐藏猫咪",
"showCat": "显示猫咪",
"passThrough": "窗口穿透",
"windowSize": "窗口尺寸",
"opacity": "不透明度"
}
},
"useTray": {
"checkUpdate": "检查更新",
"openSource": "开源地址",
"restartApp": "重启应用",
"quitApp": "退出应用"
}
},
"utils": {
"live2d": {
"hints": {
"notFound": "未找到模型主配置文件,请确认模型文件是否完整。"
}
}
}
}

View File

@@ -3,7 +3,9 @@ import { createPinia } from 'pinia'
import { createApp } from 'vue'
import App from './App.vue'
import { i18n } from './locales'
import router from './router'
import 'virtual:uno.css'
import 'ant-design-vue/dist/reset.css'
import './assets/css/global.scss'
@@ -11,4 +13,4 @@ import './assets/css/global.scss'
const pinia = createPinia()
pinia.use(createPlugin({ saveOnChange: true }))
createApp(App).use(router).use(pinia).mount('#app')
createApp(App).use(router).use(pinia).use(i18n).mount('#app')

View File

@@ -1,69 +1,135 @@
<script setup lang="ts">
import { convertFileSrc } from '@tauri-apps/api/core'
import { PhysicalSize } from '@tauri-apps/api/dpi'
import { Menu } from '@tauri-apps/api/menu'
import { sep } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { exists, readDir } from '@tauri-apps/plugin-fs'
import { useDebounceFn, useEventListener } from '@vueuse/core'
import { onUnmounted, ref, watch } from 'vue'
import { round } from 'es-toolkit'
import { nth } from 'es-toolkit/compat'
import { onMounted, onUnmounted, ref, watch } from 'vue'
import { useDevice } from '@/composables/useDevice'
import { useGamepad } from '@/composables/useGamepad'
import { useModel } from '@/composables/useModel'
import { useSharedMenu } from '@/composables/useSharedMenu'
import { hideWindow, setAlwaysOnTop, showWindow } from '@/plugins/window'
import { useWindowPosition } from '@/composables/useWindowPosition'
import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
import { useGeneralStore } from '@/stores/general.ts'
import { useModelStore } from '@/stores/model'
import { isImage } from '@/utils/is'
import { join } from '@/utils/path'
import { clearObject } from '@/utils/shared'
const { startListening } = useDevice()
const appWindow = getCurrentWebviewWindow()
const { pressedMouses, mousePosition, pressedLeftKeys, pressedRightKeys } = useDevice()
const { backgroundImage, handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel()
const { modelSize, handleLoad, handleDestroy, handleResize, handleKeyChange } = useModel()
const catStore = useCatStore()
const { getSharedMenu } = useSharedMenu()
const modelStore = useModelStore()
const generalStore = useGeneralStore()
const resizing = ref(false)
const backgroundImagePath = ref<string>()
const { stickActive } = useGamepad()
const { isMounted, setWindowPosition } = useWindowPosition()
onMounted(startListening)
onUnmounted(handleDestroy)
const handleDebounceResize = useDebounceFn(async () => {
const debouncedResize = useDebounceFn(async () => {
await handleResize()
await setWindowPosition()
resizing.value = false
}, 100)
useEventListener('resize', () => {
resizing.value = true
handleDebounceResize()
debouncedResize()
})
watch(pressedMouses, handleMouseDown)
watch(() => modelStore.currentModel, async (model) => {
if (!model) return
watch(mousePosition, handleMouseMove)
await handleLoad()
watch(pressedLeftKeys, (keys) => {
handleKeyDown('left', keys.length > 0)
})
const path = join(model.path, 'resources', 'background.png')
watch(pressedRightKeys, (keys) => {
handleKeyDown('right', keys.length > 0)
})
const existed = await exists(path)
watch(() => catStore.visible, async (value) => {
backgroundImagePath.value = existed ? convertFileSrc(path) : void 0
clearObject([modelStore.supportKeys, modelStore.pressedKeys])
const resourcePath = join(model.path, 'resources')
const groups = ['left-keys', 'right-keys']
for await (const groupName of groups) {
const groupDir = join(resourcePath, groupName)
const files = await readDir(groupDir).catch(() => [])
const imageFiles = files.filter(file => isImage(file.name))
for (const file of imageFiles) {
const fileName = file.name.split('.')[0]
modelStore.supportKeys[fileName] = join(groupDir, file.name)
}
}
setWindowPosition()
}, { deep: true, immediate: true })
watch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => {
if (!modelSize) return
const { width, height } = modelSize
appWindow.setSize(
new PhysicalSize({
width: Math.round(width * (scale / 100)),
height: Math.round(height * (scale / 100)),
}),
)
}, { immediate: true })
watch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => {
const dirs = Object.values(keys).map((path) => {
return nth(path.split(sep()), -2)!
})
const hasLeft = dirs.some(dir => dir.startsWith('left'))
const hasRight = dirs.some(dir => dir.startsWith('right'))
handleKeyChange(true, stickActive.left || hasLeft)
handleKeyChange(false, stickActive.right || hasRight)
}, { deep: true })
watch(() => catStore.window.visible, async (value) => {
value ? showWindow() : hideWindow()
})
watch(() => catStore.penetrable, (value) => {
watch(() => catStore.window.passThrough, (value) => {
appWindow.setIgnoreCursorEvents(value)
}, { immediate: true })
watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
watch(() => catStore.window.alwaysOnTop, setAlwaysOnTop, { immediate: true })
function handleWindowDrag() {
watch(() => generalStore.app.taskbarVisible, setTaskbarVisibility, { immediate: true })
function handleMouseDown() {
appWindow.startDragging()
}
async function handleContextmenu(event: MouseEvent) {
event.preventDefault()
if (event.shiftKey) return
const menu = await Menu.new({
items: await getSharedMenu(),
})
@@ -71,41 +137,52 @@ async function handleContextmenu(event: MouseEvent) {
menu.popup()
}
function resolveImagePath(key: string, side: 'left' | 'right' = 'left') {
return convertFileSrc(join(modelStore.currentModel!.path, 'resources', `${side}-keys`, `${key}.png`))
function handleMouseMove(event: MouseEvent) {
const { buttons, shiftKey, movementX, movementY } = event
if (buttons !== 2 || !shiftKey) return
const delta = (movementX + movementY) * 0.5
const nextScale = Math.max(10, Math.min(catStore.window.scale + delta, 500))
catStore.window.scale = round(nextScale)
}
</script>
<template>
<div
v-show="isMounted"
class="relative size-screen overflow-hidden children:(absolute size-full)"
:class="[catStore.mirrorMode ? '-scale-x-100' : 'scale-x-100']"
:style="{ opacity: catStore.opacity / 100 }"
:class="{ '-scale-x-100': catStore.model.mirror }"
:style="{
opacity: catStore.window.opacity / 100,
borderRadius: `${catStore.window.radius}%`,
}"
@contextmenu="handleContextmenu"
@mousedown="handleWindowDrag"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
>
<img :src="backgroundImage">
<img
v-if="backgroundImagePath"
class="object-cover"
:src="backgroundImagePath"
>
<canvas id="live2dCanvas" />
<img
v-for="key in pressedLeftKeys"
:key="key"
:src="resolveImagePath(key)"
>
<img
v-for="key in pressedRightKeys"
:key="key"
:src="resolveImagePath(key, 'right')"
v-for="path in modelStore.pressedKeys"
:key="path"
class="object-cover"
:src="convertFileSrc(path)"
>
<div
v-show="resizing"
class="flex items-center justify-center bg-black"
>
<span class="text-center text-5xl text-white">
重绘中...
<span class="text-center text-[10vw] text-white">
{{ $t('pages.main.hints.redrawing') }}
</span>
</div>
</div>

View File

@@ -7,6 +7,7 @@ import { openPath, openUrl } from '@tauri-apps/plugin-opener'
import { arch, platform, version } from '@tauri-apps/plugin-os'
import { Button, message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
@@ -15,6 +16,7 @@ import { useAppStore } from '@/stores/app'
const appStore = useAppStore()
const logDir = ref('')
const { t } = useI18n()
onMounted(async () => {
logDir.value = await appLogDir()
@@ -36,7 +38,7 @@ async function copyInfo() {
await writeText(JSON.stringify(info, null, 2))
message.success('复制成功')
message.success(t('pages.preference.about.hints.copySuccess'))
}
function feedbackIssue() {
@@ -45,16 +47,16 @@ function feedbackIssue() {
</script>
<template>
<ProList title="关于软件">
<ProList :title="$t('pages.preference.about.labels.aboutApp')">
<ProListItem
:description="`版本:v${appStore.version}`"
:description="`v${appStore.version}`"
:title="appStore.name"
>
<Button
type="primary"
@click="handleUpdate"
>
检查更新
{{ $t('pages.preference.about.buttons.checkUpdate') }}
</Button>
<template #icon>
@@ -68,20 +70,20 @@ function feedbackIssue() {
</ProListItem>
<ProListItem
description="复制软件信息并提供给 Bug Issue"
title="软件信息"
:description="$t('pages.preference.about.hints.appInfo')"
:title="$t('pages.preference.about.labels.appInfo')"
>
<Button @click="copyInfo">
复制
{{ $t('pages.preference.about.buttons.copy') }}
</Button>
</ProListItem>
<ProListItem title="开源地址">
<ProListItem :title="$t('pages.preference.about.labels.openSource')">
<Button
danger
@click="feedbackIssue"
>
反馈问题
{{ $t('pages.preference.about.buttons.feedbackIssues') }}
</Button>
<template #description>
@@ -93,10 +95,10 @@ function feedbackIssue() {
<ProListItem
:description="logDir"
title="软件日志"
:title="$t('pages.preference.about.labels.appLog')"
>
<Button @click="openPath(logDir)">
查看日志
{{ $t('pages.preference.about.buttons.viewLog') }}
</Button>
</ProListItem>
</ProList>

View File

@@ -0,0 +1,30 @@
<script setup lang="ts">
import { Select, SelectOption } from 'ant-design-vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useCatStore } from '@/stores/cat'
const catStore = useCatStore()
</script>
<template>
<ProListItem
:description="$t('pages.preference.cat.hints.position')"
:title="$t('pages.preference.cat.labels.position')"
>
<Select v-model:value="catStore.window.position">
<SelectOption value="bottomRight">
{{ $t('pages.preference.cat.options.bottomRight') }}
</SelectOption>
<SelectOption value="bottomLeft">
{{ $t('pages.preference.cat.options.bottomLeft') }}
</SelectOption>
<SelectOption value="topLeft">
{{ $t('pages.preference.cat.options.topLeft') }}
</SelectOption>
<SelectOption value="topRight">
{{ $t('pages.preference.cat.options.topRight') }}
</SelectOption>
</Select>
</ProListItem>
</template>

View File

@@ -1,84 +1,108 @@
<script setup lang="ts">
import { Slider, Switch } from 'ant-design-vue'
import { InputNumber, Slider, Switch } from 'ant-design-vue'
import Position from './components/position/index.vue'
import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useCatStore } from '@/stores/cat'
import { isWindows } from '@/utils/platform'
const catStore = useCatStore()
function scaleFormatter(value?: number) {
return value === 100 ? '默认' : `${value}%`
}
function opacityFormatter(value?: number) {
return `${value}%`
}
</script>
<template>
<ProList title="模型设置">
<ProList :title="$t('pages.preference.cat.labels.modelSettings')">
<ProListItem
description="启用后,模型将水平镜像翻转"
title="镜像模式"
:description="$t('pages.preference.cat.hints.mirrorMode')"
:title="$t('pages.preference.cat.labels.mirrorMode')"
>
<Switch v-model:checked="catStore.mirrorMode" />
<Switch v-model:checked="catStore.model.mirror" />
</ProListItem>
<ProListItem
description="启用后,每只手只显示最后按下的一个按键"
title="单键模式"
:description="$t('pages.preference.cat.hints.singleMode')"
:title="$t('pages.preference.cat.labels.singleMode')"
>
<Switch v-model:checked="catStore.singleMode" />
<Switch v-model:checked="catStore.model.single" />
</ProListItem>
<ProListItem
description="启用后,鼠标将镜像跟随手部移动"
title="鼠标镜像"
:description="$t('pages.preference.cat.hints.mouseMirror')"
:title="$t('pages.preference.cat.labels.mouseMirror')"
>
<Switch v-model:checked="catStore.mouseMirror" />
<Switch v-model:checked="catStore.model.mouseMirror" />
</ProListItem>
<ProListItem
v-if="isWindows"
:description="$t('pages.preference.cat.hints.autoReleaseDelay')"
:title="$t('pages.preference.cat.labels.autoReleaseDelay')"
>
<InputNumber
v-model:value="catStore.model.autoReleaseDelay"
addon-after="s"
class="w-28"
/>
</ProListItem>
</ProList>
<ProList title="窗口设置">
<ProList :title="$t('pages.preference.cat.labels.windowSettings')">
<Position />
<ProListItem
description="启用后,窗口不影响对其他应用程序的操作"
title="窗口穿透"
:description="$t('pages.preference.cat.hints.passThrough')"
:title="$t('pages.preference.cat.labels.passThrough')"
>
<Switch v-model:checked="catStore.penetrable" />
<Switch v-model:checked="catStore.window.passThrough" />
</ProListItem>
<ProListItem
description="启用后,窗口始终显示在其他应用程序上方"
title="窗口置顶"
:description="$t('pages.preference.cat.hints.alwaysOnTop')"
:title="$t('pages.preference.cat.labels.alwaysOnTop')"
>
<Switch v-model:checked="catStore.alwaysOnTop" />
<Switch v-model:checked="catStore.window.alwaysOnTop" />
</ProListItem>
<ProListItem
description="将鼠标移动到窗口边缘后,也可以拖动调整窗口尺寸"
title="窗口尺寸"
vertical
:description="$t('pages.preference.cat.hints.hideOnHover')"
:title="$t('pages.preference.cat.labels.hideOnHover')"
>
<Slider
v-model:value="catStore.scale"
class="m-0!"
:max="150"
:min="50"
:tip-formatter="scaleFormatter"
<Switch v-model:checked="catStore.window.hideOnHover" />
</ProListItem>
<ProListItem
:description="$t('pages.preference.cat.hints.windowSize')"
:title="$t('pages.preference.cat.labels.windowSize')"
>
<InputNumber
v-model:value="catStore.window.scale"
addon-after="%"
class="w-28"
:max="500"
:min="1"
/>
</ProListItem>
<ProListItem :title="$t('pages.preference.cat.labels.windowRadius')">
<InputNumber
v-model:value="catStore.window.radius"
addon-after="%"
class="w-28"
:min="0"
/>
</ProListItem>
<ProListItem
title="不透明度"
:title="$t('pages.preference.cat.labels.opacity')"
vertical
>
<Slider
v-model:value="catStore.opacity"
class="m-0!"
v-model:value="catStore.window.opacity"
class="m-[0]!"
:max="100"
:min="10"
:tip-formatter="opacityFormatter"
:tip-formatter="(value) => `${value}%`"
/>
</ProListItem>
</ProList>

View File

@@ -1,15 +1,17 @@
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { message } from '@tauri-apps/plugin-dialog'
import { confirm } from '@tauri-apps/plugin-dialog'
import { Space } from 'ant-design-vue'
import { checkInputMonitoringPermission, requestInputMonitoringPermission } from 'tauri-plugin-macos-permissions-api'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { isMac } from '@/utils/platform'
const authorized = ref(false)
const { t } = useI18n()
onMounted(async () => {
authorized.value = await checkInputMonitoringPermission()
@@ -20,12 +22,15 @@ onMounted(async () => {
await appWindow.setAlwaysOnTop(true)
await message('如果权限已开启,先选中后点击“-”按钮将其删除,再重新手动添加,并重启应用以确保权限生效。', {
title: '输入监控权限',
okLabel: '前往开启',
const confirmed = await confirm(t('pages.preference.general.hints.inputMonitoringPermissionGuide'), {
title: t('pages.preference.general.labels.inputMonitoringPermission'),
okLabel: t('pages.preference.general.buttons.openNow'),
cancelLabel: t('pages.preference.general.buttons.openLater'),
kind: 'warning',
})
if (!confirmed) return
await appWindow.setAlwaysOnTop(false)
requestInputMonitoringPermission()
@@ -35,11 +40,11 @@ onMounted(async () => {
<template>
<ProList
v-if="isMac"
title="权限设置"
:title="$t('pages.preference.general.labels.permissionsSettings')"
>
<ProListItem
description="开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。"
title="输入监控权限"
:description="$t('pages.preference.general.hints.inputMonitoringPermission')"
:title="$t('pages.preference.general.labels.inputMonitoringPermission')"
>
<Space
v-if="authorized"
@@ -48,7 +53,7 @@ onMounted(async () => {
>
<div class="i-solar:verified-check-bold text-4.5" />
<span>已授权</span>
<span class="whitespace-nowrap">{{ $t('pages.preference.general.status.authorized') }}</span>
</Space>
<Space
@@ -59,7 +64,7 @@ onMounted(async () => {
>
<div class="i-solar:round-arrow-right-bold text-4.5" />
<span>去授权</span>
<span class="whitespace-nowrap">{{ $t('pages.preference.general.status.authorize') }}</span>
</Space>
</ProListItem>
</ProList>

View File

@@ -0,0 +1,53 @@
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Select, SelectOption } from 'ant-design-vue'
import { onMounted, watch } from 'vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useGeneralStore } from '@/stores/general'
const generalStore = useGeneralStore()
const appWindow = getCurrentWebviewWindow()
onMounted(() => {
appWindow.onThemeChanged(async ({ payload }) => {
if (generalStore.appearance.theme !== 'auto') return
generalStore.appearance.isDark = payload === 'dark'
})
})
watch(() => generalStore.appearance.theme, async (value) => {
let nextTheme = value === 'auto' ? null : value
await appWindow.setTheme(nextTheme)
nextTheme = nextTheme ?? (await appWindow.theme())
generalStore.appearance.isDark = nextTheme === 'dark'
}, { immediate: true })
watch(() => generalStore.appearance.isDark, (value) => {
if (value) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, { immediate: true })
</script>
<template>
<ProListItem :title="$t('pages.preference.general.labels.themeMode')">
<Select v-model:value="generalStore.appearance.theme">
<SelectOption value="auto">
{{ $t('pages.preference.general.options.auto') }}
</SelectOption>
<SelectOption value="light">
{{ $t('pages.preference.general.options.lightMode') }}
</SelectOption>
<SelectOption value="dark">
{{ $t('pages.preference.general.options.darkMode') }}
</SelectOption>
</Select>
</ProListItem>
</template>

View File

@@ -1,9 +1,10 @@
<script setup lang="ts">
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
import { Switch } from 'ant-design-vue'
import { Select, Switch } from 'ant-design-vue'
import { watch } from 'vue'
import MacosPermissions from './components/macos-permissions/index.vue'
import ThemeMode from './components/theme-mode/index.vue'
import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
@@ -11,7 +12,7 @@ import { useGeneralStore } from '@/stores/general'
const generalStore = useGeneralStore()
watch(() => generalStore.autostart, async (value) => {
watch(() => generalStore.app.autostart, async (value) => {
const enabled = await isEnabled()
if (value && !enabled) {
@@ -27,15 +28,43 @@ watch(() => generalStore.autostart, async (value) => {
<template>
<MacosPermissions />
<ProList title="应用设置">
<ProListItem title="开机自启动">
<Switch v-model:checked="generalStore.autostart" />
<ProList :title="$t('pages.preference.general.labels.appSettings')">
<ProListItem :title="$t('pages.preference.general.labels.launchOnStartup')">
<Switch v-model:checked="generalStore.app.autostart" />
</ProListItem>
<ProListItem
:description="$t('pages.preference.general.hints.showTaskbarIcon')"
:title="$t('pages.preference.general.labels.showTaskbarIcon')"
>
<Switch v-model:checked="generalStore.app.taskbarVisible" />
</ProListItem>
</ProList>
<ProList title="更新设置">
<ProListItem title="自动检查更新">
<Switch v-model:checked="generalStore.autoCheckUpdate" />
<ProList :title="$t('pages.preference.general.labels.appearanceSettings')">
<ThemeMode />
<ProListItem :title="$t('pages.preference.general.labels.language')">
<Select v-model:value="generalStore.appearance.language">
<Select.Option value="zh-CN">
简体中文
</Select.Option>
<Select.Option value="en-US">
English
</Select.Option>
<Select.Option value="vi-VN">
Tiếng Việt
</Select.Option>
<Select.Option value="pt-BR">
Português
</Select.Option>
</Select>
</ProListItem>
</ProList>
<ProList :title="$t('pages.preference.general.labels.updateSettings')">
<ProListItem :title="$t('pages.preference.general.labels.autoCheckUpdate')">
<Switch v-model:checked="generalStore.update.autoCheck" />
</ProListItem>
</ProList>
</template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { EditOutlined, MenuOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
import { EditOutlined, MenuOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
</script>
@@ -15,7 +15,7 @@ import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
</template>
<FloatButton
tooltip="如何制作模型?"
:tooltip="$t('pages.preference.model.tooltips.createModel')"
@click="openUrl('https://juejin.cn/post/7509872655802269731')"
>
<template #icon>
@@ -24,7 +24,16 @@ import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
</FloatButton>
<FloatButton
tooltip="更多模型"
:tooltip="$t('pages.preference.model.tooltips.convertModel')"
@click="openUrl('https://bongocat.vteamer.cc')"
>
<template #icon>
<SyncOutlined />
</template>
</FloatButton>
<FloatButton
:tooltip="$t('pages.preference.model.tooltips.moreModels')"
@click="openUrl('https://github.com/ayangweb/Awesome-BongoCat')"
>
<template #icon>

View File

@@ -1,4 +1,6 @@
<script setup lang="ts">
import type { ModelMode } from '@/stores/model'
import { invoke } from '@tauri-apps/api/core'
import { appDataDir } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
@@ -7,6 +9,7 @@ import { readDir } from '@tauri-apps/plugin-fs'
import { message } from 'ant-design-vue'
import { nanoid } from 'nanoid'
import { onMounted, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { INVOKE_KEY } from '@/constants'
import { useModelStore } from '@/stores/model'
@@ -16,6 +19,7 @@ const dropRef = useTemplateRef('drop')
const dragenter = ref(false)
const selectPaths = ref<string[]>([])
const modelStore = useModelStore()
const { t } = useI18n()
onMounted(() => {
const appWindow = getCurrentWebviewWindow()
@@ -53,29 +57,39 @@ async function handleUpload() {
}
watch(selectPaths, async (paths) => {
for await (const path of paths) {
for await (const fromPath of paths) {
try {
const id = nanoid()
const files = await readDir(join(path, 'resources'))
let mode: ModelMode = 'standard'
const isKeyboardMode = files.some(file => file.name === 'right-keys')
const files = await readDir(join(fromPath, 'resources', 'right-keys')).catch(() => [])
if (files.length > 0) {
const fileNames = files.map(file => file.name.split('.')[0])
if (fileNames.includes('East')) {
mode = 'gamepad'
} else {
mode = 'keyboard'
}
}
const toPath = join(await appDataDir(), 'custom-models', id)
await invoke(INVOKE_KEY.COPY_DIR, {
fromPath: path,
fromPath,
toPath,
})
modelStore.models.push({
id,
path: toPath,
mode: isKeyboardMode ? 'keyboard' : 'standard',
mode,
isPreset: false,
})
message.success('导入成功')
message.success(t('pages.preference.model.hints.importSuccess'))
} catch (error) {
message.error(String(error))
}
@@ -86,12 +100,12 @@ watch(selectPaths, async (paths) => {
<template>
<div
ref="drop"
class="size-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary"
class="w-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary"
:class="{ 'border-primary': dragenter }"
@click="handleUpload"
>
<div class="i-solar:upload-square-outline text-12 text-primary" />
<span>点击或拖动至此区域导入</span>
<span>{{ $t('pages.preference.model.hints.clickOrDragToImport') }}</span>
</div>
</template>

View File

@@ -1,11 +1,15 @@
<script setup lang="ts">
import type { Model } from '@/stores/model'
import type { ColProps } from 'ant-design-vue'
import type { ComponentPublicInstance } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/core'
import { remove } from '@tauri-apps/plugin-fs'
import { revealItemInDir } from '@tauri-apps/plugin-opener'
import { Card, Col, message, Popconfirm, Row } from 'ant-design-vue'
import { useElementSize } from '@vueuse/core'
import { Card, message, Popconfirm } from 'ant-design-vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'
import FloatMenu from './components/float-menu/index.vue'
import Upload from './components/upload/index.vue'
@@ -14,12 +18,20 @@ import { useModelStore } from '@/stores/model'
import { join } from '@/utils/path'
const modelStore = useModelStore()
const firstItemRef = ref<HTMLElement>()
const { height } = useElementSize(firstItemRef)
const { t } = useI18n()
const colProps: ColProps = {
xs: 12,
md: 8,
lg: 6,
xl: 4,
function setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) {
if (!el || index > 0) return
if ('$el' in el) {
return firstItemRef.value = el.$el
}
if (el instanceof HTMLElement) {
firstItemRef.value = el
}
}
async function handleDelete(item: Model) {
@@ -28,7 +40,7 @@ async function handleDelete(item: Model) {
try {
await remove(path, { recursive: true })
message.success('删除成功')
message.success(t('pages.preference.model.hints.deleteSuccess'))
} catch (error) {
message.error(String(error))
} finally {
@@ -42,19 +54,23 @@ async function handleDelete(item: Model) {
</script>
<template>
<Row :gutter="[16, 16]">
<Col v-bind="colProps">
<Upload />
</Col>
<MasonryGrid
:columns="{ 992: 3, 1200: 4, 1600: 6, default: 8 }"
:gutter="16"
>
<MasonryGridItem>
<Upload :style="{ height: `${height}px` }" />
</MasonryGridItem>
<Col
v-for="item in modelStore.models"
<MasonryGridItem
v-for="(item, index) in modelStore.models"
:key="item.id"
v-bind="colProps"
>
<Card
:ref="(el) => setFirstItemRef(el, index)"
hoverable
size="small"
@click="modelStore.currentModel = item"
>
<template #cover>
<img
@@ -67,28 +83,30 @@ async function handleDelete(item: Model) {
<i
class="i-iconamoon:check-circle-1-bold text-4"
:class="{ 'text-success': item.id === modelStore.currentModel?.id }"
@click="modelStore.currentModel = item"
/>
<i
class="i-iconamoon:link-external-bold text-4"
@click="revealItemInDir(item.path)"
@click.stop="revealItemInDir(item.path)"
/>
<template v-if="!item.isPreset">
<Popconfirm
description="你确定要删除此模型吗?"
:description="$t('pages.preference.model.hints.deleteModel')"
placement="topRight"
title="删除模型"
:title="$t('pages.preference.model.labels.deleteModel')"
@confirm="handleDelete(item)"
>
<i class="i-iconamoon:trash-simple-bold text-4" />
<i
class="i-iconamoon:trash-simple-bold text-4"
@click.stop
/>
</Popconfirm>
</template>
</template>
</Card>
</Col>
</Row>
</MasonryGridItem>
</MasonryGrid>
<FloatMenu />
</template>

View File

@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
import ProList from '@/components/pro-list/index.vue'
import ProShortcut from '@/components/pro-shortcut/index.vue'
import { useTauriKeyPress } from '@/composables/useTauriKeyPress'
import { useTauriShortcut } from '@/composables/useTauriShortcut'
import { toggleWindowVisible } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
import { useShortcutStore } from '@/stores/shortcut.ts'
@@ -12,57 +12,57 @@ const shortcutStore = useShortcutStore()
const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore)
const catStore = useCatStore()
useTauriKeyPress(visibleCat, () => {
catStore.visible = !catStore.visible
useTauriShortcut(visibleCat, () => {
catStore.window.visible = !catStore.window.visible
})
useTauriKeyPress(visiblePreference, () => {
useTauriShortcut(visiblePreference, () => {
toggleWindowVisible('preference')
})
useTauriKeyPress(mirrorMode, () => {
catStore.mirrorMode = !catStore.mirrorMode
useTauriShortcut(mirrorMode, () => {
catStore.model.mirror = !catStore.model.mirror
})
useTauriKeyPress(penetrable, () => {
catStore.penetrable = !catStore.penetrable
useTauriShortcut(penetrable, () => {
catStore.window.passThrough = !catStore.window.passThrough
})
useTauriKeyPress(alwaysOnTop, () => {
catStore.alwaysOnTop = !catStore.alwaysOnTop
useTauriShortcut(alwaysOnTop, () => {
catStore.window.alwaysOnTop = !catStore.window.alwaysOnTop
})
</script>
<template>
<ProList title="快捷键">
<ProList :title="$t('pages.preference.shortcut.title')">
<ProShortcut
v-model="shortcutStore.visibleCat"
description="切换猫咪窗口的显示与隐藏"
title="打开猫咪"
:description="$t('pages.preference.shortcut.hints.toggleCat')"
:title="$t('pages.preference.shortcut.labels.toggleCat')"
/>
<ProShortcut
v-model="shortcutStore.visiblePreference"
description="切换偏好设置窗口的显示与隐藏"
title="打开偏好设置"
:description="$t('pages.preference.shortcut.hints.togglePreferences')"
:title="$t('pages.preference.shortcut.labels.togglePreferences')"
/>
<ProShortcut
v-model="shortcutStore.mirrorMode"
description="切换猫咪的镜像模式"
title="镜像模式"
:description="$t('pages.preference.shortcut.hints.mirrorMode')"
:title="$t('pages.preference.shortcut.labels.mirrorMode')"
/>
<ProShortcut
v-model="shortcutStore.penetrable"
description="切换猫咪窗口是否可穿透"
title="窗口穿透"
:description="$t('pages.preference.shortcut.hints.passThrough')"
:title="$t('pages.preference.shortcut.labels.passThrough')"
/>
<ProShortcut
v-model="shortcutStore.alwaysOnTop"
description="切换猫咪窗口是否置顶"
title="窗口置顶"
:description="$t('pages.preference.shortcut.hints.alwaysOnTop')"
:title="$t('pages.preference.shortcut.labels.alwaysOnTop')"
/>
</ProList>
</template>

View File

@@ -1,6 +1,8 @@
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Flex } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import About from './components/about/index.vue'
import Cat from './components/cat/index.vue'
@@ -11,49 +13,57 @@ import Shortcut from './components/shortcut/index.vue'
import UpdateApp from '@/components/update-app/index.vue'
import { useTray } from '@/composables/useTray'
import { useAppStore } from '@/stores/app'
import { useGeneralStore } from '@/stores/general'
import { isMac } from '@/utils/platform'
const { createTray } = useTray()
const appStore = useAppStore()
const current = ref(0)
const { t } = useI18n()
const generalStore = useGeneralStore()
const appWindow = getCurrentWebviewWindow()
onMounted(async () => {
createTray()
})
const menus = [
watch(() => generalStore.appearance.language, () => {
appWindow.setTitle(t('pages.preference.title'))
}, { immediate: true })
const menus = computed(() => [
{
label: '猫咪设置',
label: t('pages.preference.cat.title'),
icon: 'i-solar:cat-bold',
component: Cat,
},
{
label: '通用设置',
label: t('pages.preference.general.title'),
icon: 'i-solar:settings-minimalistic-bold',
component: General,
},
{
label: '模型管理',
label: t('pages.preference.model.title'),
icon: 'i-solar:magic-stick-3-bold',
component: Model,
},
{
label: '快捷键',
label: t('pages.preference.shortcut.title'),
icon: 'i-solar:keyboard-bold',
component: Shortcut,
},
{
label: '关于',
label: t('pages.preference.about.title'),
icon: 'i-solar:info-circle-bold',
component: About,
},
]
])
</script>
<template>
<Flex class="h-screen">
<div
class="h-full w-30 flex flex-col items-center gap-4 overflow-auto bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
class="h-full w-30 flex flex-col items-center gap-4 overflow-auto dark:(bg-color-3 bg-none) bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
:class="[isMac ? 'pt-8' : 'pt-4']"
data-tauri-drag-region
>
@@ -61,6 +71,7 @@ const menus = [
<div class="b b-color-2 rounded-2xl b-solid">
<img
class="size-15"
data-tauri-drag-region
src="/logo.png"
>
</div>
@@ -72,8 +83,8 @@ const menus = [
<div
v-for="(item, index) in menus"
:key="item.label"
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 text-color-3 transition"
:class="{ 'bg-white! text-primary-5 font-bold': current === index }"
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 dark:text-color-2 text-color-3 transition"
:class="{ 'bg-color-2! text-primary-5 dark:text-primary-7 font-bold dark:bg-color-8!': current === index }"
@click="current = index"
>
<div
@@ -90,7 +101,7 @@ const menus = [
v-for="(item, index) in menus"
v-show="current === index"
:key="item.label"
class="flex-1 overflow-auto bg-color-8 p-4"
class="flex-1 overflow-auto bg-color-8 dark:bg-color-2 p-4"
data-tauri-drag-region
>
<component :is="item.component" />

View File

@@ -10,6 +10,7 @@ const COMMAND = {
SHOW_WINDOW: 'plugin:custom-window|show_window',
HIDE_WINDOW: 'plugin:custom-window|hide_window',
SET_ALWAYS_ON_TOP: 'plugin:custom-window|set_always_on_top',
SET_TASKBAR_VISIBILITY: 'plugin:custom-window|set_taskbar_visibility',
}
export function showWindow(label?: WindowLabel) {
@@ -45,3 +46,7 @@ export async function toggleWindowVisible(label?: WindowLabel) {
return showWindow(label)
}
export async function setTaskbarVisibility(visible: boolean) {
invoke(COMMAND.SET_TASKBAR_VISIBILITY, { visible })
}

View File

@@ -2,21 +2,22 @@ import type { WindowState } from '@/composables/useWindowState'
import { getName, getVersion } from '@tauri-apps/api/app'
import { defineStore } from 'pinia'
import { onMounted, reactive, ref } from 'vue'
import { reactive, ref } from 'vue'
export const useAppStore = defineStore('app', () => {
const name = ref('')
const version = ref('')
const windowState = reactive<WindowState>({})
onMounted(async () => {
const init = async () => {
name.value = await getName()
version.value = await getVersion()
})
}
return {
name,
version,
windowState,
init,
}
})

View File

@@ -1,24 +1,90 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { reactive, ref } from 'vue'
export interface CatStore {
model: {
mirror: boolean
single: boolean
mouseMirror: boolean
autoReleaseDelay: number
}
window: {
visible: boolean
passThrough: boolean
alwaysOnTop: boolean
scale: number
opacity: number
radius: number
hideOnHover: boolean
position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
}
}
export const useCatStore = defineStore('cat', () => {
const visible = ref(false)
/* ------------ 废弃字段(后续删除) ------------ */
/** @deprecated 请使用 `model.mirror` */
const mirrorMode = ref(false)
/** @deprecated 请使用 `model.single` */
const singleMode = ref(false)
/** @deprecated 请使用 `model.mouseMirror` */
const mouseMirror = ref(false)
/** @deprecated 请使用 `window.passThrough` */
const penetrable = ref(false)
/** @deprecated 请使用 `window.alwaysOnTop` */
const alwaysOnTop = ref(true)
/** @deprecated 请使用 `window.scale` */
const scale = ref(100)
/** @deprecated 请使用 `window.opacity` */
const opacity = ref(100)
/** @deprecated 用于标识数据是否已迁移,后续版本将删除 */
const migrated = ref(false)
const model = reactive<CatStore['model']>({
mirror: false,
single: false,
mouseMirror: false,
autoReleaseDelay: 3,
})
const window = reactive<CatStore['window']>({
visible: true,
passThrough: false,
alwaysOnTop: false,
scale: 100,
opacity: 100,
radius: 0,
hideOnHover: false,
position: 'bottomRight',
})
const init = () => {
if (migrated.value) return
model.mirror = mirrorMode.value
model.single = singleMode.value
model.mouseMirror = mouseMirror.value
window.visible = true
window.passThrough = penetrable.value
window.alwaysOnTop = alwaysOnTop.value
window.scale = scale.value
window.opacity = opacity.value
migrated.value = true
}
return {
visible,
mirrorMode,
singleMode,
mouseMirror,
penetrable,
alwaysOnTop,
scale,
opacity,
migrated,
model,
window,
init,
}
})

View File

@@ -1,12 +1,94 @@
import type { Theme } from '@tauri-apps/api/window'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getLocale } from 'tauri-plugin-locale-api'
import { reactive, ref } from 'vue'
import { LANGUAGE } from '@/constants'
export type Language = typeof LANGUAGE[keyof typeof LANGUAGE]
export interface GeneralStore {
app: {
autostart: boolean
taskbarVisible: boolean
}
appearance: {
theme: 'auto' | Theme
isDark: boolean
language?: Language
}
update: {
autoCheck: boolean
}
}
export const useGeneralStore = defineStore('general', () => {
/* ------------ 废弃字段(后续删除) ------------ */
/** @deprecated 请使用 `update.autoCheck` */
const autoCheckUpdate = ref(false)
/** @deprecated 请使用 `app.autostart` */
const autostart = ref(false)
/** @deprecated 请使用 `app.taskbarVisible` */
const taskbarVisibility = ref(false)
/** @deprecated 请使用 `appearance.theme` */
const theme = ref<'auto' | Theme>('auto')
/** @deprecated 请使用 `appearance.isDark` */
const isDark = ref(false)
/** @deprecated 用于标识数据是否已迁移,后续版本将删除 */
const migrated = ref(false)
const app = reactive<GeneralStore['app']>({
autostart: false,
taskbarVisible: false,
})
const appearance = reactive<GeneralStore['appearance']>({
theme: 'auto',
isDark: false,
})
const update = reactive<GeneralStore['update']>({
autoCheck: false,
})
const getLanguage = async () => {
const locale = await getLocale<Language>()
if (Object.values(LANGUAGE).includes(locale)) {
return locale
}
return LANGUAGE.EN_US
}
const init = async () => {
appearance.language ??= await getLanguage()
if (migrated.value) return
app.autostart = autostart.value
app.taskbarVisible = taskbarVisibility.value
appearance.theme = theme.value
appearance.isDark = isDark.value
update.autoCheck = autoCheckUpdate.value
migrated.value = true
}
return {
autoCheckUpdate,
autostart,
migrated,
app,
appearance,
update,
init,
}
})

View File

@@ -1,11 +1,12 @@
import { resolveResource } from '@tauri-apps/api/path'
import { filter, find } from 'es-toolkit/compat'
import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
import { onMounted, ref } from 'vue'
import { reactive, ref } from 'vue'
import { join } from '@/utils/path'
export type ModelMode = 'standard' | 'keyboard' | 'handle'
export type ModelMode = 'standard' | 'keyboard' | 'gamepad'
export interface Model {
id: string
@@ -36,34 +37,47 @@ export const useModelStore = defineStore('model', () => {
const currentModel = ref<Model>()
const motions = ref<MotionGroup>({})
const expressions = ref<Expression[]>([])
const supportKeys = reactive<Record<string, string>>({})
const pressedKeys = reactive<Record<string, string>>({})
onMounted(async () => {
const init = async () => {
const modelsPath = await resolveResource('assets/models')
if (models.value.length === 0) {
const modes: ModelMode[] = ['standard', 'keyboard']
const nextModels = filter(models.value, { isPreset: false })
const presetModels = filter(models.value, { isPreset: true })
for await (const mode of modes) {
const path = join(modelsPath, mode)
const modes: ModelMode[] = ['gamepad', 'keyboard', 'standard']
models.value.push({
id: nanoid(),
path,
mode,
isPreset: true,
})
}
for (const mode of modes) {
const matched = find(presetModels, { mode })
nextModels.unshift({
id: matched?.id ?? nanoid(),
mode,
isPreset: true,
path: join(modelsPath, mode),
})
}
if (currentModel.value) return
const matched = find(nextModels, { id: currentModel.value?.id })
currentModel.value = models.value[0]
})
currentModel.value = matched ?? nextModels[0]
models.value = nextModels
}
return {
models,
currentModel,
motions,
expressions,
supportKeys,
pressedKeys,
init,
}
}, {
tauri: {
filterKeys: ['models', 'currentModel'],
filterKeysStrategy: 'pick',
},
})

View File

@@ -1,15 +0,0 @@
export function getImageSize(src: string) {
return new Promise<{ width: number, height: number }>((resolve, reject) => {
const img = new Image()
img.src = src
img.onload = () => {
const { naturalWidth, naturalHeight } = img
resolve({ width: naturalWidth, height: naturalHeight })
}
img.onerror = reject
})
}

View File

@@ -3,3 +3,7 @@ export function isImage(value: string) {
return regex.test(value)
}
export function inBetween(value: number, minimum: number, maximum: number) {
return value >= minimum && value <= maximum
}

View File

@@ -1,12 +1,16 @@
import type { ModelSize } from '@/composables/useModel'
import type { Cubism4InternalModel } from 'pixi-live2d-display'
import { convertFileSrc } from '@tauri-apps/api/core'
import { readDir, readTextFile } from '@tauri-apps/plugin-fs'
import JSON5 from 'json5'
import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'
import { Application, Ticker } from 'pixi.js'
import { join } from './path'
import { i18n } from '@/locales'
Live2DModel.registerTicker(Ticker)
class Live2d {
@@ -15,22 +19,21 @@ class Live2d {
constructor() { }
private mount() {
private initApp() {
if (this.app) return
const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
this.app = new Application({
view,
resizeTo: window,
backgroundAlpha: 0,
autoDensity: true,
resolution: devicePixelRatio,
})
}
public async load(path: string) {
if (!this.app) {
this.mount()
}
this.initApp()
this.destroy()
@@ -39,12 +42,12 @@ class Live2d {
const modelFile = files.find(file => file.name.endsWith('.model3.json'))
if (!modelFile) {
throw new Error('未找到模型主配置文件,请确认模型文件是否完整。')
throw new Error(i18n.global.t('utils.live2d.hints.notFound'))
}
const modelPath = join(path, modelFile.name)
const modelJSON = JSON.parse(await readTextFile(modelPath))
const modelJSON = JSON5.parse(await readTextFile(modelPath))
const modelSettings = new Cubism4ModelSettings({
...modelJSON,
@@ -59,16 +62,38 @@ class Live2d {
this.app?.stage.addChild(this.model)
const { width, height } = this.model
const { motions, expressions } = modelSettings
return {
width,
height,
motions,
expressions,
}
}
public destroy() {
if (!this.model) return
this.model?.destroy()
this.model = null
}
public resizeModel(modelSize: ModelSize) {
if (!this.model) return
const { width, height } = modelSize
const scaleX = innerWidth / width
const scaleY = innerHeight / height
const scale = Math.min(scaleX, scaleY)
this.model.scale.set(scale)
this.model.x = innerWidth / 2
this.model.y = innerHeight / 2
this.model.anchor.set(0.5)
}
public playMotion(group: string, index: number) {
@@ -98,7 +123,7 @@ class Live2d {
}
}
public setParameterValue(id: string, value: number) {
public setParameterValue(id: string, value: number | boolean) {
const coreModel = this.getCoreModel()
return coreModel?.setParameterValueById?.(id, Number(value))

View File

@@ -1,24 +1,20 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import {
cursorPosition,
monitorFromPoint,
} from '@tauri-apps/api/window'
import type { PhysicalPosition } from '@tauri-apps/api/window'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { cursorPosition, monitorFromPoint } from '@tauri-apps/api/window'
export async function getCursorMonitor(cursorPoint?: PhysicalPosition) {
cursorPoint ??= await cursorPosition()
export async function getCursorMonitor() {
const appWindow = getCurrentWebviewWindow()
const scaleFactor = await appWindow.scaleFactor()
const point = await cursorPosition()
const { x, y } = point.toLogical(scaleFactor)
const { x, y } = cursorPoint.toLogical(scaleFactor)
const monitor = await monitorFromPoint(x, y)
if (!monitor) return
return {
...monitor,
cursorPosition: point,
}
return monitor
}

View File

@@ -1,14 +1,12 @@
import type { LiteralUnion } from 'ant-design-vue/es/_util/type'
import { sep } from '@tauri-apps/api/path'
export function join(...paths: LiteralUnion<'resources' | 'left-keys' | 'right-keys' | 'background.png' | 'cover.png'>[]) {
const joinPaths = paths.map((path) => {
if (path.endsWith(sep())) {
return path.slice(0, -1)
export function join(...paths: string[]) {
const joinPaths = paths.map((path, index) => {
if (index === 0) {
return path.replace(new RegExp(`${sep()}+$`), '')
} else {
return path.replace(new RegExp(`^${sep()}+|${sep()}+$`, 'g'), '')
}
return path
})
return joinPaths.join(sep())

9
src/utils/shared.ts Normal file
View File

@@ -0,0 +1,9 @@
import { castArray } from 'es-toolkit/compat'
export function clearObject<T extends Record<string, unknown>>(targets: T | T[]) {
for (const target of castArray<T>(targets)) {
for (const key of Object.keys(target)) {
delete target[key]
}
}
}