55 Commits

Author SHA1 Message Date
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
伊墨
ce34b12f96 feat: 设置偏好设置窗口最小尺寸为 800×600 (#449) 2025-06-06 08:57:09 +08:00
ayangweb
0dbd97d205 fix: 修复偏好设置窗口出现双滚动条的问题 (#448) 2025-06-05 22:08:18 +08:00
伊墨
a6ffa0c07f fix: 修复偏好设置菜单可被右键选中的问题 (#447) 2025-06-05 22:04:29 +08:00
ayangweb
85fe938f25 fix: 修复关于页面布局异常问题 (#446) 2025-06-05 20:17:15 +08:00
ayangweb
9b5599712a refactor: refactoring window visibility control logic (#445) 2025-06-05 20:09:37 +08:00
ayangweb
64e043fb11 refactor: removed redundant cfg(dev) flags (#444) 2025-06-05 19:32:27 +08:00
ayangweb
22c2d479b6 refactor: use dynamic config to control window level (#443) 2025-06-05 19:30:01 +08:00
伊墨
80d197d9ad feat: 偏好设置新增「快捷键」界面 (#424) 2025-06-05 19:19:57 +08:00
ayangweb
806623327d chore: update issues templates (#439) 2025-06-05 15:58:12 +08:00
ayangweb
0825e5ffa5 docs: add a community communication group (#429) 2025-06-01 17:11:44 +08:00
伊墨
b2e7e0abf4 chore: enable ESLint as the default formatter in VSCode (#427) 2025-06-01 12:53:08 +08:00
ayangweb
f7da48e5c0 docs: update README.md (#422) 2025-05-31 15:23:28 +08:00
ayangweb
d7b460524c refactor: modify the link pointing to the feedback issue (#416) 2025-05-30 17:37:07 +08:00
91 changed files with 3154 additions and 1457 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,7 +1,7 @@
name: 🐞 Bug 报告
title: '[bug] '
description: 报告一个 Bug
labels: 🐞 bug
labels: bug
body:
- type: markdown
attributes:

View File

@@ -1,7 +1,7 @@
name: 💡 功能请求
title: '[feat] '
description: 提出一个想法
labels: 💡 feature request
labels: feature request
body:
- type: textarea
id: problem

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

@@ -2,6 +2,8 @@
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"eslint.format.enable": true,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
@@ -46,5 +48,8 @@
"scss",
"pcss",
"postcss"
]
],
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "./node_modules/typescript/lib"
}

423
Cargo.lock generated
View File

@@ -184,9 +184,9 @@ dependencies = [
[[package]]
name = "async-io"
version = "2.4.0"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
checksum = "1237c0ae75a0f3765f58910ff9cdd0a12eeb39ab2f4c7de23262f337f0aacbb3"
dependencies = [
"async-lock",
"cfg-if",
@@ -195,7 +195,7 @@ dependencies = [
"futures-lite",
"parking",
"polling",
"rustix 0.38.44",
"rustix 1.0.7",
"slab",
"tracing",
"windows-sys 0.59.0",
@@ -214,9 +214,9 @@ dependencies = [
[[package]]
name = "async-process"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63255f1dc2381611000436537bbedfe83183faa303a5a0edaf191edef06526bb"
checksum = "cde3f4e40e6021d7acffc90095cbd6dc54cb593903d1de5832f435eb274b85dc"
dependencies = [
"async-channel",
"async-io",
@@ -227,7 +227,7 @@ dependencies = [
"cfg-if",
"event-listener",
"futures-lite",
"rustix 0.38.44",
"rustix 1.0.7",
"tracing",
]
@@ -244,9 +244,9 @@ dependencies = [
[[package]]
name = "async-signal"
version = "0.2.10"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "637e00349800c0bdf8bfc21ebbc0b6524abea702b0da4168ac00d070d0c0b9f3"
checksum = "d7605a4e50d4b06df3898d5a70bf5fde51ed9059b0434b73105193bc27acce0d"
dependencies = [
"async-io",
"async-lock",
@@ -254,7 +254,7 @@ dependencies = [
"cfg-if",
"futures-core",
"futures-io",
"rustix 0.38.44",
"rustix 1.0.7",
"signal-hook-registry",
"slab",
"windows-sys 0.59.0",
@@ -450,9 +450,10 @@ dependencies = [
[[package]]
name = "bongo-cat"
version = "0.5.0"
version = "0.7.0"
dependencies = [
"fs_extra",
"gilrs",
"rdev",
"serde",
"serde_json",
@@ -464,6 +465,7 @@ dependencies = [
"tauri-plugin-custom-window",
"tauri-plugin-dialog",
"tauri-plugin-fs",
"tauri-plugin-global-shortcut",
"tauri-plugin-log",
"tauri-plugin-macos-permissions",
"tauri-plugin-opener",
@@ -612,9 +614,9 @@ dependencies = [
[[package]]
name = "camino"
version = "1.1.9"
version = "1.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3"
checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab"
dependencies = [
"serde",
]
@@ -654,9 +656,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.24"
version = "1.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7"
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
dependencies = [
"shlex",
]
@@ -747,7 +749,7 @@ dependencies = [
"bitflags 2.9.1",
"block",
"cocoa-foundation 0.2.1",
"core-foundation 0.10.0",
"core-foundation 0.10.1",
"core-graphics 0.24.0",
"foreign-types 0.5.0",
"libc",
@@ -776,7 +778,7 @@ checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d"
dependencies = [
"bitflags 2.9.1",
"block",
"core-foundation 0.10.0",
"core-foundation 0.10.1",
"core-graphics-types 0.2.0",
"objc",
]
@@ -828,9 +830,9 @@ dependencies = [
[[package]]
name = "core-foundation"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
@@ -862,7 +864,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.0",
"core-foundation 0.10.1",
"core-graphics-types 0.2.0",
"foreign-types 0.5.0",
"libc",
@@ -886,7 +888,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.0",
"core-foundation 0.10.1",
"libc",
]
@@ -1157,9 +1159,9 @@ dependencies = [
[[package]]
name = "dlopen2_derive"
version = "0.4.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2b99bf03862d7f545ebc28ddd33a665b50865f4dfd84031a393823879bd4c54"
checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
dependencies = [
"proc-macro2",
"quote",
@@ -1216,16 +1218,16 @@ checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "embed-resource"
version = "3.0.2"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbc6e0d8e0c03a655b53ca813f0463d2c956bc4db8138dbc89f120b066551e3"
checksum = "e8fe7d068ca6b3a5782ca5ec9afc244acd99dd441e4686a83b1c3973aba1d489"
dependencies = [
"cc",
"memchr",
"rustc_version",
"toml",
"vswhom",
"winreg 0.52.0",
"winreg 0.55.0",
]
[[package]]
@@ -1773,6 +1775,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"
@@ -1864,6 +1899,24 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "global-hotkey"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
dependencies = [
"crossbeam-channel",
"keyboard-types",
"objc2 0.6.1",
"objc2-app-kit",
"once_cell",
"serde",
"thiserror 2.0.12",
"windows-sys 0.59.0",
"x11rb",
"xkeysym",
]
[[package]]
name = "gobject-sys"
version = "0.18.0"
@@ -1962,9 +2015,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.4.0"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08"
[[package]]
name = "hex"
@@ -2065,22 +2118,26 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
"webpki-roots 1.0.0",
"webpki-roots",
]
[[package]]
name = "hyper-util"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf9f1e950e0d9d1d3c47184416723cf29c0d1f93bd8cccf37e4beb6b44f31710"
checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8"
dependencies = [
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
"http-body",
"hyper",
"ipnet",
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"tokio",
@@ -2290,6 +2347,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"
@@ -2299,12 +2367,32 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
[[package]]
name = "iri-string"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
dependencies = [
"memchr",
"serde",
]
[[package]]
name = "is-docker"
version = "0.2.0"
@@ -2509,6 +2597,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"
@@ -2529,9 +2627,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
[[package]]
name = "lock_api"
version = "0.4.12"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
@@ -2558,6 +2656,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"
@@ -2654,13 +2761,13 @@ dependencies = [
[[package]]
name = "mio"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -2720,6 +2827,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"
@@ -3160,9 +3279,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot"
version = "0.12.3"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -3170,9 +3289,9 @@ dependencies = [
[[package]]
name = "parking_lot_core"
version = "0.9.10"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
@@ -3394,15 +3513,15 @@ dependencies = [
[[package]]
name = "polling"
version = "3.7.4"
version = "3.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
dependencies = [
"cfg-if",
"concurrent-queue",
"hermit-abi",
"pin-project-lite",
"rustix 0.38.44",
"rustix 1.0.7",
"tracing",
"windows-sys 0.59.0",
]
@@ -3439,9 +3558,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.32"
version = "0.2.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6"
checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d"
dependencies = [
"proc-macro2",
"syn 2.0.101",
@@ -3747,7 +3866,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",
@@ -3756,7 +3875,7 @@ dependencies = [
"dispatch",
"enum-map",
"epoll",
"inotify",
"inotify 0.10.2",
"lazy_static",
"libc",
"log",
@@ -3839,9 +3958,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.15"
version = "0.12.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3862,7 +3981,6 @@ dependencies = [
"pin-project-lite",
"quinn",
"rustls",
"rustls-pemfile",
"rustls-pki-types",
"serde",
"serde_json",
@@ -3872,14 +3990,14 @@ dependencies = [
"tokio-rustls",
"tokio-util",
"tower",
"tower-http",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
"webpki-roots 0.26.11",
"windows-registry",
"webpki-roots",
]
[[package]]
@@ -4027,15 +4145,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "rustls-pki-types"
version = "1.12.0"
@@ -4063,6 +4172,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"
@@ -4361,9 +4481,9 @@ checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.9"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -4589,7 +4709,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.10.0",
"core-foundation 0.10.1",
"core-graphics 0.24.0",
"crossbeam-channel",
"dispatch",
@@ -4774,12 +4894,12 @@ dependencies = [
[[package]]
name = "tauri-nspanel"
version = "2.0.1"
source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#d4b9df797959f8fa4701e8a20ff69d9605bb66e9"
source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#dc1563b2c71d3699dc67028080e43aade0575bcf"
dependencies = [
"bitflags 2.9.1",
"block",
"cocoa 0.26.1",
"core-foundation 0.10.0",
"core-foundation 0.10.1",
"core-graphics 0.24.0",
"objc",
"objc-foundation",
@@ -4883,6 +5003,21 @@ dependencies = [
"url",
]
[[package]]
name = "tauri-plugin-global-shortcut"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31919f3c07bcb585afef217c0c33cde80da9ebccf5b8e2c90e0e0a535b14ab47"
dependencies = [
"global-hotkey",
"log",
"serde",
"serde_json",
"tauri",
"tauri-plugin",
"thiserror 2.0.12",
]
[[package]]
name = "tauri-plugin-log"
version = "2.4.0"
@@ -4962,9 +5097,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-pinia"
version = "3.4.0"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e87882a5a390b91bd7f785c382cd374b4a1b32ce3429cec871b16ee1ff1f8cb8"
checksum = "bec0cb197a26a9b468679887d85b38ad1860cf032d5bc2fd755d77f7e704b838"
dependencies = [
"serde",
"tauri",
@@ -5095,9 +5230,9 @@ dependencies = [
[[package]]
name = "tauri-store"
version = "0.10.5"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d35f80125c58fcbc5d408d288f7bc7d89b585c4ceb474217c553d9c0afc3963"
checksum = "f7981255eefa2868d49af173f27f0171e6e4a3c580530334f31a7be8b25446c8"
dependencies = [
"dashmap",
"futures",
@@ -5113,9 +5248,9 @@ dependencies = [
[[package]]
name = "tauri-store-macros"
version = "0.10.5"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1c7a0b908a8d874ac2c055d757273c102d386d06690f56fe6d7a93bbc4ea60"
checksum = "fef3bf6f34307c6185436ea109c6fd6c01bfb9636c16902cd7e8abcc243fbecc"
dependencies = [
"proc-macro2",
"quote",
@@ -5124,12 +5259,11 @@ dependencies = [
[[package]]
name = "tauri-store-utils"
version = "0.4.2"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bd3d928e03a75d07774ae2e4b754eb32cbf87e6154b3c7b7f1facd40a6e9497"
checksum = "baed2bbdce143a0b6e8105a0c9c7d9bfaa9c5689c3632bff12085c7209365d3f"
dependencies = [
"bon",
"semver",
"serde",
"serde_json",
"tauri",
@@ -5327,14 +5461,14 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.45.0"
version = "1.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2513ca694ef9ede0fb23fe71a4ee4107cb102b9dc1930f6d0fd77aae068ae165"
checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio 1.0.3",
"mio 1.0.4",
"pin-project-lite",
"signal-hook-registry",
"socket2",
@@ -5455,6 +5589,24 @@ dependencies = [
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
dependencies = [
"bitflags 2.9.1",
"bytes",
"futures-util",
"http",
"http-body",
"iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
@@ -5681,6 +5833,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"
@@ -5971,15 +6129,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpki-roots"
version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
"webpki-roots 1.0.0",
]
[[package]]
name = "webpki-roots"
version = "1.0.0"
@@ -6115,7 +6264,7 @@ dependencies = [
"windows-interface",
"windows-link",
"windows-result",
"windows-strings 0.4.2",
"windows-strings",
]
[[package]]
@@ -6167,17 +6316,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -6187,15 +6325,6 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
@@ -6280,29 +6409,13 @@ dependencies = [
"windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc 0.52.6",
"windows_i686_gnu 0.52.6",
"windows_i686_gnullvm 0.52.6",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.6",
"windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b"
dependencies = [
"windows_aarch64_gnullvm 0.53.0",
"windows_aarch64_msvc 0.53.0",
"windows_i686_gnu 0.53.0",
"windows_i686_gnullvm 0.53.0",
"windows_i686_msvc 0.53.0",
"windows_x86_64_gnu 0.53.0",
"windows_x86_64_gnullvm 0.53.0",
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
@@ -6339,12 +6452,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
@@ -6363,12 +6470,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
@@ -6387,24 +6488,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
@@ -6423,12 +6512,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_i686_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
@@ -6447,12 +6530,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
@@ -6471,12 +6548,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
@@ -6495,12 +6566,6 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
[[package]]
name = "winnow"
version = "0.5.40"
@@ -6530,12 +6595,12 @@ dependencies = [
[[package]]
name = "winreg"
version = "0.52.0"
version = "0.55.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -6673,6 +6738,12 @@ dependencies = [
"rustix 1.0.7",
]
[[package]]
name = "xkeysym"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
[[package]]
name = "yoke"
version = "0.8.0"
@@ -6717,7 +6788,7 @@ dependencies = [
"futures-core",
"futures-lite",
"hex",
"nix",
"nix 0.30.1",
"ordered-stream",
"serde",
"serde_repr",

View File

@@ -51,9 +51,9 @@
</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) |
## 开发背景
@@ -72,9 +72,26 @@
- 适配 macOS、Windows 和 Linux(x11)。
- 根据据键盘或鼠标操作,同步移动鼠标或敲击键盘。
- 支持导入自定义模型,自由打造专属猫咪形象。
- 完全开源,代码公开透明,绝不收集任何用户数据。
- 支持离线运行,无需联网,保护用户隐私。
## 更多模型
你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:
📦 [Awesome-BongoCat](https://github.com/ayangweb/Awesome-BongoCat)
## 社区交流
<a href="https://qm.qq.com/q/AS3gNv2Vzy">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/5ad8e4278c525cca6d3b4426c30b6d299d8a9654.png" />
<source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" />
<img alt="QQ Group" src="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" height="250" />
</picture>
</a>
## 贡献指南
感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。
@@ -92,3 +109,7 @@
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
</picture>
</a>
## 致谢
- 特别感谢 [UpgradeLink](https://www.toolsetlink.com/) 提供高效稳定的自动更新服务,让本项目得以持续为用户带来最新版本的优质体验。

View File

@@ -1,7 +1,7 @@
{
"name": "bongo-cat",
"type": "module",
"version": "0.5.0",
"version": "0.7.0",
"private": true,
"author": {
"name": "ayangweb",
@@ -27,43 +27,45 @@
"@tauri-apps/plugin-clipboard-manager": "~2.2.2",
"@tauri-apps/plugin-dialog": "~2.2.2",
"@tauri-apps/plugin-fs": "~2.3.0",
"@tauri-apps/plugin-global-shortcut": "~2.2.1",
"@tauri-apps/plugin-log": "~2.3.1",
"@tauri-apps/plugin-opener": "~2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-updater": "~2.7.1",
"@tauri-store/pinia": "^3.4.0",
"@vueuse/core": "^13.2.0",
"@tauri-store/pinia": "^3.7.0",
"@vueuse/core": "^13.3.0",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.13",
"es-toolkit": "^1.38.0",
"is-url": "^1.2.4",
"nanoid": "^5.1.5",
"pinia": "^3.0.2",
"pinia": "^3.0.3",
"pixi-live2d-display": "^0.4.0",
"pixi.js": "^6.5.10",
"tauri-plugin-macos-permissions-api": "^2.3.0",
"vue": "^3.5.14",
"vue": "^3.5.16",
"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.2",
"@antfu/eslint-config": "^4.13.3",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@iconify-json/iconamoon": "^1.2.2",
"@iconify-json/solar": "^1.2.2",
"@tauri-apps/cli": "^2.5.0",
"@types/is-url": "^1.2.32",
"@types/node": "^22.15.21",
"@unocss/eslint-plugin": "^66.1.2",
"@types/node": "^22.15.29",
"@unocss/eslint-plugin": "^66.1.3",
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^9.27.0",
"eslint": "^9.28.0",
"eslint-plugin-format": "^1.0.1",
"lint-staged": "^15.5.2",
"npm-run-all": "^4.1.5",
"release-it": "^18.1.2",
"sass": "^1.89.0",
"sass": "^1.89.1",
"simple-git-hooks": "^2.13.0",
"tsx": "^4.19.4",
"typescript": "~5.6.3",

1759
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "bongo-cat"
version = "0.5.0"
version = "0.7.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"
@@ -37,6 +36,9 @@ tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
fs_extra = "1"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-global-shortcut = "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,7 @@
"core:window:allow-set-ignore-cursor-events",
"core:window:allow-set-decorations",
"core:window:allow-set-position",
"core:window:allow-set-theme",
"custom-window:default",
"os:default",
"process:default",
@@ -42,6 +43,9 @@
"**/*"
]
},
"clipboard-manager:allow-write-text"
"clipboard-manager:allow-write-text",
"global-shortcut:allow-is-registered",
"global-shortcut:allow-register",
"global-shortcut:allow-unregister"
]
}

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,9 +1,6 @@
#![allow(deprecated)]
use tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};
use tauri_nspanel::{
WebviewWindowExt,
cocoa::appkit::{NSMainMenuWindowLevel, NSWindowCollectionBehavior},
panel_delegate,
};
use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
use tauri_plugin_custom_window::MAIN_WINDOW_LABEL;
#[allow(non_upper_case_globals)]
@@ -26,8 +23,6 @@ pub fn platform(
let panel = main_window.to_panel().unwrap();
panel.set_level(NSMainMenuWindowLevel);
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel | NSResizableWindowMask);
panel.set_collection_behaviour(

View File

@@ -3,27 +3,21 @@ 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,
main_window: WebviewWindow,
preference_window: WebviewWindow,
) {
#[cfg(any(dev, debug_assertions))]
#[cfg(debug_assertions)]
main_window.open_devtools();
platform(app_handle, main_window.clone(), preference_window.clone());

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,
@@ -47,6 +59,7 @@ pub fn run() {
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.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

@@ -29,7 +29,9 @@
"url": "index.html/#/preference",
"visible": false,
"titleBarStyle": "Overlay",
"hiddenTitle": true
"hiddenTitle": true,
"minWidth": 800,
"minHeight": 600
}
],
"security": {
@@ -61,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

@@ -3,7 +3,7 @@ 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 { ConfigProvider, theme } from 'ant-design-vue'
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { isString } from 'es-toolkit'
import isURL from 'is-url'
@@ -19,24 +19,29 @@ import { useAppStore } from './stores/app'
import { useCatStore } from './stores/cat'
import { useGeneralStore } from './stores/general'
import { useModelStore } from './stores/model'
import { useShortcutStore } from './stores/shortcut.ts'
const { generateColorVars } = useThemeVars()
const appStore = useAppStore()
const modelStore = useModelStore()
const catStore = useCatStore()
const generalStore = useGeneralStore()
const shortcutStore = useShortcutStore()
const appWindow = getCurrentWebviewWindow()
const { isRestored, restoreState } = useWindowState()
const { darkAlgorithm, defaultAlgorithm } = theme
onMounted(async () => {
generateColorVars()
await appStore.$tauri.start()
await modelStore.$tauri.start()
await modelStore.init()
await catStore.$tauri.start()
await generalStore.$tauri.start()
restoreState()
await shortcutStore.$tauri.start()
await restoreState()
catStore.init()
})
useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
@@ -75,7 +80,12 @@ useEventListener('click', (event) => {
</script>
<template>
<ConfigProvider :locale="zhCN">
<ConfigProvider
:locale="zhCN"
:theme="{
algorithm: generalStore.isDark ? darkAlgorithm : defaultAlgorithm,
}"
>
<RouterView v-if="isRestored" />
</ConfigProvider>
</template>

View File

@@ -2,18 +2,14 @@
import { Flex } from 'ant-design-vue'
import { computed, useSlots } from 'vue'
const { title, icon, description, vertical } = defineProps<{
const { title, description, vertical } = defineProps<{
title: string
icon?: string
description?: string
vertical?: boolean
}>()
const slots = useSlots()
const hasIcon = computed(() => {
return icon || slots.icon
})
const hasDescription = computed(() => {
return description || slots.description
})
@@ -22,29 +18,19 @@ const hasDescription = computed(() => {
<template>
<Flex
:align="vertical ? void 0 : 'center'"
class="b b-color-2 rounded-lg b-solid bg-white p-4"
class="b b-color-2 rounded-lg b-solid bg-color-3 p-4"
gap="middle"
justify="space-between"
:vertical="vertical"
>
<Flex align="center">
<slot name="icon">
<div
class="text-4"
:class="icon"
/>
</slot>
<Flex
:class="{ 'ml-4': hasIcon }"
vertical
>
<Flex vertical>
<div class="text-sm font-medium">
{{ title }}
</div>
<div
class="text-xs [&_a]:(active:text-color-primary-7 hover:text-color-primary-5 text-color-3) text-color-3"
class="break-all text-xs [&_a]:(active:text-color-primary-7 hover:text-color-primary-5 text-color-3) text-color-3"
:class="{ 'mt-2': hasDescription }"
>
<slot name="description">

View File

@@ -0,0 +1,127 @@
<script setup lang="ts">
import type { Key } from '@/utils/keyboard'
import { find, map, remove, some, split } from 'es-toolkit/compat'
import { ref, useTemplateRef, watch } from 'vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { keys, modifierKeys, standardKeys } from '@/utils/keyboard'
const props = defineProps<{
title: string
description?: string
}>()
const modelValue = defineModel<string>()
const shortcutInputRef = useTemplateRef('shortcutInput')
const isFocusing = ref(false)
const isHovering = ref(false)
const pressedKeys = ref<Key[]>([])
watch(modelValue, () => {
parseModelValue()
}, { immediate: true })
function parseModelValue() {
if (!modelValue.value) {
return pressedKeys.value = []
}
pressedKeys.value = split(modelValue.value, '+').map((tauriKey) => {
return find(keys, { tauriKey })!
})
}
function getEventKey(event: KeyboardEvent) {
const { key, code } = event
const eventKey = key.replace('Meta', 'Command')
const isModifierKey = some(modifierKeys, { eventKey })
return isModifierKey ? eventKey : code
}
function isValidShortcut() {
if (pressedKeys.value?.[0]?.eventKey?.startsWith('F')) {
return true
}
const hasModifierKey = some(pressedKeys.value, ({ eventKey }) => {
return some(modifierKeys, { eventKey })
})
const hasStandardKey = some(pressedKeys.value, ({ eventKey }) => {
return some(standardKeys, { eventKey })
})
return hasModifierKey && hasStandardKey
}
function handleFocus() {
isFocusing.value = true
pressedKeys.value = []
}
function handleBlur() {
isFocusing.value = false
if (!isValidShortcut()) {
return parseModelValue()
}
modelValue.value = map(pressedKeys.value, 'tauriKey').join('+')
}
function handleKeyDown(event: KeyboardEvent) {
const eventKey = getEventKey(event)
const matched = find(keys, { eventKey })
const isInvalid = !matched
const isDuplicate = some(pressedKeys.value, { eventKey })
if (isInvalid || isDuplicate) return
pressedKeys.value.push(matched)
if (isValidShortcut()) {
shortcutInputRef.value?.blur()
}
}
function handleKeyUp(event: KeyboardEvent) {
remove(pressedKeys.value, { eventKey: getEventKey(event) })
}
</script>
<template>
<ProListItem v-bind="props">
<div
ref="shortcutInput"
align="center"
class="relative h-8 min-w-32 flex cursor-text items-center justify-center b b-color-1 hover:b-primary-5 rounded-md b-solid px-2.5 text-color-3 outline-none transition focus:(b-primary shadow-[0_0_0_2px_rgba(5,145,255,0.1)])"
justify="center"
:tabindex="0"
@blur="handleBlur"
@focus="handleFocus"
@keydown="handleKeyDown"
@keyup="handleKeyUp"
@mouseout="isHovering = false"
@mouseover="isHovering = true"
>
<span v-if="pressedKeys.length === 0">
{{ isFocusing ? '按下录制快捷键' : '点击录制快捷键' }}
</span>
<span class="text-primary font-bold">
{{ map(pressedKeys, 'symbol').join(' ') }}
</span>
<div
class="i-iconamoon:close-circle-1 absolute right-2 cursor-pointer text-4 transition hover:text-primary"
:hidden="isFocusing || !isHovering || pressedKeys.length === 0"
@mousedown.prevent="modelValue = ''"
/>
</div>
</ProListItem>
</template>

View File

@@ -11,7 +11,7 @@ import { computed, reactive, watch } from 'vue'
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'
@@ -67,7 +67,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
@@ -122,7 +127,10 @@ async function handleOk() {
} catch (error) {
message.error(String(error))
} finally {
state.downloading = false
Object.assign(state, {
downloading: false,
downloadProgress: 0,
})
}
}
</script>

View File

@@ -1,32 +1,25 @@
import type { Ref } from 'vue'
import type { CursorPoint } from '@/utils/monitor'
import { readDir } from '@tauri-apps/plugin-fs'
import { uniq } from 'es-toolkit'
import { reactive, ref, watch } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { useDebounceFn } from '@vueuse/core'
import { isEqual, mapValues } from 'es-toolkit'
import { ref } 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 { isWindows } from '@/utils/platform'
interface MouseButtonEvent {
kind: 'MousePress' | 'MouseRelease'
value: string
}
interface MouseMoveValue {
x: number
y: number
}
interface MouseMoveEvent {
kind: 'MouseMove'
value: MouseMoveValue
value: CursorPoint
}
interface KeyboardEvent {
@@ -37,107 +30,43 @@ 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 lastCursorPoint = ref<CursorPoint>({ x: 0, y: 0 })
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 startListening = () => {
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
}
const handleRelease = (array: Ref<string[]>, value?: string) => {
if (!value) return
array.value = array.value.filter(item => item !== value)
}
const debouncedRelease = useDebounceFn(handleRelease, 100)
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') && unsupportedKeys) {
nextKey = key.replace(/F(\d+)/, 'Fn')
}
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
if (key.startsWith(item) && unsupportedKeys) {
const regex = new RegExp(`^(${item}).*`)
nextKey = key.replace(regex, '$1')
}
}
if (!supportKeys.includes(nextKey)) continue
return nextKey
if (key.startsWith('F') && unsupportedKey) {
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')
}
}
return nextKey
}
const handleScheduleRelease = (keys: Ref<string[]>, key: string, delay = 500) => {
if (releaseTimers.has(key)) {
clearTimeout(releaseTimers.get(key))
}
const processMouseMove = (point: CursorPoint) => {
const roundedValue = mapValues(point, Math.round)
const timer = setTimeout(() => {
handleRelease(keys, key)
if (isEqual(lastCursorPoint.value, roundedValue)) return
releaseTimers.delete(key)
}, delay)
lastCursorPoint.value = roundedValue
releaseTimers.set(key, timer)
return handleMouseMove(point)
}
useTauriListen<DeviceEvent>(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => {
@@ -148,41 +77,30 @@ export function useDevice() {
if (!nextValue) return
const isLeftSide = supportLeftKeys.value.includes(nextValue)
const pressedKeys = isLeftSide ? pressedLeftKeys : pressedRightKeys
if (nextValue === 'CapsLock') {
handlePress(pressedKeys, nextValue)
handlePress(nextValue)
return handleScheduleRelease(pressedKeys, nextValue, 100)
return debouncedRelease(nextValue)
}
if (kind === 'KeyboardPress') {
if (isWindows) {
handleScheduleRelease(pressedKeys, nextValue)
}
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 processMouseMove(value)
}
})
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 { CursorPoint } from '@/utils/monitor'
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(
@@ -80,23 +71,51 @@ export function useModel() {
catStore.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.singleMode) {
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(point: CursorPoint) {
const monitor = await getCursorMonitor(point)
if (!monitor) return
const { size, position, cursorPosition } = monitor
const { size, position, cursorPoint } = 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)
@@ -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,7 +1,7 @@
import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
import { range } from 'es-toolkit'
import { hideWindow, showWindow } from '@/plugins/window'
import { showWindow } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
import { isMac } from '@/utils/platform'
@@ -66,12 +66,6 @@ export function useSharedMenu() {
MenuItem.new({
text: catStore.visible ? '隐藏猫咪' : '显示猫咪',
action: () => {
if (catStore.visible) {
hideWindow('main')
} else {
showWindow('main')
}
catStore.visible = !catStore.visible
},
}),

View File

@@ -0,0 +1,33 @@
import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut'
import type { Ref } from 'vue'
import {
isRegistered,
register,
unregister,
} from '@tauri-apps/plugin-global-shortcut'
import { ref, watch } from 'vue'
export function useTauriShortcut(shortcut: Ref<string, string>, callback: ShortcutHandler) {
const oldShortcut = ref(shortcut.value)
watch(shortcut, async (value) => {
if (oldShortcut.value) {
const registered = await isRegistered(oldShortcut.value)
if (registered) {
await unregister(oldShortcut.value)
}
}
if (!value) return
await register(value, (event) => {
if (event.state === 'Released') return
callback(event)
})
oldShortcut.value = value
}, { immediate: true })
}

View File

@@ -8,6 +8,7 @@ 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 { GITHUB_LINK, LISTEN_KEY } from '../constants'
import { showWindow } from '../plugins/window'
@@ -23,9 +24,13 @@ export function useTray() {
const catStore = useCatStore()
const { getSharedMenu } = useSharedMenu()
watchDebounced(() => catStore, () => {
watch([() => catStore.visible, () => catStore.penetrable], () => {
updateTrayMenu()
}, { deep: true, debounce: 500 })
})
watchDebounced([() => catStore.scale, () => catStore.opacity], () => {
updateTrayMenu()
}, { debounce: 200 })
const createTray = async () => {
const tray = await getTrayById()
@@ -37,14 +42,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,
}

View File

@@ -1,12 +1,18 @@
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',
}

View File

@@ -1,30 +1,43 @@
<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 { setAlwaysOnTop } from '@/plugins/window'
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()
onMounted(startListening)
onUnmounted(handleDestroy)
const handleDebounceResize = useDebounceFn(async () => {
const debouncedResize = useDebounceFn(async () => {
await handleResize()
resizing.value = false
@@ -33,19 +46,65 @@ const handleDebounceResize = useDebounceFn(async () => {
useEventListener('resize', () => {
resizing.value = true
handleDebounceResize()
debouncedResize()
})
watch(pressedMouses, handleMouseDown)
watch(() => modelStore.currentModel, async (model) => {
if (!model) return
watch(mousePosition, handleMouseMove)
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)
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)
}
}
}, { deep: true, immediate: true })
watch([() => catStore.scale, modelSize], async () => {
if (!modelSize.value) return
const { width, height } = modelSize.value
appWindow.setSize(
new PhysicalSize({
width: Math.round(width * (catStore.scale / 100)),
height: Math.round(height * (catStore.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.visible, async (value) => {
value ? showWindow() : hideWindow()
})
watch(() => catStore.penetrable, (value) => {
@@ -54,13 +113,17 @@ watch(() => catStore.penetrable, (value) => {
watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
function handleWindowDrag() {
watch(() => generalStore.taskbarVisibility, 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(),
})
@@ -68,33 +131,38 @@ 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.scale + delta, 500))
catStore.scale = round(nextScale)
}
</script>
<template>
<div
class="relative size-screen overflow-hidden children:(absolute size-full)"
:class="[catStore.mirrorMode ? '-scale-x-100' : 'scale-x-100']"
:class="{ '-scale-x-100': catStore.mirrorMode }"
:style="{ opacity: catStore.opacity / 100 }"
@contextmenu="handleContextmenu"
@mousedown="handleWindowDrag"
@mousedown="handleMouseDown"
@mousemove="handleMouseMove"
>
<img :src="backgroundImage">
<img
v-if="backgroundImagePath"
: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"
:src="convertFileSrc(path)"
>
<div

View File

@@ -40,7 +40,7 @@ async function copyInfo() {
}
function feedbackIssue() {
openUrl(`${GITHUB_LINK}/issues/new`)
openUrl(`${GITHUB_LINK}/issues/new/choose`)
}
</script>
@@ -68,7 +68,7 @@ function feedbackIssue() {
</ProListItem>
<ProListItem
description="复制软件信息并提供给 Bug Issue"
description="复制软件信息并提供给 Bug Issue"
title="软件信息"
>
<Button @click="copyInfo">

View File

@@ -1,39 +1,31 @@
<script setup lang="ts">
import { Slider, Switch } from 'ant-design-vue'
import { InputNumber, Slider, Switch } from 'ant-design-vue'
import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useCatStore } from '@/stores/cat'
const catStore = useCatStore()
function scaleFormatter(value?: number) {
return value === 100 ? '默认' : `${value}%`
}
function opacityFormatter(value?: number) {
return `${value}%`
}
</script>
<template>
<ProList title="模型设置">
<ProListItem
description="启用后,模型将水平镜像翻转"
description="启用后,模型将水平镜像翻转"
title="镜像模式"
>
<Switch v-model:checked="catStore.mirrorMode" />
</ProListItem>
<ProListItem
description="启用后,每只手只显示最后按下的一个按键"
description="启用后,每只手只显示最后按下的一个按键"
title="单键模式"
>
<Switch v-model:checked="catStore.singleMode" />
</ProListItem>
<ProListItem
description="启用后,鼠标将镜像跟随手部移动"
description="启用后,鼠标将镜像跟随手部移动"
title="鼠标镜像"
>
<Switch v-model:checked="catStore.mouseMirror" />
@@ -42,31 +34,32 @@ function opacityFormatter(value?: number) {
<ProList title="窗口设置">
<ProListItem
description="启用后,窗口不影响对其他应用程序的操作"
description="启用后,窗口不影响对其他应用程序的操作"
title="窗口穿透"
>
<Switch v-model:checked="catStore.penetrable" />
</ProListItem>
<ProListItem
description="启用后,窗口始终显示在其他应用程序上方"
description="启用后,窗口始终显示在其他应用程序上方"
title="窗口置顶"
>
<Switch v-model:checked="catStore.alwaysOnTop" />
</ProListItem>
<ProListItem
description="将鼠标移动到窗口边缘后,也可以拖动调整窗口尺寸"
description="将鼠标移窗口边缘,或按住 Shift 并右键拖动,也可已调整窗口大小。"
title="窗口尺寸"
vertical
>
<Slider
<InputNumber
v-model:value="catStore.scale"
class="m-0!"
:max="150"
:min="50"
:tip-formatter="scaleFormatter"
/>
class="w-28"
:min="1"
>
<template #addonAfter>
%
</template>
</InputNumber>
</ProListItem>
<ProListItem
@@ -78,7 +71,7 @@ function opacityFormatter(value?: number) {
class="m-0!"
:max="100"
:min="10"
:tip-formatter="opacityFormatter"
:tip-formatter="(value) => `${value}%`"
/>
</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.theme !== 'auto') return
generalStore.isDark = payload === 'dark'
})
})
watch(() => generalStore.theme, async (value) => {
let nextTheme = value === 'auto' ? null : value
await appWindow.setTheme(nextTheme)
nextTheme = nextTheme ?? (await appWindow.theme())
generalStore.isDark = nextTheme === 'dark'
}, { immediate: true })
watch(() => generalStore.isDark, (value) => {
if (value) {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}, { immediate: true })
</script>
<template>
<ProListItem title="主题模式">
<Select v-model:value="generalStore.theme">
<SelectOption value="auto">
跟随系统
</SelectOption>
<SelectOption value="light">
亮色模式
</SelectOption>
<SelectOption value="dark">
暗色模式
</SelectOption>
</Select>
</ProListItem>
</template>

View File

@@ -4,6 +4,7 @@ import { 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'
@@ -31,6 +32,17 @@ watch(() => generalStore.autostart, async (value) => {
<ProListItem title="开机自启动">
<Switch v-model:checked="generalStore.autostart" />
</ProListItem>
<ProListItem
description="启用后,即可通过 OBS Studio 捕获窗口。"
title="显示任务栏图标"
>
<Switch v-model:checked="generalStore.taskbarVisibility" />
</ProListItem>
</ProList>
<ProList title="外观设置">
<ThemeMode />
</ProList>
<ProList title="更新设置">

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="制作模型"
@click="openUrl('https://juejin.cn/post/7509872655802269731')"
>
<template #icon>
@@ -23,6 +23,15 @@ import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
</template>
</FloatButton>
<FloatButton
tooltip="转换模型"
@click="openUrl('https://bongocat.vteamer.cc')"
>
<template #icon>
<SyncOutlined />
</template>
</FloatButton>
<FloatButton
tooltip="更多模型"
@click="openUrl('https://github.com/ayangweb/Awesome-BongoCat')"

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'
@@ -53,25 +55,35 @@ 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,
})
@@ -86,7 +98,7 @@ 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"
>

View File

@@ -1,11 +1,14 @@
<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 { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'
import FloatMenu from './components/float-menu/index.vue'
import Upload from './components/upload/index.vue'
@@ -14,12 +17,20 @@ import { useModelStore } from '@/stores/model'
import { join } from '@/utils/path'
const modelStore = useModelStore()
const firstItemRef = ref<HTMLElement>()
const colProps: ColProps = {
xs: 12,
md: 8,
lg: 6,
xl: 4,
const { height } = useElementSize(firstItemRef)
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) {
@@ -42,19 +53,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,12 +82,11 @@ 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">
@@ -82,13 +96,16 @@ async function handleDelete(item: Model) {
title="删除模型"
@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

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

View File

@@ -6,6 +6,7 @@ import About from './components/about/index.vue'
import Cat from './components/cat/index.vue'
import General from './components/general/index.vue'
import Model from './components/model/index.vue'
import Shortcut from './components/shortcut/index.vue'
import UpdateApp from '@/components/update-app/index.vue'
import { useTray } from '@/composables/useTray'
@@ -36,6 +37,11 @@ const menus = [
icon: 'i-solar:magic-stick-3-bold',
component: Model,
},
{
label: '快捷键',
icon: 'i-solar:keyboard-bold',
component: Shortcut,
},
{
label: '关于',
icon: 'i-solar:info-circle-bold',
@@ -47,7 +53,7 @@ const menus = [
<template>
<Flex class="h-screen">
<div
class="h-full w-30 flex flex-col items-center gap-4 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
>
@@ -55,6 +61,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>
@@ -66,9 +73,9 @@ 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 }"
@mousedown="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
class="size-8"
@@ -84,7 +91,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

@@ -1,5 +1,6 @@
import { invoke } from '@tauri-apps/api/core'
import { emit } from '@tauri-apps/api/event'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { LISTEN_KEY } from '../constants'
@@ -9,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) {
@@ -30,3 +32,21 @@ export function hideWindow(label?: WindowLabel) {
export function setAlwaysOnTop(alwaysOnTop: boolean) {
invoke(COMMAND.SET_ALWAYS_ON_TOP, { alwaysOnTop })
}
export async function toggleWindowVisible(label?: WindowLabel) {
const appWindow = getCurrentWebviewWindow()
if (appWindow.label !== label) return
const visible = await appWindow.isVisible()
if (visible) {
return hideWindow(label)
}
return showWindow(label)
}
export async function setTaskbarVisibility(visible: boolean) {
invoke(COMMAND.SET_TASKBAR_VISIBILITY, { visible })
}

View File

@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useCatStore = defineStore('cat', () => {
const visible = ref(true)
const visible = ref(false)
const mirrorMode = ref(false)
const singleMode = ref(false)
const mouseMirror = ref(false)
@@ -11,6 +11,10 @@ export const useCatStore = defineStore('cat', () => {
const scale = ref(100)
const opacity = ref(100)
const init = () => {
visible.value = true
}
return {
visible,
mirrorMode,
@@ -20,5 +24,6 @@ export const useCatStore = defineStore('cat', () => {
alwaysOnTop,
scale,
opacity,
init,
}
})

View File

@@ -1,12 +1,20 @@
import type { Theme } from '@tauri-apps/api/window'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useGeneralStore = defineStore('general', () => {
const autoCheckUpdate = ref(false)
const autostart = ref(false)
const taskbarVisibility = ref(false)
const theme = ref<'auto' | Theme>('auto')
const isDark = ref(false)
return {
autoCheckUpdate,
autostart,
taskbarVisibility,
theme,
isDark,
}
})

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',
},
})

20
src/stores/shortcut.ts Normal file
View File

@@ -0,0 +1,20 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop'
export const useShortcutStore = defineStore('shortcut', () => {
const visibleCat = ref('')
const visiblePreference = ref('')
const mirrorMode = ref('')
const penetrable = ref('')
const alwaysOnTop = ref('')
return {
visibleCat,
visiblePreference,
mirrorMode,
penetrable,
alwaysOnTop,
}
})

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
})
}

276
src/utils/keyboard.ts Normal file
View File

@@ -0,0 +1,276 @@
import { isMac } from './platform'
export interface Key {
eventKey: string
tauriKey?: string
symbol?: string
}
export const modifierKeys: Key[] = [
{
eventKey: 'Shift',
symbol: isMac ? '⇧' : 'Shift',
},
{
eventKey: 'Control',
symbol: isMac ? '⌃' : 'Ctrl',
},
{
eventKey: 'Alt',
symbol: isMac ? '⌥' : 'Alt',
},
{
eventKey: 'Command',
symbol: isMac ? '⌘' : 'Super',
},
].map((item) => {
return { ...item, tauriKey: item.eventKey }
})
export const standardKeys: Key[] = [
// 第一排
{
eventKey: 'Escape',
symbol: isMac ? '⎋' : 'Esc',
},
{
eventKey: 'F1',
},
{
eventKey: 'F2',
},
{
eventKey: 'F3',
},
{
eventKey: 'F4',
},
{
eventKey: 'F5',
},
{
eventKey: 'F6',
},
{
eventKey: 'F7',
},
{
eventKey: 'F8',
},
{
eventKey: 'F9',
},
{
eventKey: 'F10',
},
{
eventKey: 'F11',
},
{
eventKey: 'F12',
}, // 第二排
{
eventKey: 'Backquote',
symbol: '`',
},
{
eventKey: 'Digit1',
},
{
eventKey: 'Digit2',
},
{
eventKey: 'Digit3',
},
{
eventKey: 'Digit4',
},
{
eventKey: 'Digit5',
},
{
eventKey: 'Digit6',
},
{
eventKey: 'Digit7',
},
{
eventKey: 'Digit8',
},
{
eventKey: 'Digit9',
},
{
eventKey: 'Digit0',
},
{
eventKey: 'Minus',
tauriKey: '-',
symbol: '-',
},
{
eventKey: 'Equal',
tauriKey: '=',
symbol: '=',
},
{
eventKey: 'Backspace',
symbol: isMac ? '⌫' : void 0,
},
// 第三排
{
eventKey: 'Tab',
symbol: isMac ? '⇥' : void 0,
},
{
eventKey: 'KeyQ',
},
{
eventKey: 'KeyW',
},
{
eventKey: 'KeyE',
},
{
eventKey: 'KeyR',
},
{
eventKey: 'KeyT',
},
{
eventKey: 'KeyY',
},
{
eventKey: 'KeyU',
},
{
eventKey: 'KeyI',
},
{
eventKey: 'KeyO',
},
{
eventKey: 'KeyP',
},
{
eventKey: 'BracketLeft',
symbol: '[',
},
{
eventKey: 'BracketRight',
symbol: ']',
},
{
eventKey: 'Backslash',
symbol: '\\',
},
// 第四排
{
eventKey: 'KeyA',
},
{
eventKey: 'KeyS',
},
{
eventKey: 'KeyD',
},
{
eventKey: 'KeyF',
},
{
eventKey: 'KeyG',
},
{
eventKey: 'KeyH',
},
{
eventKey: 'KeyJ',
},
{
eventKey: 'KeyK',
},
{
eventKey: 'KeyL',
},
{
eventKey: 'Semicolon',
symbol: ';',
},
{
eventKey: 'Quote',
symbol: '\'',
},
{
eventKey: 'Enter',
symbol: isMac ? '↩︎' : void 0,
},
// 第五排
{
eventKey: 'KeyZ',
},
{
eventKey: 'KeyX',
},
{
eventKey: 'KeyC',
},
{
eventKey: 'KeyV',
},
{
eventKey: 'KeyB',
},
{
eventKey: 'KeyN',
},
{
eventKey: 'KeyM',
},
{
eventKey: 'Comma',
symbol: ',',
},
{
eventKey: 'Period',
symbol: '.',
},
{
eventKey: 'Slash',
symbol: '/',
},
// 第六排
{
eventKey: 'Space',
symbol: isMac ? '␣' : void 0,
},
// 方向键
{
eventKey: 'ArrowUp',
symbol: '↑',
},
{
eventKey: 'ArrowDown',
symbol: '↓',
},
{
eventKey: 'ArrowLeft',
symbol: '←',
},
{
eventKey: 'ArrowRight',
symbol: '→',
},
].map((item) => {
const { eventKey } = item
item.symbol ??= eventKey
item.tauriKey ??= eventKey
if (eventKey.startsWith('Digit') || eventKey.startsWith('Key')) {
item.tauriKey = item.symbol = eventKey.slice(-1)
}
return item
})
export const keys = modifierKeys.concat(standardKeys)

View File

@@ -1,3 +1,4 @@
import type { ModelSize } from '@/composables/useModel'
import type { Cubism4InternalModel } from 'pixi-live2d-display'
import { convertFileSrc } from '@tauri-apps/api/core'
@@ -15,22 +16,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()
@@ -59,9 +59,12 @@ class Live2d {
this.app?.stage.addChild(this.model)
const { width, height } = this.model
const { motions, expressions } = modelSettings
return {
width,
height,
motions,
expressions,
}
@@ -71,6 +74,21 @@ class Live2d {
this.model?.destroy()
}
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) {
return this.model?.motion(group, index)
}
@@ -98,7 +116,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,17 +1,26 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import {
cursorPosition,
monitorFromPoint,
} from '@tauri-apps/api/window'
import { monitorFromPoint } from '@tauri-apps/api/window'
import { mapValues } from 'es-toolkit'
export async function getCursorMonitor() {
const appWindow = getCurrentWebviewWindow()
import { isMac } from './platform'
const scaleFactor = await appWindow.scaleFactor()
export interface CursorPoint {
x: number
y: number
}
const point = await cursorPosition()
export async function getCursorMonitor(point: CursorPoint) {
let cursorPoint = point
const { x, y } = point.toLogical(scaleFactor)
if (isMac) {
const appWindow = getCurrentWebviewWindow()
const scaleFactor = await appWindow.scaleFactor()
cursorPoint = mapValues(cursorPoint, value => value * scaleFactor)
}
const { x, y } = point
const monitor = await monitorFromPoint(x, y)
@@ -19,6 +28,6 @@ export async function getCursorMonitor() {
return {
...monitor,
cursorPosition: point,
cursorPoint,
}
}

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]
}
}
}