Repository: ayangweb/BongoCat
Branch: master
Commit: d9fb19aa2f45
Files: 134
Total size: 181.2 KB
Directory structure:
gitextract__za4gjq7/
├── .commitlintrc
├── .github/
│ ├── CONTRIBUTING.md
│ ├── DOWNLOAD_GUIDE.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ └── feature_request.yml
│ └── workflows/
│ ├── release.yml
│ ├── sync-to-gitee.yml
│ └── upgradelink.yml
├── .gitignore
├── .release-it.ts
├── .vscode/
│ ├── extensions.json
│ └── settings.json
├── Cargo.toml
├── LICENSE
├── README.md
├── eslint.config.ts
├── index.html
├── package.json
├── scripts/
│ ├── buildIcon.ts
│ └── release.ts
├── src/
│ ├── App.vue
│ ├── assets/
│ │ └── css/
│ │ └── global.scss
│ ├── components/
│ │ ├── pro-list/
│ │ │ └── index.vue
│ │ ├── pro-list-item/
│ │ │ └── index.vue
│ │ ├── pro-shortcut/
│ │ │ └── index.vue
│ │ └── update-app/
│ │ └── index.vue
│ ├── composables/
│ │ ├── useDevice.ts
│ │ ├── useGamepad.ts
│ │ ├── useModel.ts
│ │ ├── useSharedMenu.ts
│ │ ├── useTauriListen.ts
│ │ ├── useTauriShortcut.ts
│ │ ├── useThemeVars.ts
│ │ ├── useTray.ts
│ │ ├── useWindowPosition.ts
│ │ └── useWindowState.ts
│ ├── constants/
│ │ └── index.ts
│ ├── locales/
│ │ ├── en-US.json
│ │ ├── index.ts
│ │ ├── pt-BR.json
│ │ ├── vi-VN.json
│ │ └── zh-CN.json
│ ├── main.ts
│ ├── pages/
│ │ ├── main/
│ │ │ └── index.vue
│ │ └── preference/
│ │ ├── components/
│ │ │ ├── about/
│ │ │ │ └── index.vue
│ │ │ ├── cat/
│ │ │ │ ├── components/
│ │ │ │ │ └── position/
│ │ │ │ │ └── index.vue
│ │ │ │ └── index.vue
│ │ │ ├── general/
│ │ │ │ ├── components/
│ │ │ │ │ ├── macos-permissions/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── theme-mode/
│ │ │ │ │ └── index.vue
│ │ │ │ └── index.vue
│ │ │ ├── model/
│ │ │ │ ├── components/
│ │ │ │ │ ├── float-menu/
│ │ │ │ │ │ └── index.vue
│ │ │ │ │ └── upload/
│ │ │ │ │ └── index.vue
│ │ │ │ └── index.vue
│ │ │ └── shortcut/
│ │ │ └── index.vue
│ │ └── index.vue
│ ├── plugins/
│ │ └── window.ts
│ ├── router/
│ │ └── index.ts
│ ├── stores/
│ │ ├── app.ts
│ │ ├── cat.ts
│ │ ├── general.ts
│ │ ├── model.ts
│ │ └── shortcut.ts
│ ├── utils/
│ │ ├── is.ts
│ │ ├── keyboard.ts
│ │ ├── live2d.ts
│ │ ├── monitor.ts
│ │ ├── path.ts
│ │ ├── platform.ts
│ │ └── shared.ts
│ └── vite-env.d.ts
├── src-tauri/
│ ├── .gitignore
│ ├── BongoCat.desktop
│ ├── Cargo.toml
│ ├── assets/
│ │ └── models/
│ │ ├── gamepad/
│ │ │ ├── cat.model3.json
│ │ │ ├── demomodel3.cdi3.json
│ │ │ ├── demomodel3.moc3
│ │ │ ├── exp_1.exp3.json
│ │ │ ├── exp_2.exp3.json
│ │ │ ├── live2d_expression0.exp3.json
│ │ │ ├── live2d_expression1.exp3.json
│ │ │ ├── live2d_expression2.exp3.json
│ │ │ ├── live2d_motion1.flac
│ │ │ ├── live2d_motion1.motion3.json
│ │ │ └── live2d_motion2.motion3.json
│ │ ├── keyboard/
│ │ │ ├── cat.model3.json
│ │ │ ├── demomodel2.cdi3.json
│ │ │ ├── demomodel2.moc3
│ │ │ ├── exp_1.exp3.json
│ │ │ ├── exp_2.exp3.json
│ │ │ ├── live2d_expression0.exp3.json
│ │ │ ├── live2d_expression1.exp3.json
│ │ │ ├── live2d_expression2.exp3.json
│ │ │ ├── live2d_motion1.flac
│ │ │ ├── live2d_motion1.motion3.json
│ │ │ └── live2d_motion2.motion3.json
│ │ └── standard/
│ │ ├── cat.model3.json
│ │ ├── demomodel.cdi3.json
│ │ ├── demomodel.moc3
│ │ ├── exp_1.exp3.json
│ │ ├── exp_2.exp3.json
│ │ ├── live2d_expression0.exp3.json
│ │ ├── live2d_expression1.exp3.json
│ │ ├── live2d_expression2.exp3.json
│ │ ├── live2d_motion1.flac
│ │ ├── live2d_motion1.motion3.json
│ │ └── live2d_motion2.motion3.json
│ ├── build.rs
│ ├── capabilities/
│ │ └── default.json
│ ├── src/
│ │ ├── core/
│ │ │ ├── device.rs
│ │ │ ├── gamepad.rs
│ │ │ ├── mod.rs
│ │ │ ├── prevent_default.rs
│ │ │ └── setup/
│ │ │ ├── common.rs
│ │ │ ├── macos.rs
│ │ │ └── mod.rs
│ │ ├── lib.rs
│ │ ├── main.rs
│ │ ├── plugins/
│ │ │ └── window/
│ │ │ ├── Cargo.toml
│ │ │ ├── build.rs
│ │ │ ├── permissions/
│ │ │ │ └── default.toml
│ │ │ └── src/
│ │ │ ├── commands/
│ │ │ │ ├── common.rs
│ │ │ │ ├── macos.rs
│ │ │ │ └── mod.rs
│ │ │ └── lib.rs
│ │ └── utils/
│ │ ├── fs_extra.rs
│ │ └── mod.rs
│ ├── tauri.conf.json
│ ├── tauri.linux.conf.json
│ ├── tauri.macos.conf.json
│ └── tauri.windows.conf.json
├── tsconfig.json
├── tsconfig.node.json
├── uno.config.ts
└── vite.config.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .commitlintrc
================================================
{
"extends": "@commitlint/config-conventional"
}
================================================
FILE: .github/CONTRIBUTING.md
================================================
# 贡献指南
非常感谢您对 BongoCat 的关注和贡献!在您提交贡献之前,请先花一些时间阅读以下指南,以确保您的贡献能够顺利进行。
## 透明的开发
所有工作都在 GitHub 上公开进行。无论是核心团队成员还是外部贡献者的 Pull Request,都需要经过相同的 review 流程。
## 提交 Issue
我们使用 [Github Issues](https://github.com/ayangweb/BongoCat/issues) 进行 Bug 报告和新 Feature 建议。在提交 Issue 之前,请确保已经搜索过类似的问题,因为它们可能已经得到解答或正在被修复。对于 Bug 报告,请包含可用于重现问题的完整步骤。对于新 Feature 建议,请指出你想要的更改以及期望的行为。
## 提交 Pull Request
### 共建流程
- 认领 issue:在 Github 建立 Issue 并认领(或直接认领已有 Issue),告知大家自己正在修复,避免重复工作。
- 项目开发:在完成准备工作后,进行 Bug 修复或功能开发。
- 提交 PR。
### 准备工作
- [Rust](https://v2.tauri.app/start/prerequisites/): 请自行根据官网步骤安装 rust 环境。
- [Node.js](https://nodejs.org/en/): 用于运行项目。
- [Pnpm](https://pnpm.io/):本项目使用 Pnpm 进行包管理。
### 下载依赖
```shell
pnpm install
```
### 启动应用
```shell
pnpm tauri dev
```
### 打包应用
> 如果需要打包后进行调试,请在以下命令后面加上 `--debug`
```shell
pnpm tauri build
```
## Commit 指南
Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)。
### Commit 类型
以下是 commit 类型列表:
- feat: 新特性或功能
- fix: 缺陷修复
- docs: 文档更新
- style: 代码风格更新
- refactor: 代码重构,不引入新功能和缺陷修复
- perf: 性能优化
- chore: 其他提交
期待您的参与,让我们一起使 BongoCat 变得更好!
================================================
FILE: .github/DOWNLOAD_GUIDE.md
================================================
# 下载指南
## 系统要求
- macOS 12 或更高版本。
- Windows 10 或更高版本。
- Linux 带有 X11 环境。
## macOS
### 手动下载
- Apple Silicon:下载 `BongoCat_aarch64.dmg`
- Intel Chip:下载 `BongoCat_x64.dmg`
### Homebrew 下载
1. 添加 BongoCat 的 tap 源:
```bash
brew tap ayangweb/BongoCat
```
2. 安装:
```bash
brew install --no-quarantine bongo-cat
```
3. 更新:
```bash
brew upgrade bongo-cat
```
4. 卸载:
```bash
brew uninstall --cask bongo-cat
brew untap ayangweb/BongoCat
```
## Windows
- 64 位系统:下载 `BongoCat_x64.exe`
- 32 位系统:下载 `BongoCat_x86.exe`
- ARM64 架构:下载 `BongoCat_arm64.exe`
## Linux(X11)
### 手动下载
- 64 位系统:
- Debian / Ubuntu:下载 `BongoCat_amd64.deb`
- Fedora / RHEL:下载 `BongoCat_x86_64.rpm`
- 通用版本:下载 `BongoCat_amd64.AppImage`
- ARM64 架构:
- Debian / Ubuntu:下载 `BongoCat_arm64.deb`
- Fedora / RHEL:下载 `BongoCat_aarch64.rpm`
- 通用版本:下载 `BongoCat_aarch64.AppImage`
### AUR 下载
- Manjaro / ArchLinux: `yay -S bongo-cat`
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐞 Bug 报告
title: '[bug] '
description: 报告一个 Bug
labels: bug
body:
- type: markdown
attributes:
value: |
## 温馨提示
1. 请先查阅现有的 [issues](https://github.com/ayangweb/BongoCat/issues)。
2. 请确保你使用的是[最新版本](https://github.com/ayangweb/BongoCat/releases/latest)。
3. 请确保该问题不是由其他软件引起的。
4. 请始终保持友好与尊重,感谢你的理解与配合。
- type: textarea
id: description
attributes:
label: 描述 Bug
description: 请详细描述 Bug 并提供截图或视频以帮助我们更好地理解问题。
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: 重现步骤
description: 请详细列出重现问题的步骤,并附带截图或视频。
- type: textarea
id: expected-behavior
attributes:
label: 预期行为
description: 请描述你期望发生的行为。
- type: textarea
id: info
attributes:
render: text
label: 软件信息
description: 请前往偏好设置窗口的「关于 > 关于软件 > 软件信息」复制软件信息。
validations:
required: true
- type: textarea
id: context
attributes:
label: 附加信息
description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 💡 功能请求
title: '[feat] '
description: 提出一个想法
labels: feature request
body:
- type: textarea
id: problem
attributes:
label: 描述问题
description: 请清晰地描述此功能将解决的具体问题。
validations:
required: true
- type: textarea
id: solution
attributes:
label: 描述您希望的解决方案
description: 请清晰地描述您期望的变更或改进。
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: 考虑的替代方案
description: 提供您考虑过的其他替代解决方案。
- type: textarea
id: context
attributes:
label: 附加信息
description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。
================================================
FILE: .github/workflows/release.yml
================================================
name: BongoCat Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set output
id: vars
run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Generate changelog
id: create_release
run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }}
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
build-app:
needs: create-release
permissions:
contents: write
strategy:
fail-fast: false
matrix:
include:
- platform: macos-latest
target: aarch64-apple-darwin
- platform: macos-latest
target: x86_64-apple-darwin
- platform: windows-latest
target: x86_64-pc-windows-msvc
- platform: windows-latest
target: i686-pc-windows-msvc
- platform: windows-latest
target: aarch64-pc-windows-msvc
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
- platform: ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup node
uses: actions/setup-node@v4
with:
node-version: 20
- uses: pnpm/action-setup@v3
with:
version: latest
- name: Install rust target
run: rustup target add ${{ matrix.target }}
- name: Install dependencies (ubuntu only)
if: startsWith(matrix.platform, 'ubuntu')
run: |
sudo apt-get update
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
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: target
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version: 20
cache: pnpm
- name: Install front-end dependencies
run: pnpm install --frozen-lockfile
- name: Build the app
uses: tauri-apps/tauri-action@v0
env:
CI: false
PLATFORM: ${{ matrix.platform }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
tagName: ${{ github.ref_name }}
releaseName: BongoCat ${{ needs.create-release.outputs.APP_VERSION }}
releaseBody: ''
releaseDraft: true
prerelease: false
args: --target ${{ matrix.target }}
================================================
FILE: .github/workflows/sync-to-gitee.yml
================================================
name: Sync Github Repos To Gitee
on:
push:
branches:
- master
jobs:
repo-sync:
runs-on: ubuntu-latest
steps:
- name: Sync Github Repos To Gitee
uses: Yikun/hub-mirror-action@master
with:
src: github/ayangweb
dst: gitee/ayangweb
dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
dst_token: ${{ secrets.GITEE_TOKEN }}
static_list: BongoCat
force_update: true
================================================
FILE: .github/workflows/upgradelink.yml
================================================
name: Upload Release to UpgradeLink
on:
release:
types: [published]
workflow_dispatch:
jobs:
upgradeLink-upload:
permissions:
contents: write
runs-on: ubuntu-latest
steps:
- name: Send a request to UpgradeLink
uses: toolsetlink/upgradelink-action@v5
with:
source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json'
access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }}
github-token: ${{ secrets.RELEASE_TOKEN }}
================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
target
# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
================================================
FILE: .release-it.ts
================================================
/* eslint-disable no-template-curly-in-string */
import type { Config } from 'release-it'
export default {
git: {
commitMessage: 'v${version}',
tagName: 'v${version}',
},
npm: {
publish: false,
},
hooks: {
'after:bump': 'tsx scripts/release.ts',
},
} satisfies Config
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"tauri-apps.tauri-vscode",
"rust-lang.rust-analyzer",
"antfu.unocss",
"dbaeumer.vscode-eslint"
]
}
================================================
FILE: .vscode/settings.json
================================================
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"eslint.format.enable": true,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
"typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "./node_modules/typescript/lib",
"i18n-ally.localesPaths": ["src/locales"],
"i18n-ally.keystyle": "nested",
"i18n-ally.displayLanguage": "zh-CN"
}
================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "2"
members = [ "src-tauri" ]
[profile.release]
strip = true
opt-level = 3
codegen-units = 1
panic = "abort"
debug-assertions = false
overflow-checks = false
lto = true
[workspace.dependencies]
tauri = "2"
serde = "1"
serde_json = "1"
fs_extra = "1"
tauri-plugin = { version = "2", features = [ "build" ] }
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-plugin-custom-window = { path = "./src-tauri/src/plugins/window" }
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2025 ayangweb
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================

| macOS | Windows | Linux(x11) |
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|  |  |  |
## 赞助商
## 开发背景
本项目的灵感来源于 [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(x11),让更多的用户都能与这只可爱的猫咪互动!
## 下载
- [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1)
- [GitHub Releases](https://github.com/ayangweb/BongoCat/releases)
不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。
## 功能介绍
- 适配 macOS、Windows 和 Linux(x11)。
- 根据键盘、鼠标或手柄的操作,同步对应的动作。
- 支持导入自定义模型,自由打造专属猫咪形象。
- 完全开源,代码公开透明,绝不收集任何用户数据。
- 支持离线运行,无需联网,保护用户隐私。
## 模型转换
如果你想将 Bongo-Cat-Mver 应用中的模型转换为兼容 BongoCat 的格式,可以使用以下工具:
🔗 [在线转换](https://bongocat.vteamer.cc)
## 更多模型
你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:
📦 [Awesome-BongoCat](https://github.com/ayangweb/Awesome-BongoCat)
## 社区交流
| QQ 群 1 |
QQ 群 2 |
|
|
## 赞赏
每一份认可都值得被珍视!赞赏随缘,心意无价,谢谢你的支持 ❤️
## 贡献指南
感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。
## 历史星标
================================================
FILE: eslint.config.ts
================================================
import antfu from '@antfu/eslint-config'
export default antfu({
formatters: true,
unocss: true,
rules: {
'antfu/if-newline': 'off',
'style/brace-style': ['error', '1tbs'],
'ts/no-use-before-define': 'off',
'unused-imports/no-unused-imports': 'error',
'perfectionist/sort-imports': 'off',
'import/order': [
'error',
{
'newlines-between': 'always',
'groups': ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
'alphabetize': {
order: 'asc',
caseInsensitive: true,
},
},
],
'vue/attributes-order': ['error', { alphabetical: true }],
'vue/max-attributes-per-line': 'error',
},
ignores: ['**/*.toml'],
})
================================================
FILE: index.html
================================================
BongoCat
================================================
FILE: package.json
================================================
{
"name": "bongo-cat",
"type": "module",
"version": "0.9.0",
"private": true,
"author": {
"name": "ayangweb",
"email": "ayangweb@foxmail.com"
},
"scripts": {
"dev": "run-s build:icon dev:vite",
"build": "run-s build:*",
"dev:vite": "vite",
"build:vite": "vite build",
"build:icon": "tsx scripts/buildIcon.ts",
"preview": "vite preview",
"tauri": "tauri",
"lint": "eslint --fix src",
"preinstall": "npx only-allow pnpm",
"prepare": "simple-git-hooks",
"release": "release-it"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-autostart": "~2.3.0",
"@tauri-apps/plugin-clipboard-manager": "~2.2.2",
"@tauri-apps/plugin-dialog": "~2.2.2",
"@tauri-apps/plugin-fs": "~2.3.0",
"@tauri-apps/plugin-global-shortcut": "~2.2.1",
"@tauri-apps/plugin-log": "~2.3.1",
"@tauri-apps/plugin-opener": "~2.2.7",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-updater": "~2.7.1",
"@tauri-store/pinia": "^3.7.0",
"@vueuse/core": "^13.3.0",
"ant-design-vue": "^4.2.6",
"dayjs": "^1.11.13",
"es-toolkit": "^1.38.0",
"is-url": "^1.2.4",
"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"
},
"devDependencies": {
"@antfu/eslint-config": "^4.13.3",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@iconify-json/iconamoon": "^1.2.2",
"@iconify-json/solar": "^1.2.2",
"@tauri-apps/cli": "^2.5.0",
"@types/is-url": "^1.2.32",
"@types/node": "^22.15.29",
"@unocss/eslint-plugin": "^66.1.3",
"@vitejs/plugin-vue": "^5.2.4",
"eslint": "^9.28.0",
"eslint-plugin-format": "^1.0.1",
"lint-staged": "^15.5.2",
"npm-run-all": "^4.1.5",
"release-it": "^18.1.2",
"sass": "^1.89.1",
"simple-git-hooks": "^2.13.0",
"tsx": "^4.19.4",
"typescript": "~5.6.3",
"unocss": "66.1.0-beta.7",
"vite": "^6.3.5"
},
"simple-git-hooks": {
"commit-msg": "npx --no-install commitlint -e",
"pre-commit": "npx lint-staged"
},
"lint-staged": {
"*": "eslint --fix"
}
}
================================================
FILE: scripts/buildIcon.ts
================================================
import { execSync } from 'node:child_process'
import { env, platform } from 'node:process'
(() => {
const isMac = env.PLATFORM?.startsWith('macos') ?? platform === 'darwin'
const logoName = isMac ? 'logo-mac' : 'logo'
const command = `tauri icon src-tauri/assets/${logoName}.png`
execSync(command, { stdio: 'inherit' })
})()
================================================
FILE: scripts/release.ts
================================================
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { name, version } from '../package.json'
const __dirname = dirname(fileURLToPath(import.meta.url));
(() => {
const tomlPath = resolve(__dirname, '..', 'src-tauri', 'Cargo.toml')
const lockPath = resolve(__dirname, '..', 'Cargo.lock')
for (const path of [tomlPath, lockPath]) {
let content = readFileSync(path, 'utf-8')
const regexp = new RegExp(
`(name\\s*=\\s*"${name}"\\s*version\\s*=\\s*)"(\\d+\\.\\d+\\.\\d+(-\\w+\\.\\d+)?)"`,
)
content = content.replace(regexp, `$1"${version}"`)
writeFileSync(path, content)
}
})()
================================================
FILE: src/App.vue
================================================
================================================
FILE: src/assets/css/global.scss
================================================
html {
--uno: select-none overscroll-none antialiased;
color-scheme: light;
body {
--uno: transition-opacity-300;
}
&.dark {
color-scheme: dark;
}
img {
-webkit-user-drag: none;
}
button {
outline: none !important;
}
.ant-card {
.ant-card-actions {
> li {
--uno: flex items-center justify-center;
> span {
--uno: inline-flex items-center justify-center min-w-unset;
}
}
}
}
}
================================================
FILE: src/components/pro-list/index.vue
================================================
{{ title }}
================================================
FILE: src/components/pro-list-item/index.vue
================================================
{{ title }}
{{ description }}
================================================
FILE: src/components/pro-shortcut/index.vue
================================================
{{ isFocusing ? $t('components.proShortcut.hints.pressRecordShortcut') : $t('components.proShortcut.hints.clickRecordShortcut') }}
{{ map(pressedKeys, 'symbol').join(' ') }}
================================================
FILE: src/components/update-app/index.vue
================================================
{{ state.downloading ? downloadProgress : $t('components.updateApp.buttons.updateNow') }}
{{ $t('components.updateApp.labels.updateVersion') }}
{{ state.update?.currentVersion }} 👉
{{ state.update?.version }}
{{ $t('components.updateApp.labels.updateTime') }}
{{ state.update?.date }}
{{ $t('components.updateApp.labels.changelog') }}
================================================
FILE: src/composables/useDevice.ts
================================================
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { cursorPosition } from '@tauri-apps/api/window'
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 { inBetween } from '@/utils/is'
import { isWindows } from '@/utils/platform'
interface MouseButtonEvent {
kind: 'MousePress' | 'MouseRelease'
value: string
}
export interface CursorPoint {
x: number
y: number
}
interface MouseMoveEvent {
kind: 'MouseMove'
value: CursorPoint
}
interface KeyboardEvent {
kind: 'KeyboardPress' | 'KeyboardRelease'
value: string
}
type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent
export function useDevice() {
const modelStore = useModelStore()
const releaseTimers = new Map()
const catStore = useCatStore()
const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()
const startListening = () => {
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
}
const getSupportedKey = (key: string) => {
let nextKey = key
const unsupportedKey = !modelStore.supportKeys[nextKey]
if (key.startsWith('F') && unsupportedKey) {
nextKey = key.replace(/F(\d+)/, 'Fn')
}
for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
if (key.startsWith(item) && unsupportedKey) {
const regex = new RegExp(`^(${item}).*`)
nextKey = key.replace(regex, '$1')
}
}
return nextKey
}
const 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)
}
}
}
const handleAutoRelease = (key: string, delay = 100) => {
handlePress(key)
if (releaseTimers.has(key)) {
clearTimeout(releaseTimers.get(key))
}
const timer = setTimeout(() => {
handleRelease(key)
releaseTimers.delete(key)
}, delay)
releaseTimers.set(key, timer)
}
useTauriListen(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => {
const { kind, value } = payload
if (kind === 'KeyboardPress' || kind === 'KeyboardRelease') {
const nextValue = getSupportedKey(value)
if (!nextValue) return
if (nextValue === 'CapsLock') {
return handleAutoRelease(nextValue)
}
if (kind === 'KeyboardPress') {
if (isWindows) {
const delay = catStore.model.autoReleaseDelay * 1000
return handleAutoRelease(nextValue, delay)
}
return handlePress(nextValue)
}
return handleRelease(nextValue)
}
switch (kind) {
case 'MousePress':
return handleMouseChange(value)
case 'MouseRelease':
return handleMouseChange(value, false)
case 'MouseMove':
return handleCursorMove()
}
})
return {
startListening,
}
}
================================================
FILE: src/composables/useGamepad.ts
================================================
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({
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(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,
}
}
================================================
FILE: src/composables/useModel.ts
================================================
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 { nth } from 'es-toolkit/compat'
import { ref } from 'vue'
import live2d from '../utils/live2d'
import { useCatStore } from '@/stores/cat'
import { useModelStore } from '@/stores/model'
import { getCursorMonitor } from '@/utils/monitor'
const appWindow = getCurrentWebviewWindow()
export interface ModelSize {
width: number
height: number
}
export function useModel() {
const modelStore = useModelStore()
const catStore = useCatStore()
const modelSize = ref()
async function handleLoad() {
try {
if (!modelStore.currentModel) return
const { path } = modelStore.currentModel
await resolveResource(path)
const { width, height, ...rest } = await live2d.load(path)
modelSize.value = { width, height }
handleResize()
Object.assign(modelStore, rest)
} catch (error) {
message.error(String(error))
}
}
function handleDestroy() {
live2d.destroy()
}
async function handleResize() {
if (!modelSize.value) return
live2d.resizeModel(modelSize.value)
const { width, height } = modelSize.value
if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) {
await appWindow.setSize(
new LogicalSize({
width: innerWidth,
height: Math.ceil(innerWidth * (height / width)),
}),
)
}
const size = await appWindow.size()
catStore.window.scale = round((size.width / width) * 100)
}
const handlePress = (key: string) => {
const path = modelStore.supportKeys[key]
if (!path) return
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
}
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 } = monitor
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)
if (isNil(min) || isNil(max)) continue
const isXAxis = id.endsWith('X')
const ratio = isXAxis ? xRatio : yRatio
let value = max - (ratio * (max - min))
if (isXAxis && catStore.model.mouseMirror) {
value *= -1
}
live2d.setParameterValue(id, value)
}
}
async function handleAxisChange(id: string, value: number) {
const { min, max } = live2d.getParameterRange(id)
live2d.setParameterValue(id, Math.max(min, value * max))
}
return {
modelSize,
handlePress,
handleRelease,
handleLoad,
handleDestroy,
handleResize,
handleKeyChange,
handleMouseChange,
handleMouseMove,
handleAxisChange,
}
}
================================================
FILE: src/composables/useSharedMenu.ts
================================================
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'
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}%`,
checked: catStore.window.scale === item,
action: () => {
catStore.window.scale = item
},
})
})
if (!options.includes(catStore.window.scale)) {
items.unshift(CheckMenuItem.new({
text: `${catStore.window.scale}%`,
checked: true,
enabled: false,
}))
}
return Promise.all(items)
}
const getOpacityMenuItems = async () => {
const options = range(25, 101, 25)
const items = options.map((item) => {
return CheckMenuItem.new({
text: `${item}%`,
checked: catStore.window.opacity === item,
action: () => {
catStore.window.opacity = item
},
})
})
if (!options.includes(catStore.window.opacity)) {
items.unshift(CheckMenuItem.new({
text: `${catStore.window.opacity}%`,
checked: true,
enabled: false,
}))
}
return Promise.all(items)
}
const getSharedMenu = async () => {
return await Promise.all([
MenuItem.new({
text: t('composables.useSharedMenu.labels.preference'),
accelerator: isMac ? 'Cmd+,' : '',
action: () => showWindow('preference'),
}),
MenuItem.new({
text: catStore.window.visible ? t('composables.useSharedMenu.labels.hideCat') : t('composables.useSharedMenu.labels.showCat'),
action: () => {
catStore.window.visible = !catStore.window.visible
},
}),
PredefinedMenuItem.new({ item: 'Separator' }),
CheckMenuItem.new({
text: t('composables.useSharedMenu.labels.passThrough'),
checked: catStore.window.passThrough,
action: () => {
catStore.window.passThrough = !catStore.window.passThrough
},
}),
Submenu.new({
text: t('composables.useSharedMenu.labels.windowSize'),
items: await getScaleMenuItems(),
}),
Submenu.new({
text: t('composables.useSharedMenu.labels.opacity'),
items: await getOpacityMenuItems(),
}),
])
}
return {
getSharedMenu,
}
}
================================================
FILE: src/composables/useTauriListen.ts
================================================
import { listen } from '@tauri-apps/api/event'
import { noop } from '@vueuse/core'
import { onMounted, onUnmounted, ref } from 'vue'
export function useTauriListen(...args: Parameters>) {
const unlisten = ref(noop)
onMounted(async () => {
unlisten.value = await listen(...args)
})
onUnmounted(() => {
unlisten.value()
})
}
================================================
FILE: src/composables/useTauriShortcut.ts
================================================
import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut'
import type { Ref } from 'vue'
import {
isRegistered,
register,
unregister,
} from '@tauri-apps/plugin-global-shortcut'
import { ref, watch } from 'vue'
export function useTauriShortcut(shortcut: Ref, callback: ShortcutHandler) {
const oldShortcut = ref(shortcut.value)
watch(shortcut, async (value) => {
if (oldShortcut.value) {
const registered = await isRegistered(oldShortcut.value)
if (registered) {
await unregister(oldShortcut.value)
}
}
if (!value) return
await register(value, (event) => {
if (event.state === 'Released') return
callback(event)
})
oldShortcut.value = value
}, { immediate: true })
}
================================================
FILE: src/composables/useThemeVars.ts
================================================
import { theme } from 'ant-design-vue'
import { kebabCase } from 'es-toolkit'
export function useThemeVars() {
const { defaultAlgorithm, darkAlgorithm, defaultConfig } = theme
const generateColorVars = () => {
const { token } = defaultConfig
const colors = [
defaultAlgorithm(token),
darkAlgorithm(token),
]
for (const [index, item] of colors.entries()) {
const isDark = index !== 0
const vars: Record = {}
for (const [key, value] of Object.entries(item)) {
vars[`--ant-${kebabCase(key)}`] = value
}
const style = document.createElement('style')
style.dataset.theme = isDark ? 'dark' : 'light'
const selector = isDark ? 'html.dark' : ':root'
const values = Object.entries(vars).map(([key, value]) => `${key}: ${value};`)
style.innerHTML = `${selector}{\n${values.join('\n')}\n}`
document.head.appendChild(style)
}
}
return {
generateColorVars,
}
}
================================================
FILE: src/composables/useTray.ts
================================================
import type { TrayIconOptions } from '@tauri-apps/api/tray'
import { getName, getVersion } from '@tauri-apps/api/app'
import { emit } from '@tauri-apps/api/event'
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
import { resolveResource } from '@tauri-apps/api/path'
import { TrayIcon } from '@tauri-apps/api/tray'
import { openUrl } from '@tauri-apps/plugin-opener'
import { exit, relaunch } from '@tauri-apps/plugin-process'
import { watchDebounced } from '@vueuse/core'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { GITHUB_LINK, LISTEN_KEY } from '../constants'
import { showWindow } from '../plugins/window'
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.window.visible, () => catStore.window.passThrough, () => generalStore.appearance.language], () => {
updateTrayMenu()
})
watchDebounced([() => catStore.window.scale, () => catStore.window.opacity], () => {
updateTrayMenu()
}, { debounce: 200 })
const createTray = async () => {
const tray = await getTrayById()
if (tray) return
const appName = await getName()
const appVersion = await getVersion()
const menu = await getTrayMenu()
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: true,
menuOnLeftClick: true,
}
return TrayIcon.new(options)
}
const getTrayById = () => {
return TrayIcon.getById(TRAY_ID)
}
const getTrayMenu = async () => {
const appVersion = await getVersion()
const items = await Promise.all([
...await getSharedMenu(),
PredefinedMenuItem.new({ item: 'Separator' }),
MenuItem.new({
text: t('composables.useTray.checkUpdate'),
action: () => {
showWindow()
emit(LISTEN_KEY.UPDATE_APP)
},
}),
MenuItem.new({
text: t('composables.useTray.openSource'),
action: () => openUrl(GITHUB_LINK),
}),
PredefinedMenuItem.new({ item: 'Separator' }),
MenuItem.new({
text: `v${appVersion}`,
enabled: false,
}),
MenuItem.new({
text: t('composables.useTray.restartApp'),
action: relaunch,
}),
MenuItem.new({
text: t('composables.useTray.quitApp'),
accelerator: isMac ? 'Cmd+Q' : '',
action: () => exit(0),
}),
])
return Menu.new({ items })
}
const updateTrayMenu = async () => {
const tray = await getTrayById()
if (!tray) return
const menu = await getTrayMenu()
tray.setMenu(menu)
}
return {
createTray,
}
}
================================================
FILE: src/composables/useWindowPosition.ts
================================================
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,
}
}
================================================
FILE: src/composables/useWindowState.ts
================================================
import type { Event } from '@tauri-apps/api/event'
import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { availableMonitors } from '@tauri-apps/api/window'
import { isNumber } from 'es-toolkit/compat'
import { onMounted, ref } from 'vue'
import { useAppStore } from '@/stores/app'
export type WindowState = Record | undefined>
const appWindow = getCurrentWebviewWindow()
const { label } = appWindow
export function useWindowState() {
const appStore = useAppStore()
const isRestored = ref(false)
onMounted(() => {
appWindow.onMoved(onChange)
appWindow.onResized(onChange)
})
const onChange = async (event: Event) => {
const minimized = await appWindow.isMinimized()
if (minimized) return
appStore.windowState[label] ??= {}
Object.assign(appStore.windowState[label], event.payload)
}
const restoreState = async () => {
const { x, y, width, height } = appStore.windowState[label] ?? {}
if (isNumber(x) && isNumber(y)) {
const monitors = await availableMonitors()
const monitor = monitors.find((monitor) => {
const { position, size } = monitor
const inBoundsX = x >= position.x && x <= position.x + size.width
const inBoundsY = y >= position.y && y <= position.y + size.height
return inBoundsX && inBoundsY
})
if (monitor) {
await appWindow.setPosition(new PhysicalPosition(x, y))
}
}
if (width && height) {
await appWindow.setSize(new PhysicalSize(width, height))
}
isRestored.value = true
}
return {
isRestored,
restoreState,
}
}
================================================
FILE: src/constants/index.ts
================================================
export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'
export const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'
export const LISTEN_KEY = {
SHOW_WINDOW: 'show-window',
HIDE_WINDOW: 'hide-window',
DEVICE_CHANGED: 'device-changed',
UPDATE_APP: 'update-app',
GAMEPAD_CHANGED: 'gamepad-changed',
}
export const INVOKE_KEY = {
COPY_DIR: 'copy_dir',
START_DEVICE_LISTENING: 'start_device_listening',
START_GAMEPAD_LISTING: 'start_gamepad_listing',
STOP_GAMEPAD_LISTING: 'stop_gamepad_listing',
}
export const LANGUAGE = {
ZH_CN: 'zh-CN',
EN_US: 'en-US',
VI_VN: 'vi-VN',
PT_BR: 'pt-BR',
} as const
================================================
FILE: src/locales/en-US.json
================================================
{
"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."
}
}
}
}
================================================
FILE: src/locales/index.ts
================================================
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.ZH_CN]: antdZhCN,
[LANGUAGE.EN_US]: antdEnUS,
[LANGUAGE.VI_VN]: antdViVN,
[LANGUAGE.PT_BR]: antdPtBR,
}
return antdLanguage[language]
}
================================================
FILE: src/locales/pt-BR.json
================================================
{
"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."
}
}
}
}
================================================
FILE: src/locales/vi-VN.json
================================================
{
"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."
}
}
}
}
================================================
FILE: src/locales/zh-CN.json
================================================
{
"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": "未找到模型主配置文件,请确认模型文件是否完整。"
}
}
}
}
================================================
FILE: src/main.ts
================================================
import { createPlugin } from '@tauri-store/pinia'
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'
const pinia = createPinia()
pinia.use(createPlugin({ saveOnChange: true }))
createApp(App).use(router).use(pinia).use(i18n).mount('#app')
================================================
FILE: src/pages/main/index.vue
================================================
{{ $t('pages.main.hints.redrawing') }}
================================================
FILE: src/pages/preference/components/about/index.vue
================================================
{{ GITHUB_LINK }}
================================================
FILE: src/pages/preference/components/cat/components/position/index.vue
================================================
================================================
FILE: src/pages/preference/components/cat/index.vue
================================================
================================================
FILE: src/pages/preference/components/general/components/macos-permissions/index.vue
================================================
{{ $t('pages.preference.general.status.authorized') }}
{{ $t('pages.preference.general.status.authorize') }}
================================================
FILE: src/pages/preference/components/general/components/theme-mode/index.vue
================================================
================================================
FILE: src/pages/preference/components/general/index.vue
================================================
================================================
FILE: src/pages/preference/components/model/components/float-menu/index.vue
================================================
================================================
FILE: src/pages/preference/components/model/components/upload/index.vue
================================================
{{ $t('pages.preference.model.hints.clickOrDragToImport') }}
================================================
FILE: src/pages/preference/components/model/index.vue
================================================
================================================
FILE: src/pages/preference/components/shortcut/index.vue
================================================
================================================
FILE: src/pages/preference/index.vue
================================================
================================================
FILE: src/plugins/window.ts
================================================
import { invoke } from '@tauri-apps/api/core'
import { emit } from '@tauri-apps/api/event'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { LISTEN_KEY } from '../constants'
type WindowLabel = 'main' | 'preference'
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) {
if (label) {
emit(LISTEN_KEY.SHOW_WINDOW, label)
} else {
invoke(COMMAND.SHOW_WINDOW)
}
}
export function hideWindow(label?: WindowLabel) {
if (label) {
emit(LISTEN_KEY.HIDE_WINDOW, label)
} else {
invoke(COMMAND.HIDE_WINDOW)
}
}
export function setAlwaysOnTop(alwaysOnTop: boolean) {
invoke(COMMAND.SET_ALWAYS_ON_TOP, { alwaysOnTop })
}
export async function toggleWindowVisible(label?: WindowLabel) {
const appWindow = getCurrentWebviewWindow()
if (appWindow.label !== label) return
const visible = await appWindow.isVisible()
if (visible) {
return hideWindow(label)
}
return showWindow(label)
}
export async function setTaskbarVisibility(visible: boolean) {
invoke(COMMAND.SET_TASKBAR_VISIBILITY, { visible })
}
================================================
FILE: src/router/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router'
import { createRouter, createWebHashHistory } from 'vue-router'
import Main from '../pages/main/index.vue'
import Preference from '../pages/preference/index.vue'
const routes: Readonly = [
{
path: '/',
component: Main,
},
{
path: '/preference',
component: Preference,
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router
================================================
FILE: src/stores/app.ts
================================================
import type { WindowState } from '@/composables/useWindowState'
import { getName, getVersion } from '@tauri-apps/api/app'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'
export const useAppStore = defineStore('app', () => {
const name = ref('')
const version = ref('')
const windowState = reactive({})
const init = async () => {
name.value = await getName()
version.value = await getVersion()
}
return {
name,
version,
windowState,
init,
}
})
================================================
FILE: src/stores/cat.ts
================================================
import { defineStore } from 'pinia'
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', () => {
/* ------------ 废弃字段(后续删除) ------------ */
/** @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({
mirror: false,
single: false,
mouseMirror: false,
autoReleaseDelay: 3,
})
const window = reactive({
visible: true,
passThrough: false,
alwaysOnTop: false,
scale: 100,
opacity: 100,
radius: 0,
hideOnHover: false,
position: 'bottomRight',
})
const init = () => {
if (migrated.value) return
model.mirror = mirrorMode.value
model.single = singleMode.value
model.mouseMirror = mouseMirror.value
window.visible = true
window.passThrough = penetrable.value
window.alwaysOnTop = alwaysOnTop.value
window.scale = scale.value
window.opacity = opacity.value
migrated.value = true
}
return {
migrated,
model,
window,
init,
}
})
================================================
FILE: src/stores/general.ts
================================================
import type { Theme } from '@tauri-apps/api/window'
import { defineStore } from 'pinia'
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({
autostart: false,
taskbarVisible: false,
})
const appearance = reactive({
theme: 'auto',
isDark: false,
})
const update = reactive({
autoCheck: false,
})
const getLanguage = async () => {
const locale = await getLocale()
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 {
migrated,
app,
appearance,
update,
init,
}
})
================================================
FILE: src/stores/model.ts
================================================
import { resolveResource } from '@tauri-apps/api/path'
import { filter, find } from 'es-toolkit/compat'
import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'
import { join } from '@/utils/path'
export type ModelMode = 'standard' | 'keyboard' | 'gamepad'
export interface Model {
id: string
path: string
mode: ModelMode
isPreset: boolean
}
interface Motion {
Name: string
File: string
Sound?: string
FadeInTime: number
FadeOutTime: number
Description?: string
}
type MotionGroup = Record
interface Expression {
Name: string
File: string
Description?: string
}
export const useModelStore = defineStore('model', () => {
const models = ref([])
const currentModel = ref()
const motions = ref({})
const expressions = ref([])
const supportKeys = reactive>({})
const pressedKeys = reactive>({})
const init = async () => {
const modelsPath = await resolveResource('assets/models')
const nextModels = filter(models.value, { isPreset: false })
const presetModels = filter(models.value, { isPreset: true })
const modes: ModelMode[] = ['gamepad', 'keyboard', 'standard']
for (const mode of modes) {
const matched = find(presetModels, { mode })
nextModels.unshift({
id: matched?.id ?? nanoid(),
mode,
isPreset: true,
path: join(modelsPath, mode),
})
}
const matched = find(nextModels, { id: currentModel.value?.id })
currentModel.value = matched ?? nextModels[0]
models.value = nextModels
}
return {
models,
currentModel,
motions,
expressions,
supportKeys,
pressedKeys,
init,
}
}, {
tauri: {
filterKeys: ['models', 'currentModel'],
filterKeysStrategy: 'pick',
},
})
================================================
FILE: src/stores/shortcut.ts
================================================
import { defineStore } from 'pinia'
import { ref } from 'vue'
export type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop'
export const useShortcutStore = defineStore('shortcut', () => {
const visibleCat = ref('')
const visiblePreference = ref('')
const mirrorMode = ref('')
const penetrable = ref('')
const alwaysOnTop = ref('')
return {
visibleCat,
visiblePreference,
mirrorMode,
penetrable,
alwaysOnTop,
}
})
================================================
FILE: src/utils/is.ts
================================================
export function isImage(value: string) {
const regex = /\.(?:jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i
return regex.test(value)
}
export function inBetween(value: number, minimum: number, maximum: number) {
return value >= minimum && value <= maximum
}
================================================
FILE: src/utils/keyboard.ts
================================================
import { isMac } from './platform'
export interface Key {
eventKey: string
tauriKey?: string
symbol?: string
}
export const modifierKeys: Key[] = [
{
eventKey: 'Shift',
symbol: isMac ? '⇧' : 'Shift',
},
{
eventKey: 'Control',
symbol: isMac ? '⌃' : 'Ctrl',
},
{
eventKey: 'Alt',
symbol: isMac ? '⌥' : 'Alt',
},
{
eventKey: 'Command',
symbol: isMac ? '⌘' : 'Super',
},
].map((item) => {
return { ...item, tauriKey: item.eventKey }
})
export const standardKeys: Key[] = [
// 第一排
{
eventKey: 'Escape',
symbol: isMac ? '⎋' : 'Esc',
},
{
eventKey: 'F1',
},
{
eventKey: 'F2',
},
{
eventKey: 'F3',
},
{
eventKey: 'F4',
},
{
eventKey: 'F5',
},
{
eventKey: 'F6',
},
{
eventKey: 'F7',
},
{
eventKey: 'F8',
},
{
eventKey: 'F9',
},
{
eventKey: 'F10',
},
{
eventKey: 'F11',
},
{
eventKey: 'F12',
}, // 第二排
{
eventKey: 'Backquote',
symbol: '`',
},
{
eventKey: 'Digit1',
},
{
eventKey: 'Digit2',
},
{
eventKey: 'Digit3',
},
{
eventKey: 'Digit4',
},
{
eventKey: 'Digit5',
},
{
eventKey: 'Digit6',
},
{
eventKey: 'Digit7',
},
{
eventKey: 'Digit8',
},
{
eventKey: 'Digit9',
},
{
eventKey: 'Digit0',
},
{
eventKey: 'Minus',
tauriKey: '-',
symbol: '-',
},
{
eventKey: 'Equal',
tauriKey: '=',
symbol: '=',
},
{
eventKey: 'Backspace',
symbol: isMac ? '⌫' : void 0,
},
// 第三排
{
eventKey: 'Tab',
symbol: isMac ? '⇥' : void 0,
},
{
eventKey: 'KeyQ',
},
{
eventKey: 'KeyW',
},
{
eventKey: 'KeyE',
},
{
eventKey: 'KeyR',
},
{
eventKey: 'KeyT',
},
{
eventKey: 'KeyY',
},
{
eventKey: 'KeyU',
},
{
eventKey: 'KeyI',
},
{
eventKey: 'KeyO',
},
{
eventKey: 'KeyP',
},
{
eventKey: 'BracketLeft',
symbol: '[',
},
{
eventKey: 'BracketRight',
symbol: ']',
},
{
eventKey: 'Backslash',
symbol: '\\',
},
// 第四排
{
eventKey: 'KeyA',
},
{
eventKey: 'KeyS',
},
{
eventKey: 'KeyD',
},
{
eventKey: 'KeyF',
},
{
eventKey: 'KeyG',
},
{
eventKey: 'KeyH',
},
{
eventKey: 'KeyJ',
},
{
eventKey: 'KeyK',
},
{
eventKey: 'KeyL',
},
{
eventKey: 'Semicolon',
symbol: ';',
},
{
eventKey: 'Quote',
symbol: '\'',
},
{
eventKey: 'Enter',
symbol: isMac ? '↩︎' : void 0,
},
// 第五排
{
eventKey: 'KeyZ',
},
{
eventKey: 'KeyX',
},
{
eventKey: 'KeyC',
},
{
eventKey: 'KeyV',
},
{
eventKey: 'KeyB',
},
{
eventKey: 'KeyN',
},
{
eventKey: 'KeyM',
},
{
eventKey: 'Comma',
symbol: ',',
},
{
eventKey: 'Period',
symbol: '.',
},
{
eventKey: 'Slash',
symbol: '/',
},
// 第六排
{
eventKey: 'Space',
symbol: isMac ? '␣' : void 0,
},
// 方向键
{
eventKey: 'ArrowUp',
symbol: '↑',
},
{
eventKey: 'ArrowDown',
symbol: '↓',
},
{
eventKey: 'ArrowLeft',
symbol: '←',
},
{
eventKey: 'ArrowRight',
symbol: '→',
},
].map((item) => {
const { eventKey } = item
item.symbol ??= eventKey
item.tauriKey ??= eventKey
if (eventKey.startsWith('Digit') || eventKey.startsWith('Key')) {
item.tauriKey = item.symbol = eventKey.slice(-1)
}
return item
})
export const keys = modifierKeys.concat(standardKeys)
================================================
FILE: src/utils/live2d.ts
================================================
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
constructor() { }
private initApp() {
if (this.app) return
const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
this.app = new Application({
view,
resizeTo: window,
backgroundAlpha: 0,
resolution: devicePixelRatio,
})
}
public async load(path: string) {
this.initApp()
this.destroy()
const files = await readDir(path)
const modelFile = files.find(file => file.name.endsWith('.model3.json'))
if (!modelFile) {
throw new Error(i18n.global.t('utils.live2d.hints.notFound'))
}
const modelPath = join(path, modelFile.name)
const modelJSON = JSON5.parse(await readTextFile(modelPath))
const modelSettings = new Cubism4ModelSettings({
...modelJSON,
url: convertFileSrc(modelPath),
})
modelSettings.replaceFiles((file) => {
return convertFileSrc(join(path, file))
})
this.model = await Live2DModel.from(modelSettings)
this.app?.stage.addChild(this.model)
const { width, height } = this.model
const { motions, expressions } = modelSettings
return {
width,
height,
motions,
expressions,
}
}
public destroy() {
if (!this.model) return
this.model?.destroy()
this.model = null
}
public resizeModel(modelSize: ModelSize) {
if (!this.model) return
const { width, height } = modelSize
const scaleX = innerWidth / width
const scaleY = innerHeight / height
const scale = Math.min(scaleX, scaleY)
this.model.scale.set(scale)
this.model.x = innerWidth / 2
this.model.y = innerHeight / 2
this.model.anchor.set(0.5)
}
public playMotion(group: string, index: number) {
return this.model?.motion(group, index)
}
public playExpressions(index: number) {
return this.model?.expression(index)
}
public getCoreModel() {
const internalModel = this.model?.internalModel as Cubism4InternalModel
return internalModel?.coreModel
}
public getParameterRange(id: string) {
const coreModel = this.getCoreModel()
const index = coreModel?.getParameterIndex(id)
const min = coreModel?.getParameterMinimumValue(index)
const max = coreModel?.getParameterMaximumValue(index)
return {
min,
max,
}
}
public setParameterValue(id: string, value: number | boolean) {
const coreModel = this.getCoreModel()
return coreModel?.setParameterValueById?.(id, Number(value))
}
}
const live2d = new Live2d()
export default live2d
================================================
FILE: src/utils/monitor.ts
================================================
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()
const appWindow = getCurrentWebviewWindow()
const scaleFactor = await appWindow.scaleFactor()
const { x, y } = cursorPoint.toLogical(scaleFactor)
const monitor = await monitorFromPoint(x, y)
if (!monitor) return
return monitor
}
================================================
FILE: src/utils/path.ts
================================================
import { sep } from '@tauri-apps/api/path'
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 joinPaths.join(sep())
}
================================================
FILE: src/utils/platform.ts
================================================
import { platform } from '@tauri-apps/plugin-os'
export const isMac = platform() === 'macos'
export const isWindows = platform() === 'windows'
export const isLinux = platform() === 'linux'
================================================
FILE: src/utils/shared.ts
================================================
import { castArray } from 'es-toolkit/compat'
export function clearObject>(targets: T | T[]) {
for (const target of castArray(targets)) {
for (const key of Object.keys(target)) {
delete target[key]
}
}
}
================================================
FILE: src/vite-env.d.ts
================================================
///
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent