Compare commits
51 Commits
linux-wayl
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9fb19aa2f | ||
|
|
ef17ba9ebc | ||
|
|
d69669bd3a | ||
|
|
2fc6ec9d2f | ||
|
|
50803eb2ce | ||
|
|
57892f3ab3 | ||
|
|
b59ec09354 | ||
|
|
49b5d9954c | ||
|
|
f1fac05975 | ||
|
|
6554508c30 | ||
|
|
7e7c1aded0 | ||
|
|
1e24c04da2 | ||
|
|
392963f6fd | ||
|
|
29623577ff | ||
|
|
a75c2d6a74 | ||
|
|
af76a58fba | ||
|
|
cd0066d2fa | ||
|
|
4b717363c1 | ||
|
|
c486a32372 | ||
|
|
ec4150d68b | ||
|
|
3aa24f5b75 | ||
|
|
5c0de3afb6 | ||
|
|
bbdb63ff6d | ||
|
|
c163f52fd0 | ||
|
|
ff4d590f4c | ||
|
|
e4084aabb5 | ||
|
|
9abd640197 | ||
|
|
54872012b1 | ||
|
|
0eac8e489a | ||
|
|
7a997a4bfd | ||
|
|
9aeee8d105 | ||
|
|
4c0a30d621 | ||
|
|
c33703e368 | ||
|
|
36aaf1067e | ||
|
|
ce3b9bc544 | ||
|
|
aa3b09f171 | ||
|
|
38adb1801e | ||
|
|
7e5fb0dbe1 | ||
|
|
52c0c714f3 | ||
|
|
ed06f39976 | ||
|
|
07767324fb | ||
|
|
55aea542df | ||
|
|
db542c3fe1 | ||
|
|
344669a85c | ||
|
|
39e4052b1b | ||
|
|
61d0bd28ad | ||
|
|
13bf0a8376 | ||
|
|
93137883e6 | ||
|
|
1e844d0ab9 | ||
|
|
c50b197fb3 | ||
|
|
8bb06f6cc8 |
2
.github/workflows/release.yml
vendored
@@ -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
|
||||
|
||||
6
.vscode/settings.json
vendored
@@ -51,5 +51,9 @@
|
||||
],
|
||||
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"typescript.tsdk": "./node_modules/typescript/lib"
|
||||
"typescript.tsdk": "./node_modules/typescript/lib",
|
||||
|
||||
"i18n-ally.localesPaths": ["src/locales"],
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.displayLanguage": "zh-CN"
|
||||
}
|
||||
|
||||
169
Cargo.lock
generated
@@ -450,11 +450,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bongo-cat"
|
||||
version = "0.6.2"
|
||||
version = "0.9.0"
|
||||
dependencies = [
|
||||
"fs_extra",
|
||||
"input",
|
||||
"nix",
|
||||
"gilrs",
|
||||
"rdev",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -467,6 +466,7 @@ dependencies = [
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-locale",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-macos-permissions",
|
||||
"tauri-plugin-opener",
|
||||
@@ -973,6 +973,18 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "current_locale"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0be8ddcccda8be68d8e31a421ceea7c79857404daa052434ae30ce2f402cd10"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"objc",
|
||||
"objc-foundation",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.20.11"
|
||||
@@ -1776,6 +1788,39 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs"
|
||||
version = "0.11.0"
|
||||
source = "git+https://github.com/ayangweb/gilrs#f6cecd62943ebbaf3905318e5c71130c9cc4d516"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"gilrs-core",
|
||||
"log",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gilrs-core"
|
||||
version = "0.6.4"
|
||||
source = "git+https://github.com/ayangweb/gilrs#f6cecd62943ebbaf3905318e5c71130c9cc4d516"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"inotify 0.11.0",
|
||||
"io-kit-sys",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"rusty-xinput",
|
||||
"uuid",
|
||||
"vec_map",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
@@ -1981,12 +2026,6 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.1"
|
||||
@@ -2321,6 +2360,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
@@ -2331,33 +2381,13 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input"
|
||||
version = "0.9.1"
|
||||
name = "io-kit-sys"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9"
|
||||
checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"input-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"udev",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input-sys"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
"core-foundation-sys",
|
||||
"mach2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2639,6 +2669,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macos-accessibility-client"
|
||||
version = "0.0.1"
|
||||
@@ -2801,6 +2840,18 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.30.1"
|
||||
@@ -3481,7 +3532,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.5.1",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix 1.0.7",
|
||||
"tracing",
|
||||
@@ -3828,7 +3879,7 @@ checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539"
|
||||
[[package]]
|
||||
name = "rdev"
|
||||
version = "0.5.0-2"
|
||||
source = "git+https://github.com/ayangweb/rdev#a8d5724f4533a69958280aa4df6bf6bca042d064"
|
||||
source = "git+https://github.com/kunkunsh/rdev#cb9a29e19668a52e4e67d8a0ca6739c1807f8d3f"
|
||||
dependencies = [
|
||||
"cocoa 0.24.1",
|
||||
"core-foundation 0.9.4",
|
||||
@@ -3837,7 +3888,7 @@ dependencies = [
|
||||
"dispatch",
|
||||
"enum-map",
|
||||
"epoll",
|
||||
"inotify",
|
||||
"inotify 0.10.2",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -4134,6 +4185,17 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-xinput"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3335c2b62e1e48dd927f6c8941705386e3697fa944aabcb10431bea7ee47ef3"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"log",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
@@ -4969,6 +5031,19 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-locale"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5686c1fe8fb748c45ba0b516db42f78dd6fde91b16bc5248de438eff8d2e6abd"
|
||||
dependencies = [
|
||||
"current_locale",
|
||||
"serde",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.4.0"
|
||||
@@ -5654,18 +5729,6 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "udev"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
@@ -5796,6 +5859,12 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
|
||||
|
||||
[[package]]
|
||||
name = "version-compare"
|
||||
version = "0.2.0"
|
||||
@@ -6745,7 +6814,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-lite",
|
||||
"hex",
|
||||
"nix",
|
||||
"nix 0.30.1",
|
||||
"ordered-stream",
|
||||
"serde",
|
||||
"serde_repr",
|
||||
|
||||
138
README.md
@@ -2,50 +2,25 @@
|
||||
|
||||
<div align="center">
|
||||
<div>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases">
|
||||
<img
|
||||
alt="Windows"
|
||||
src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg=="
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases">
|
||||
<img
|
||||
alt="MacOS"
|
||||
src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases">
|
||||
<img
|
||||
alt="Linux"
|
||||
src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases"><img alt="Windows" src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg==" /></a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases"><img alt="MacOS" src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white" /></a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases"><img alt="Linux" src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white" /></a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a href="./LICENSE">
|
||||
<img
|
||||
src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases/latest">
|
||||
<img
|
||||
src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases">
|
||||
<img
|
||||
src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square"
|
||||
/>
|
||||
</a>
|
||||
<a href="./LICENSE"><img src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square" /></a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases/latest"><img src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square"/></a>
|
||||
<a href="https://github.com/ayangweb/BongoCat/releases"><img src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square"/></a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://trendshift.io/developers/8507" target="_blank"><img src="https://trendshift.io/api/badge/developers/8507" alt="ayangweb | Trendshift" width="250" height="55" /></a>
|
||||
<a href="https://trendshift.io/repositories/14605" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14605" alt="ayangweb%2FBongoCat | Trendshift" width="250" height="55" /></a>
|
||||
<a href="https://hellogithub.com/repository/7d23863fd4be47b39e816193ded385c9" target="_blank">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
|
||||
<img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
|
||||
<img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" width="250" height="55" />
|
||||
</picture>
|
||||
</a>
|
||||
</p>
|
||||
@@ -55,30 +30,38 @@
|
||||
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
|  |  |  |
|
||||
|
||||
## 赞助商
|
||||
|
||||
<a href="https://www.toolsetlink.com">
|
||||
<img height="54" alt="UpgradeLink" src="https://github.com/user-attachments/assets/6b84fb0f-3f1d-44b5-9932-2298bc999d8d" />
|
||||
</a>
|
||||
|
||||
## 开发背景
|
||||
|
||||
本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱,但仅支持 Windows 平台。作为一名深度 macOS 用户,我特别希望在自己的设备上也能使用这款可爱的猫咪,于是我决定开发一个适配 macOS 的版本。
|
||||
|
||||
同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux,让更多的用户都能与这只可爱的猫咪互动!
|
||||
同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux(x11),让更多的用户都能与这只可爱的猫咪互动!
|
||||
|
||||
## 下载
|
||||
|
||||
- [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1)
|
||||
- [GitHub Releases](https://github.com/ayangweb/BongoCat/releases)
|
||||
|
||||
> 不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。
|
||||
不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。
|
||||
|
||||
## 功能介绍
|
||||
|
||||
- 适配 macOS、Windows 和 Linux。
|
||||
- 根据据键盘或鼠标操作,同步移动鼠标或敲击键盘。
|
||||
- 适配 macOS、Windows 和 Linux(x11)。
|
||||
- 根据键盘、鼠标或手柄的操作,同步对应的动作。
|
||||
- 支持导入自定义模型,自由打造专属猫咪形象。
|
||||
- 完全开源,代码公开透明,绝不收集任何用户数据。
|
||||
- 支持离线运行,无需联网,保护用户隐私。
|
||||
|
||||
## 使用提示
|
||||
## 模型转换
|
||||
|
||||
- Linux 下需要用户系统安装 libinput 并加入 `input` 用户组,方可在 X11 和 Wayland 下正常使用。
|
||||
如果你想将 Bongo-Cat-Mver 应用中的模型转换为兼容 BongoCat 的格式,可以使用以下工具:
|
||||
|
||||
🔗 [在线转换](https://bongocat.vteamer.cc)
|
||||
|
||||
## 更多模型
|
||||
|
||||
@@ -88,20 +71,73 @@
|
||||
|
||||
## 社区交流
|
||||
|
||||
<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>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>QQ 群 1</th>
|
||||
<th>QQ 群 2</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="https://qm.qq.com/q/AS3gNv2Vzy">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://i0.hdslb.com/bfs/openplatform/8ecdc4982ab01b59d7731fcca3ec26631a274560.png"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png"
|
||||
/>
|
||||
<img
|
||||
alt="QQ Group 1"
|
||||
src="https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png"
|
||||
height="250"
|
||||
/>
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<a href="https://qm.qq.com/q/TmltLAod2O">
|
||||
<picture>
|
||||
<source
|
||||
media="(prefers-color-scheme: dark)"
|
||||
srcset="https://i0.hdslb.com/bfs/openplatform/473c522487ff33e0f32b15466aeb0734f17161c8.png"
|
||||
/>
|
||||
<source
|
||||
media="(prefers-color-scheme: light)"
|
||||
srcset="https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png"
|
||||
/>
|
||||
<img
|
||||
alt="QQ Group 2"
|
||||
src="https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png"
|
||||
height="250"
|
||||
/>
|
||||
</picture>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
## 赞赏
|
||||
|
||||
每一份认可都值得被珍视!赞赏随缘,心意无价,谢谢你的支持 ❤️
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/e7438bff14cdfb6bfd0feacbb482f99ea4093294.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png" />
|
||||
<img alt="微信赞赏码" src="https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png" height="250" />
|
||||
</picture>
|
||||
|
||||
## 贡献指南
|
||||
|
||||
感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。
|
||||
|
||||
<a href="https://github.com/ayangweb/BongoCat/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=ayangweb/BongoCat" />
|
||||
<a href="https://openomy.com/ayangweb/BongoCat" target="_blank" style="display: block; width: 100%;" align="center">
|
||||
<img src="https://openomy.com/svg?repo=ayangweb/BongoCat&chart=bubble" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
|
||||
</a>
|
||||
|
||||
## 历史星标
|
||||
@@ -113,7 +149,3 @@
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 致谢
|
||||
|
||||
- 特别感谢 [UpgradeLink](https://www.toolsetlink.com/) 提供高效稳定的自动更新服务,让本项目得以持续为用户带来最新版本的优质体验。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bongo-cat",
|
||||
"type": "module",
|
||||
"version": "0.6.2",
|
||||
"version": "0.9.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "ayangweb",
|
||||
@@ -39,12 +39,15 @@
|
||||
"dayjs": "^1.11.13",
|
||||
"es-toolkit": "^1.38.0",
|
||||
"is-url": "^1.2.4",
|
||||
"json5": "^2.2.3",
|
||||
"nanoid": "^5.1.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pixi-live2d-display": "^0.4.0",
|
||||
"pixi.js": "^6.5.10",
|
||||
"tauri-plugin-locale-api": "^2.0.1",
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
||||
"vue": "^3.5.16",
|
||||
"vue-i18n": "^11.1.12",
|
||||
"vue-markdown-render": "^2.2.1",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue3-masonry-css": "^1.0.7"
|
||||
|
||||
60
pnpm-lock.yaml
generated
@@ -62,6 +62,9 @@ importers:
|
||||
is-url:
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
json5:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
nanoid:
|
||||
specifier: ^5.1.5
|
||||
version: 5.1.5
|
||||
@@ -74,12 +77,18 @@ importers:
|
||||
pixi.js:
|
||||
specifier: ^6.5.10
|
||||
version: 6.5.10
|
||||
tauri-plugin-locale-api:
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
tauri-plugin-macos-permissions-api:
|
||||
specifier: ^2.3.0
|
||||
version: 2.3.0
|
||||
vue:
|
||||
specifier: ^3.5.16
|
||||
version: 3.5.16(typescript@5.6.3)
|
||||
vue-i18n:
|
||||
specifier: ^11.1.12
|
||||
version: 11.1.12(vue@3.5.16(typescript@5.6.3))
|
||||
vue-markdown-render:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.1(vue@3.5.16(typescript@5.6.3))
|
||||
@@ -733,6 +742,18 @@ packages:
|
||||
'@types/node':
|
||||
optional: true
|
||||
|
||||
'@intlify/core-base@11.1.12':
|
||||
resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/message-compiler@11.1.12':
|
||||
resolution: {integrity: sha512-Fv9iQSJoJaXl4ZGkOCN1LDM3trzze0AS2zRz2EHLiwenwL6t0Ki9KySYlyr27yVOj5aVz0e55JePO+kELIvfdQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@intlify/shared@11.1.12':
|
||||
resolution: {integrity: sha512-Om86EjuQtA69hdNj3GQec9ZC0L0vPSAnXzB3gP/gyJ7+mA7t06d9aOAiqMZ+xEOsumGP4eEBlfl8zF2LOTzf2A==}
|
||||
engines: {node: '>= 16'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@@ -3209,6 +3230,11 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
json5@2.2.3:
|
||||
resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==}
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsonc-eslint-parser@2.4.0:
|
||||
resolution: {integrity: sha512-WYDyuc/uFcGp6YtM2H0uKmUwieOuzeE/5YocFJLnLfclZ4inf3mRn8ZVy1s7Hxji7Jxm6Ss8gqpexD/GlKoGgg==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
@@ -4411,6 +4437,9 @@ packages:
|
||||
resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
tauri-plugin-locale-api@2.0.1:
|
||||
resolution: {integrity: sha512-kiNcbP6ZKUpPbFseePrR9eb8kTRw8tSJ0ZBpQk4mDyGcbK3xGu0/dy7Cx8lBfC/p2MlmnyO4uRVIwta7FoV6eQ==}
|
||||
|
||||
tauri-plugin-macos-permissions-api@2.3.0:
|
||||
resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==}
|
||||
|
||||
@@ -4652,6 +4681,12 @@ packages:
|
||||
peerDependencies:
|
||||
vue: ^3.4.37
|
||||
|
||||
vue-i18n@11.1.12:
|
||||
resolution: {integrity: sha512-BnstPj3KLHLrsqbVU2UOrPmr0+Mv11bsUZG0PyCOzsawCivk8W00GMXHeVUWIDOgNaScCuZah47CZFE+Wnl8mw==}
|
||||
engines: {node: '>= 16'}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
vue-markdown-render@2.2.1:
|
||||
resolution: {integrity: sha512-XkYnC0PMdbs6Vy6j/gZXSvCuOS0787Se5COwXlepRqiqPiunyCIeTPQAO2XnB4Yl04EOHXwLx5y6IuszMWSgyQ==}
|
||||
peerDependencies:
|
||||
@@ -5354,6 +5389,18 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/node': 22.15.29
|
||||
|
||||
'@intlify/core-base@11.1.12':
|
||||
dependencies:
|
||||
'@intlify/message-compiler': 11.1.12
|
||||
'@intlify/shared': 11.1.12
|
||||
|
||||
'@intlify/message-compiler@11.1.12':
|
||||
dependencies:
|
||||
'@intlify/shared': 11.1.12
|
||||
source-map-js: 1.2.1
|
||||
|
||||
'@intlify/shared@11.1.12': {}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.8':
|
||||
dependencies:
|
||||
'@jridgewell/set-array': 1.2.1
|
||||
@@ -7992,6 +8039,8 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsonc-eslint-parser@2.4.0:
|
||||
dependencies:
|
||||
acorn: 8.14.1
|
||||
@@ -9437,6 +9486,10 @@ snapshots:
|
||||
|
||||
tapable@2.2.2: {}
|
||||
|
||||
tauri-plugin-locale-api@2.0.1:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.5.0
|
||||
|
||||
tauri-plugin-macos-permissions-api@2.3.0:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.5.0
|
||||
@@ -9710,6 +9763,13 @@ snapshots:
|
||||
dependencies:
|
||||
vue: 3.5.16(typescript@5.6.3)
|
||||
|
||||
vue-i18n@11.1.12(vue@3.5.16(typescript@5.6.3)):
|
||||
dependencies:
|
||||
'@intlify/core-base': 11.1.12
|
||||
'@intlify/shared': 11.1.12
|
||||
'@vue/devtools-api': 6.6.4
|
||||
vue: 3.5.16(typescript@5.6.3)
|
||||
|
||||
vue-markdown-render@2.2.1(vue@3.5.16(typescript@5.6.3)):
|
||||
dependencies:
|
||||
markdown-it: 13.0.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bongo-cat"
|
||||
version = "0.6.2"
|
||||
version = "0.9.0"
|
||||
description = "A Tauri App"
|
||||
authors = [ "ayangweb" ]
|
||||
edition = "2024"
|
||||
@@ -37,16 +37,12 @@ tauri-plugin-fs = "2"
|
||||
fs_extra = "1"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-locale = "2"
|
||||
rdev = { git = "https://github.com/kunkunsh/rdev" }
|
||||
gilrs = { git = "https://github.com/ayangweb/gilrs", default-features = false, features = ["xinput"] }
|
||||
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel.workspace = true
|
||||
|
||||
[target."cfg(not(target_os = \"linux\"))".dependencies]
|
||||
rdev = { git = "https://github.com/ayangweb/rdev" }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
nix = { version = "0.30", features = ["poll"] }
|
||||
input = "0.9"
|
||||
|
||||
[features]
|
||||
cargo-clippy = []
|
||||
|
||||
69
src-tauri/assets/models/gamepad/cat.model3.json
Normal 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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/demomodel3.1024/texture_00.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
src-tauri/assets/models/gamepad/demomodel3.1024/texture_01.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
src-tauri/assets/models/gamepad/demomodel3.1024/texture_02.png
Normal file
|
After Width: | Height: | Size: 96 KiB |
289
src-tauri/assets/models/gamepad/demomodel3.cdi3.json
Normal 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": "闪电"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/demomodel3.moc3
Normal file
15
src-tauri/assets/models/gamepad/exp_1.exp3.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Value": 0.321,
|
||||
"Blend": "Multiply"
|
||||
},
|
||||
{
|
||||
"Id": "ParamEyeROpen",
|
||||
"Value": 0.313,
|
||||
"Blend": "Multiply"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
src-tauri/assets/models/gamepad/exp_2.exp3.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "ParamEyeLOpen",
|
||||
"Value": -1,
|
||||
"Blend": "Add"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"Parameters": []
|
||||
}
|
||||
11
src-tauri/assets/models/gamepad/live2d_expression1.exp3.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"FadeInTime": 0.8,
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "Param4",
|
||||
"Value": 1,
|
||||
"Blend": "Add"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
src-tauri/assets/models/gamepad/live2d_expression2.exp3.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"Type": "Live2D Expression",
|
||||
"FadeInTime": 0.5,
|
||||
"Parameters": [
|
||||
{
|
||||
"Id": "Param5",
|
||||
"Value": 1,
|
||||
"Blend": "Add"
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/live2d_motion1.flac
Normal file
76
src-tauri/assets/models/gamepad/live2d_motion1.motion3.json
Normal 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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
77
src-tauri/assets/models/gamepad/live2d_motion2.motion3.json
Normal 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
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
src-tauri/assets/models/gamepad/resources/background.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
src-tauri/assets/models/gamepad/resources/cover.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
src-tauri/assets/models/gamepad/resources/left-keys/DPadDown.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/left-keys/DPadLeft.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/left-keys/DPadUp.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/East.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/North.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/South.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
src-tauri/assets/models/gamepad/resources/right-keys/West.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
24
src-tauri/assets/models/keyboard/cat.model3.json
Executable file → Normal 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
@@ -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
|
||||
|
||||
BIN
src-tauri/assets/tray-mac.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -14,6 +14,8 @@
|
||||
"core:window:allow-set-ignore-cursor-events",
|
||||
"core:window:allow-set-decorations",
|
||||
"core:window:allow-set-position",
|
||||
"core:window:allow-set-theme",
|
||||
"core:window:allow-set-title",
|
||||
"custom-window:default",
|
||||
"os:default",
|
||||
"process:default",
|
||||
@@ -45,6 +47,7 @@
|
||||
"clipboard-manager:allow-write-text",
|
||||
"global-shortcut:allow-is-registered",
|
||||
"global-shortcut:allow-register",
|
||||
"global-shortcut:allow-unregister"
|
||||
"global-shortcut:allow-unregister",
|
||||
"locale:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,40 +1,54 @@
|
||||
use rdev::{Event, EventType, listen};
|
||||
use serde_json::json;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
use crate::core::{device::{DeviceEvent, DeviceKind}};
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum DeviceEventKind {
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
MouseMove,
|
||||
KeyboardPress,
|
||||
KeyboardRelease,
|
||||
}
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceEvent {
|
||||
kind: DeviceEventKind,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
static IS_LISTENING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_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_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,
|
||||
@@ -1,128 +0,0 @@
|
||||
use input::{
|
||||
event::{
|
||||
keyboard::{KeyState, KeyboardEventTrait},
|
||||
pointer::ButtonState,
|
||||
PointerEvent,
|
||||
},
|
||||
Event, Libinput, LibinputInterface,
|
||||
};
|
||||
use nix::{
|
||||
libc::{O_RDONLY, O_RDWR, O_WRONLY},
|
||||
poll::{poll, PollFd, PollFlags, PollTimeout},
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions}, os::{fd::{AsFd, OwnedFd}, unix::prelude::OpenOptionsExt}, path::Path
|
||||
};
|
||||
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
use crate::core::{device::{DeviceEvent, DeviceKind}, setup::key_from_code};
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub struct Interface;
|
||||
|
||||
impl LibinputInterface for Interface {
|
||||
fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
|
||||
OpenOptions::new()
|
||||
.custom_flags(flags)
|
||||
.read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0))
|
||||
.write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0))
|
||||
.open(path)
|
||||
.map(|file| file.into())
|
||||
.map_err(|err| err.raw_os_error().unwrap())
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
fn close_restricted(&mut self, fd: OwnedFd) {
|
||||
File::from(fd);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_device_event(event: &Event) -> Option<DeviceEvent> {
|
||||
match event {
|
||||
Event::Keyboard(ev) => {
|
||||
let key_code = ev.key();
|
||||
let key_name = match key_from_code(key_code) {
|
||||
Some(name) => name.to_string(),
|
||||
None => format!("Unknown({})", key_code),
|
||||
};
|
||||
match ev.key_state() {
|
||||
KeyState::Pressed => Some(DeviceEvent {
|
||||
kind: DeviceKind::KeyboardPress,
|
||||
value: json!(key_name),
|
||||
}),
|
||||
KeyState::Released => Some(DeviceEvent{
|
||||
kind: DeviceKind::KeyboardRelease,
|
||||
value: json!(key_name),
|
||||
})
|
||||
}
|
||||
},
|
||||
Event::Pointer(ev) => {
|
||||
match ev {
|
||||
PointerEvent::Button(e) => {
|
||||
let btn_code = e.button();
|
||||
let btn_name = match btn_code {
|
||||
0x110 => String::from("Left"),
|
||||
0x111 => String::from("Right"),
|
||||
0x112 => String::from("Middle"),
|
||||
_ => format!("Unknown({})", btn_code as u8),
|
||||
};
|
||||
match e.button_state() {
|
||||
ButtonState::Pressed => Some(DeviceEvent {
|
||||
kind: DeviceKind::MousePress,
|
||||
value: json!(btn_name),
|
||||
}),
|
||||
ButtonState::Released => Some(DeviceEvent {
|
||||
kind: DeviceKind::MouseRelease,
|
||||
value: json!(btn_name),
|
||||
})
|
||||
}
|
||||
},
|
||||
PointerEvent::Motion(e) => {
|
||||
Some(DeviceEvent {
|
||||
kind: DeviceKind::MouseMove,
|
||||
value: json!({
|
||||
"x": e.dx_unaccelerated(),
|
||||
"y": e.dy_unaccelerated()
|
||||
}),
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_string());
|
||||
}
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
let mut input = Libinput::new_with_udev(Interface);
|
||||
match input.udev_assign_seat("seat0") {
|
||||
Ok(_) => {
|
||||
let input_clone = &input.clone();
|
||||
let mut pollfds = [PollFd::new(input_clone.as_fd(), PollFlags::POLLIN)];
|
||||
while poll(&mut pollfds, PollTimeout::NONE).is_ok() {
|
||||
input.dispatch().unwrap();
|
||||
for event in &mut input {
|
||||
let device_event = build_device_event(&event);
|
||||
if let Some(e) = device_event {
|
||||
app_handle.emit("device-changed", e).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => return Err("Failed to assign seat".to_string()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub mod common;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub use common::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum DeviceKind {
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
MouseMove,
|
||||
KeyboardPress,
|
||||
KeyboardRelease,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceEvent {
|
||||
kind: DeviceKind,
|
||||
value: Value,
|
||||
}
|
||||
61
src-tauri/src/core/gamepad.rs
Normal 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);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod device;
|
||||
pub mod gamepad;
|
||||
pub mod prevent_default;
|
||||
pub mod setup;
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
use tauri::{AppHandle, WebviewWindow};
|
||||
|
||||
pub fn platform(
|
||||
_app_handle: &AppHandle,
|
||||
_main_window: WebviewWindow,
|
||||
_preference_window: WebviewWindow,
|
||||
) {
|
||||
}
|
||||
|
||||
pub fn key_from_code(code: u32) -> Option<&'static str> {
|
||||
match code {
|
||||
// Function key
|
||||
1 => Some("Escape"),
|
||||
28 => Some("Return"),
|
||||
14 => Some("Backspace"),
|
||||
15 => Some("Tab"),
|
||||
57 => Some("Space"),
|
||||
58 => Some("CapsLock"),
|
||||
99 => Some("PrintScreen"),
|
||||
70 => Some("ScrollLock"),
|
||||
119 => Some("Pause"),
|
||||
69 => Some("NumLock"),
|
||||
110 => Some("Insert"),
|
||||
102 => Some("Home"),
|
||||
107 => Some("End"),
|
||||
104 => Some("PageUp"),
|
||||
109 => Some("PageDown"),
|
||||
111 => Some("Delete"),
|
||||
|
||||
// Arrow key
|
||||
103 => Some("UpArrow"),
|
||||
108 => Some("DownArrow"),
|
||||
105 => Some("LeftArrow"),
|
||||
106 => Some("RightArrow"),
|
||||
|
||||
// F key
|
||||
59 => Some("F1"),
|
||||
60 => Some("F2"),
|
||||
61 => Some("F3"),
|
||||
62 => Some("F4"),
|
||||
63 => Some("F5"),
|
||||
64 => Some("F6"),
|
||||
65 => Some("F7"),
|
||||
66 => Some("F8"),
|
||||
67 => Some("F9"),
|
||||
68 => Some("F10"),
|
||||
87 => Some("F11"),
|
||||
88 => Some("F12"),
|
||||
|
||||
// Numeric
|
||||
2 => Some("Num1"),
|
||||
3 => Some("Num2"),
|
||||
4 => Some("Num3"),
|
||||
5 => Some("Num4"),
|
||||
6 => Some("Num5"),
|
||||
7 => Some("Num6"),
|
||||
8 => Some("Num7"),
|
||||
9 => Some("Num8"),
|
||||
10 => Some("Num9"),
|
||||
11 => Some("Num0"),
|
||||
|
||||
// Alphabetic
|
||||
16 => Some("KeyQ"),
|
||||
17 => Some("KeyW"),
|
||||
18 => Some("KeyE"),
|
||||
19 => Some("KeyR"),
|
||||
20 => Some("KeyT"),
|
||||
21 => Some("KeyY"),
|
||||
22 => Some("KeyU"),
|
||||
23 => Some("KeyI"),
|
||||
24 => Some("KeyO"),
|
||||
25 => Some("KeyP"),
|
||||
30 => Some("KeyA"),
|
||||
31 => Some("KeyS"),
|
||||
32 => Some("KeyD"),
|
||||
33 => Some("KeyF"),
|
||||
34 => Some("KeyG"),
|
||||
35 => Some("KeyH"),
|
||||
36 => Some("KeyJ"),
|
||||
37 => Some("KeyK"),
|
||||
38 => Some("KeyL"),
|
||||
44 => Some("KeyZ"),
|
||||
45 => Some("KeyX"),
|
||||
46 => Some("KeyC"),
|
||||
47 => Some("KeyV"),
|
||||
48 => Some("KeyB"),
|
||||
49 => Some("KeyN"),
|
||||
50 => Some("KeyM"),
|
||||
|
||||
// Symbolic
|
||||
41 => Some("BackQuote"),
|
||||
12 => Some("Minus"),
|
||||
13 => Some("Equal"),
|
||||
26 => Some("LeftBracket"),
|
||||
27 => Some("RightBracket"),
|
||||
39 => Some("SemiColon"),
|
||||
40 => Some("Quote"),
|
||||
43 => Some("BackSlash"),
|
||||
86 => Some("IntlBackslash"),
|
||||
89 => Some("IntlRo"),
|
||||
124 => Some("IntlYen"),
|
||||
101 => Some("KanaMode"),
|
||||
51 => Some("Comma"),
|
||||
52 => Some("Dot"),
|
||||
53 => Some("Slash"),
|
||||
|
||||
// Control key
|
||||
29 => Some("ControlLeft"),
|
||||
97 => Some("ControlRight"),
|
||||
42 => Some("ShiftLeft"),
|
||||
54 => Some("ShiftRight"),
|
||||
56 => Some("Alt"),
|
||||
100 => Some("AltGr"),
|
||||
125 => Some("MetaLeft"),
|
||||
126 => Some("MetaRight"),
|
||||
127 => Some("Apps"),
|
||||
|
||||
// NumPad
|
||||
55 => Some("KpMultiply"),
|
||||
78 => Some("KpMinus"),
|
||||
74 => Some("KpPlus"),
|
||||
98 => Some("KpDivide"),
|
||||
117 => Some("KpEqual"),
|
||||
121 => Some("KpComma"),
|
||||
96 => Some("KpReturn"),
|
||||
83 => Some("KpDecimal"),
|
||||
79 => Some("Kp1"),
|
||||
80 => Some("Kp2"),
|
||||
81 => Some("Kp3"),
|
||||
75 => Some("Kp4"),
|
||||
76 => Some("Kp5"),
|
||||
77 => Some("Kp6"),
|
||||
71 => Some("Kp7"),
|
||||
72 => Some("Kp8"),
|
||||
73 => Some("Kp9"),
|
||||
82 => Some("Kp0"),
|
||||
|
||||
// Media key
|
||||
115 => Some("VolumeUp"),
|
||||
114 => Some("VolumeDown"),
|
||||
113 => Some("VolumeMute"),
|
||||
|
||||
// Language key
|
||||
90 => Some("Lang1"),
|
||||
91 => Some("Lang2"),
|
||||
92 => Some("Lang3"),
|
||||
93 => Some("Lang4"),
|
||||
94 => Some("Lang5"),
|
||||
|
||||
// Other
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ pub fn platform(
|
||||
"window_did_resize" => {
|
||||
window_move_event();
|
||||
|
||||
if let Ok(size) = main_window.inner_size() {
|
||||
if let Ok(size) = main_window.outer_size() {
|
||||
let _ = main_window.emit_to(target, WINDOW_RESIZED_EVENT, size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,20 +3,14 @@ use tauri::{AppHandle, WebviewWindow};
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub mod common;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::*;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
pub use common::*;
|
||||
|
||||
pub fn default(
|
||||
app_handle: &AppHandle,
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
mod core;
|
||||
mod utils;
|
||||
|
||||
use core::{device::start_device_listening, 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::{
|
||||
@@ -23,7 +27,12 @@ pub fn run() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(generate_handler![copy_dir, start_device_listening])
|
||||
.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())
|
||||
@@ -36,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,
|
||||
@@ -46,6 +60,7 @@ pub fn run() {
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
|
||||
.plugin(tauri_plugin_locale::init())
|
||||
.on_window_event(|window, event| match event {
|
||||
WindowEvent::CloseRequested { api, .. } => {
|
||||
let _ = window.hide();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -74,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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
"transparent": true,
|
||||
"decorations": false,
|
||||
"acceptFirstMouse": true,
|
||||
"skipTaskbar": true
|
||||
"skipTaskbar": true,
|
||||
"maximizable": false
|
||||
},
|
||||
{
|
||||
"label": "preference",
|
||||
"title": "偏好设置",
|
||||
"url": "index.html/#/preference",
|
||||
"visible": false,
|
||||
"titleBarStyle": "Overlay",
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"category": "Game",
|
||||
"category": "Entertainment",
|
||||
"createUpdaterArtifacts": true,
|
||||
"targets": ["nsis", "dmg", "app", "appimage", "deb", "rpm"],
|
||||
"shortDescription": "BongoCat",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"identifier": "com.ayangweb.BongoCat",
|
||||
"bundle": {
|
||||
"resources": ["assets/tray.png", "assets/models"]
|
||||
"resources": ["assets/tray-mac.png", "assets/models"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,14 @@
|
||||
"windows": {
|
||||
"digestAlgorithm": "sha256",
|
||||
"nsis": {
|
||||
"languages": ["SimpChinese"],
|
||||
"installMode": "both"
|
||||
"languages": [
|
||||
"English",
|
||||
"Vietnamese",
|
||||
"SimpChinese",
|
||||
"PortugueseBR"
|
||||
],
|
||||
"installMode": "both",
|
||||
"displayLanguageSelector": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
26
src/App.vue
@@ -3,17 +3,18 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { error } from '@tauri-apps/plugin-log'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { ConfigProvider } from 'ant-design-vue'
|
||||
import zhCN from 'ant-design-vue/es/locale/zh_CN'
|
||||
import { ConfigProvider, theme } from 'ant-design-vue'
|
||||
import { isString } from 'es-toolkit'
|
||||
import isURL from 'is-url'
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { RouterView } from 'vue-router'
|
||||
|
||||
import { useTauriListen } from './composables/useTauriListen'
|
||||
import { useThemeVars } from './composables/useThemeVars'
|
||||
import { useWindowState } from './composables/useWindowState'
|
||||
import { LISTEN_KEY } from './constants'
|
||||
import { LANGUAGE, LISTEN_KEY } from './constants'
|
||||
import { getAntdLocale } from './locales/index.ts'
|
||||
import { hideWindow, showWindow } from './plugins/window'
|
||||
import { useAppStore } from './stores/app'
|
||||
import { useCatStore } from './stores/cat'
|
||||
@@ -29,18 +30,26 @@ const generalStore = useGeneralStore()
|
||||
const shortcutStore = useShortcutStore()
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const { isRestored, restoreState } = useWindowState()
|
||||
const { darkAlgorithm, defaultAlgorithm } = theme
|
||||
const { locale } = useI18n()
|
||||
|
||||
onMounted(async () => {
|
||||
generateColorVars()
|
||||
|
||||
await appStore.$tauri.start()
|
||||
await appStore.init()
|
||||
await modelStore.$tauri.start()
|
||||
await modelStore.init()
|
||||
await catStore.$tauri.start()
|
||||
catStore.init()
|
||||
await generalStore.$tauri.start()
|
||||
await generalStore.init()
|
||||
await shortcutStore.$tauri.start()
|
||||
await restoreState()
|
||||
catStore.init()
|
||||
})
|
||||
|
||||
watch(() => generalStore.appearance.language, (value) => {
|
||||
locale.value = value ?? LANGUAGE.EN_US
|
||||
})
|
||||
|
||||
useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
|
||||
@@ -79,7 +88,12 @@ useEventListener('click', (event) => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfigProvider :locale="zhCN">
|
||||
<ConfigProvider
|
||||
:locale="getAntdLocale(generalStore.appearance.language)"
|
||||
:theme="{
|
||||
algorithm: generalStore.appearance.isDark ? darkAlgorithm : defaultAlgorithm,
|
||||
}"
|
||||
>
|
||||
<RouterView v-if="isRestored" />
|
||||
</ConfigProvider>
|
||||
</template>
|
||||
|
||||
@@ -3,6 +3,10 @@ html {
|
||||
|
||||
color-scheme: light;
|
||||
|
||||
body {
|
||||
--uno: transition-opacity-300;
|
||||
}
|
||||
|
||||
&.dark {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,15 @@ const hasDescription = computed(() => {
|
||||
<template>
|
||||
<Flex
|
||||
:align="vertical ? void 0 : 'center'"
|
||||
class="b b-color-2 rounded-lg b-solid bg-white p-4"
|
||||
gap="middle"
|
||||
class="b b-color-2 rounded-lg b-solid bg-color-3 p-4"
|
||||
:gap="vertical ? 'middle' : 'large'"
|
||||
justify="space-between"
|
||||
:vertical="vertical"
|
||||
>
|
||||
<Flex align="center">
|
||||
<Flex
|
||||
align="center"
|
||||
class="flex-1"
|
||||
>
|
||||
<Flex vertical>
|
||||
<div class="text-sm font-medium">
|
||||
{{ title }}
|
||||
|
||||
@@ -110,7 +110,7 @@ function handleKeyUp(event: KeyboardEvent) {
|
||||
@mouseover="isHovering = true"
|
||||
>
|
||||
<span v-if="pressedKeys.length === 0">
|
||||
{{ isFocusing ? '按下录制快捷键' : '点击录制快捷键' }}
|
||||
{{ isFocusing ? $t('components.proShortcut.hints.pressRecordShortcut') : $t('components.proShortcut.hints.clickRecordShortcut') }}
|
||||
</span>
|
||||
|
||||
<span class="text-primary font-bold">
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Flex, message, Modal } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { computed, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import VueMarkdown from 'vue-markdown-render'
|
||||
|
||||
import { useTauriListen } from '@/composables/useTauriListen'
|
||||
@@ -32,10 +33,11 @@ const state = reactive<State>({
|
||||
downloadProgress: 0,
|
||||
})
|
||||
const MESSAGE_KEY = 'updatable'
|
||||
const { t } = useI18n()
|
||||
|
||||
const { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24)
|
||||
|
||||
watch(() => generalStore.autoCheckUpdate, (value) => {
|
||||
watch(() => generalStore.update.autoCheck, (value) => {
|
||||
pause()
|
||||
|
||||
if (!value) return
|
||||
@@ -51,7 +53,7 @@ useTauriListen<boolean>(LISTEN_KEY.UPDATE_APP, () => {
|
||||
message.loading({
|
||||
key: MESSAGE_KEY,
|
||||
duration: 0,
|
||||
content: '正在检查更新...',
|
||||
content: t('components.updateApp.hints.checkingUpdates'),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -91,7 +93,7 @@ async function checkUpdate(visibleMessage = false) {
|
||||
|
||||
message.destroy(MESSAGE_KEY)
|
||||
} else if (visibleMessage) {
|
||||
message.success({ key: MESSAGE_KEY, content: '当前已是最新版本🎉' })
|
||||
message.success({ key: MESSAGE_KEY, content: t('components.updateApp.hints.alreadyLatest') })
|
||||
}
|
||||
} catch (error) {
|
||||
if (!visibleMessage) return
|
||||
@@ -127,7 +129,10 @@ async function handleOk() {
|
||||
} catch (error) {
|
||||
message.error(String(error))
|
||||
} finally {
|
||||
state.downloading = false
|
||||
Object.assign(state, {
|
||||
downloading: false,
|
||||
downloadProgress: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -135,15 +140,15 @@ async function handleOk() {
|
||||
<template>
|
||||
<Modal
|
||||
v-model:open="state.open"
|
||||
cancel-text="稍后更新"
|
||||
:cancel-text="$t('components.updateApp.buttons.updateLater')"
|
||||
centered
|
||||
:closable="false"
|
||||
:mask-closable="false"
|
||||
title="发现新版本🥳"
|
||||
:title="$t('components.updateApp.title')"
|
||||
@ok="handleOk"
|
||||
>
|
||||
<template #okText>
|
||||
{{ state.downloading ? downloadProgress : "立即更新" }}
|
||||
{{ state.downloading ? downloadProgress : $t('components.updateApp.buttons.updateNow') }}
|
||||
</template>
|
||||
|
||||
<Flex
|
||||
@@ -152,7 +157,7 @@ async function handleOk() {
|
||||
vertical
|
||||
>
|
||||
<Flex align="center">
|
||||
<span>更新版本:</span>
|
||||
<span>{{ $t('components.updateApp.labels.updateVersion') }}</span>
|
||||
<span>
|
||||
<span>{{ state.update?.currentVersion }} 👉 </span>
|
||||
<a
|
||||
@@ -164,12 +169,12 @@ async function handleOk() {
|
||||
</Flex>
|
||||
|
||||
<Flex align="center">
|
||||
<span>更新时间:</span>
|
||||
<span>{{ $t('components.updateApp.labels.updateTime') }}</span>
|
||||
<span>{{ state.update?.date }}</span>
|
||||
</Flex>
|
||||
|
||||
<Flex vertical>
|
||||
<span>更新日志:</span>
|
||||
<span>{{ $t('components.updateApp.labels.changelog') }}</span>
|
||||
|
||||
<VueMarkdown
|
||||
class="update-note max-h-40 overflow-auto"
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { cursorPosition } from '@tauri-apps/api/window'
|
||||
|
||||
import { readDir } from '@tauri-apps/plugin-fs'
|
||||
import { uniq } from 'es-toolkit'
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
|
||||
import { LISTEN_KEY } from '../constants'
|
||||
import { INVOKE_KEY, LISTEN_KEY } from '../constants'
|
||||
|
||||
import { useModel } from './useModel'
|
||||
import { useTauriListen } from './useTauriListen'
|
||||
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import { isImage } from '@/utils/is'
|
||||
import { join } from '@/utils/path'
|
||||
import { inBetween } from '@/utils/is'
|
||||
import { isWindows } from '@/utils/platform'
|
||||
|
||||
interface MouseButtonEvent {
|
||||
@@ -19,14 +17,14 @@ interface MouseButtonEvent {
|
||||
value: string
|
||||
}
|
||||
|
||||
interface MouseMoveValue {
|
||||
export interface CursorPoint {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
interface MouseMoveEvent {
|
||||
kind: 'MouseMove'
|
||||
value: MouseMoveValue
|
||||
value: CursorPoint
|
||||
}
|
||||
|
||||
interface KeyboardEvent {
|
||||
@@ -37,102 +35,64 @@ interface KeyboardEvent {
|
||||
type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent
|
||||
|
||||
export function useDevice() {
|
||||
const supportLeftKeys = ref<string[]>([])
|
||||
const supportRightKeys = ref<string[]>([])
|
||||
const pressedMouses = ref<string[]>([])
|
||||
const mousePosition = reactive<MouseMoveValue>({ x: 0, y: 0 })
|
||||
const pressedLeftKeys = ref<string[]>([])
|
||||
const pressedRightKeys = ref<string[]>([])
|
||||
const catStore = useCatStore()
|
||||
const modelStore = useModelStore()
|
||||
const releaseTimers = new Map<string, NodeJS.Timeout>()
|
||||
const catStore = useCatStore()
|
||||
const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()
|
||||
|
||||
watch(() => modelStore.currentModel, async (model) => {
|
||||
if (!model) return
|
||||
|
||||
const keySides = [
|
||||
{
|
||||
side: 'left',
|
||||
supportKeys: supportLeftKeys,
|
||||
pressedKeys: pressedLeftKeys,
|
||||
},
|
||||
{
|
||||
side: 'right',
|
||||
supportKeys: supportRightKeys,
|
||||
pressedKeys: pressedRightKeys,
|
||||
},
|
||||
]
|
||||
|
||||
for await (const item of keySides) {
|
||||
const { side, supportKeys, pressedKeys } = item
|
||||
|
||||
try {
|
||||
const files = await readDir(join(model.path, 'resources', `${side}-keys`))
|
||||
|
||||
const imageFiles = files.filter(file => isImage(file.name))
|
||||
|
||||
supportKeys.value = imageFiles.map((item) => {
|
||||
return item.name.split('.')[0]
|
||||
})
|
||||
|
||||
pressedKeys.value = pressedKeys.value.filter((key) => {
|
||||
return supportKeys.value.includes(key)
|
||||
})
|
||||
} catch {
|
||||
supportKeys.value = []
|
||||
pressedKeys.value = []
|
||||
}
|
||||
}
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
const handlePress = (array: Ref<string[]>, value?: string) => {
|
||||
if (!value) return
|
||||
|
||||
if (catStore.singleMode) {
|
||||
array.value = [value]
|
||||
} else {
|
||||
array.value = uniq(array.value.concat(value))
|
||||
}
|
||||
}
|
||||
|
||||
const handleRelease = (array: Ref<string[]>, value?: string) => {
|
||||
if (!value) return
|
||||
|
||||
array.value = array.value.filter(item => item !== value)
|
||||
const startListening = () => {
|
||||
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
|
||||
}
|
||||
|
||||
const getSupportedKey = (key: string) => {
|
||||
for (const side of ['left', 'right']) {
|
||||
let nextKey = key
|
||||
let nextKey = key
|
||||
|
||||
const supportKeys = side === 'left' ? supportLeftKeys.value : supportRightKeys.value
|
||||
const unsupportedKey = !modelStore.supportKeys[nextKey]
|
||||
|
||||
const unsupportedKeys = !supportKeys.includes(key)
|
||||
if (key.startsWith('F') && unsupportedKey) {
|
||||
nextKey = key.replace(/F(\d+)/, 'Fn')
|
||||
}
|
||||
|
||||
if (key.startsWith('F') && unsupportedKeys) {
|
||||
nextKey = key.replace(/F(\d+)/, 'Fn')
|
||||
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
|
||||
if (key.startsWith(item) && unsupportedKey) {
|
||||
const regex = new RegExp(`^(${item}).*`)
|
||||
nextKey = key.replace(regex, '$1')
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
|
||||
if (key.startsWith(item) && unsupportedKeys) {
|
||||
const regex = new RegExp(`^(${item}).*`)
|
||||
nextKey = key.replace(regex, '$1')
|
||||
}
|
||||
return nextKey
|
||||
}
|
||||
|
||||
const handleCursorMove = async () => {
|
||||
const cursorPoint = await cursorPosition()
|
||||
|
||||
handleMouseMove(cursorPoint)
|
||||
|
||||
if (catStore.window.hideOnHover) {
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const position = await appWindow.outerPosition()
|
||||
const { width, height } = await appWindow.innerSize()
|
||||
|
||||
const isInWindow = inBetween(cursorPoint.x, position.x, position.x + width)
|
||||
&& inBetween(cursorPoint.y, position.y, position.y + height)
|
||||
|
||||
document.body.style.setProperty('opacity', isInWindow ? '0' : 'unset')
|
||||
|
||||
if (!catStore.window.passThrough) {
|
||||
appWindow.setIgnoreCursorEvents(isInWindow)
|
||||
}
|
||||
|
||||
if (!supportKeys.includes(nextKey)) continue
|
||||
|
||||
return nextKey
|
||||
}
|
||||
}
|
||||
|
||||
const handleScheduleRelease = (keys: Ref<string[]>, key: string, delay = 500) => {
|
||||
const handleAutoRelease = (key: string, delay = 100) => {
|
||||
handlePress(key)
|
||||
|
||||
if (releaseTimers.has(key)) {
|
||||
clearTimeout(releaseTimers.get(key))
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
handleRelease(keys, key)
|
||||
handleRelease(key)
|
||||
|
||||
releaseTimers.delete(key)
|
||||
}, delay)
|
||||
@@ -148,41 +108,34 @@ export function useDevice() {
|
||||
|
||||
if (!nextValue) return
|
||||
|
||||
const isLeftSide = supportLeftKeys.value.includes(nextValue)
|
||||
|
||||
const pressedKeys = isLeftSide ? pressedLeftKeys : pressedRightKeys
|
||||
|
||||
if (nextValue === 'CapsLock') {
|
||||
handlePress(pressedKeys, nextValue)
|
||||
|
||||
return handleScheduleRelease(pressedKeys, nextValue, 100)
|
||||
return handleAutoRelease(nextValue)
|
||||
}
|
||||
|
||||
if (kind === 'KeyboardPress') {
|
||||
if (isWindows) {
|
||||
handleScheduleRelease(pressedKeys, nextValue)
|
||||
const delay = catStore.model.autoReleaseDelay * 1000
|
||||
|
||||
return handleAutoRelease(nextValue, delay)
|
||||
}
|
||||
|
||||
return handlePress(pressedKeys, nextValue)
|
||||
return handlePress(nextValue)
|
||||
}
|
||||
|
||||
return handleRelease(pressedKeys, nextValue)
|
||||
return handleRelease(nextValue)
|
||||
}
|
||||
|
||||
switch (kind) {
|
||||
case 'MousePress':
|
||||
return handlePress(pressedMouses, value)
|
||||
return handleMouseChange(value)
|
||||
case 'MouseRelease':
|
||||
return handleRelease(pressedMouses, value)
|
||||
return handleMouseChange(value, false)
|
||||
case 'MouseMove':
|
||||
return Object.assign(mousePosition, value)
|
||||
return handleCursorMove()
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
pressedMouses,
|
||||
mousePosition,
|
||||
pressedLeftKeys,
|
||||
pressedRightKeys,
|
||||
startListening,
|
||||
}
|
||||
}
|
||||
|
||||
104
src/composables/useGamepad.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'
|
||||
import { resolveResource } from '@tauri-apps/api/path'
|
||||
import type { PhysicalPosition } from '@tauri-apps/api/dpi'
|
||||
|
||||
import { LogicalSize } from '@tauri-apps/api/dpi'
|
||||
import { resolveResource, sep } from '@tauri-apps/api/path'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { isNil, round } from 'es-toolkit'
|
||||
import { ref, 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 { getCursorMonitor } from '@/utils/monitor'
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
interface ModelSize {
|
||||
export interface ModelSize {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
@@ -23,21 +26,6 @@ export function useModel() {
|
||||
const catStore = useCatStore()
|
||||
const modelSize = ref<ModelSize>()
|
||||
|
||||
watch(() => modelStore.currentModel, handleLoad, { deep: true, immediate: true })
|
||||
|
||||
watch([() => catStore.scale, modelSize], async () => {
|
||||
if (!modelSize.value) return
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
|
||||
appWindow.setSize(
|
||||
new PhysicalSize({
|
||||
width: round(width * (catStore.scale / 100)),
|
||||
height: round(height * (catStore.scale / 100)),
|
||||
}),
|
||||
)
|
||||
}, { immediate: true })
|
||||
|
||||
async function handleLoad() {
|
||||
try {
|
||||
if (!modelStore.currentModel) return
|
||||
@@ -65,7 +53,7 @@ export function useModel() {
|
||||
async function handleResize() {
|
||||
if (!modelSize.value) return
|
||||
|
||||
live2d.fitModel()
|
||||
live2d.resizeModel(modelSize.value)
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
|
||||
@@ -80,26 +68,54 @@ export function useModel() {
|
||||
|
||||
const size = await appWindow.size()
|
||||
|
||||
catStore.scale = round((size.width / width) * 100)
|
||||
catStore.window.scale = round((size.width / width) * 100)
|
||||
}
|
||||
|
||||
function handleKeyDown(side: 'left' | 'right', pressed: boolean) {
|
||||
const id = side === 'left' ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
|
||||
const handlePress = (key: string) => {
|
||||
const path = modelStore.supportKeys[key]
|
||||
|
||||
const { min, max } = live2d.getParameterRange(id)
|
||||
if (!path) return
|
||||
|
||||
live2d.setParameterValue(id, pressed ? max : min)
|
||||
if (catStore.model.single) {
|
||||
const dirName = nth(path.split(sep()), -2)!
|
||||
|
||||
const filterKeys = Object.entries(modelStore.pressedKeys).filter(([, value]) => {
|
||||
return value.includes(dirName)
|
||||
})
|
||||
|
||||
for (const [key] of filterKeys) {
|
||||
handleRelease(key)
|
||||
}
|
||||
}
|
||||
|
||||
modelStore.pressedKeys[key] = path
|
||||
}
|
||||
|
||||
async function handleMouseMove() {
|
||||
const monitor = await getCursorMonitor()
|
||||
const handleRelease = (key: string) => {
|
||||
delete modelStore.pressedKeys[key]
|
||||
}
|
||||
|
||||
function handleKeyChange(isLeft = true, pressed = true) {
|
||||
const id = isLeft ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
|
||||
|
||||
live2d.setParameterValue(id, pressed)
|
||||
}
|
||||
|
||||
function handleMouseChange(key: string, pressed = true) {
|
||||
const id = key === 'Left' ? 'ParamMouseLeftDown' : 'ParamMouseRightDown'
|
||||
|
||||
live2d.setParameterValue(id, pressed)
|
||||
}
|
||||
|
||||
async function handleMouseMove(cursorPoint: PhysicalPosition) {
|
||||
const monitor = await getCursorMonitor(cursorPoint)
|
||||
|
||||
if (!monitor) return
|
||||
|
||||
const { size, position, cursorPosition } = monitor
|
||||
const { size, position } = monitor
|
||||
|
||||
const xRatio = (cursorPosition.x - position.x) / size.width
|
||||
const yRatio = (cursorPosition.y - position.y) / size.height
|
||||
const xRatio = (cursorPoint.x - position.x) / size.width
|
||||
const yRatio = (cursorPoint.y - position.y) / size.height
|
||||
|
||||
for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) {
|
||||
const { min, max } = live2d.getParameterRange(id)
|
||||
@@ -111,7 +127,7 @@ export function useModel() {
|
||||
const ratio = isXAxis ? xRatio : yRatio
|
||||
let value = max - (ratio * (max - min))
|
||||
|
||||
if (isXAxis && catStore.mouseMirror) {
|
||||
if (isXAxis && catStore.model.mouseMirror) {
|
||||
value *= -1
|
||||
}
|
||||
|
||||
@@ -119,25 +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 {
|
||||
modelSize,
|
||||
handlePress,
|
||||
handleRelease,
|
||||
handleLoad,
|
||||
handleDestroy,
|
||||
handleResize,
|
||||
handleKeyDown,
|
||||
handleKeyChange,
|
||||
handleMouseChange,
|
||||
handleMouseMove,
|
||||
handleMouseDown,
|
||||
handleAxisChange,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
|
||||
import { range } from 'es-toolkit'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { showWindow } from '@/plugins/window'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
@@ -7,23 +8,24 @@ import { isMac } from '@/utils/platform'
|
||||
|
||||
export function useSharedMenu() {
|
||||
const catStore = useCatStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
const getScaleMenuItems = async () => {
|
||||
const options = range(50, 151, 25)
|
||||
|
||||
const items = options.map((item) => {
|
||||
return CheckMenuItem.new({
|
||||
text: item === 100 ? '默认' : `${item}%`,
|
||||
checked: catStore.scale === item,
|
||||
text: `${item}%`,
|
||||
checked: catStore.window.scale === item,
|
||||
action: () => {
|
||||
catStore.scale = item
|
||||
catStore.window.scale = item
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (!options.includes(catStore.scale)) {
|
||||
if (!options.includes(catStore.window.scale)) {
|
||||
items.unshift(CheckMenuItem.new({
|
||||
text: `${catStore.scale}%`,
|
||||
text: `${catStore.window.scale}%`,
|
||||
checked: true,
|
||||
enabled: false,
|
||||
}))
|
||||
@@ -38,16 +40,16 @@ export function useSharedMenu() {
|
||||
const items = options.map((item) => {
|
||||
return CheckMenuItem.new({
|
||||
text: `${item}%`,
|
||||
checked: catStore.opacity === item,
|
||||
checked: catStore.window.opacity === item,
|
||||
action: () => {
|
||||
catStore.opacity = item
|
||||
catStore.window.opacity = item
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
if (!options.includes(catStore.opacity)) {
|
||||
if (!options.includes(catStore.window.opacity)) {
|
||||
items.unshift(CheckMenuItem.new({
|
||||
text: `${catStore.opacity}%`,
|
||||
text: `${catStore.window.opacity}%`,
|
||||
checked: true,
|
||||
enabled: false,
|
||||
}))
|
||||
@@ -59,30 +61,30 @@ export function useSharedMenu() {
|
||||
const getSharedMenu = async () => {
|
||||
return await Promise.all([
|
||||
MenuItem.new({
|
||||
text: '偏好设置...',
|
||||
text: t('composables.useSharedMenu.labels.preference'),
|
||||
accelerator: isMac ? 'Cmd+,' : '',
|
||||
action: () => showWindow('preference'),
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: catStore.visible ? '隐藏猫咪' : '显示猫咪',
|
||||
text: catStore.window.visible ? t('composables.useSharedMenu.labels.hideCat') : t('composables.useSharedMenu.labels.showCat'),
|
||||
action: () => {
|
||||
catStore.visible = !catStore.visible
|
||||
catStore.window.visible = !catStore.window.visible
|
||||
},
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: 'Separator' }),
|
||||
CheckMenuItem.new({
|
||||
text: '窗口穿透',
|
||||
checked: catStore.penetrable,
|
||||
text: t('composables.useSharedMenu.labels.passThrough'),
|
||||
checked: catStore.window.passThrough,
|
||||
action: () => {
|
||||
catStore.penetrable = !catStore.penetrable
|
||||
catStore.window.passThrough = !catStore.window.passThrough
|
||||
},
|
||||
}),
|
||||
Submenu.new({
|
||||
text: '窗口尺寸',
|
||||
text: t('composables.useSharedMenu.labels.windowSize'),
|
||||
items: await getScaleMenuItems(),
|
||||
}),
|
||||
Submenu.new({
|
||||
text: '不透明度',
|
||||
text: t('composables.useSharedMenu.labels.opacity'),
|
||||
items: await getOpacityMenuItems(),
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from '@tauri-apps/plugin-global-shortcut'
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
export function useTauriKeyPress(shortcut: Ref<string, string>, callback: ShortcutHandler) {
|
||||
export function useTauriShortcut(shortcut: Ref<string, string>, callback: ShortcutHandler) {
|
||||
const oldShortcut = ref(shortcut.value)
|
||||
|
||||
watch(shortcut, async (value) => {
|
||||
@@ -9,6 +9,7 @@ import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { GITHUB_LINK, LISTEN_KEY } from '../constants'
|
||||
import { showWindow } from '../plugins/window'
|
||||
@@ -17,18 +18,21 @@ import { isMac } from '../utils/platform'
|
||||
import { useSharedMenu } from './useSharedMenu'
|
||||
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useGeneralStore } from '@/stores/general'
|
||||
|
||||
const TRAY_ID = 'BONGO_CAT_TRAY'
|
||||
|
||||
export function useTray() {
|
||||
const catStore = useCatStore()
|
||||
const generalStore = useGeneralStore()
|
||||
const { getSharedMenu } = useSharedMenu()
|
||||
const { t } = useI18n()
|
||||
|
||||
watch([() => catStore.visible, () => catStore.penetrable], () => {
|
||||
watch([() => catStore.window.visible, () => catStore.window.passThrough, () => generalStore.appearance.language], () => {
|
||||
updateTrayMenu()
|
||||
})
|
||||
|
||||
watchDebounced([() => catStore.scale, () => catStore.opacity], () => {
|
||||
watchDebounced([() => catStore.window.scale, () => catStore.window.opacity], () => {
|
||||
updateTrayMenu()
|
||||
}, { debounce: 200 })
|
||||
|
||||
@@ -42,14 +46,15 @@ export function useTray() {
|
||||
|
||||
const menu = await getTrayMenu()
|
||||
|
||||
const icon = await resolveResource('assets/tray.png')
|
||||
const path = isMac ? 'assets/tray-mac.png' : 'assets/tray.png'
|
||||
const icon = await resolveResource(path)
|
||||
|
||||
const options: TrayIconOptions = {
|
||||
menu,
|
||||
icon,
|
||||
id: TRAY_ID,
|
||||
tooltip: `${appName} v${appVersion}`,
|
||||
iconAsTemplate: false,
|
||||
iconAsTemplate: true,
|
||||
menuOnLeftClick: true,
|
||||
}
|
||||
|
||||
@@ -67,7 +72,7 @@ export function useTray() {
|
||||
...await getSharedMenu(),
|
||||
PredefinedMenuItem.new({ item: 'Separator' }),
|
||||
MenuItem.new({
|
||||
text: '检查更新',
|
||||
text: t('composables.useTray.checkUpdate'),
|
||||
action: () => {
|
||||
showWindow()
|
||||
|
||||
@@ -75,20 +80,20 @@ export function useTray() {
|
||||
},
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: '开源地址',
|
||||
text: t('composables.useTray.openSource'),
|
||||
action: () => openUrl(GITHUB_LINK),
|
||||
}),
|
||||
PredefinedMenuItem.new({ item: 'Separator' }),
|
||||
MenuItem.new({
|
||||
text: `版本 ${appVersion}`,
|
||||
text: `v${appVersion}`,
|
||||
enabled: false,
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: '重启应用',
|
||||
text: t('composables.useTray.restartApp'),
|
||||
action: relaunch,
|
||||
}),
|
||||
MenuItem.new({
|
||||
text: '退出应用',
|
||||
text: t('composables.useTray.quitApp'),
|
||||
accelerator: isMac ? 'Cmd+Q' : '',
|
||||
action: () => exit(0),
|
||||
}),
|
||||
|
||||
47
src/composables/useWindowPosition.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { PhysicalPosition } from '@tauri-apps/api/dpi'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { getCursorMonitor } from '@/utils/monitor'
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
export function useWindowPosition() {
|
||||
const catStore = useCatStore()
|
||||
const isMounted = ref(false)
|
||||
|
||||
const setWindowPosition = async () => {
|
||||
const monitor = await getCursorMonitor()
|
||||
|
||||
if (!monitor) return
|
||||
|
||||
const windowSize = await appWindow.outerSize()
|
||||
|
||||
switch (catStore.window.position) {
|
||||
case 'topLeft':
|
||||
return appWindow.setPosition(new PhysicalPosition(0, 0))
|
||||
case 'topRight':
|
||||
return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, 0))
|
||||
case 'bottomLeft':
|
||||
return appWindow.setPosition(new PhysicalPosition(0, monitor.size.height - windowSize.height))
|
||||
default:
|
||||
return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, monitor.size.height - windowSize.height))
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await setWindowPosition()
|
||||
|
||||
isMounted.value = true
|
||||
|
||||
appWindow.onScaleChanged(setWindowPosition)
|
||||
})
|
||||
|
||||
watch(() => catStore.window.position, setWindowPosition)
|
||||
|
||||
return {
|
||||
isMounted,
|
||||
setWindowPosition,
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,19 @@ export const LISTEN_KEY = {
|
||||
HIDE_WINDOW: 'hide-window',
|
||||
DEVICE_CHANGED: 'device-changed',
|
||||
UPDATE_APP: 'update-app',
|
||||
GAMEPAD_CHANGED: 'gamepad-changed',
|
||||
}
|
||||
|
||||
export const INVOKE_KEY = {
|
||||
COPY_DIR: 'copy_dir',
|
||||
START_DEVICE_LISTENING: 'start_device_listening',
|
||||
START_GAMEPAD_LISTING: 'start_gamepad_listing',
|
||||
STOP_GAMEPAD_LISTING: 'stop_gamepad_listing',
|
||||
}
|
||||
|
||||
export const LANGUAGE = {
|
||||
ZH_CN: 'zh-CN',
|
||||
EN_US: 'en-US',
|
||||
VI_VN: 'vi-VN',
|
||||
PT_BR: 'pt-BR',
|
||||
} as const
|
||||
|
||||
182
src/locales/en-US.json
Normal file
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"pages": {
|
||||
"main": {
|
||||
"hints": {
|
||||
"redrawing": "Redrawing..."
|
||||
}
|
||||
},
|
||||
"preference": {
|
||||
"title": "Preferences",
|
||||
"cat": {
|
||||
"title": "Cat",
|
||||
"labels": {
|
||||
"modelSettings": "Model Settings",
|
||||
"mirrorMode": "Mirror Mode",
|
||||
"singleMode": "Single Key Mode",
|
||||
"mouseMirror": "Mouse Mirror",
|
||||
"windowSettings": "Window Settings",
|
||||
"passThrough": "Pass Through",
|
||||
"alwaysOnTop": "Always on Top",
|
||||
"windowSize": "Window Size",
|
||||
"windowRadius": "Window Radius",
|
||||
"opacity": "Opacity",
|
||||
"autoReleaseDelay": "Auto Release Delay",
|
||||
"hideOnHover": "Hide on Hover",
|
||||
"position": "Window Position"
|
||||
},
|
||||
"hints": {
|
||||
"mirrorMode": "When enabled, the model will be mirrored horizontally.",
|
||||
"singleMode": "When enabled, only the last pressed key is displayed for each hand.",
|
||||
"mouseMirror": "When enabled, the mouse will mirror the hand movement.",
|
||||
"passThrough": "When enabled, clicks pass through the window without affecting it.",
|
||||
"alwaysOnTop": "When enabled, the window stays above all other windows.",
|
||||
"windowSize": "Move mouse to window edge, or hold Shift and right-drag to resize.",
|
||||
"autoReleaseDelay": "On Windows, some system keys cannot capture release events and will auto-release after timeout.",
|
||||
"hideOnHover": "When enabled, the window hides when mouse hovers over it.",
|
||||
"position": "Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes."
|
||||
},
|
||||
"options": {
|
||||
"topLeft": "Top Left",
|
||||
"topRight": "Top Right",
|
||||
"bottomLeft": "Bottom Left",
|
||||
"bottomRight": "Bottom Right"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "General",
|
||||
"labels": {
|
||||
"appSettings": "Application Settings",
|
||||
"launchOnStartup": "Launch on Startup",
|
||||
"showTaskbarIcon": "Show Taskbar Icon",
|
||||
"appearanceSettings": "Appearance Settings",
|
||||
"themeMode": "Theme Mode",
|
||||
"language": "Language",
|
||||
"updateSettings": "Update Settings",
|
||||
"autoCheckUpdate": "Auto Check for Updates",
|
||||
"permissionsSettings": "Permissions Settings",
|
||||
"inputMonitoringPermission": "Input Monitoring Permission"
|
||||
},
|
||||
"options": {
|
||||
"auto": "System",
|
||||
"lightMode": "Light",
|
||||
"darkMode": "Dark"
|
||||
},
|
||||
"hints": {
|
||||
"showTaskbarIcon": "When enabled, the window can be captured via OBS Studio.",
|
||||
"inputMonitoringPermission": "Enable input monitoring to receive keyboard and mouse events from the system.",
|
||||
"inputMonitoringPermissionGuide": "If the permission is already enabled, select it and click the \"-\" button to remove it, then manually add it again and restart the app."
|
||||
},
|
||||
"status": {
|
||||
"authorized": "Authorized",
|
||||
"authorize": "Go to Enable"
|
||||
},
|
||||
"buttons": {
|
||||
"openNow": "Open Now",
|
||||
"openLater": "Open Later"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"title": "Model",
|
||||
"labels": {
|
||||
"deleteModel": "Delete Model"
|
||||
},
|
||||
"hints": {
|
||||
"deleteSuccess": "Deleted Successfully",
|
||||
"deleteModel": "Are you sure you want to delete this model?",
|
||||
"importSuccess": "Imported Successfully",
|
||||
"clickOrDragToImport": "Click or drag here to import"
|
||||
},
|
||||
"tooltips": {
|
||||
"createModel": "Create Model",
|
||||
"convertModel": "Convert Model",
|
||||
"moreModels": "More Models"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"title": "Shortcuts",
|
||||
"labels": {
|
||||
"toggleCat": "Toggle Cat",
|
||||
"togglePreferences": "Toggle Preferences",
|
||||
"mirrorMode": "Mirror Mode",
|
||||
"passThrough": "Pass Through",
|
||||
"alwaysOnTop": "Always on Top"
|
||||
},
|
||||
"hints": {
|
||||
"toggleCat": "Toggle the visibility of the cat window.",
|
||||
"togglePreferences": "Toggle the visibility of the preferences window.",
|
||||
"mirrorMode": "Toggle the cat's mirror mode.",
|
||||
"passThrough": "Toggle whether the cat window is pass-through.",
|
||||
"alwaysOnTop": "Toggle whether the cat window stays on top."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "About",
|
||||
"labels": {
|
||||
"aboutApp": "About App",
|
||||
"appLog": "App Logs",
|
||||
"appInfo": "App Info",
|
||||
"openSource": "Open Source"
|
||||
},
|
||||
"hints": {
|
||||
"appInfo": "Copy app information and provide it to bug issue.",
|
||||
"copySuccess": "Copied Successfully"
|
||||
},
|
||||
"buttons": {
|
||||
"checkUpdate": "Check for Updates",
|
||||
"copy": "Copy",
|
||||
"feedbackIssues": "Feedback Issues",
|
||||
"viewLog": "View Logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"proShortcut": {
|
||||
"hints": {
|
||||
"pressRecordShortcut": "Press to record shortcut",
|
||||
"clickRecordShortcut": "Click to record shortcut"
|
||||
}
|
||||
},
|
||||
"updateApp": {
|
||||
"title": "New Version Found 🥳",
|
||||
"labels": {
|
||||
"updateVersion": "Update Version: ",
|
||||
"updateTime": "Update Time: ",
|
||||
"changelog": "Changelog: "
|
||||
},
|
||||
"hints": {
|
||||
"checkingUpdates": "Checking for updates...",
|
||||
"alreadyLatest": "Already on the latest version 🎉"
|
||||
},
|
||||
"buttons": {
|
||||
"updateNow": "Update Now",
|
||||
"updateLater": "Update Later"
|
||||
}
|
||||
}
|
||||
},
|
||||
"composables": {
|
||||
"useSharedMenu": {
|
||||
"labels": {
|
||||
"preference": "Preferences...",
|
||||
"hideCat": "Hide Cat",
|
||||
"showCat": "Show Cat",
|
||||
"passThrough": "Pass Through",
|
||||
"windowSize": "Window Size",
|
||||
"opacity": "Opacity"
|
||||
}
|
||||
},
|
||||
"useTray": {
|
||||
"checkUpdate": "Check for Updates",
|
||||
"openSource": "Open Source",
|
||||
"restartApp": "Restart App",
|
||||
"quitApp": "Quit App"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"live2d": {
|
||||
"hints": {
|
||||
"notFound": "Model master configuration file not found, please ensure the model files are complete."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/locales/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Language } from '@/stores/general'
|
||||
import type { Locale as AntdLocale } from 'ant-design-vue/es/locale'
|
||||
|
||||
import antdEnUS from 'ant-design-vue/locale/en_US'
|
||||
import antdPtBR from 'ant-design-vue/locale/pt_BR'
|
||||
import antdViVN from 'ant-design-vue/locale/vi_VN'
|
||||
import antdZhCN from 'ant-design-vue/locale/zh_CN'
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
import enUS from './en-US.json'
|
||||
import ptBR from './pt-BR.json'
|
||||
import viVN from './vi-VN.json'
|
||||
import zhCN from './zh-CN.json'
|
||||
|
||||
import { LANGUAGE } from '@/constants'
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: LANGUAGE.EN_US,
|
||||
fallbackLocale: LANGUAGE.EN_US,
|
||||
messages: {
|
||||
[LANGUAGE.ZH_CN]: zhCN,
|
||||
[LANGUAGE.EN_US]: enUS,
|
||||
[LANGUAGE.VI_VN]: viVN,
|
||||
[LANGUAGE.PT_BR]: ptBR,
|
||||
},
|
||||
})
|
||||
|
||||
export function getAntdLocale(language: Language = LANGUAGE.EN_US) {
|
||||
const antdLanguage: Record<Language, AntdLocale> = {
|
||||
[LANGUAGE.ZH_CN]: antdZhCN,
|
||||
[LANGUAGE.EN_US]: antdEnUS,
|
||||
[LANGUAGE.VI_VN]: antdViVN,
|
||||
[LANGUAGE.PT_BR]: antdPtBR,
|
||||
}
|
||||
|
||||
return antdLanguage[language]
|
||||
}
|
||||
182
src/locales/pt-BR.json
Normal file
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"pages": {
|
||||
"main": {
|
||||
"hints": {
|
||||
"redrawing": "Redimensionando..."
|
||||
}
|
||||
},
|
||||
"preference": {
|
||||
"title": "Preferências",
|
||||
"cat": {
|
||||
"title": "Gato",
|
||||
"labels": {
|
||||
"modelSettings": "Configurações do Modelo",
|
||||
"mirrorMode": "Modo Espelho",
|
||||
"singleMode": "Mostrar Apenas Última Tecla",
|
||||
"mouseMirror": "Espelho do Mouse",
|
||||
"windowSettings": "Configurações da Janela",
|
||||
"passThrough": "Janela Transparente",
|
||||
"alwaysOnTop": "Sempre no Topo",
|
||||
"windowSize": "Tamanho da Janela",
|
||||
"windowRadius": "Raio da Janela",
|
||||
"opacity": "Opacidade",
|
||||
"autoReleaseDelay": "Atraso de Liberação Automática",
|
||||
"hideOnHover": "Ocultar ao Passar o Mouse",
|
||||
"position": "Posição da Janela"
|
||||
},
|
||||
"hints": {
|
||||
"mirrorMode": "Quando ativado, o modelo será invertido horizontalmente.",
|
||||
"singleMode": "Quando ativado, apenas a última tecla pressionada em cada mão é exibida (evita mostrar múltiplas mãos ao pressionar várias teclas ao mesmo tempo).",
|
||||
"mouseMirror": "Quando ativado, o mouse espelhará o movimento da mão.",
|
||||
"passThrough": "Quando ativado, a janela não afetará operações em outros aplicativos.",
|
||||
"alwaysOnTop": "Quando ativado, a janela sempre ficará acima de outros aplicativos.",
|
||||
"windowSize": "Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.",
|
||||
"autoReleaseDelay": "Devido ao Windows não capturar eventos de liberação de certas teclas de nível do sistema, elas serão automaticamente tratadas como liberadas após um tempo limite.",
|
||||
"hideOnHover": "Quando ativado, a janela será ocultada quando o mouse passar sobre ela.",
|
||||
"position": "Entra em vigor após inicializar o aplicativo ou quando este parâmetro, o tamanho da janela, o modelo ou a resolução de tela é alterado."
|
||||
},
|
||||
"options": {
|
||||
"topLeft": "Canto Superior Esquerdo",
|
||||
"topRight": "Canto Superior Direito",
|
||||
"bottomLeft": "Canto Inferior Esquerdo",
|
||||
"bottomRight": "Canto Inferior Direito"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "Geral",
|
||||
"labels": {
|
||||
"appSettings": "Configurações do aplicativo",
|
||||
"launchOnStartup": "Iniciar na inicialização",
|
||||
"showTaskbarIcon": "Mostrar ícone na barra de tarefas",
|
||||
"appearanceSettings": "Configurações de aparência",
|
||||
"themeMode": "Tema",
|
||||
"language": "Idiomas",
|
||||
"updateSettings": "Configurações de atualização",
|
||||
"autoCheckUpdate": "Verificar atualizações automaticamente",
|
||||
"permissionsSettings": "Configurações de Permissões",
|
||||
"inputMonitoringPermission": "Permissão de Monitoramento de Entrada"
|
||||
},
|
||||
"options": {
|
||||
"auto": "Sistema",
|
||||
"lightMode": "Claro",
|
||||
"darkMode": "Escuro"
|
||||
},
|
||||
"hints": {
|
||||
"showTaskbarIcon": "Uma vez ativado, você pode capturar a janela via OBS Studio.",
|
||||
"inputMonitoringPermission": "Ative a permissão de monitoramento de entrada para receber eventos de teclado e mouse do sistema para responder às suas ações.",
|
||||
"inputMonitoringPermissionGuide": "Se a permissão já estiver ativada, primeiro selecione-a e clique no botão \"-\" para removê-la. Em seguida, adicione-a novamente manualmente e reinicie o aplicativo para garantir que a permissão entre em vigor."
|
||||
},
|
||||
"status": {
|
||||
"authorized": "Autorizado",
|
||||
"authorize": "Ir para Ativar"
|
||||
},
|
||||
"buttons": {
|
||||
"openNow": "Abrir Agora",
|
||||
"openLater": "Abrir Mais Tarde"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"title": "Modelo",
|
||||
"labels": {
|
||||
"deleteModel": "Excluir modelo"
|
||||
},
|
||||
"hints": {
|
||||
"deleteSuccess": "Excluído com sucesso",
|
||||
"deleteModel": "Tem certeza de que deseja excluir este modelo?",
|
||||
"importSuccess": "Importação bem-sucedida",
|
||||
"clickOrDragToImport": "Clique ou arraste para importar"
|
||||
},
|
||||
"tooltips": {
|
||||
"createModel": "Criar modelo",
|
||||
"convertModel": "Converter modelo",
|
||||
"moreModels": "Mais modelos"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"title": "Atalhos",
|
||||
"labels": {
|
||||
"toggleCat": "Mostrar/Ocultar Gato",
|
||||
"togglePreferences": "Abrir Preferências",
|
||||
"mirrorMode": "Modo Espelho",
|
||||
"passThrough": "Janela Transparente",
|
||||
"alwaysOnTop": "Sempre no Topo"
|
||||
},
|
||||
"hints": {
|
||||
"toggleCat": "Alternar a visibilidade da janela do gato.",
|
||||
"togglePreferences": "Alternar a visibilidade da janela de preferências.",
|
||||
"mirrorMode": "Alternar o modo espelho do gato.",
|
||||
"passThrough": "Alternar se a janela do gato é clicável.",
|
||||
"alwaysOnTop": "Alternar se a janela do gato permanece no topo."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Sobre",
|
||||
"labels": {
|
||||
"aboutApp": "Sobre o Aplicativo",
|
||||
"appLog": "Logs do Aplicativo",
|
||||
"appInfo": "Informações do Aplicativo",
|
||||
"openSource": "Código Aberto"
|
||||
},
|
||||
"hints": {
|
||||
"appInfo": "Copiar informações do aplicativo para incluir em relatórios de bugs.",
|
||||
"copySuccess": "Copiado com sucesso"
|
||||
},
|
||||
"buttons": {
|
||||
"checkUpdate": "Verificar atualizações",
|
||||
"copy": "Copiar",
|
||||
"feedbackIssues": "Reportar Problema",
|
||||
"viewLog": "Ver Logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"proShortcut": {
|
||||
"hints": {
|
||||
"pressRecordShortcut": "Pressione as teclas para gravar atalho",
|
||||
"clickRecordShortcut": "Clique para gravar atalho"
|
||||
}
|
||||
},
|
||||
"updateApp": {
|
||||
"title": "Nova versão encontrada 🥳",
|
||||
"labels": {
|
||||
"updateVersion": "Versão: ",
|
||||
"updateTime": "Hora da atualização: ",
|
||||
"changelog": "Registro de alterações: "
|
||||
},
|
||||
"hints": {
|
||||
"checkingUpdates": "Verificando atualizações...",
|
||||
"alreadyLatest": "Você já está na versão mais recente 🎉"
|
||||
},
|
||||
"buttons": {
|
||||
"updateNow": "Atualizar Agora",
|
||||
"updateLater": "Atualizar mais tarde"
|
||||
}
|
||||
}
|
||||
},
|
||||
"composables": {
|
||||
"useSharedMenu": {
|
||||
"labels": {
|
||||
"preference": "Preferências...",
|
||||
"hideCat": "Ocultar Gato",
|
||||
"showCat": "Mostrar Gato",
|
||||
"passThrough": "Janela Transparente",
|
||||
"windowSize": "Tamanho da Janela",
|
||||
"opacity": "Opacidade"
|
||||
}
|
||||
},
|
||||
"useTray": {
|
||||
"checkUpdate": "Verificar atualizações",
|
||||
"openSource": "Código Fonte",
|
||||
"restartApp": "Reiniciar",
|
||||
"quitApp": "Sair"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"live2d": {
|
||||
"hints": {
|
||||
"notFound": "Arquivo de configuração principal do modelo não encontrado. Verifique se os arquivos do modelo estão completos."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/locales/vi-VN.json
Normal file
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"pages": {
|
||||
"main": {
|
||||
"hints": {
|
||||
"redrawing": "Đang đổi kích thước..."
|
||||
}
|
||||
},
|
||||
"preference": {
|
||||
"title": "Tùy chỉnh",
|
||||
"cat": {
|
||||
"title": "Mèo",
|
||||
"labels": {
|
||||
"modelSettings": "Cài đặt Mô hình",
|
||||
"mirrorMode": "Chế độ gương",
|
||||
"singleMode": "Chỉ hiển thị phím cuối cùng",
|
||||
"mouseMirror": "Phản chiếu chuột",
|
||||
"windowSettings": "Cài đặt Cửa sổ",
|
||||
"passThrough": "Click xuyên",
|
||||
"alwaysOnTop": "Luôn trên cùng",
|
||||
"windowSize": "Kích thước",
|
||||
"windowRadius": "Độ bo tròn cửa sổ",
|
||||
"opacity": "Độ mờ",
|
||||
"autoReleaseDelay": "Độ trễ tự động nhả phím",
|
||||
"hideOnHover": "Ẩn khi di chuột",
|
||||
"position": "Vị trí cửa sổ"
|
||||
},
|
||||
"hints": {
|
||||
"mirrorMode": "Bật để lật ngang mô hình.",
|
||||
"singleMode": "Khi bật, mỗi tay mèo chỉ hiển thị phím vừa nhấn cuối cùng (tránh hiện nhiều tay khi nhấn nhiều phím cùng lúc).",
|
||||
"mouseMirror": "Khi bật, chuột của mô hình sẽ phản chiếu theo chuyển động chuột thực tế.",
|
||||
"passThrough": "Bật để cửa sổ không ảnh hưởng đến thao tác trên ứng dụng khác.",
|
||||
"alwaysOnTop": "Bật để cửa sổ luôn nằm trên ứng dụng khác.",
|
||||
"windowSize": "Di chuyển chuột đến mép cửa sổ hoặc giữ Shift và kéo chuột phải để thay đổi kích thước.",
|
||||
"autoReleaseDelay": "Do Windows không bắt được sự kiện nhả của một số phím hệ thống, các phím đó sẽ được tự động xem như đã nhả sau khi hết thời gian chờ.",
|
||||
"hideOnHover": "Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào.",
|
||||
"position": "Có hiệu lực sau khi khởi động ứng dụng hoặc khi tham số này, kích thước cửa sổ, mô hình hoặc độ phân giải màn hình thay đổi."
|
||||
},
|
||||
"options": {
|
||||
"topLeft": "Góc trên cùng bên trái",
|
||||
"topRight": "Góc trên cùng bên phải",
|
||||
"bottomLeft": "Góc dưới cùng bên trái",
|
||||
"bottomRight": "Góc dưới cùng bên phải"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "Chung",
|
||||
"labels": {
|
||||
"appSettings": "Cài đặt ứng dụng",
|
||||
"launchOnStartup": "Khởi động cùng hệ thống",
|
||||
"showTaskbarIcon": "Hiện biểu tượng trên thanh tác vụ (icon taskbar)",
|
||||
"appearanceSettings": "Cài đặt giao diện",
|
||||
"themeMode": "Giao diện",
|
||||
"language": "Ngôn ngữ",
|
||||
"updateSettings": "Cài đặt cập nhật",
|
||||
"autoCheckUpdate": "Tự động kiểm tra cập nhật",
|
||||
"permissionsSettings": "Cài đặt quyền",
|
||||
"inputMonitoringPermission": "Quyền giám sát đầu vào"
|
||||
},
|
||||
"options": {
|
||||
"auto": "Theo hệ thống",
|
||||
"lightMode": "Sáng",
|
||||
"darkMode": "Tối"
|
||||
},
|
||||
"hints": {
|
||||
"showTaskbarIcon": "Bật để có thể quay cửa sổ qua OBS.",
|
||||
"inputMonitoringPermission": "Bật quyền giám sát để nhận sự kiện bàn phím và chuột từ hệ thống nhằm phản hồi thao tác của bạn.",
|
||||
"inputMonitoringPermissionGuide": "Nếu quyền đã được bật, hãy chọn nó và nhấn nút \"-\" để xóa. Sau đó thêm lại thủ công và khởi động lại ứng dụng để đảm bảo quyền được áp dụng."
|
||||
},
|
||||
"status": {
|
||||
"authorized": "Đã cấp quyền",
|
||||
"authorize": "Đi đến Bật"
|
||||
},
|
||||
"buttons": {
|
||||
"openNow": "Mở ngay",
|
||||
"openLater": "Mở sau"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"title": "Mô hình",
|
||||
"labels": {
|
||||
"deleteModel": "Xóa mô hình"
|
||||
},
|
||||
"hints": {
|
||||
"deleteSuccess": "Xóa thành công",
|
||||
"deleteModel": "Bạn chắc muốn xóa mô hình này?",
|
||||
"importSuccess": "Nhập thành công",
|
||||
"clickOrDragToImport": "Nhấp hoặc kéo tệp vào đây"
|
||||
},
|
||||
"tooltips": {
|
||||
"createModel": "Tạo mô hình",
|
||||
"convertModel": "Chuyển đổi mô hình",
|
||||
"moreModels": "Khám phá mô hình khác"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"title": "Phím tắt",
|
||||
"labels": {
|
||||
"toggleCat": "Ẩn/Hiện Mèo",
|
||||
"togglePreferences": "Mở Tùy chỉnh",
|
||||
"mirrorMode": "Chế độ gương",
|
||||
"passThrough": "Click xuyên",
|
||||
"alwaysOnTop": "Luôn trên cùng"
|
||||
},
|
||||
"hints": {
|
||||
"toggleCat": "Bật/Tắt cửa sổ mèo.",
|
||||
"togglePreferences": "Bật/Tắt cửa sổ tùy chỉnh.",
|
||||
"mirrorMode": "Bật/Tắt chế độ gương.",
|
||||
"passThrough": "Bật/Tắt cho phép click xuyên cửa sổ mèo.",
|
||||
"alwaysOnTop": "Bật/Tắt luôn giữ cửa sổ mèo trên cùng."
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "Giới thiệu",
|
||||
"labels": {
|
||||
"aboutApp": "Thông tin ứng dụng",
|
||||
"appLog": "Nhật ký ứng dụng",
|
||||
"appInfo": "Thông tin ứng dụng",
|
||||
"openSource": "Mã nguồn"
|
||||
},
|
||||
"hints": {
|
||||
"appInfo": "Sao chép thông tin để gửi bug.",
|
||||
"copySuccess": "Đã sao chép"
|
||||
},
|
||||
"buttons": {
|
||||
"checkUpdate": "Kiểm tra cập nhật",
|
||||
"copy": "Sao chép",
|
||||
"feedbackIssues": "Báo lỗi",
|
||||
"viewLog": "Xem nhật ký"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"proShortcut": {
|
||||
"hints": {
|
||||
"pressRecordShortcut": "Nhấn phím/tổ hợp phím để ghi",
|
||||
"clickRecordShortcut": "Click để ghi phím tắt"
|
||||
}
|
||||
},
|
||||
"updateApp": {
|
||||
"title": "Đã tìm thấy phiên bản mới 🥳",
|
||||
"labels": {
|
||||
"updateVersion": "Phiên bản: ",
|
||||
"updateTime": "Thời gian cập nhật: ",
|
||||
"changelog": "Nhật ký thay đổi: "
|
||||
},
|
||||
"hints": {
|
||||
"checkingUpdates": "Đang kiểm tra cập nhật...",
|
||||
"alreadyLatest": "Bạn đang dùng phiên bản mới nhất 🎉"
|
||||
},
|
||||
"buttons": {
|
||||
"updateNow": "Cập nhật ngay",
|
||||
"updateLater": "Để sau"
|
||||
}
|
||||
}
|
||||
},
|
||||
"composables": {
|
||||
"useSharedMenu": {
|
||||
"labels": {
|
||||
"preference": "Tùy chỉnh...",
|
||||
"hideCat": "Ẩn Mèo",
|
||||
"showCat": "Hiện Mèo",
|
||||
"passThrough": "Click xuyên",
|
||||
"windowSize": "Kích thước",
|
||||
"opacity": "Độ mờ"
|
||||
}
|
||||
},
|
||||
"useTray": {
|
||||
"checkUpdate": "Kiểm tra cập nhật",
|
||||
"openSource": "Mã nguồn",
|
||||
"restartApp": "Khởi động lại",
|
||||
"quitApp": "Thoát"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"live2d": {
|
||||
"hints": {
|
||||
"notFound": "Không tìm thấy tệp cấu hình chính của mô hình, vui lòng xác nhận các tệp mô hình có đầy đủ không."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
182
src/locales/zh-CN.json
Normal file
@@ -0,0 +1,182 @@
|
||||
{
|
||||
"pages": {
|
||||
"main": {
|
||||
"hints": {
|
||||
"redrawing": "重绘中..."
|
||||
}
|
||||
},
|
||||
"preference": {
|
||||
"title": "偏好设置",
|
||||
"cat": {
|
||||
"title": "猫咪设置",
|
||||
"labels": {
|
||||
"modelSettings": "模型设置",
|
||||
"mirrorMode": "镜像模式",
|
||||
"singleMode": "单键模式",
|
||||
"mouseMirror": "鼠标镜像",
|
||||
"windowSettings": "窗口设置",
|
||||
"passThrough": "窗口穿透",
|
||||
"alwaysOnTop": "窗口置顶",
|
||||
"windowSize": "窗口尺寸",
|
||||
"windowRadius": "窗口圆角",
|
||||
"opacity": "不透明度",
|
||||
"autoReleaseDelay": "按键自动释放延迟",
|
||||
"hideOnHover": "鼠标移入隐藏",
|
||||
"position": "窗口位置"
|
||||
},
|
||||
"hints": {
|
||||
"mirrorMode": "启用后,模型将水平镜像翻转。",
|
||||
"singleMode": "启用后,每只手只显示最后按下的一个按键。",
|
||||
"mouseMirror": "启用后,鼠标将镜像跟随手部移动。",
|
||||
"passThrough": "启用后,窗口不影响对其他应用程序的操作。",
|
||||
"alwaysOnTop": "启用后,窗口始终显示在其他应用程序上方。",
|
||||
"windowSize": "将鼠标移至窗口边缘,或按住 Shift 并右键拖动,也可以调整窗口大小。",
|
||||
"autoReleaseDelay": "由于 Windows 下部分系统级按键无法捕获释放事件,超时后将自动视为已释放。",
|
||||
"hideOnHover": "启用后,鼠标悬停在窗口上时,窗口会隐藏。",
|
||||
"position": "应用启动后,或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。"
|
||||
},
|
||||
"options": {
|
||||
"topLeft": "左上角",
|
||||
"topRight": "右上角",
|
||||
"bottomLeft": "左下角",
|
||||
"bottomRight": "右下角"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"title": "通用设置",
|
||||
"labels": {
|
||||
"appSettings": "应用设置",
|
||||
"launchOnStartup": "开机自启动",
|
||||
"showTaskbarIcon": "显示任务栏图标",
|
||||
"appearanceSettings": "外观设置",
|
||||
"themeMode": "主题模式",
|
||||
"language": "语言",
|
||||
"updateSettings": "更新设置",
|
||||
"autoCheckUpdate": "自动检查更新",
|
||||
"permissionsSettings": "权限设置",
|
||||
"inputMonitoringPermission": "输入监控权限"
|
||||
},
|
||||
"options": {
|
||||
"auto": "跟随系统",
|
||||
"lightMode": "亮色模式",
|
||||
"darkMode": "暗色模式"
|
||||
},
|
||||
"hints": {
|
||||
"showTaskbarIcon": "启用后,即可通过 OBS Studio 捕获窗口。",
|
||||
"inputMonitoringPermission": "开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。",
|
||||
"inputMonitoringPermissionGuide": "如果权限已开启,请先选中并点击“-”按钮将其删除,然后重新手动添加,最后重启应用以确保权限生效。"
|
||||
},
|
||||
"status": {
|
||||
"authorized": "已授权",
|
||||
"authorize": "去授权"
|
||||
},
|
||||
"buttons": {
|
||||
"openNow": "前往开启",
|
||||
"openLater": "稍后开启"
|
||||
}
|
||||
},
|
||||
"model": {
|
||||
"title": "模型管理",
|
||||
"labels": {
|
||||
"deleteModel": "删除模型"
|
||||
},
|
||||
"hints": {
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteModel": "你确定要删除此模型吗?",
|
||||
"importSuccess": "导入成功",
|
||||
"clickOrDragToImport": "点击或拖动至此区域导入"
|
||||
},
|
||||
"tooltips": {
|
||||
"createModel": "制作模型",
|
||||
"convertModel": "转换模型",
|
||||
"moreModels": "更多模型"
|
||||
}
|
||||
},
|
||||
"shortcut": {
|
||||
"title": "快捷键",
|
||||
"labels": {
|
||||
"toggleCat": "打开猫咪",
|
||||
"togglePreferences": "打开偏好设置",
|
||||
"mirrorMode": "镜像模式",
|
||||
"passThrough": "窗口穿透",
|
||||
"alwaysOnTop": "窗口置顶"
|
||||
},
|
||||
"hints": {
|
||||
"toggleCat": "切换猫咪窗口的显示与隐藏。",
|
||||
"togglePreferences": "切换偏好设置窗口的显示与隐藏。",
|
||||
"mirrorMode": "切换猫咪的镜像模式。",
|
||||
"passThrough": "切换猫咪窗口是否可穿透。",
|
||||
"alwaysOnTop": "切换猫咪窗口是否置顶。"
|
||||
}
|
||||
},
|
||||
"about": {
|
||||
"title": "关于",
|
||||
"labels": {
|
||||
"aboutApp": "关于软件",
|
||||
"appLog": "软件日志",
|
||||
"appInfo": "软件信息",
|
||||
"openSource": "开源地址"
|
||||
},
|
||||
"hints": {
|
||||
"appInfo": "复制软件信息并提供给 Bug Issue。",
|
||||
"copySuccess": "复制成功"
|
||||
},
|
||||
"buttons": {
|
||||
"checkUpdate": "检查更新",
|
||||
"copy": "复制",
|
||||
"feedbackIssues": "反馈问题",
|
||||
"viewLog": "查看日志"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"proShortcut": {
|
||||
"hints": {
|
||||
"pressRecordShortcut": "按下录制快捷键",
|
||||
"clickRecordShortcut": "点击录制快捷键"
|
||||
}
|
||||
},
|
||||
"updateApp": {
|
||||
"title": "发现新版本🥳",
|
||||
"labels": {
|
||||
"updateVersion": "更新版本:",
|
||||
"updateTime": "更新时间:",
|
||||
"changelog": "更新日志:"
|
||||
},
|
||||
"hints": {
|
||||
"checkingUpdates": "正在检查更新...",
|
||||
"alreadyLatest": "当前已是最新版本🎉"
|
||||
},
|
||||
"buttons": {
|
||||
"updateNow": "立即更新",
|
||||
"updateLater": "稍后更新"
|
||||
}
|
||||
}
|
||||
},
|
||||
"composables": {
|
||||
"useSharedMenu": {
|
||||
"labels": {
|
||||
"preference": "偏好设置...",
|
||||
"hideCat": "隐藏猫咪",
|
||||
"showCat": "显示猫咪",
|
||||
"passThrough": "窗口穿透",
|
||||
"windowSize": "窗口尺寸",
|
||||
"opacity": "不透明度"
|
||||
}
|
||||
},
|
||||
"useTray": {
|
||||
"checkUpdate": "检查更新",
|
||||
"openSource": "开源地址",
|
||||
"restartApp": "重启应用",
|
||||
"quitApp": "退出应用"
|
||||
}
|
||||
},
|
||||
"utils": {
|
||||
"live2d": {
|
||||
"hints": {
|
||||
"notFound": "未找到模型主配置文件,请确认模型文件是否完整。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import App from './App.vue'
|
||||
import { i18n } from './locales'
|
||||
import router from './router'
|
||||
|
||||
import 'virtual:uno.css'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import './assets/css/global.scss'
|
||||
@@ -11,4 +13,4 @@ import './assets/css/global.scss'
|
||||
const pinia = createPinia()
|
||||
pinia.use(createPlugin({ saveOnChange: true }))
|
||||
|
||||
createApp(App).use(router).use(pinia).mount('#app')
|
||||
createApp(App).use(router).use(pinia).use(i18n).mount('#app')
|
||||
|
||||
@@ -1,38 +1,49 @@
|
||||
<script setup lang="ts">
|
||||
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
||||
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 } from '@tauri-apps/plugin-fs'
|
||||
import { exists, readDir } from '@tauri-apps/plugin-fs'
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core'
|
||||
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 { INVOKE_KEY } from '@/constants'
|
||||
import { hideWindow, setAlwaysOnTop, showWindow } from '@/plugins/window'
|
||||
import { useWindowPosition } from '@/composables/useWindowPosition'
|
||||
import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useGeneralStore } from '@/stores/general.ts'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import { isImage } from '@/utils/is'
|
||||
import { join } from '@/utils/path'
|
||||
import { clearObject } from '@/utils/shared'
|
||||
|
||||
const { startListening } = useDevice()
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const { pressedMouses, mousePosition, pressedLeftKeys, pressedRightKeys } = useDevice()
|
||||
const { handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel()
|
||||
const { modelSize, handleLoad, handleDestroy, handleResize, handleKeyChange } = useModel()
|
||||
const catStore = useCatStore()
|
||||
const { getSharedMenu } = useSharedMenu()
|
||||
const modelStore = useModelStore()
|
||||
const generalStore = useGeneralStore()
|
||||
const resizing = ref(false)
|
||||
const backgroundImagePath = ref<string>()
|
||||
const { stickActive } = useGamepad()
|
||||
const { isMounted, setWindowPosition } = useWindowPosition()
|
||||
|
||||
onMounted(() => {
|
||||
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
|
||||
})
|
||||
onMounted(startListening)
|
||||
|
||||
onUnmounted(handleDestroy)
|
||||
|
||||
const debouncedResize = useDebounceFn(async () => {
|
||||
await handleResize()
|
||||
|
||||
await setWindowPosition()
|
||||
|
||||
resizing.value = false
|
||||
}, 100)
|
||||
|
||||
@@ -42,45 +53,83 @@ useEventListener('resize', () => {
|
||||
debouncedResize()
|
||||
})
|
||||
|
||||
watch(pressedMouses, handleMouseDown)
|
||||
|
||||
watch(mousePosition, handleMouseMove)
|
||||
|
||||
watch(pressedLeftKeys, (keys) => {
|
||||
handleKeyDown('left', keys.length > 0)
|
||||
})
|
||||
|
||||
watch(pressedRightKeys, (keys) => {
|
||||
handleKeyDown('right', keys.length > 0)
|
||||
})
|
||||
|
||||
watch(() => catStore.visible, async (value) => {
|
||||
value ? showWindow() : hideWindow()
|
||||
})
|
||||
|
||||
watch(() => catStore.penetrable, (value) => {
|
||||
appWindow.setIgnoreCursorEvents(value)
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
|
||||
|
||||
watch(() => modelStore.currentModel, async (model) => {
|
||||
if (!model) return
|
||||
|
||||
await handleLoad()
|
||||
|
||||
const path = join(model.path, 'resources', 'background.png')
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
setWindowPosition()
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
function handleWindowDrag() {
|
||||
watch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => {
|
||||
if (!modelSize) return
|
||||
|
||||
const { width, height } = modelSize
|
||||
|
||||
appWindow.setSize(
|
||||
new PhysicalSize({
|
||||
width: Math.round(width * (scale / 100)),
|
||||
height: Math.round(height * (scale / 100)),
|
||||
}),
|
||||
)
|
||||
}, { immediate: true })
|
||||
|
||||
watch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => {
|
||||
const dirs = Object.values(keys).map((path) => {
|
||||
return nth(path.split(sep()), -2)!
|
||||
})
|
||||
|
||||
const hasLeft = dirs.some(dir => dir.startsWith('left'))
|
||||
const hasRight = dirs.some(dir => dir.startsWith('right'))
|
||||
|
||||
handleKeyChange(true, stickActive.left || hasLeft)
|
||||
handleKeyChange(false, stickActive.right || hasRight)
|
||||
}, { deep: true })
|
||||
|
||||
watch(() => catStore.window.visible, async (value) => {
|
||||
value ? showWindow() : hideWindow()
|
||||
})
|
||||
|
||||
watch(() => catStore.window.passThrough, (value) => {
|
||||
appWindow.setIgnoreCursorEvents(value)
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => catStore.window.alwaysOnTop, setAlwaysOnTop, { immediate: true })
|
||||
|
||||
watch(() => generalStore.app.taskbarVisible, setTaskbarVisibility, { immediate: true })
|
||||
|
||||
function handleMouseDown() {
|
||||
appWindow.startDragging()
|
||||
}
|
||||
|
||||
async function handleContextmenu(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
if (event.shiftKey) return
|
||||
|
||||
const menu = await Menu.new({
|
||||
items: await getSharedMenu(),
|
||||
})
|
||||
@@ -88,44 +137,52 @@ async function handleContextmenu(event: MouseEvent) {
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') {
|
||||
return convertFileSrc(join(modelStore.currentModel!.path, 'resources', `${side}-keys`, `${key}.png`))
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
const { buttons, shiftKey, movementX, movementY } = event
|
||||
|
||||
if (buttons !== 2 || !shiftKey) return
|
||||
|
||||
const delta = (movementX + movementY) * 0.5
|
||||
const nextScale = Math.max(10, Math.min(catStore.window.scale + delta, 500))
|
||||
|
||||
catStore.window.scale = round(nextScale)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-show="isMounted"
|
||||
class="relative size-screen overflow-hidden children:(absolute size-full)"
|
||||
:class="{ '-scale-x-100': catStore.mirrorMode }"
|
||||
:style="{ opacity: catStore.opacity / 100 }"
|
||||
:class="{ '-scale-x-100': catStore.model.mirror }"
|
||||
:style="{
|
||||
opacity: catStore.window.opacity / 100,
|
||||
borderRadius: `${catStore.window.radius}%`,
|
||||
}"
|
||||
@contextmenu="handleContextmenu"
|
||||
@mousedown="handleWindowDrag"
|
||||
@mousedown="handleMouseDown"
|
||||
@mousemove="handleMouseMove"
|
||||
>
|
||||
<img
|
||||
v-if="backgroundImagePath"
|
||||
class="object-cover"
|
||||
:src="backgroundImagePath"
|
||||
>
|
||||
|
||||
<canvas id="live2dCanvas" />
|
||||
|
||||
<img
|
||||
v-for="key in pressedLeftKeys"
|
||||
:key="key"
|
||||
:src="resolveKeyImagePath(key)"
|
||||
>
|
||||
|
||||
<img
|
||||
v-for="key in pressedRightKeys"
|
||||
:key="key"
|
||||
:src="resolveKeyImagePath(key, 'right')"
|
||||
v-for="path in modelStore.pressedKeys"
|
||||
:key="path"
|
||||
class="object-cover"
|
||||
:src="convertFileSrc(path)"
|
||||
>
|
||||
|
||||
<div
|
||||
v-show="resizing"
|
||||
class="flex items-center justify-center bg-black"
|
||||
>
|
||||
<span class="text-center text-5xl text-white">
|
||||
重绘中...
|
||||
<span class="text-center text-[10vw] text-white">
|
||||
{{ $t('pages.main.hints.redrawing') }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { openPath, openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { arch, platform, version } from '@tauri-apps/plugin-os'
|
||||
import { Button, message } from 'ant-design-vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
@@ -15,6 +16,7 @@ import { useAppStore } from '@/stores/app'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const logDir = ref('')
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(async () => {
|
||||
logDir.value = await appLogDir()
|
||||
@@ -36,7 +38,7 @@ async function copyInfo() {
|
||||
|
||||
await writeText(JSON.stringify(info, null, 2))
|
||||
|
||||
message.success('复制成功')
|
||||
message.success(t('pages.preference.about.hints.copySuccess'))
|
||||
}
|
||||
|
||||
function feedbackIssue() {
|
||||
@@ -45,16 +47,16 @@ function feedbackIssue() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProList title="关于软件">
|
||||
<ProList :title="$t('pages.preference.about.labels.aboutApp')">
|
||||
<ProListItem
|
||||
:description="`版本:v${appStore.version}`"
|
||||
:description="`v${appStore.version}`"
|
||||
:title="appStore.name"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleUpdate"
|
||||
>
|
||||
检查更新
|
||||
{{ $t('pages.preference.about.buttons.checkUpdate') }}
|
||||
</Button>
|
||||
|
||||
<template #icon>
|
||||
@@ -68,20 +70,20 @@ function feedbackIssue() {
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
description="复制软件信息并提供给 Bug Issue"
|
||||
title="软件信息"
|
||||
:description="$t('pages.preference.about.hints.appInfo')"
|
||||
:title="$t('pages.preference.about.labels.appInfo')"
|
||||
>
|
||||
<Button @click="copyInfo">
|
||||
复制
|
||||
{{ $t('pages.preference.about.buttons.copy') }}
|
||||
</Button>
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem title="开源地址">
|
||||
<ProListItem :title="$t('pages.preference.about.labels.openSource')">
|
||||
<Button
|
||||
danger
|
||||
@click="feedbackIssue"
|
||||
>
|
||||
反馈问题
|
||||
{{ $t('pages.preference.about.buttons.feedbackIssues') }}
|
||||
</Button>
|
||||
|
||||
<template #description>
|
||||
@@ -93,10 +95,10 @@ function feedbackIssue() {
|
||||
|
||||
<ProListItem
|
||||
:description="logDir"
|
||||
title="软件日志"
|
||||
:title="$t('pages.preference.about.labels.appLog')"
|
||||
>
|
||||
<Button @click="openPath(logDir)">
|
||||
查看日志
|
||||
{{ $t('pages.preference.about.buttons.viewLog') }}
|
||||
</Button>
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import { Select, SelectOption } from 'ant-design-vue'
|
||||
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
|
||||
const catStore = useCatStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProListItem
|
||||
:description="$t('pages.preference.cat.hints.position')"
|
||||
:title="$t('pages.preference.cat.labels.position')"
|
||||
>
|
||||
<Select v-model:value="catStore.window.position">
|
||||
<SelectOption value="bottomRight">
|
||||
{{ $t('pages.preference.cat.options.bottomRight') }}
|
||||
</SelectOption>
|
||||
<SelectOption value="bottomLeft">
|
||||
{{ $t('pages.preference.cat.options.bottomLeft') }}
|
||||
</SelectOption>
|
||||
<SelectOption value="topLeft">
|
||||
{{ $t('pages.preference.cat.options.topLeft') }}
|
||||
</SelectOption>
|
||||
<SelectOption value="topRight">
|
||||
{{ $t('pages.preference.cat.options.topRight') }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</ProListItem>
|
||||
</template>
|
||||
@@ -1,81 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { InputNumber, Slider, Switch } from 'ant-design-vue'
|
||||
|
||||
import Position from './components/position/index.vue'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { isWindows } from '@/utils/platform'
|
||||
|
||||
const catStore = useCatStore()
|
||||
|
||||
function opacityFormatter(value?: number) {
|
||||
return `${value}%`
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProList title="模型设置">
|
||||
<ProList :title="$t('pages.preference.cat.labels.modelSettings')">
|
||||
<ProListItem
|
||||
description="启用后,模型将水平镜像翻转"
|
||||
title="镜像模式"
|
||||
:description="$t('pages.preference.cat.hints.mirrorMode')"
|
||||
:title="$t('pages.preference.cat.labels.mirrorMode')"
|
||||
>
|
||||
<Switch v-model:checked="catStore.mirrorMode" />
|
||||
<Switch v-model:checked="catStore.model.mirror" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
description="启用后,每只手只显示最后按下的一个按键"
|
||||
title="单键模式"
|
||||
:description="$t('pages.preference.cat.hints.singleMode')"
|
||||
:title="$t('pages.preference.cat.labels.singleMode')"
|
||||
>
|
||||
<Switch v-model:checked="catStore.singleMode" />
|
||||
<Switch v-model:checked="catStore.model.single" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
description="启用后,鼠标将镜像跟随手部移动"
|
||||
title="鼠标镜像"
|
||||
:description="$t('pages.preference.cat.hints.mouseMirror')"
|
||||
:title="$t('pages.preference.cat.labels.mouseMirror')"
|
||||
>
|
||||
<Switch v-model:checked="catStore.mouseMirror" />
|
||||
<Switch v-model:checked="catStore.model.mouseMirror" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
v-if="isWindows"
|
||||
:description="$t('pages.preference.cat.hints.autoReleaseDelay')"
|
||||
:title="$t('pages.preference.cat.labels.autoReleaseDelay')"
|
||||
>
|
||||
<InputNumber
|
||||
v-model:value="catStore.model.autoReleaseDelay"
|
||||
addon-after="s"
|
||||
class="w-28"
|
||||
/>
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
|
||||
<ProList title="窗口设置">
|
||||
<ProList :title="$t('pages.preference.cat.labels.windowSettings')">
|
||||
<Position />
|
||||
|
||||
<ProListItem
|
||||
description="启用后,窗口不影响对其他应用程序的操作"
|
||||
title="窗口穿透"
|
||||
:description="$t('pages.preference.cat.hints.passThrough')"
|
||||
:title="$t('pages.preference.cat.labels.passThrough')"
|
||||
>
|
||||
<Switch v-model:checked="catStore.penetrable" />
|
||||
<Switch v-model:checked="catStore.window.passThrough" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
description="启用后,窗口始终显示在其他应用程序上方"
|
||||
title="窗口置顶"
|
||||
:description="$t('pages.preference.cat.hints.alwaysOnTop')"
|
||||
:title="$t('pages.preference.cat.labels.alwaysOnTop')"
|
||||
>
|
||||
<Switch v-model:checked="catStore.alwaysOnTop" />
|
||||
<Switch v-model:checked="catStore.window.alwaysOnTop" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
description="将鼠标移动到窗口边缘后,也可以拖动调整窗口尺寸"
|
||||
title="窗口尺寸"
|
||||
:description="$t('pages.preference.cat.hints.hideOnHover')"
|
||||
:title="$t('pages.preference.cat.labels.hideOnHover')"
|
||||
>
|
||||
<Switch v-model:checked="catStore.window.hideOnHover" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
:description="$t('pages.preference.cat.hints.windowSize')"
|
||||
:title="$t('pages.preference.cat.labels.windowSize')"
|
||||
>
|
||||
<InputNumber
|
||||
v-model:value="catStore.scale"
|
||||
v-model:value="catStore.window.scale"
|
||||
addon-after="%"
|
||||
class="w-28"
|
||||
:max="500"
|
||||
:min="1"
|
||||
>
|
||||
<template #addonAfter>
|
||||
%
|
||||
</template>
|
||||
</InputNumber>
|
||||
/>
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem :title="$t('pages.preference.cat.labels.windowRadius')">
|
||||
<InputNumber
|
||||
v-model:value="catStore.window.radius"
|
||||
addon-after="%"
|
||||
class="w-28"
|
||||
:min="0"
|
||||
/>
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
title="不透明度"
|
||||
:title="$t('pages.preference.cat.labels.opacity')"
|
||||
vertical
|
||||
>
|
||||
<Slider
|
||||
v-model:value="catStore.opacity"
|
||||
class="m-0!"
|
||||
v-model:value="catStore.window.opacity"
|
||||
class="m-[0]!"
|
||||
:max="100"
|
||||
:min="10"
|
||||
:tip-formatter="opacityFormatter"
|
||||
:tip-formatter="(value) => `${value}%`"
|
||||
/>
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { message } from '@tauri-apps/plugin-dialog'
|
||||
import { confirm } from '@tauri-apps/plugin-dialog'
|
||||
import { Space } from 'ant-design-vue'
|
||||
import { checkInputMonitoringPermission, requestInputMonitoringPermission } from 'tauri-plugin-macos-permissions-api'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
import { isMac } from '@/utils/platform'
|
||||
|
||||
const authorized = ref(false)
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(async () => {
|
||||
authorized.value = await checkInputMonitoringPermission()
|
||||
@@ -20,12 +22,15 @@ onMounted(async () => {
|
||||
|
||||
await appWindow.setAlwaysOnTop(true)
|
||||
|
||||
await message('如果权限已开启,先选中后点击“-”按钮将其删除,再重新手动添加,并重启应用以确保权限生效。', {
|
||||
title: '输入监控权限',
|
||||
okLabel: '前往开启',
|
||||
const confirmed = await confirm(t('pages.preference.general.hints.inputMonitoringPermissionGuide'), {
|
||||
title: t('pages.preference.general.labels.inputMonitoringPermission'),
|
||||
okLabel: t('pages.preference.general.buttons.openNow'),
|
||||
cancelLabel: t('pages.preference.general.buttons.openLater'),
|
||||
kind: 'warning',
|
||||
})
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
await appWindow.setAlwaysOnTop(false)
|
||||
|
||||
requestInputMonitoringPermission()
|
||||
@@ -35,11 +40,11 @@ onMounted(async () => {
|
||||
<template>
|
||||
<ProList
|
||||
v-if="isMac"
|
||||
title="权限设置"
|
||||
:title="$t('pages.preference.general.labels.permissionsSettings')"
|
||||
>
|
||||
<ProListItem
|
||||
description="开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。"
|
||||
title="输入监控权限"
|
||||
:description="$t('pages.preference.general.hints.inputMonitoringPermission')"
|
||||
:title="$t('pages.preference.general.labels.inputMonitoringPermission')"
|
||||
>
|
||||
<Space
|
||||
v-if="authorized"
|
||||
@@ -48,7 +53,7 @@ onMounted(async () => {
|
||||
>
|
||||
<div class="i-solar:verified-check-bold text-4.5" />
|
||||
|
||||
<span>已授权</span>
|
||||
<span class="whitespace-nowrap">{{ $t('pages.preference.general.status.authorized') }}</span>
|
||||
</Space>
|
||||
|
||||
<Space
|
||||
@@ -59,7 +64,7 @@ onMounted(async () => {
|
||||
>
|
||||
<div class="i-solar:round-arrow-right-bold text-4.5" />
|
||||
|
||||
<span>去授权</span>
|
||||
<span class="whitespace-nowrap">{{ $t('pages.preference.general.status.authorize') }}</span>
|
||||
</Space>
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { Select, SelectOption } from 'ant-design-vue'
|
||||
import { onMounted, watch } from 'vue'
|
||||
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
import { useGeneralStore } from '@/stores/general'
|
||||
|
||||
const generalStore = useGeneralStore()
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
onMounted(() => {
|
||||
appWindow.onThemeChanged(async ({ payload }) => {
|
||||
if (generalStore.appearance.theme !== 'auto') return
|
||||
|
||||
generalStore.appearance.isDark = payload === 'dark'
|
||||
})
|
||||
})
|
||||
|
||||
watch(() => generalStore.appearance.theme, async (value) => {
|
||||
let nextTheme = value === 'auto' ? null : value
|
||||
|
||||
await appWindow.setTheme(nextTheme)
|
||||
|
||||
nextTheme = nextTheme ?? (await appWindow.theme())
|
||||
|
||||
generalStore.appearance.isDark = nextTheme === 'dark'
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => generalStore.appearance.isDark, (value) => {
|
||||
if (value) {
|
||||
document.documentElement.classList.add('dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
}
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProListItem :title="$t('pages.preference.general.labels.themeMode')">
|
||||
<Select v-model:value="generalStore.appearance.theme">
|
||||
<SelectOption value="auto">
|
||||
{{ $t('pages.preference.general.options.auto') }}
|
||||
</SelectOption>
|
||||
<SelectOption value="light">
|
||||
{{ $t('pages.preference.general.options.lightMode') }}
|
||||
</SelectOption>
|
||||
<SelectOption value="dark">
|
||||
{{ $t('pages.preference.general.options.darkMode') }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
</ProListItem>
|
||||
</template>
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
import { Switch } from 'ant-design-vue'
|
||||
import { Select, Switch } from 'ant-design-vue'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import MacosPermissions from './components/macos-permissions/index.vue'
|
||||
import ThemeMode from './components/theme-mode/index.vue'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
@@ -11,7 +12,7 @@ import { useGeneralStore } from '@/stores/general'
|
||||
|
||||
const generalStore = useGeneralStore()
|
||||
|
||||
watch(() => generalStore.autostart, async (value) => {
|
||||
watch(() => generalStore.app.autostart, async (value) => {
|
||||
const enabled = await isEnabled()
|
||||
|
||||
if (value && !enabled) {
|
||||
@@ -27,15 +28,43 @@ watch(() => generalStore.autostart, async (value) => {
|
||||
<template>
|
||||
<MacosPermissions />
|
||||
|
||||
<ProList title="应用设置">
|
||||
<ProListItem title="开机自启动">
|
||||
<Switch v-model:checked="generalStore.autostart" />
|
||||
<ProList :title="$t('pages.preference.general.labels.appSettings')">
|
||||
<ProListItem :title="$t('pages.preference.general.labels.launchOnStartup')">
|
||||
<Switch v-model:checked="generalStore.app.autostart" />
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
:description="$t('pages.preference.general.hints.showTaskbarIcon')"
|
||||
:title="$t('pages.preference.general.labels.showTaskbarIcon')"
|
||||
>
|
||||
<Switch v-model:checked="generalStore.app.taskbarVisible" />
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
|
||||
<ProList title="更新设置">
|
||||
<ProListItem title="自动检查更新">
|
||||
<Switch v-model:checked="generalStore.autoCheckUpdate" />
|
||||
<ProList :title="$t('pages.preference.general.labels.appearanceSettings')">
|
||||
<ThemeMode />
|
||||
|
||||
<ProListItem :title="$t('pages.preference.general.labels.language')">
|
||||
<Select v-model:value="generalStore.appearance.language">
|
||||
<Select.Option value="zh-CN">
|
||||
简体中文
|
||||
</Select.Option>
|
||||
<Select.Option value="en-US">
|
||||
English
|
||||
</Select.Option>
|
||||
<Select.Option value="vi-VN">
|
||||
Tiếng Việt
|
||||
</Select.Option>
|
||||
<Select.Option value="pt-BR">
|
||||
Português
|
||||
</Select.Option>
|
||||
</Select>
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
|
||||
<ProList :title="$t('pages.preference.general.labels.updateSettings')">
|
||||
<ProListItem :title="$t('pages.preference.general.labels.autoCheckUpdate')">
|
||||
<Switch v-model:checked="generalStore.update.autoCheck" />
|
||||
</ProListItem>
|
||||
</ProList>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { EditOutlined, MenuOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
|
||||
import { EditOutlined, MenuOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
|
||||
</script>
|
||||
@@ -15,7 +15,7 @@ import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
|
||||
</template>
|
||||
|
||||
<FloatButton
|
||||
tooltip="如何制作模型?"
|
||||
:tooltip="$t('pages.preference.model.tooltips.createModel')"
|
||||
@click="openUrl('https://juejin.cn/post/7509872655802269731')"
|
||||
>
|
||||
<template #icon>
|
||||
@@ -24,7 +24,16 @@ import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
|
||||
</FloatButton>
|
||||
|
||||
<FloatButton
|
||||
tooltip="更多模型"
|
||||
:tooltip="$t('pages.preference.model.tooltips.convertModel')"
|
||||
@click="openUrl('https://bongocat.vteamer.cc')"
|
||||
>
|
||||
<template #icon>
|
||||
<SyncOutlined />
|
||||
</template>
|
||||
</FloatButton>
|
||||
|
||||
<FloatButton
|
||||
:tooltip="$t('pages.preference.model.tooltips.moreModels')"
|
||||
@click="openUrl('https://github.com/ayangweb/Awesome-BongoCat')"
|
||||
>
|
||||
<template #icon>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import type { ModelMode } from '@/stores/model'
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { appDataDir } from '@tauri-apps/api/path'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
@@ -7,6 +9,7 @@ import { readDir } from '@tauri-apps/plugin-fs'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { onMounted, ref, useTemplateRef, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { INVOKE_KEY } from '@/constants'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
@@ -16,6 +19,7 @@ const dropRef = useTemplateRef('drop')
|
||||
const dragenter = ref(false)
|
||||
const selectPaths = ref<string[]>([])
|
||||
const modelStore = useModelStore()
|
||||
const { t } = useI18n()
|
||||
|
||||
onMounted(() => {
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
@@ -53,29 +57,39 @@ async function handleUpload() {
|
||||
}
|
||||
|
||||
watch(selectPaths, async (paths) => {
|
||||
for await (const path of paths) {
|
||||
for await (const fromPath of paths) {
|
||||
try {
|
||||
const id = nanoid()
|
||||
|
||||
const files = await readDir(join(path, 'resources'))
|
||||
let mode: ModelMode = 'standard'
|
||||
|
||||
const isKeyboardMode = files.some(file => file.name === 'right-keys')
|
||||
const files = await readDir(join(fromPath, 'resources', 'right-keys')).catch(() => [])
|
||||
|
||||
if (files.length > 0) {
|
||||
const fileNames = files.map(file => file.name.split('.')[0])
|
||||
|
||||
if (fileNames.includes('East')) {
|
||||
mode = 'gamepad'
|
||||
} else {
|
||||
mode = 'keyboard'
|
||||
}
|
||||
}
|
||||
|
||||
const toPath = join(await appDataDir(), 'custom-models', id)
|
||||
|
||||
await invoke(INVOKE_KEY.COPY_DIR, {
|
||||
fromPath: path,
|
||||
fromPath,
|
||||
toPath,
|
||||
})
|
||||
|
||||
modelStore.models.push({
|
||||
id,
|
||||
path: toPath,
|
||||
mode: isKeyboardMode ? 'keyboard' : 'standard',
|
||||
mode,
|
||||
isPreset: false,
|
||||
})
|
||||
|
||||
message.success('导入成功')
|
||||
message.success(t('pages.preference.model.hints.importSuccess'))
|
||||
} catch (error) {
|
||||
message.error(String(error))
|
||||
}
|
||||
@@ -92,6 +106,6 @@ watch(selectPaths, async (paths) => {
|
||||
>
|
||||
<div class="i-solar:upload-square-outline text-12 text-primary" />
|
||||
|
||||
<span>点击或拖动至此区域导入</span>
|
||||
<span>{{ $t('pages.preference.model.hints.clickOrDragToImport') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,6 +8,7 @@ import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { Card, message, Popconfirm } from 'ant-design-vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'
|
||||
|
||||
import FloatMenu from './components/float-menu/index.vue'
|
||||
@@ -18,8 +19,8 @@ import { join } from '@/utils/path'
|
||||
|
||||
const modelStore = useModelStore()
|
||||
const firstItemRef = ref<HTMLElement>()
|
||||
|
||||
const { height } = useElementSize(firstItemRef)
|
||||
const { t } = useI18n()
|
||||
|
||||
function setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) {
|
||||
if (!el || index > 0) return
|
||||
@@ -39,7 +40,7 @@ async function handleDelete(item: Model) {
|
||||
try {
|
||||
await remove(path, { recursive: true })
|
||||
|
||||
message.success('删除成功')
|
||||
message.success(t('pages.preference.model.hints.deleteSuccess'))
|
||||
} catch (error) {
|
||||
message.error(String(error))
|
||||
} finally {
|
||||
@@ -91,9 +92,9 @@ async function handleDelete(item: Model) {
|
||||
|
||||
<template v-if="!item.isPreset">
|
||||
<Popconfirm
|
||||
description="你确定要删除此模型吗?"
|
||||
:description="$t('pages.preference.model.hints.deleteModel')"
|
||||
placement="topRight"
|
||||
title="删除模型"
|
||||
:title="$t('pages.preference.model.labels.deleteModel')"
|
||||
@confirm="handleDelete(item)"
|
||||
>
|
||||
<i
|
||||
|
||||
@@ -3,7 +3,7 @@ import { storeToRefs } from 'pinia'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProShortcut from '@/components/pro-shortcut/index.vue'
|
||||
import { useTauriKeyPress } from '@/composables/useTauriKeyPress'
|
||||
import { useTauriShortcut } from '@/composables/useTauriShortcut'
|
||||
import { toggleWindowVisible } from '@/plugins/window'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useShortcutStore } from '@/stores/shortcut.ts'
|
||||
@@ -12,57 +12,57 @@ const shortcutStore = useShortcutStore()
|
||||
const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore)
|
||||
const catStore = useCatStore()
|
||||
|
||||
useTauriKeyPress(visibleCat, () => {
|
||||
catStore.visible = !catStore.visible
|
||||
useTauriShortcut(visibleCat, () => {
|
||||
catStore.window.visible = !catStore.window.visible
|
||||
})
|
||||
|
||||
useTauriKeyPress(visiblePreference, () => {
|
||||
useTauriShortcut(visiblePreference, () => {
|
||||
toggleWindowVisible('preference')
|
||||
})
|
||||
|
||||
useTauriKeyPress(mirrorMode, () => {
|
||||
catStore.mirrorMode = !catStore.mirrorMode
|
||||
useTauriShortcut(mirrorMode, () => {
|
||||
catStore.model.mirror = !catStore.model.mirror
|
||||
})
|
||||
|
||||
useTauriKeyPress(penetrable, () => {
|
||||
catStore.penetrable = !catStore.penetrable
|
||||
useTauriShortcut(penetrable, () => {
|
||||
catStore.window.passThrough = !catStore.window.passThrough
|
||||
})
|
||||
|
||||
useTauriKeyPress(alwaysOnTop, () => {
|
||||
catStore.alwaysOnTop = !catStore.alwaysOnTop
|
||||
useTauriShortcut(alwaysOnTop, () => {
|
||||
catStore.window.alwaysOnTop = !catStore.window.alwaysOnTop
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ProList title="快捷键">
|
||||
<ProList :title="$t('pages.preference.shortcut.title')">
|
||||
<ProShortcut
|
||||
v-model="shortcutStore.visibleCat"
|
||||
description="切换猫咪窗口的显示与隐藏"
|
||||
title="打开猫咪"
|
||||
:description="$t('pages.preference.shortcut.hints.toggleCat')"
|
||||
:title="$t('pages.preference.shortcut.labels.toggleCat')"
|
||||
/>
|
||||
|
||||
<ProShortcut
|
||||
v-model="shortcutStore.visiblePreference"
|
||||
description="切换偏好设置窗口的显示与隐藏"
|
||||
title="打开偏好设置"
|
||||
:description="$t('pages.preference.shortcut.hints.togglePreferences')"
|
||||
:title="$t('pages.preference.shortcut.labels.togglePreferences')"
|
||||
/>
|
||||
|
||||
<ProShortcut
|
||||
v-model="shortcutStore.mirrorMode"
|
||||
description="切换猫咪的镜像模式"
|
||||
title="镜像模式"
|
||||
:description="$t('pages.preference.shortcut.hints.mirrorMode')"
|
||||
:title="$t('pages.preference.shortcut.labels.mirrorMode')"
|
||||
/>
|
||||
|
||||
<ProShortcut
|
||||
v-model="shortcutStore.penetrable"
|
||||
description="切换猫咪窗口是否可穿透"
|
||||
title="窗口穿透"
|
||||
:description="$t('pages.preference.shortcut.hints.passThrough')"
|
||||
:title="$t('pages.preference.shortcut.labels.passThrough')"
|
||||
/>
|
||||
|
||||
<ProShortcut
|
||||
v-model="shortcutStore.alwaysOnTop"
|
||||
description="切换猫咪窗口是否置顶"
|
||||
title="窗口置顶"
|
||||
:description="$t('pages.preference.shortcut.hints.alwaysOnTop')"
|
||||
:title="$t('pages.preference.shortcut.labels.alwaysOnTop')"
|
||||
/>
|
||||
</ProList>
|
||||
</template>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { Flex } from 'ant-design-vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import About from './components/about/index.vue'
|
||||
import Cat from './components/cat/index.vue'
|
||||
@@ -11,49 +13,57 @@ import Shortcut from './components/shortcut/index.vue'
|
||||
import UpdateApp from '@/components/update-app/index.vue'
|
||||
import { useTray } from '@/composables/useTray'
|
||||
import { useAppStore } from '@/stores/app'
|
||||
import { useGeneralStore } from '@/stores/general'
|
||||
import { isMac } from '@/utils/platform'
|
||||
|
||||
const { createTray } = useTray()
|
||||
const appStore = useAppStore()
|
||||
const current = ref(0)
|
||||
const { t } = useI18n()
|
||||
const generalStore = useGeneralStore()
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
onMounted(async () => {
|
||||
createTray()
|
||||
})
|
||||
|
||||
const menus = [
|
||||
watch(() => generalStore.appearance.language, () => {
|
||||
appWindow.setTitle(t('pages.preference.title'))
|
||||
}, { immediate: true })
|
||||
|
||||
const menus = computed(() => [
|
||||
{
|
||||
label: '猫咪设置',
|
||||
label: t('pages.preference.cat.title'),
|
||||
icon: 'i-solar:cat-bold',
|
||||
component: Cat,
|
||||
},
|
||||
{
|
||||
label: '通用设置',
|
||||
label: t('pages.preference.general.title'),
|
||||
icon: 'i-solar:settings-minimalistic-bold',
|
||||
component: General,
|
||||
},
|
||||
{
|
||||
label: '模型管理',
|
||||
label: t('pages.preference.model.title'),
|
||||
icon: 'i-solar:magic-stick-3-bold',
|
||||
component: Model,
|
||||
},
|
||||
{
|
||||
label: '快捷键',
|
||||
label: t('pages.preference.shortcut.title'),
|
||||
icon: 'i-solar:keyboard-bold',
|
||||
component: Shortcut,
|
||||
},
|
||||
{
|
||||
label: '关于',
|
||||
label: t('pages.preference.about.title'),
|
||||
icon: 'i-solar:info-circle-bold',
|
||||
component: About,
|
||||
},
|
||||
]
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Flex class="h-screen">
|
||||
<div
|
||||
class="h-full w-30 flex flex-col items-center gap-4 overflow-auto bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
|
||||
class="h-full w-30 flex flex-col items-center gap-4 overflow-auto dark:(bg-color-3 bg-none) bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
|
||||
:class="[isMac ? 'pt-8' : 'pt-4']"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
@@ -73,8 +83,8 @@ const menus = [
|
||||
<div
|
||||
v-for="(item, index) in menus"
|
||||
:key="item.label"
|
||||
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 text-color-3 transition"
|
||||
:class="{ 'bg-white! text-primary-5 font-bold': current === index }"
|
||||
class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 dark:text-color-2 text-color-3 transition"
|
||||
:class="{ 'bg-color-2! text-primary-5 dark:text-primary-7 font-bold dark:bg-color-8!': current === index }"
|
||||
@click="current = index"
|
||||
>
|
||||
<div
|
||||
@@ -91,7 +101,7 @@ const menus = [
|
||||
v-for="(item, index) in menus"
|
||||
v-show="current === index"
|
||||
:key="item.label"
|
||||
class="flex-1 overflow-auto bg-color-8 p-4"
|
||||
class="flex-1 overflow-auto bg-color-8 dark:bg-color-2 p-4"
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<component :is="item.component" />
|
||||
|
||||
@@ -10,6 +10,7 @@ const COMMAND = {
|
||||
SHOW_WINDOW: 'plugin:custom-window|show_window',
|
||||
HIDE_WINDOW: 'plugin:custom-window|hide_window',
|
||||
SET_ALWAYS_ON_TOP: 'plugin:custom-window|set_always_on_top',
|
||||
SET_TASKBAR_VISIBILITY: 'plugin:custom-window|set_taskbar_visibility',
|
||||
}
|
||||
|
||||
export function showWindow(label?: WindowLabel) {
|
||||
@@ -45,3 +46,7 @@ export async function toggleWindowVisible(label?: WindowLabel) {
|
||||
|
||||
return showWindow(label)
|
||||
}
|
||||
|
||||
export async function setTaskbarVisibility(visible: boolean) {
|
||||
invoke(COMMAND.SET_TASKBAR_VISIBILITY, { visible })
|
||||
}
|
||||
|
||||
@@ -2,21 +2,22 @@ import type { WindowState } from '@/composables/useWindowState'
|
||||
|
||||
import { getName, getVersion } from '@tauri-apps/api/app'
|
||||
import { defineStore } from 'pinia'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
export const useAppStore = defineStore('app', () => {
|
||||
const name = ref('')
|
||||
const version = ref('')
|
||||
const windowState = reactive<WindowState>({})
|
||||
|
||||
onMounted(async () => {
|
||||
const init = async () => {
|
||||
name.value = await getName()
|
||||
version.value = await getVersion()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
version,
|
||||
windowState,
|
||||
init,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,29 +1,90 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
export interface CatStore {
|
||||
model: {
|
||||
mirror: boolean
|
||||
single: boolean
|
||||
mouseMirror: boolean
|
||||
autoReleaseDelay: number
|
||||
}
|
||||
window: {
|
||||
visible: boolean
|
||||
passThrough: boolean
|
||||
alwaysOnTop: boolean
|
||||
scale: number
|
||||
opacity: number
|
||||
radius: number
|
||||
hideOnHover: boolean
|
||||
position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
|
||||
}
|
||||
}
|
||||
|
||||
export const useCatStore = defineStore('cat', () => {
|
||||
const visible = ref(false)
|
||||
/* ------------ 废弃字段(后续删除) ------------ */
|
||||
|
||||
/** @deprecated 请使用 `model.mirror` */
|
||||
const mirrorMode = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `model.single` */
|
||||
const singleMode = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `model.mouseMirror` */
|
||||
const mouseMirror = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `window.passThrough` */
|
||||
const penetrable = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `window.alwaysOnTop` */
|
||||
const alwaysOnTop = ref(true)
|
||||
|
||||
/** @deprecated 请使用 `window.scale` */
|
||||
const scale = ref(100)
|
||||
|
||||
/** @deprecated 请使用 `window.opacity` */
|
||||
const opacity = ref(100)
|
||||
|
||||
/** @deprecated 用于标识数据是否已迁移,后续版本将删除 */
|
||||
const migrated = ref(false)
|
||||
|
||||
const model = reactive<CatStore['model']>({
|
||||
mirror: false,
|
||||
single: false,
|
||||
mouseMirror: false,
|
||||
autoReleaseDelay: 3,
|
||||
})
|
||||
|
||||
const window = reactive<CatStore['window']>({
|
||||
visible: true,
|
||||
passThrough: false,
|
||||
alwaysOnTop: false,
|
||||
scale: 100,
|
||||
opacity: 100,
|
||||
radius: 0,
|
||||
hideOnHover: false,
|
||||
position: 'bottomRight',
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
visible.value = true
|
||||
if (migrated.value) return
|
||||
|
||||
model.mirror = mirrorMode.value
|
||||
model.single = singleMode.value
|
||||
model.mouseMirror = mouseMirror.value
|
||||
|
||||
window.visible = true
|
||||
window.passThrough = penetrable.value
|
||||
window.alwaysOnTop = alwaysOnTop.value
|
||||
window.scale = scale.value
|
||||
window.opacity = opacity.value
|
||||
|
||||
migrated.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
visible,
|
||||
mirrorMode,
|
||||
singleMode,
|
||||
mouseMirror,
|
||||
penetrable,
|
||||
alwaysOnTop,
|
||||
scale,
|
||||
opacity,
|
||||
migrated,
|
||||
model,
|
||||
window,
|
||||
init,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,12 +1,94 @@
|
||||
import type { Theme } from '@tauri-apps/api/window'
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getLocale } from 'tauri-plugin-locale-api'
|
||||
import { reactive, ref } from 'vue'
|
||||
|
||||
import { LANGUAGE } from '@/constants'
|
||||
|
||||
export type Language = typeof LANGUAGE[keyof typeof LANGUAGE]
|
||||
|
||||
export interface GeneralStore {
|
||||
app: {
|
||||
autostart: boolean
|
||||
taskbarVisible: boolean
|
||||
}
|
||||
appearance: {
|
||||
theme: 'auto' | Theme
|
||||
isDark: boolean
|
||||
language?: Language
|
||||
}
|
||||
update: {
|
||||
autoCheck: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export const useGeneralStore = defineStore('general', () => {
|
||||
/* ------------ 废弃字段(后续删除) ------------ */
|
||||
|
||||
/** @deprecated 请使用 `update.autoCheck` */
|
||||
const autoCheckUpdate = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `app.autostart` */
|
||||
const autostart = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `app.taskbarVisible` */
|
||||
const taskbarVisibility = ref(false)
|
||||
|
||||
/** @deprecated 请使用 `appearance.theme` */
|
||||
const theme = ref<'auto' | Theme>('auto')
|
||||
|
||||
/** @deprecated 请使用 `appearance.isDark` */
|
||||
const isDark = ref(false)
|
||||
|
||||
/** @deprecated 用于标识数据是否已迁移,后续版本将删除 */
|
||||
const migrated = ref(false)
|
||||
|
||||
const app = reactive<GeneralStore['app']>({
|
||||
autostart: false,
|
||||
taskbarVisible: false,
|
||||
})
|
||||
|
||||
const appearance = reactive<GeneralStore['appearance']>({
|
||||
theme: 'auto',
|
||||
isDark: false,
|
||||
})
|
||||
|
||||
const update = reactive<GeneralStore['update']>({
|
||||
autoCheck: false,
|
||||
})
|
||||
|
||||
const getLanguage = async () => {
|
||||
const locale = await getLocale<Language>()
|
||||
|
||||
if (Object.values(LANGUAGE).includes(locale)) {
|
||||
return locale
|
||||
}
|
||||
|
||||
return LANGUAGE.EN_US
|
||||
}
|
||||
|
||||
const init = async () => {
|
||||
appearance.language ??= await getLanguage()
|
||||
|
||||
if (migrated.value) return
|
||||
|
||||
app.autostart = autostart.value
|
||||
app.taskbarVisible = taskbarVisibility.value
|
||||
|
||||
appearance.theme = theme.value
|
||||
appearance.isDark = isDark.value
|
||||
|
||||
update.autoCheck = autoCheckUpdate.value
|
||||
|
||||
migrated.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
autoCheckUpdate,
|
||||
autostart,
|
||||
migrated,
|
||||
app,
|
||||
appearance,
|
||||
update,
|
||||
init,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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 { 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,41 +37,33 @@ 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>>({})
|
||||
|
||||
const init = async () => {
|
||||
const presetModelsPath = await resolveResource('assets/models')
|
||||
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 (const mode of modes) {
|
||||
models.value.push({
|
||||
mode,
|
||||
id: nanoid(),
|
||||
isPreset: true,
|
||||
path: join(presetModelsPath, mode),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
models.value = models.value.map((item) => {
|
||||
const { isPreset, mode } = item
|
||||
const modes: ModelMode[] = ['gamepad', 'keyboard', 'standard']
|
||||
|
||||
if (!isPreset) return item
|
||||
for (const mode of modes) {
|
||||
const matched = find(presetModels, { mode })
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: join(presetModelsPath, mode),
|
||||
}
|
||||
nextModels.unshift({
|
||||
id: matched?.id ?? nanoid(),
|
||||
mode,
|
||||
isPreset: true,
|
||||
path: join(modelsPath, mode),
|
||||
})
|
||||
}
|
||||
|
||||
const matched = models.value.find(item => item.id === currentModel.value?.id)
|
||||
const matched = find(nextModels, { id: currentModel.value?.id })
|
||||
|
||||
if (matched) {
|
||||
return currentModel.value = matched
|
||||
}
|
||||
currentModel.value = matched ?? nextModels[0]
|
||||
|
||||
currentModel.value = models.value[0]
|
||||
models.value = nextModels
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -78,6 +71,13 @@ export const useModelStore = defineStore('model', () => {
|
||||
currentModel,
|
||||
motions,
|
||||
expressions,
|
||||
supportKeys,
|
||||
pressedKeys,
|
||||
init,
|
||||
}
|
||||
}, {
|
||||
tauri: {
|
||||
filterKeys: ['models', 'currentModel'],
|
||||
filterKeysStrategy: 'pick',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -3,3 +3,7 @@ export function isImage(value: string) {
|
||||
|
||||
return regex.test(value)
|
||||
}
|
||||
|
||||
export function inBetween(value: number, minimum: number, maximum: number) {
|
||||
return value >= minimum && value <= maximum
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { ModelSize } from '@/composables/useModel'
|
||||
import type { Cubism4InternalModel } from 'pixi-live2d-display'
|
||||
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { readDir, readTextFile } from '@tauri-apps/plugin-fs'
|
||||
import JSON5 from 'json5'
|
||||
import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'
|
||||
import { Application, Ticker } from 'pixi.js'
|
||||
|
||||
import { join } from './path'
|
||||
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
Live2DModel.registerTicker(Ticker)
|
||||
|
||||
class Live2d {
|
||||
private app: Application | null = null
|
||||
public model: Live2DModel | null = null
|
||||
private modelWidth = 0
|
||||
private modelHeight = 0
|
||||
|
||||
constructor() { }
|
||||
|
||||
@@ -40,12 +42,12 @@ class Live2d {
|
||||
const modelFile = files.find(file => file.name.endsWith('.model3.json'))
|
||||
|
||||
if (!modelFile) {
|
||||
throw new Error('未找到模型主配置文件,请确认模型文件是否完整。')
|
||||
throw new Error(i18n.global.t('utils.live2d.hints.notFound'))
|
||||
}
|
||||
|
||||
const modelPath = join(path, modelFile.name)
|
||||
|
||||
const modelJSON = JSON.parse(await readTextFile(modelPath))
|
||||
const modelJSON = JSON5.parse(await readTextFile(modelPath))
|
||||
|
||||
const modelSettings = new Cubism4ModelSettings({
|
||||
...modelJSON,
|
||||
@@ -58,12 +60,9 @@ class Live2d {
|
||||
|
||||
this.model = await Live2DModel.from(modelSettings)
|
||||
|
||||
const { width, height } = this.model
|
||||
this.modelWidth = width
|
||||
this.modelHeight = height
|
||||
|
||||
this.app?.stage.addChild(this.model)
|
||||
|
||||
const { width, height } = this.model
|
||||
const { motions, expressions } = modelSettings
|
||||
|
||||
return {
|
||||
@@ -75,14 +74,20 @@ class Live2d {
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.model?.destroy()
|
||||
}
|
||||
|
||||
public fitModel() {
|
||||
if (!this.model) return
|
||||
|
||||
const scaleX = innerWidth / this.modelWidth
|
||||
const scaleY = innerHeight / this.modelHeight
|
||||
this.model?.destroy()
|
||||
|
||||
this.model = null
|
||||
}
|
||||
|
||||
public resizeModel(modelSize: ModelSize) {
|
||||
if (!this.model) return
|
||||
|
||||
const { width, height } = modelSize
|
||||
|
||||
const scaleX = innerWidth / width
|
||||
const scaleY = innerHeight / height
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
this.model.scale.set(scale)
|
||||
@@ -118,7 +123,7 @@ class Live2d {
|
||||
}
|
||||
}
|
||||
|
||||
public setParameterValue(id: string, value: number) {
|
||||
public setParameterValue(id: string, value: number | boolean) {
|
||||
const coreModel = this.getCoreModel()
|
||||
|
||||
return coreModel?.setParameterValueById?.(id, Number(value))
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import {
|
||||
cursorPosition,
|
||||
monitorFromPoint,
|
||||
} from '@tauri-apps/api/window'
|
||||
import type { PhysicalPosition } from '@tauri-apps/api/window'
|
||||
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { cursorPosition, monitorFromPoint } from '@tauri-apps/api/window'
|
||||
|
||||
export async function getCursorMonitor(cursorPoint?: PhysicalPosition) {
|
||||
cursorPoint ??= await cursorPosition()
|
||||
|
||||
export async function getCursorMonitor() {
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
const scaleFactor = await appWindow.scaleFactor()
|
||||
|
||||
const point = await cursorPosition()
|
||||
|
||||
const { x, y } = point.toLogical(scaleFactor)
|
||||
const { x, y } = cursorPoint.toLogical(scaleFactor)
|
||||
|
||||
const monitor = await monitorFromPoint(x, y)
|
||||
|
||||
if (!monitor) return
|
||||
|
||||
return {
|
||||
...monitor,
|
||||
cursorPosition: point,
|
||||
}
|
||||
return monitor
|
||||
}
|
||||
|
||||
@@ -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
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||