The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .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.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── eslint.config.ts
├── index.html
├── package.json
├── pnpm-lock.yaml
├── public
    ├── js
    │   ├── live2d.min.js
    │   └── live2dcubismcore.min.js
    └── logo.png
├── scripts
    ├── buildIcon.ts
    └── release.ts
├── src-tauri
    ├── .gitignore
    ├── BongoCat.desktop
    ├── Cargo.toml
    ├── assets
    │   ├── logo-mac.png
    │   ├── logo.png
    │   ├── models
    │   │   ├── keyboard
    │   │   │   ├── cat.model3.json
    │   │   │   ├── demomodel2.1024
    │   │   │   │   ├── texture_00.png
    │   │   │   │   ├── texture_01.png
    │   │   │   │   └── texture_02.png
    │   │   │   ├── 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
    │   │   │   └── resources
    │   │   │   │   ├── background.png
    │   │   │   │   ├── cover.png
    │   │   │   │   ├── left-keys
    │   │   │   │       ├── Alt.png
    │   │   │   │       ├── AltGr.png
    │   │   │   │       ├── BackQuote.png
    │   │   │   │       ├── Backspace.png
    │   │   │   │       ├── CapsLock.png
    │   │   │   │       ├── Control.png
    │   │   │   │       ├── ControlLeft.png
    │   │   │   │       ├── ControlRight.png
    │   │   │   │       ├── Delete.png
    │   │   │   │       ├── Escape.png
    │   │   │   │       ├── Fn.png
    │   │   │   │       ├── KeyA.png
    │   │   │   │       ├── KeyB.png
    │   │   │   │       ├── KeyC.png
    │   │   │   │       ├── KeyD.png
    │   │   │   │       ├── KeyE.png
    │   │   │   │       ├── KeyF.png
    │   │   │   │       ├── KeyG.png
    │   │   │   │       ├── KeyH.png
    │   │   │   │       ├── KeyI.png
    │   │   │   │       ├── KeyJ.png
    │   │   │   │       ├── KeyK.png
    │   │   │   │       ├── KeyL.png
    │   │   │   │       ├── KeyM.png
    │   │   │   │       ├── KeyN.png
    │   │   │   │       ├── KeyO.png
    │   │   │   │       ├── KeyP.png
    │   │   │   │       ├── KeyQ.png
    │   │   │   │       ├── KeyR.png
    │   │   │   │       ├── KeyS.png
    │   │   │   │       ├── KeyT.png
    │   │   │   │       ├── KeyU.png
    │   │   │   │       ├── KeyV.png
    │   │   │   │       ├── KeyW.png
    │   │   │   │       ├── KeyX.png
    │   │   │   │       ├── KeyY.png
    │   │   │   │       ├── KeyZ.png
    │   │   │   │       ├── Meta.png
    │   │   │   │       ├── Num0.png
    │   │   │   │       ├── Num1.png
    │   │   │   │       ├── Num2.png
    │   │   │   │       ├── Num3.png
    │   │   │   │       ├── Num4.png
    │   │   │   │       ├── Num5.png
    │   │   │   │       ├── Num6.png
    │   │   │   │       ├── Num7.png
    │   │   │   │       ├── Num8.png
    │   │   │   │       ├── Num9.png
    │   │   │   │       ├── Return.png
    │   │   │   │       ├── Shift.png
    │   │   │   │       ├── ShiftLeft.png
    │   │   │   │       ├── ShiftRight.png
    │   │   │   │       ├── Slash.png
    │   │   │   │       ├── Space.png
    │   │   │   │       └── Tab.png
    │   │   │   │   └── right-keys
    │   │   │   │       ├── DownArrow.png
    │   │   │   │       ├── LeftArrow.png
    │   │   │   │       ├── RightArrow.png
    │   │   │   │       └── UpArrow.png
    │   │   └── standard
    │   │   │   ├── cat.model3.json
    │   │   │   ├── demomodel.1024
    │   │   │       ├── texture_00.png
    │   │   │       ├── texture_01.png
    │   │   │       └── texture_02.png
    │   │   │   ├── 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
    │   │   │   └── resources
    │   │   │       ├── background.png
    │   │   │       ├── cover.png
    │   │   │       └── left-keys
    │   │   │           ├── Alt.png
    │   │   │           ├── AltGr.png
    │   │   │           ├── BackQuote.png
    │   │   │           ├── Backspace.png
    │   │   │           ├── CapsLock.png
    │   │   │           ├── Control.png
    │   │   │           ├── ControlLeft.png
    │   │   │           ├── ControlRight.png
    │   │   │           ├── Delete.png
    │   │   │           ├── Escape.png
    │   │   │           ├── Fn.png
    │   │   │           ├── KeyA.png
    │   │   │           ├── KeyB.png
    │   │   │           ├── KeyC.png
    │   │   │           ├── KeyD.png
    │   │   │           ├── KeyE.png
    │   │   │           ├── KeyF.png
    │   │   │           ├── KeyG.png
    │   │   │           ├── KeyH.png
    │   │   │           ├── KeyI.png
    │   │   │           ├── KeyJ.png
    │   │   │           ├── KeyK.png
    │   │   │           ├── KeyL.png
    │   │   │           ├── KeyM.png
    │   │   │           ├── KeyN.png
    │   │   │           ├── KeyO.png
    │   │   │           ├── KeyP.png
    │   │   │           ├── KeyQ.png
    │   │   │           ├── KeyR.png
    │   │   │           ├── KeyS.png
    │   │   │           ├── KeyT.png
    │   │   │           ├── KeyU.png
    │   │   │           ├── KeyV.png
    │   │   │           ├── KeyW.png
    │   │   │           ├── KeyX.png
    │   │   │           ├── KeyY.png
    │   │   │           ├── KeyZ.png
    │   │   │           ├── Meta.png
    │   │   │           ├── Num0.png
    │   │   │           ├── Num1.png
    │   │   │           ├── Num2.png
    │   │   │           ├── Num3.png
    │   │   │           ├── Num4.png
    │   │   │           ├── Num5.png
    │   │   │           ├── Num6.png
    │   │   │           ├── Num7.png
    │   │   │           ├── Num8.png
    │   │   │           ├── Num9.png
    │   │   │           ├── Return.png
    │   │   │           ├── Shift.png
    │   │   │           ├── ShiftLeft.png
    │   │   │           ├── ShiftRight.png
    │   │   │           ├── Slash.png
    │   │   │           ├── Space.png
    │   │   │           └── Tab.png
    │   └── tray.png
    ├── build.rs
    ├── capabilities
    │   └── default.json
    ├── src
    │   ├── core
    │   │   ├── device
    │   │   │   ├── common.rs
    │   │   │   ├── linux.rs
    │   │   │   └── mod.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
├── src
    ├── App.vue
    ├── assets
    │   └── css
    │   │   └── global.scss
    ├── components
    │   ├── pro-list-item
    │   │   └── index.vue
    │   ├── pro-list
    │   │   └── index.vue
    │   ├── pro-shortcut
    │   │   └── index.vue
    │   └── update-app
    │   │   └── index.vue
    ├── composables
    │   ├── useDevice.ts
    │   ├── useModel.ts
    │   ├── useSharedMenu.ts
    │   ├── useTauriKeyPress.ts
    │   ├── useTauriListen.ts
    │   ├── useThemeVars.ts
    │   ├── useTray.ts
    │   └── useWindowState.ts
    ├── constants
    │   └── index.ts
    ├── main.ts
    ├── pages
    │   ├── main
    │   │   └── index.vue
    │   └── preference
    │   │   ├── components
    │   │       ├── about
    │   │       │   └── index.vue
    │   │       ├── cat
    │   │       │   └── index.vue
    │   │       ├── general
    │   │       │   ├── components
    │   │       │   │   └── macos-permissions
    │   │       │   │   │   └── 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
    └── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── uno.config.ts
└── vite.config.ts


/.commitlintrc:
--------------------------------------------------------------------------------
1 | {
2 | 	"extends": "@commitlint/config-conventional"
3 | }
4 | 


--------------------------------------------------------------------------------
/.github/CONTRIBUTING.md:
--------------------------------------------------------------------------------
 1 | # 贡献指南
 2 | 
 3 | 非常感谢您对 BongoCat 的关注和贡献!在您提交贡献之前,请先花一些时间阅读以下指南,以确保您的贡献能够顺利进行。
 4 | 
 5 | ## 透明的开发
 6 | 
 7 | 所有工作都在 GitHub 上公开进行。无论是核心团队成员还是外部贡献者的 Pull Request,都需要经过相同的 review 流程。
 8 | 
 9 | ## 提交 Issue
10 | 
11 | 我们使用 [Github Issues](https://github.com/ayangweb/BongoCat/issues) 进行 Bug 报告和新 Feature 建议。在提交 Issue 之前,请确保已经搜索过类似的问题,因为它们可能已经得到解答或正在被修复。对于 Bug 报告,请包含可用于重现问题的完整步骤。对于新 Feature 建议,请指出你想要的更改以及期望的行为。
12 | 
13 | ## 提交 Pull Request
14 | 
15 | ### 共建流程
16 | 
17 | - 认领 issue:在 Github 建立 Issue 并认领(或直接认领已有 Issue),告知大家自己正在修复,避免重复工作。
18 | - 项目开发:在完成准备工作后,进行 Bug 修复或功能开发。
19 | - 提交 PR。
20 | 
21 | ### 准备工作
22 | 
23 | - [Rust](https://v2.tauri.app/start/prerequisites/): 请自行根据官网步骤安装 rust 环境。
24 | - [Node.js](https://nodejs.org/en/): 用于运行项目。
25 | - [Pnpm](https://pnpm.io/):本项目使用 Pnpm 进行包管理。
26 | 
27 | ### 下载依赖
28 | 
29 | ```shell
30 | pnpm install
31 | ```
32 | 
33 | ### 启动应用
34 | 
35 | ```shell
36 | pnpm tauri dev
37 | ```
38 | 
39 | ### 打包应用
40 | 
41 | > 如果需要打包后进行调试,请在以下命令后面加上 `--debug`
42 | 
43 | ```shell
44 | pnpm tauri build
45 | ```
46 | 
47 | ## Commit 指南
48 | 
49 | Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)。
50 | 
51 | ### Commit 类型
52 | 
53 | 以下是 commit 类型列表:
54 | 
55 | - feat: 新特性或功能
56 | - fix: 缺陷修复
57 | - docs: 文档更新
58 | - style: 代码风格更新
59 | - refactor: 代码重构,不引入新功能和缺陷修复
60 | - perf: 性能优化
61 | - chore: 其他提交
62 | 
63 | 期待您的参与,让我们一起使 BongoCat 变得更好!
64 | 


--------------------------------------------------------------------------------
/.github/DOWNLOAD_GUIDE.md:
--------------------------------------------------------------------------------
 1 | # 下载指南
 2 | 
 3 | ## 系统要求
 4 | 
 5 | - macOS 12 或更高版本。
 6 | - Windows 10 或更高版本。
 7 | - Linux 带有 X11 环境。
 8 | 
 9 | ## macOS
10 | 
11 | ### 手动下载
12 | 
13 | - Apple Silicon:下载 `BongoCat_aarch64.dmg`
14 | - Intel Chip:下载 `BongoCat_x64.dmg`
15 | 
16 | ### Homebrew 下载
17 | 
18 | 1. 添加 BongoCat 的 tap 源:
19 | 
20 | ```bash
21 | brew tap ayangweb/BongoCat
22 | ```
23 | 
24 | 2. 安装:
25 | 
26 | ```bash
27 | brew install --no-quarantine bongo-cat
28 | ```
29 | 
30 | 3. 更新:
31 | 
32 | ```bash
33 | brew upgrade bongo-cat
34 | ```
35 | 
36 | 4. 卸载:
37 | 
38 | ```bash
39 | brew uninstall --cask bongo-cat
40 | 
41 | brew untap ayangweb/BongoCat
42 | ```
43 | 
44 | ## Windows
45 | 
46 | - 64 位系统:下载 `BongoCat_x64.exe`
47 | - 32 位系统:下载 `BongoCat_x86.exe`
48 | - ARM64 架构:下载 `BongoCat_arm64.exe`
49 | 
50 | ## Linux(X11)
51 | 
52 | ### 手动下载
53 | 
54 | - 64 位系统:
55 |   - Debian / Ubuntu:下载 `BongoCat_amd64.deb`
56 |   - Fedora / RHEL:下载 `BongoCat_x86_64.rpm`
57 |   - 通用版本:下载 `BongoCat_amd64.AppImage`
58 | - ARM64 架构:
59 |   - Debian / Ubuntu:下载 `BongoCat_arm64.deb`
60 |   - Fedora / RHEL:下载 `BongoCat_aarch64.rpm`
61 |   - 通用版本:下载 `BongoCat_aarch64.AppImage`
62 | 
63 | ### AUR 下载
64 | 
65 | - Manjaro / ArchLinux: `yay -S bongo-cat`
66 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
 1 | name: 🐞 Bug 报告
 2 | title: '[bug] '
 3 | description: 报告一个 Bug
 4 | labels: bug
 5 | body:
 6 |   - type: markdown
 7 |     attributes:
 8 |       value: |
 9 |         ## 温馨提示
10 |         1. 请先查阅现有的 [issues](https://github.com/ayangweb/BongoCat/issues)。
11 |         2. 请确保你使用的是[最新版本](https://github.com/ayangweb/BongoCat/releases/latest)。
12 |         3. 请确保该问题不是由其他软件引起的。
13 |         4. 请始终保持友好与尊重,感谢你的理解与配合。
14 | 
15 |   - type: textarea
16 |     id: description
17 |     attributes:
18 |       label: 描述 Bug
19 |       description: 请详细描述 Bug 并提供截图或视频以帮助我们更好地理解问题。
20 |     validations:
21 |       required: true
22 | 
23 |   - type: textarea
24 |     id: reproduction
25 |     attributes:
26 |       label: 重现步骤
27 |       description: 请详细列出重现问题的步骤,并附带截图或视频。
28 | 
29 |   - type: textarea
30 |     id: expected-behavior
31 |     attributes:
32 |       label: 预期行为
33 |       description: 请描述你期望发生的行为。
34 | 
35 |   - type: textarea
36 |     id: info
37 |     attributes:
38 |       render: text
39 |       label: 软件信息
40 |       description: 请前往偏好设置窗口的「关于 > 关于软件 > 软件信息」复制软件信息。
41 |     validations:
42 |       required: true
43 | 
44 |   - type: textarea
45 |     id: context
46 |     attributes:
47 |       label: 附加信息
48 |       description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。
49 | 


--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
 1 | name: 💡 功能请求
 2 | title: '[feat] '
 3 | description: 提出一个想法
 4 | labels: feature request
 5 | body:
 6 |   - type: textarea
 7 |     id: problem
 8 |     attributes:
 9 |       label: 描述问题
10 |       description: 请清晰地描述此功能将解决的具体问题。
11 |     validations:
12 |       required: true
13 | 
14 |   - type: textarea
15 |     id: solution
16 |     attributes:
17 |       label: 描述您希望的解决方案
18 |       description: 请清晰地描述您期望的变更或改进。
19 |     validations:
20 |       required: true
21 | 
22 |   - type: textarea
23 |     id: alternatives
24 |     attributes:
25 |       label: 考虑的替代方案
26 |       description: 提供您考虑过的其他替代解决方案。
27 | 
28 |   - type: textarea
29 |     id: context
30 |     attributes:
31 |       label: 附加信息
32 |       description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。
33 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
  1 | name: BongoCat Release
  2 | 
  3 | on:
  4 |   push:
  5 |     tags:
  6 |       - 'v*'
  7 |   workflow_dispatch:
  8 | 
  9 | jobs:
 10 |   create-release:
 11 |     runs-on: ubuntu-latest
 12 |     steps:
 13 |       - uses: actions/checkout@v4
 14 |         with:
 15 |           fetch-depth: 0
 16 |       - name: Set output
 17 |         id: vars
 18 |         run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT
 19 | 
 20 |       - name: Setup node
 21 |         uses: actions/setup-node@v4
 22 |         with:
 23 |           node-version: 20
 24 | 
 25 |       - name: Generate changelog
 26 |         id: create_release
 27 |         run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }}
 28 |         env:
 29 |           GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
 30 | 
 31 |   build-app:
 32 |     needs: create-release
 33 |     permissions:
 34 |       contents: write
 35 |     strategy:
 36 |       fail-fast: false
 37 |       matrix:
 38 |         include:
 39 |           - platform: macos-latest
 40 |             target: aarch64-apple-darwin
 41 |           - platform: macos-latest
 42 |             target: x86_64-apple-darwin
 43 | 
 44 |           - platform: windows-latest
 45 |             target: x86_64-pc-windows-msvc
 46 |           - platform: windows-latest
 47 |             target: i686-pc-windows-msvc
 48 |           - platform: windows-latest
 49 |             target: aarch64-pc-windows-msvc
 50 | 
 51 |           - platform: ubuntu-22.04
 52 |             target: x86_64-unknown-linux-gnu
 53 |           - platform: ubuntu-22.04-arm
 54 |             target: aarch64-unknown-linux-gnu
 55 | 
 56 |     runs-on: ${{ matrix.platform }}
 57 |     steps:
 58 |       - name: Checkout repository
 59 |         uses: actions/checkout@v4
 60 |       - name: Setup node
 61 |         uses: actions/setup-node@v4
 62 |         with:
 63 |           node-version: 20
 64 |       - uses: pnpm/action-setup@v3
 65 |         with:
 66 |           version: latest
 67 | 
 68 |       - name: Install rust target
 69 |         run: rustup target add ${{ matrix.target }}
 70 | 
 71 |       - name: Install dependencies (ubuntu only)
 72 |         if: startsWith(matrix.platform, 'ubuntu')
 73 |         run: |
 74 |           sudo apt-get update
 75 |           sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils
 76 | 
 77 |       - name: Install Rust stable
 78 |         uses: dtolnay/rust-toolchain@stable
 79 | 
 80 |       - name: Rust cache
 81 |         uses: swatinem/rust-cache@v2
 82 |         with:
 83 |           workspaces: target
 84 | 
 85 |       - name: Sync node version and setup cache
 86 |         uses: actions/setup-node@v4
 87 |         with:
 88 |           node-version: 20
 89 |           cache: pnpm
 90 | 
 91 |       - name: Install front-end dependencies
 92 |         run: pnpm install --frozen-lockfile
 93 | 
 94 |       - name: Build the app
 95 |         uses: tauri-apps/tauri-action@v0
 96 |         env:
 97 |           CI: false
 98 |           PLATFORM: ${{ matrix.platform }}
 99 |           GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
100 |           TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
101 |           TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
102 |         with:
103 |           tagName: ${{ github.ref_name }}
104 |           releaseName: BongoCat ${{ needs.create-release.outputs.APP_VERSION }}
105 |           releaseBody: ''
106 |           releaseDraft: true
107 |           prerelease: false
108 |           args: --target ${{ matrix.target }}
109 | 


--------------------------------------------------------------------------------
/.github/workflows/sync-to-gitee.yml:
--------------------------------------------------------------------------------
 1 | name: Sync Github Repos To Gitee
 2 | on:
 3 |   push:
 4 |     branches:
 5 |       - master
 6 | 
 7 | jobs:
 8 |   repo-sync:
 9 |     runs-on: ubuntu-latest
10 |     steps:
11 |       - name: Sync Github Repos To Gitee
12 |         uses: Yikun/hub-mirror-action@master
13 |         with:
14 |           src: github/ayangweb
15 |           dst: gitee/ayangweb
16 |           dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
17 |           dst_token: ${{ secrets.GITEE_TOKEN }}
18 |           static_list: BongoCat
19 |           force_update: true
20 | 


--------------------------------------------------------------------------------
/.github/workflows/upgradelink.yml:
--------------------------------------------------------------------------------
 1 | name: Upload Release to UpgradeLink
 2 | 
 3 | on:
 4 |   release:
 5 |     types: [published]
 6 |   workflow_dispatch:
 7 | 
 8 | jobs:
 9 |   upgradeLink-upload:
10 |     permissions:
11 |       contents: write
12 |     runs-on: ubuntu-latest
13 |     steps:
14 |       - name: Send a request to UpgradeLink
15 |         uses: toolsetlink/upgradelink-action@v5
16 |         with:
17 |           source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json'
18 |           access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
19 |           tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }}
20 |           github-token: ${{ secrets.RELEASE_TOKEN }}
21 | 


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | target
15 | 
16 | # Editor directories and files
17 | .idea
18 | .DS_Store
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 | 


--------------------------------------------------------------------------------
/.release-it.ts:
--------------------------------------------------------------------------------
 1 | /* eslint-disable no-template-curly-in-string */
 2 | import type { Config } from 'release-it'
 3 | 
 4 | export default {
 5 |   git: {
 6 |     commitMessage: 'v${version}',
 7 |     tagName: 'v${version}',
 8 |   },
 9 |   npm: {
10 |     publish: false,
11 |   },
12 |   hooks: {
13 |     'after:bump': 'tsx scripts/release.ts',
14 |   },
15 | } satisfies Config
16 | 


--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 |   "recommendations": [
3 |     "tauri-apps.tauri-vscode",
4 |     "rust-lang.rust-analyzer",
5 |     "antfu.unocss",
6 |     "dbaeumer.vscode-eslint"
7 |   ]
8 | }
9 | 


--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   // Disable the default formatter, use eslint instead
 3 |   "prettier.enable": false,
 4 | 
 5 |   "eslint.format.enable": true,
 6 | 
 7 |   // Auto fix
 8 |   "editor.codeActionsOnSave": {
 9 |     "source.fixAll.eslint": "explicit",
10 |     "source.organizeImports": "never"
11 |   },
12 | 
13 |   // Silent the stylistic rules in you IDE, but still auto fix them
14 |   "eslint.rules.customizations": [
15 |     { "rule": "style/*", "severity": "off", "fixable": true },
16 |     { "rule": "format/*", "severity": "off", "fixable": true },
17 |     { "rule": "*-indent", "severity": "off", "fixable": true },
18 |     { "rule": "*-spacing", "severity": "off", "fixable": true },
19 |     { "rule": "*-spaces", "severity": "off", "fixable": true },
20 |     { "rule": "*-order", "severity": "off", "fixable": true },
21 |     { "rule": "*-dangle", "severity": "off", "fixable": true },
22 |     { "rule": "*-newline", "severity": "off", "fixable": true },
23 |     { "rule": "*quotes", "severity": "off", "fixable": true },
24 |     { "rule": "*semi", "severity": "off", "fixable": true }
25 |   ],
26 | 
27 |   // Enable eslint for all supported languages
28 |   "eslint.validate": [
29 |     "javascript",
30 |     "javascriptreact",
31 |     "typescript",
32 |     "typescriptreact",
33 |     "vue",
34 |     "html",
35 |     "markdown",
36 |     "json",
37 |     "json5",
38 |     "jsonc",
39 |     "yaml",
40 |     "toml",
41 |     "xml",
42 |     "gql",
43 |     "graphql",
44 |     "astro",
45 |     "svelte",
46 |     "css",
47 |     "less",
48 |     "scss",
49 |     "pcss",
50 |     "postcss"
51 |   ],
52 | 
53 |   "typescript.enablePromptUseWorkspaceTsdk": true,
54 |   "typescript.tsdk": "./node_modules/typescript/lib"
55 | }
56 | 


--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [workspace]
 2 | resolver = "2"
 3 | members = [ "src-tauri" ]
 4 | 
 5 | [profile.release]
 6 | strip = true
 7 | opt-level = 3
 8 | codegen-units = 1
 9 | panic = "abort"
10 | debug-assertions = false
11 | overflow-checks = false
12 | lto = true
13 | 
14 | [workspace.dependencies]
15 | tauri = "2"
16 | serde = "1"
17 | serde_json = "1"
18 | fs_extra = "1"
19 | tauri-plugin = { version = "2", features = [ "build" ] }
20 | tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
21 | tauri-plugin-custom-window = { path = "./src-tauri/src/plugins/window" }
22 | 


--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
 1 | MIT License
 2 | 
 3 | Copyright (c) 2025 ayangweb
 4 | 
 5 | Permission is hereby granted, free of charge, to any person obtaining a copy
 6 | of this software and associated documentation files (the "Software"), to deal
 7 | in the Software without restriction, including without limitation the rights
 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 | 
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 | 
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | ![BongoCat](https://socialify.git.ci/ayangweb/BongoCat/image?custom_description=&description=1&font=Source+Code+Pro&forks=1&issues=1&logo=https%3A%2F%2Fgithub.com%2Fayangweb%2FBongoCat%2Fblob%2Fmaster%2Fsrc-tauri%2Fassets%2Flogo-mac.png%3Fraw%3Dtrue&name=1&owner=1&pattern=Floating+Cogs&pulls=1&stargazers=1&theme=Auto)
  2 | 
  3 | <div align="center">
  4 |   <div>
  5 |     <a href="https://github.com/ayangweb/BongoCat/releases">
  6 |       <img
  7 |         alt="Windows"
  8 |         src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg=="
  9 |       />
 10 |     </a>
 11 |     <a href="https://github.com/ayangweb/BongoCat/releases">
 12 |       <img
 13 |         alt="MacOS"
 14 |         src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white"
 15 |       />
 16 |     </a>
 17 |     <a href="https://github.com/ayangweb/BongoCat/releases">
 18 |       <img
 19 |         alt="Linux"
 20 |         src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white"
 21 |       />
 22 |     </a>
 23 |   </div>
 24 | 
 25 |   <p>
 26 |     <a href="./LICENSE">
 27 |       <img
 28 |         src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square"
 29 |       />
 30 |     </a>
 31 |     <a href="https://github.com/ayangweb/BongoCat/releases/latest">
 32 |       <img
 33 |         src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square"
 34 |       />
 35 |     </a>
 36 |     <a href="https://github.com/ayangweb/BongoCat/releases">
 37 |       <img
 38 |         src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square"
 39 |       />
 40 |     </a>
 41 |   </p>
 42 | 
 43 |   <p>
 44 |     <a href="https://hellogithub.com/repository/7d23863fd4be47b39e816193ded385c9" target="_blank">
 45 |       <picture>
 46 |         <source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=dark" />
 47 |         <source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
 48 |         <img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
 49 |       </picture>
 50 |     </a>
 51 |   </p>
 52 | </div>
 53 | 
 54 | | macOS                                                                                        | Windows                                                                                        | Linux(x11)                                                                                   |
 55 | | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
 56 | | ![macOS](https://i0.hdslb.com/bfs/openplatform/dff276b96d49c5d6c431b74b531aab72191b3d87.png) | ![Windows](https://i0.hdslb.com/bfs/openplatform/a4149b753856ee7f401989da902cf3b5ad35b39e.png) | ![Linux](https://i0.hdslb.com/bfs/openplatform/3b49f961819d3ff63b2b80251c1cc13c27e986b0.png) |
 57 | 
 58 | ## 开发背景
 59 | 
 60 | 本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱,但仅支持 Windows 平台。作为一名深度 macOS 用户,我特别希望在自己的设备上也能使用这款可爱的猫咪,于是我决定开发一个适配 macOS 的版本。
 61 | 
 62 | 同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux,让更多的用户都能与这只可爱的猫咪互动!
 63 | 
 64 | ## 下载
 65 | 
 66 | - [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1)
 67 | - [GitHub Releases](https://github.com/ayangweb/BongoCat/releases)
 68 | 
 69 | > 不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。
 70 | 
 71 | ## 功能介绍
 72 | 
 73 | - 适配 macOS、Windows 和 Linux。
 74 | - 根据据键盘或鼠标操作,同步移动鼠标或敲击键盘。
 75 | - 支持导入自定义模型,自由打造专属猫咪形象。
 76 | - 完全开源,代码公开透明,绝不收集任何用户数据。
 77 | - 支持离线运行,无需联网,保护用户隐私。
 78 | 
 79 | ## 使用提示
 80 | 
 81 | - Linux 下需要用户系统安装 libinput 并加入 `input` 用户组,方可在 X11 和 Wayland 下正常使用。
 82 | 
 83 | ## 更多模型
 84 | 
 85 | 你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:
 86 | 
 87 | 📦 [Awesome-BongoCat](https://github.com/ayangweb/Awesome-BongoCat)
 88 | 
 89 | ## 社区交流
 90 | 
 91 | <a href="https://qm.qq.com/q/AS3gNv2Vzy">
 92 |  <picture>
 93 |    <source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/5ad8e4278c525cca6d3b4426c30b6d299d8a9654.png" />
 94 |    <source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" />
 95 |    <img alt="QQ Group" src="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" height="250" />
 96 |  </picture>
 97 | </a>
 98 | 
 99 | ## 贡献指南
100 | 
101 | 感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。
102 | 
103 | <a href="https://github.com/ayangweb/BongoCat/graphs/contributors">
104 |   <img src="https://contrib.rocks/image?repo=ayangweb/BongoCat" />
105 | </a>
106 | 
107 | ## 历史星标
108 | 
109 | <a href="https://www.star-history.com/#ayangweb/BongoCat&Date">
110 |  <picture>
111 |    <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date&theme=dark" />
112 |    <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
113 |    <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
114 |  </picture>
115 | </a>
116 | 
117 | ## 致谢
118 | 
119 | - 特别感谢 [UpgradeLink](https://www.toolsetlink.com/) 提供高效稳定的自动更新服务,让本项目得以持续为用户带来最新版本的优质体验。
120 | 


--------------------------------------------------------------------------------
/eslint.config.ts:
--------------------------------------------------------------------------------
 1 | import antfu from '@antfu/eslint-config'
 2 | 
 3 | export default antfu({
 4 |   formatters: true,
 5 |   unocss: true,
 6 |   rules: {
 7 |     'antfu/if-newline': 'off',
 8 |     'style/brace-style': ['error', '1tbs'],
 9 |     'ts/no-use-before-define': 'off',
10 |     'unused-imports/no-unused-imports': 'error',
11 |     'perfectionist/sort-imports': 'off',
12 |     'import/order': [
13 |       'error',
14 |       {
15 |         'newlines-between': 'always',
16 |         'groups': ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
17 |         'alphabetize': {
18 |           order: 'asc',
19 |           caseInsensitive: true,
20 |         },
21 |       },
22 |     ],
23 |     'vue/attributes-order': ['error', { alphabetical: true }],
24 |     'vue/max-attributes-per-line': 'error',
25 |   },
26 |   ignores: ['**/*.toml'],
27 | })
28 | 


--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
 1 | <!doctype html>
 2 | <html lang="zh">
 3 |   <head>
 4 |     <meta charset="UTF-8" />
 5 |     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 6 |     <title>BongoCat</title>
 7 |     <script src="/js/live2dcubismcore.min.js"></script>
 8 |     <script src="/js/live2d.min.js"></script>
 9 |   </head>
10 | 
11 |   <body>
12 |     <div id="app"></div>
13 |     <script type="module" src="/src/main.ts"></script>
14 |   </body>
15 | </html>
16 | 


--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "name": "bongo-cat",
 3 |   "type": "module",
 4 |   "version": "0.6.2",
 5 |   "private": true,
 6 |   "author": {
 7 |     "name": "ayangweb",
 8 |     "email": "ayangweb@foxmail.com"
 9 |   },
10 |   "scripts": {
11 |     "dev": "run-s build:icon dev:vite",
12 |     "build": "run-s build:*",
13 |     "dev:vite": "vite",
14 |     "build:vite": "vite build",
15 |     "build:icon": "tsx scripts/buildIcon.ts",
16 |     "preview": "vite preview",
17 |     "tauri": "tauri",
18 |     "lint": "eslint --fix src",
19 |     "preinstall": "npx only-allow pnpm",
20 |     "prepare": "simple-git-hooks",
21 |     "release": "release-it"
22 |   },
23 |   "dependencies": {
24 |     "@ant-design/icons-vue": "^7.0.1",
25 |     "@tauri-apps/api": "^2.5.0",
26 |     "@tauri-apps/plugin-autostart": "~2.3.0",
27 |     "@tauri-apps/plugin-clipboard-manager": "~2.2.2",
28 |     "@tauri-apps/plugin-dialog": "~2.2.2",
29 |     "@tauri-apps/plugin-fs": "~2.3.0",
30 |     "@tauri-apps/plugin-global-shortcut": "~2.2.1",
31 |     "@tauri-apps/plugin-log": "~2.3.1",
32 |     "@tauri-apps/plugin-opener": "~2.2.7",
33 |     "@tauri-apps/plugin-os": "^2.2.1",
34 |     "@tauri-apps/plugin-process": "^2.2.1",
35 |     "@tauri-apps/plugin-updater": "~2.7.1",
36 |     "@tauri-store/pinia": "^3.7.0",
37 |     "@vueuse/core": "^13.3.0",
38 |     "ant-design-vue": "^4.2.6",
39 |     "dayjs": "^1.11.13",
40 |     "es-toolkit": "^1.38.0",
41 |     "is-url": "^1.2.4",
42 |     "nanoid": "^5.1.5",
43 |     "pinia": "^3.0.3",
44 |     "pixi-live2d-display": "^0.4.0",
45 |     "pixi.js": "^6.5.10",
46 |     "tauri-plugin-macos-permissions-api": "^2.3.0",
47 |     "vue": "^3.5.16",
48 |     "vue-markdown-render": "^2.2.1",
49 |     "vue-router": "^4.5.1",
50 |     "vue3-masonry-css": "^1.0.7"
51 |   },
52 |   "devDependencies": {
53 |     "@antfu/eslint-config": "^4.13.3",
54 |     "@commitlint/cli": "^19.8.1",
55 |     "@commitlint/config-conventional": "^19.8.1",
56 |     "@iconify-json/iconamoon": "^1.2.2",
57 |     "@iconify-json/solar": "^1.2.2",
58 |     "@tauri-apps/cli": "^2.5.0",
59 |     "@types/is-url": "^1.2.32",
60 |     "@types/node": "^22.15.29",
61 |     "@unocss/eslint-plugin": "^66.1.3",
62 |     "@vitejs/plugin-vue": "^5.2.4",
63 |     "eslint": "^9.28.0",
64 |     "eslint-plugin-format": "^1.0.1",
65 |     "lint-staged": "^15.5.2",
66 |     "npm-run-all": "^4.1.5",
67 |     "release-it": "^18.1.2",
68 |     "sass": "^1.89.1",
69 |     "simple-git-hooks": "^2.13.0",
70 |     "tsx": "^4.19.4",
71 |     "typescript": "~5.6.3",
72 |     "unocss": "66.1.0-beta.7",
73 |     "vite": "^6.3.5"
74 |   },
75 |   "simple-git-hooks": {
76 |     "commit-msg": "npx --no-install commitlint -e",
77 |     "pre-commit": "npx lint-staged"
78 |   },
79 |   "lint-staged": {
80 |     "*": "eslint --fix"
81 |   }
82 | }
83 | 


--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/public/logo.png


--------------------------------------------------------------------------------
/scripts/buildIcon.ts:
--------------------------------------------------------------------------------
 1 | import { execSync } from 'node:child_process'
 2 | import { env, platform } from 'node:process'
 3 | 
 4 | (() => {
 5 |   const isMac = env.PLATFORM?.startsWith('macos') ?? platform === 'darwin'
 6 | 
 7 |   const logoName = isMac ? 'logo-mac' : 'logo'
 8 | 
 9 |   const command = `tauri icon src-tauri/assets/${logoName}.png`
10 | 
11 |   execSync(command, { stdio: 'inherit' })
12 | })()
13 | 


--------------------------------------------------------------------------------
/scripts/release.ts:
--------------------------------------------------------------------------------
 1 | import { readFileSync, writeFileSync } from 'node:fs'
 2 | import { dirname, resolve } from 'node:path'
 3 | import { fileURLToPath } from 'node:url'
 4 | 
 5 | import { name, version } from '../package.json'
 6 | 
 7 | const __dirname = dirname(fileURLToPath(import.meta.url));
 8 | 
 9 | (() => {
10 |   const tomlPath = resolve(__dirname, '..', 'src-tauri', 'Cargo.toml')
11 |   const lockPath = resolve(__dirname, '..', 'Cargo.lock')
12 | 
13 |   for (const path of [tomlPath, lockPath]) {
14 |     let content = readFileSync(path, 'utf-8')
15 | 
16 |     const regexp = new RegExp(
17 |       `(name\\s*=\\s*"${name}"\\s*version\\s*=\\s*)"(\\d+\\.\\d+\\.\\d+(-\\w+\\.\\d+)?)"`,
18 |     )
19 | 
20 |     content = content.replace(regexp, `$1"${version}"`)
21 | 
22 |     writeFileSync(path, content)
23 |   }
24 | })()
25 | 


--------------------------------------------------------------------------------
/src-tauri/.gitignore:
--------------------------------------------------------------------------------
 1 | # Generated by Cargo
 2 | # will have compiled files and executables
 3 | /target/
 4 | 
 5 | # Generated by Tauri
 6 | # will have schema files for capabilities auto-completion
 7 | /gen/schemas
 8 | 
 9 | icons
10 | autogenerated
11 | schemas


--------------------------------------------------------------------------------
/src-tauri/BongoCat.desktop:
--------------------------------------------------------------------------------
1 | [Desktop Entry]
2 | Type=Application
3 | Name={{{name}}}
4 | Exec={{{exec}}}
5 | Icon={{{icon}}}
6 | Categories={{{categories}}}
7 | Comment={{{comment}}}
8 | Terminal=false
9 | 


--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "bongo-cat"
 3 | version = "0.6.2"
 4 | description = "A Tauri App"
 5 | authors = [ "ayangweb" ]
 6 | edition = "2024"
 7 | 
 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 9 | 
10 | [lib]
11 | # The `_lib` suffix may seem redundant but it is necessary
12 | # to make the lib name unique and wouldn't conflict with the bin name.
13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
14 | name = "bongo_cat_lib"
15 | crate-type = ["staticlib", "cdylib", "rlib"]
16 | 
17 | [build-dependencies]
18 | tauri-build = { version = "2", features = [] }
19 | 
20 | [dependencies]
21 | tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-private-api", "image-png"] }
22 | serde = { workspace = true, features = ["derive"] }
23 | serde_json.workspace = true
24 | tauri-plugin-custom-window.workspace = true
25 | tauri-plugin-os = "2"
26 | tauri-plugin-process = "2"
27 | tauri-plugin-opener = "2"
28 | tauri-plugin-pinia = "3"
29 | tauri-plugin-log = "2"
30 | tauri-plugin-updater = "2"
31 | tauri-plugin-prevent-default = "1"
32 | tauri-plugin-single-instance = "2"
33 | tauri-plugin-autostart = "2"
34 | tauri-plugin-macos-permissions = "2"
35 | tauri-plugin-dialog = "2"
36 | tauri-plugin-fs = "2"
37 | fs_extra = "1"
38 | tauri-plugin-clipboard-manager = "2"
39 | tauri-plugin-global-shortcut = "2"
40 | 
41 | [target."cfg(target_os = \"macos\")".dependencies]
42 | tauri-nspanel.workspace = true
43 | 
44 | [target."cfg(not(target_os = \"linux\"))".dependencies]
45 | rdev = { git = "https://github.com/ayangweb/rdev" }
46 | 
47 | [target."cfg(target_os = \"linux\")".dependencies]
48 | nix = { version = "0.30", features = ["poll"] }
49 | input = "0.9"
50 | 
51 | [features]
52 | cargo-clippy = []
53 | 


--------------------------------------------------------------------------------
/src-tauri/assets/logo-mac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/logo-mac.png


--------------------------------------------------------------------------------
/src-tauri/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/logo.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/cat.model3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Version": 3,
 3 |   "FileReferences": {
 4 |     "Moc": "demomodel2.moc3",
 5 |     "Textures": [
 6 |       "demomodel2.1024/texture_00.png",
 7 |       "demomodel2.1024/texture_01.png",
 8 |       "demomodel2.1024/texture_02.png"
 9 |     ],
10 |     "DisplayInfo": "demomodel2.cdi3.json",
11 |     "Expressions": [
12 |       {
13 |         "Name": "默认喵",
14 |         "File": "live2d_expression0.exp3.json"
15 |       },
16 |       {
17 |         "Name": "社会喵",
18 |         "File": "live2d_expression1.exp3.json",
19 |         "Description": "喵喵我叼根小烟,耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
20 |       },
21 |       {
22 |         "Name": "天使喵",
23 |         "File": "live2d_expression2.exp3.json"
24 |       }
25 |     ],
26 |     "Motions": {
27 |       "CAT_motion": [
28 |         {
29 |           "Name": "雷霆喵",
30 |           "File": "live2d_motion1.motion3.json",
31 |           "Sound": "live2d_motion1.flac",
32 |           "FadeInTime": 0,
33 |           "FadeOutTime": 0
34 |         },
35 |         {
36 |           "Name": "摇摆喵",
37 |           "File": "live2d_motion2.motion3.json",
38 |           "FadeInTime": 0,
39 |           "FadeOutTime": 0
40 |         }
41 |       ]
42 |     }
43 |   },
44 |   "Groups": [
45 |     {
46 |       "Target": "Parameter",
47 |       "Name": "EyeBlink",
48 |       "Ids": [
49 |         "ParamEyeLOpen",
50 |         "ParamEyeROpen"
51 |       ]
52 |     },
53 |     {
54 |       "Target": "Parameter",
55 |       "Name": "LipSync",
56 |       "Ids": []
57 |     }
58 |   ]
59 | }
60 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/demomodel2.1024/texture_00.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.1024/texture_00.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/demomodel2.1024/texture_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.1024/texture_01.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/demomodel2.1024/texture_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.1024/texture_02.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/demomodel2.cdi3.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Version": 3,
  3 |   "Parameters": [
  4 |     {
  5 |       "Id": "ParamAngleX",
  6 |       "GroupId": "",
  7 |       "Name": "角度 X"
  8 |     },
  9 |     {
 10 |       "Id": "ParamAngleY",
 11 |       "GroupId": "",
 12 |       "Name": "角度 Y"
 13 |     },
 14 |     {
 15 |       "Id": "CatParamRightHandDown",
 16 |       "GroupId": "",
 17 |       "Name": "右手按下"
 18 |     },
 19 |     {
 20 |       "Id": "CatParamLeftHandDown",
 21 |       "GroupId": "",
 22 |       "Name": "左手按下"
 23 |     },
 24 |     {
 25 |       "Id": "ParamAngleZ",
 26 |       "GroupId": "",
 27 |       "Name": "角度 Z"
 28 |     },
 29 |     {
 30 |       "Id": "ParamEyeLOpen",
 31 |       "GroupId": "",
 32 |       "Name": "左眼 开闭"
 33 |     },
 34 |     {
 35 |       "Id": "ParamEyeLSmile",
 36 |       "GroupId": "",
 37 |       "Name": "左眼 微笑"
 38 |     },
 39 |     {
 40 |       "Id": "ParamEyeROpen",
 41 |       "GroupId": "",
 42 |       "Name": "右眼"
 43 |     },
 44 |     {
 45 |       "Id": "ParamEyeRSmile",
 46 |       "GroupId": "",
 47 |       "Name": "右眼 微笑"
 48 |     },
 49 |     {
 50 |       "Id": "Param3",
 51 |       "GroupId": "",
 52 |       "Name": "挥手"
 53 |     },
 54 |     {
 55 |       "Id": "Param",
 56 |       "GroupId": "ParamGroup",
 57 |       "Name": "开启闪电"
 58 |     },
 59 |     {
 60 |       "Id": "Param2",
 61 |       "GroupId": "ParamGroup",
 62 |       "Name": "闪电划过"
 63 |     },
 64 |     {
 65 |       "Id": "Param4",
 66 |       "GroupId": "ParamGroup2",
 67 |       "Name": "表情:thuglife"
 68 |     },
 69 |     {
 70 |       "Id": "Param5",
 71 |       "GroupId": "ParamGroup2",
 72 |       "Name": "表情:升天"
 73 |     },
 74 |     {
 75 |       "Id": "ParamEyeBallX",
 76 |       "GroupId": "",
 77 |       "Name": "眼球 X"
 78 |     },
 79 |     {
 80 |       "Id": "ParamEyeBallY",
 81 |       "GroupId": "",
 82 |       "Name": "眼球 Y"
 83 |     },
 84 |     {
 85 |       "Id": "ParamBrowLY",
 86 |       "GroupId": "",
 87 |       "Name": "左眉上下"
 88 |     },
 89 |     {
 90 |       "Id": "ParamBrowRY",
 91 |       "GroupId": "",
 92 |       "Name": "右眉 上下"
 93 |     },
 94 |     {
 95 |       "Id": "ParamBrowLX",
 96 |       "GroupId": "",
 97 |       "Name": "左眉 左右"
 98 |     },
 99 |     {
100 |       "Id": "ParamBrowRX",
101 |       "GroupId": "",
102 |       "Name": "右眉 左右"
103 |     },
104 |     {
105 |       "Id": "ParamBrowLAngle",
106 |       "GroupId": "",
107 |       "Name": "左眉 角度"
108 |     },
109 |     {
110 |       "Id": "ParamBrowRAngle",
111 |       "GroupId": "",
112 |       "Name": "右眉 角度"
113 |     },
114 |     {
115 |       "Id": "ParamBrowLForm",
116 |       "GroupId": "",
117 |       "Name": "左眉 変形"
118 |     },
119 |     {
120 |       "Id": "ParamBrowRForm",
121 |       "GroupId": "",
122 |       "Name": "右眉 変形"
123 |     },
124 |     {
125 |       "Id": "ParamMouthForm",
126 |       "GroupId": "",
127 |       "Name": "嘴部 变形"
128 |     },
129 |     {
130 |       "Id": "ParamMouthOpenY",
131 |       "GroupId": "",
132 |       "Name": "嘴巴 张开和闭合"
133 |     },
134 |     {
135 |       "Id": "ParamCheek",
136 |       "GroupId": "",
137 |       "Name": "脸颊"
138 |     },
139 |     {
140 |       "Id": "ParamBodyAngleX",
141 |       "GroupId": "",
142 |       "Name": "身体旋转 X"
143 |     },
144 |     {
145 |       "Id": "ParamBodyAngleY",
146 |       "GroupId": "",
147 |       "Name": "身体旋转 Y"
148 |     },
149 |     {
150 |       "Id": "ParamBodyAngleZ",
151 |       "GroupId": "",
152 |       "Name": "身体旋转 Z"
153 |     },
154 |     {
155 |       "Id": "ParamBreath",
156 |       "GroupId": "",
157 |       "Name": "呼吸"
158 |     },
159 |     {
160 |       "Id": "ParamHairFront",
161 |       "GroupId": "",
162 |       "Name": "摇动 前发"
163 |     },
164 |     {
165 |       "Id": "ParamHairSide",
166 |       "GroupId": "",
167 |       "Name": "摇动 侧发"
168 |     },
169 |     {
170 |       "Id": "ParamHairBack",
171 |       "GroupId": "",
172 |       "Name": "摇动 后发"
173 |     }
174 |   ],
175 |   "ParameterGroups": [
176 |     {
177 |       "Id": "ParamGroup",
178 |       "GroupId": "",
179 |       "Name": "闪电"
180 |     },
181 |     {
182 |       "Id": "ParamGroup2",
183 |       "GroupId": "",
184 |       "Name": "表情"
185 |     }
186 |   ],
187 |   "Parts": [
188 |     {
189 |       "Id": "Part11",
190 |       "Name": "demomodel.psd(未找到对应图层)"
191 |     },
192 |     {
193 |       "Id": "Part7",
194 |       "Name": "demomodel.psd(未找到对应图层)"
195 |     },
196 |     {
197 |       "Id": "Part3",
198 |       "Name": "demomodel.psd(未找到对应图层)"
199 |     },
200 |     {
201 |       "Id": "Part2",
202 |       "Name": "demomodel.psd(未找到对应图层)"
203 |     },
204 |     {
205 |       "Id": "Part",
206 |       "Name": "demomodel.psd(未找到对应图层)"
207 |     },
208 |     {
209 |       "Id": "Part10",
210 |       "Name": "天使环"
211 |     },
212 |     {
213 |       "Id": "Part5",
214 |       "Name": "demomodel.psd(未找到对应图层)"
215 |     },
216 |     {
217 |       "Id": "PartSketch0",
218 |       "Name": "[ 参考图 ]"
219 |     },
220 |     {
221 |       "Id": "Part8",
222 |       "Name": "thug life"
223 |     },
224 |     {
225 |       "Id": "Part6",
226 |       "Name": "闪电"
227 |     },
228 |     {
229 |       "Id": "Part4",
230 |       "Name": "闪电"
231 |     }
232 |   ]
233 | }
234 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/demomodel2.moc3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.moc3


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/exp_1.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "Parameters": [
 4 |     {
 5 |       "Id": "ParamEyeLOpen",
 6 |       "Value": 0.321,
 7 |       "Blend": "Multiply"
 8 |     },
 9 |     {
10 |       "Id": "ParamEyeROpen",
11 |       "Value": 0.313,
12 |       "Blend": "Multiply"
13 |     }
14 |   ]
15 | }
16 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/exp_2.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "Parameters": [
 4 |     {
 5 |       "Id": "ParamEyeLOpen",
 6 |       "Value": -1,
 7 |       "Blend": "Add"
 8 |     }
 9 |   ]
10 | }
11 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/live2d_expression0.exp3.json:
--------------------------------------------------------------------------------
1 | {
2 |   "Type": "Live2D Expression",
3 |   "Parameters": []
4 | }
5 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/live2d_expression1.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "FadeInTime": 0.8,
 4 |   "Parameters": [
 5 |     {
 6 |       "Id": "Param4",
 7 |       "Value": 1,
 8 |       "Blend": "Add"
 9 |     }
10 |   ]
11 | }
12 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/live2d_expression2.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "FadeInTime": 0.5,
 4 |   "Parameters": [
 5 |     {
 6 |       "Id": "Param5",
 7 |       "Value": 1,
 8 |       "Blend": "Add"
 9 |     }
10 |   ]
11 | }
12 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/live2d_motion1.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/live2d_motion1.flac


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/live2d_motion1.motion3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Version": 3,
 3 |   "Meta": {
 4 |     "Duration": 1.633,
 5 |     "Fps": 30.0,
 6 |     "Loop": true,
 7 |     "AreBeziersRestricted": false,
 8 |     "CurveCount": 2,
 9 |     "TotalSegmentCount": 8,
10 |     "TotalPointCount": 20,
11 |     "UserDataCount": 0,
12 |     "TotalUserDataSize": 0
13 |   },
14 |   "Curves": [
15 |     {
16 |       "Target": "Parameter",
17 |       "Id": "Param",
18 |       "Segments": [
19 |         0,
20 |         0,
21 |         1,
22 |         0.033,
23 |         0,
24 |         0.067,
25 |         1,
26 |         0.1,
27 |         1,
28 |         1,
29 |         0.411,
30 |         1,
31 |         0.722,
32 |         1,
33 |         1.033,
34 |         1,
35 |         1,
36 |         1.189,
37 |         1,
38 |         1.344,
39 |         0,
40 |         1.5,
41 |         0,
42 |         0,
43 |         1.633,
44 |         0
45 |       ]
46 |     },
47 |     {
48 |       "Target": "Parameter",
49 |       "Id": "Param2",
50 |       "Segments": [
51 |         0,
52 |         0,
53 |         0,
54 |         0.067,
55 |         0,
56 |         1,
57 |         0.1,
58 |         0,
59 |         0.133,
60 |         0.142,
61 |         0.167,
62 |         0.2,
63 |         1,
64 |         0.489,
65 |         0.764,
66 |         0.811,
67 |         1,
68 |         1.133,
69 |         1,
70 |         0,
71 |         1.633,
72 |         1
73 |       ]
74 |     }
75 |   ]
76 | }
77 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/live2d_motion2.motion3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Version": 3,
 3 |   "Meta": {
 4 |     "Duration": 2.333,
 5 |     "Fps": 30.0,
 6 |     "Loop": true,
 7 |     "AreBeziersRestricted": true,
 8 |     "CurveCount": 2,
 9 |     "TotalSegmentCount": 7,
10 |     "TotalPointCount": 21,
11 |     "UserDataCount": 0,
12 |     "TotalUserDataSize": 0
13 |   },
14 |   "Curves": [
15 |     {
16 |       "Target": "Parameter",
17 |       "Id": "CatParamLeftHandDown",
18 |       "Segments": [
19 |         0,
20 |         0,
21 |         0,
22 |         2.333,
23 |         0
24 |       ]
25 |     },
26 |     {
27 |       "Target": "Parameter",
28 |       "Id": "Param3",
29 |       "Segments": [
30 |         0,
31 |         0,
32 |         1,
33 |         0.133,
34 |         0,
35 |         0.267,
36 |         30,
37 |         0.4,
38 |         30,
39 |         1,
40 |         0.522,
41 |         30,
42 |         0.644,
43 |         0,
44 |         0.767,
45 |         0,
46 |         1,
47 |         0.9,
48 |         0,
49 |         1.033,
50 |         30,
51 |         1.167,
52 |         30,
53 |         1,
54 |         1.3,
55 |         30,
56 |         1.433,
57 |         0,
58 |         1.567,
59 |         0,
60 |         1,
61 |         1.7,
62 |         0,
63 |         1.833,
64 |         30,
65 |         1.967,
66 |         30,
67 |         1,
68 |         2.089,
69 |         30,
70 |         2.211,
71 |         0,
72 |         2.333,
73 |         0
74 |       ]
75 |     }
76 |   ]
77 | }
78 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/background.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/cover.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Alt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Alt.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/AltGr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/AltGr.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/BackQuote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/BackQuote.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Backspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Backspace.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/CapsLock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/CapsLock.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Control.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Control.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/ControlLeft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ControlLeft.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/ControlRight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ControlRight.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Delete.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Escape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Escape.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Fn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Fn.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyA.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyA.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyB.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyB.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyC.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyD.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyE.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyF.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyF.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyG.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyH.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyH.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyI.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyJ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyJ.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyK.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyL.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyM.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyN.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyO.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyP.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyQ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyQ.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyR.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyS.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyT.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyU.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyV.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyV.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyW.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyW.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyX.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyY.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyY.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/KeyZ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyZ.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Meta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Meta.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num0.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num1.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num2.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num3.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num4.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num5.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num6.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num7.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num8.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Num9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num9.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Return.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Return.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Shift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Shift.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/ShiftLeft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ShiftLeft.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/ShiftRight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ShiftRight.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Slash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Slash.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Space.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Space.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/left-keys/Tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Tab.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/right-keys/DownArrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/DownArrow.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/right-keys/LeftArrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/LeftArrow.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/right-keys/RightArrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/RightArrow.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/keyboard/resources/right-keys/UpArrow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/UpArrow.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/cat.model3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Version": 3,
 3 |   "FileReferences": {
 4 |     "Moc": "demomodel.moc3",
 5 |     "Textures": [
 6 |       "demomodel.1024/texture_00.png",
 7 |       "demomodel.1024/texture_01.png",
 8 |       "demomodel.1024/texture_02.png"
 9 |     ],
10 |     "DisplayInfo": "demomodel.cdi3.json",
11 |     "Expressions": [
12 |       {
13 |         "Name": "默认喵",
14 |         "File": "live2d_expression0.exp3.json"
15 |       },
16 |       {
17 |         "Name": "社会喵",
18 |         "File": "live2d_expression1.exp3.json",
19 |         "Description": "喵喵我叼根小烟,耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!"
20 | 
21 |       },
22 |       {
23 |         "Name": "天使喵",
24 |         "File": "live2d_expression2.exp3.json"
25 |       }
26 |     ],
27 |     "Motions": {
28 |       "CAT_motion": [
29 |         {
30 |           "Name": "雷霆喵",
31 |           "File": "live2d_motion1.motion3.json",
32 |           "Sound": "live2d_motion1.flac",
33 |           "FadeInTime": 0,
34 |           "FadeOutTime": 0
35 |         },
36 |         {
37 |           "Name": "摇摆喵",
38 |           "File": "live2d_motion2.motion3.json",
39 |           "FadeInTime": 0,
40 |           "FadeOutTime": 0
41 |         }
42 |       ]
43 |     }
44 |   },
45 |   "Groups": [
46 |     {
47 |       "Target": "Parameter",
48 |       "Name": "EyeBlink",
49 |       "Ids": [
50 |         "ParamEyeLOpen",
51 |         "ParamEyeROpen"
52 |       ]
53 |     },
54 |     {
55 |       "Target": "Parameter",
56 |       "Name": "LipSync",
57 |       "Ids": []
58 |     }
59 |   ],
60 |   "HitAreas": []
61 | }
62 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/demomodel.1024/texture_00.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.1024/texture_00.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/demomodel.1024/texture_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.1024/texture_01.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/demomodel.1024/texture_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.1024/texture_02.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/demomodel.cdi3.json:
--------------------------------------------------------------------------------
  1 | {
  2 |   "Version": 3,
  3 |   "Parameters": [
  4 |     {
  5 |       "Id": "ParamAngleX",
  6 |       "GroupId": "",
  7 |       "Name": "角度 X"
  8 |     },
  9 |     {
 10 |       "Id": "ParamAngleY",
 11 |       "GroupId": "",
 12 |       "Name": "角度 Y"
 13 |     },
 14 |     {
 15 |       "Id": "ParamMouseX",
 16 |       "GroupId": "",
 17 |       "Name": "鼠标X"
 18 |     },
 19 |     {
 20 |       "Id": "ParamMouseY",
 21 |       "GroupId": "",
 22 |       "Name": "鼠标Y"
 23 |     },
 24 |     {
 25 |       "Id": "ParamMouseLeftDown",
 26 |       "GroupId": "",
 27 |       "Name": "鼠标左键按下"
 28 |     },
 29 |     {
 30 |       "Id": "ParamMouseRightDown",
 31 |       "GroupId": "",
 32 |       "Name": "鼠标右键按下"
 33 |     },
 34 |     {
 35 |       "Id": "CatParamLeftHandDown",
 36 |       "GroupId": "",
 37 |       "Name": "键盘按下"
 38 |     },
 39 |     {
 40 |       "Id": "ParamAngleZ",
 41 |       "GroupId": "",
 42 |       "Name": "角度 Z"
 43 |     },
 44 |     {
 45 |       "Id": "ParamEyeLOpen",
 46 |       "GroupId": "",
 47 |       "Name": "左眼 开闭"
 48 |     },
 49 |     {
 50 |       "Id": "ParamEyeLSmile",
 51 |       "GroupId": "",
 52 |       "Name": "左眼 微笑"
 53 |     },
 54 |     {
 55 |       "Id": "ParamEyeROpen",
 56 |       "GroupId": "",
 57 |       "Name": "右眼"
 58 |     },
 59 |     {
 60 |       "Id": "ParamEyeRSmile",
 61 |       "GroupId": "",
 62 |       "Name": "右眼 微笑"
 63 |     },
 64 |     {
 65 |       "Id": "Param3",
 66 |       "GroupId": "",
 67 |       "Name": "挥手"
 68 |     },
 69 |     {
 70 |       "Id": "Param",
 71 |       "GroupId": "ParamGroup",
 72 |       "Name": "开启闪电"
 73 |     },
 74 |     {
 75 |       "Id": "Param2",
 76 |       "GroupId": "ParamGroup",
 77 |       "Name": "闪电划过"
 78 |     },
 79 |     {
 80 |       "Id": "Param4",
 81 |       "GroupId": "ParamGroup2",
 82 |       "Name": "表情:thuglife"
 83 |     },
 84 |     {
 85 |       "Id": "Param5",
 86 |       "GroupId": "ParamGroup2",
 87 |       "Name": "表情:升天"
 88 |     },
 89 |     {
 90 |       "Id": "ParamEyeBallX",
 91 |       "GroupId": "",
 92 |       "Name": "眼球 X"
 93 |     },
 94 |     {
 95 |       "Id": "ParamEyeBallY",
 96 |       "GroupId": "",
 97 |       "Name": "眼球 Y"
 98 |     },
 99 |     {
100 |       "Id": "ParamBrowLY",
101 |       "GroupId": "",
102 |       "Name": "左眉上下"
103 |     },
104 |     {
105 |       "Id": "ParamBrowRY",
106 |       "GroupId": "",
107 |       "Name": "右眉 上下"
108 |     },
109 |     {
110 |       "Id": "ParamBrowLX",
111 |       "GroupId": "",
112 |       "Name": "左眉 左右"
113 |     },
114 |     {
115 |       "Id": "ParamBrowRX",
116 |       "GroupId": "",
117 |       "Name": "右眉 左右"
118 |     },
119 |     {
120 |       "Id": "ParamBrowLAngle",
121 |       "GroupId": "",
122 |       "Name": "左眉 角度"
123 |     },
124 |     {
125 |       "Id": "ParamBrowRAngle",
126 |       "GroupId": "",
127 |       "Name": "右眉 角度"
128 |     },
129 |     {
130 |       "Id": "ParamBrowLForm",
131 |       "GroupId": "",
132 |       "Name": "左眉 変形"
133 |     },
134 |     {
135 |       "Id": "ParamBrowRForm",
136 |       "GroupId": "",
137 |       "Name": "右眉 変形"
138 |     },
139 |     {
140 |       "Id": "ParamMouthForm",
141 |       "GroupId": "",
142 |       "Name": "嘴部 变形"
143 |     },
144 |     {
145 |       "Id": "ParamMouthOpenY",
146 |       "GroupId": "",
147 |       "Name": "嘴巴 张开和闭合"
148 |     },
149 |     {
150 |       "Id": "ParamCheek",
151 |       "GroupId": "",
152 |       "Name": "脸颊"
153 |     },
154 |     {
155 |       "Id": "ParamBodyAngleX",
156 |       "GroupId": "",
157 |       "Name": "身体旋转 X"
158 |     },
159 |     {
160 |       "Id": "ParamBodyAngleY",
161 |       "GroupId": "",
162 |       "Name": "身体旋转 Y"
163 |     },
164 |     {
165 |       "Id": "ParamBodyAngleZ",
166 |       "GroupId": "",
167 |       "Name": "身体旋转 Z"
168 |     },
169 |     {
170 |       "Id": "ParamBreath",
171 |       "GroupId": "",
172 |       "Name": "呼吸"
173 |     },
174 |     {
175 |       "Id": "ParamHairFront",
176 |       "GroupId": "",
177 |       "Name": "摇动 前发"
178 |     },
179 |     {
180 |       "Id": "ParamHairSide",
181 |       "GroupId": "",
182 |       "Name": "摇动 侧发"
183 |     },
184 |     {
185 |       "Id": "ParamHairBack",
186 |       "GroupId": "",
187 |       "Name": "摇动 后发"
188 |     }
189 |   ],
190 |   "ParameterGroups": [
191 |     {
192 |       "Id": "ParamGroup",
193 |       "GroupId": "",
194 |       "Name": "闪电"
195 |     },
196 |     {
197 |       "Id": "ParamGroup2",
198 |       "GroupId": "",
199 |       "Name": "表情"
200 |     }
201 |   ],
202 |   "Parts": [
203 |     {
204 |       "Id": "Part11",
205 |       "Name": "demomodel.psd(未找到对应图层)"
206 |     },
207 |     {
208 |       "Id": "Part7",
209 |       "Name": "demomodel.psd(未找到对应图层)"
210 |     },
211 |     {
212 |       "Id": "Part3",
213 |       "Name": "demomodel.psd(未找到对应图层)"
214 |     },
215 |     {
216 |       "Id": "Part2",
217 |       "Name": "demomodel.psd(未找到对应图层)"
218 |     },
219 |     {
220 |       "Id": "Part",
221 |       "Name": "demomodel.psd(未找到对应图层)"
222 |     },
223 |     {
224 |       "Id": "Part10",
225 |       "Name": "天使环"
226 |     },
227 |     {
228 |       "Id": "Part5",
229 |       "Name": "demomodel.psd(未找到对应图层)"
230 |     },
231 |     {
232 |       "Id": "Part8",
233 |       "Name": "thug life"
234 |     },
235 |     {
236 |       "Id": "Part6",
237 |       "Name": "闪电"
238 |     },
239 |     {
240 |       "Id": "Part4",
241 |       "Name": "闪电"
242 |     }
243 |   ]
244 | }
245 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/demomodel.moc3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.moc3


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/exp_1.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "Parameters": [
 4 |     {
 5 |       "Id": "ParamEyeLOpen",
 6 |       "Value": 0.321,
 7 |       "Blend": "Multiply"
 8 |     },
 9 |     {
10 |       "Id": "ParamEyeROpen",
11 |       "Value": 0.313,
12 |       "Blend": "Multiply"
13 |     }
14 |   ]
15 | }
16 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/exp_2.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "Parameters": [
 4 |     {
 5 |       "Id": "ParamEyeLOpen",
 6 |       "Value": -1,
 7 |       "Blend": "Add"
 8 |     }
 9 |   ]
10 | }
11 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/live2d_expression0.exp3.json:
--------------------------------------------------------------------------------
1 | {
2 |   "Type": "Live2D Expression",
3 |   "Parameters": []
4 | }
5 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/live2d_expression1.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "FadeInTime": 0.8,
 4 |   "Parameters": [
 5 |     {
 6 |       "Id": "Param4",
 7 |       "Value": 1,
 8 |       "Blend": "Add"
 9 |     }
10 |   ]
11 | }
12 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/live2d_expression2.exp3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Type": "Live2D Expression",
 3 |   "FadeInTime": 0.5,
 4 |   "Parameters": [
 5 |     {
 6 |       "Id": "Param5",
 7 |       "Value": 1,
 8 |       "Blend": "Add"
 9 |     }
10 |   ]
11 | }
12 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/live2d_motion1.flac:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/live2d_motion1.flac


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/live2d_motion1.motion3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Version": 3,
 3 |   "Meta": {
 4 |     "Duration": 1.633,
 5 |     "Fps": 30.0,
 6 |     "Loop": true,
 7 |     "AreBeziersRestricted": false,
 8 |     "CurveCount": 2,
 9 |     "TotalSegmentCount": 8,
10 |     "TotalPointCount": 20,
11 |     "UserDataCount": 0,
12 |     "TotalUserDataSize": 0
13 |   },
14 |   "Curves": [
15 |     {
16 |       "Target": "Parameter",
17 |       "Id": "Param",
18 |       "Segments": [
19 |         0,
20 |         0,
21 |         1,
22 |         0.033,
23 |         0,
24 |         0.067,
25 |         1,
26 |         0.1,
27 |         1,
28 |         1,
29 |         0.411,
30 |         1,
31 |         0.722,
32 |         1,
33 |         1.033,
34 |         1,
35 |         1,
36 |         1.189,
37 |         1,
38 |         1.344,
39 |         0,
40 |         1.5,
41 |         0,
42 |         0,
43 |         1.633,
44 |         0
45 |       ]
46 |     },
47 |     {
48 |       "Target": "Parameter",
49 |       "Id": "Param2",
50 |       "Segments": [
51 |         0,
52 |         0,
53 |         0,
54 |         0.067,
55 |         0,
56 |         1,
57 |         0.1,
58 |         0,
59 |         0.133,
60 |         0.142,
61 |         0.167,
62 |         0.2,
63 |         1,
64 |         0.489,
65 |         0.764,
66 |         0.811,
67 |         1,
68 |         1.133,
69 |         1,
70 |         0,
71 |         1.633,
72 |         1
73 |       ]
74 |     }
75 |   ]
76 | }
77 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/live2d_motion2.motion3.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "Version": 3,
 3 |   "Meta": {
 4 |     "Duration": 2.333,
 5 |     "Fps": 30.0,
 6 |     "Loop": true,
 7 |     "AreBeziersRestricted": true,
 8 |     "CurveCount": 2,
 9 |     "TotalSegmentCount": 7,
10 |     "TotalPointCount": 21,
11 |     "UserDataCount": 0,
12 |     "TotalUserDataSize": 0
13 |   },
14 |   "Curves": [
15 |     {
16 |       "Target": "Parameter",
17 |       "Id": "CatParamLeftHandDown",
18 |       "Segments": [
19 |         0,
20 |         0,
21 |         0,
22 |         2.333,
23 |         0
24 |       ]
25 |     },
26 |     {
27 |       "Target": "Parameter",
28 |       "Id": "Param3",
29 |       "Segments": [
30 |         0,
31 |         0,
32 |         1,
33 |         0.133,
34 |         0,
35 |         0.267,
36 |         30,
37 |         0.4,
38 |         30,
39 |         1,
40 |         0.522,
41 |         30,
42 |         0.644,
43 |         0,
44 |         0.767,
45 |         0,
46 |         1,
47 |         0.9,
48 |         0,
49 |         1.033,
50 |         30,
51 |         1.167,
52 |         30,
53 |         1,
54 |         1.3,
55 |         30,
56 |         1.433,
57 |         0,
58 |         1.567,
59 |         0,
60 |         1,
61 |         1.7,
62 |         0,
63 |         1.833,
64 |         30,
65 |         1.967,
66 |         30,
67 |         1,
68 |         2.089,
69 |         30,
70 |         2.211,
71 |         0,
72 |         2.333,
73 |         0
74 |       ]
75 |     }
76 |   ]
77 | }
78 | 


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/background.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/cover.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/cover.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Alt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Alt.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/AltGr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/AltGr.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/BackQuote.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/BackQuote.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Backspace.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Backspace.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/CapsLock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/CapsLock.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Control.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Control.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/ControlLeft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ControlLeft.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/ControlRight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ControlRight.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Delete.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Delete.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Escape.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Escape.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Fn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Fn.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyA.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyA.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyB.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyB.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyC.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyC.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyD.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyD.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyE.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyE.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyF.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyF.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyG.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyG.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyH.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyH.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyI.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyI.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyJ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyJ.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyK.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyK.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyL.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyL.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyM.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyM.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyN.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyN.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyO.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyO.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyP.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyP.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyQ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyQ.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyR.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyR.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyS.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyS.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyT.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyT.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyU.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyU.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyV.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyV.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyW.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyW.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyX.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyX.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyY.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyY.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/KeyZ.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyZ.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Meta.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Meta.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num0.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num1.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num2.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num3.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num4.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num5.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num6.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num7.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num8.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Num9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num9.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Return.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Return.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Shift.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Shift.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/ShiftLeft.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ShiftLeft.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/ShiftRight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ShiftRight.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Slash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Slash.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Space.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Space.png


--------------------------------------------------------------------------------
/src-tauri/assets/models/standard/resources/left-keys/Tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Tab.png


--------------------------------------------------------------------------------
/src-tauri/assets/tray.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/tray.png


--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 |     tauri_build::build()
3 | }
4 | 


--------------------------------------------------------------------------------
/src-tauri/capabilities/default.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "../gen/schemas/desktop-schema.json",
 3 |   "identifier": "default",
 4 |   "description": "Capability for the main window",
 5 |   "windows": [
 6 |     "*"
 7 |   ],
 8 |   "permissions": [
 9 |     "core:default",
10 |     "core:window:allow-start-dragging",
11 |     "core:window:allow-set-size",
12 |     "core:window:deny-internal-toggle-maximize",
13 |     "core:window:allow-set-always-on-top",
14 |     "core:window:allow-set-ignore-cursor-events",
15 |     "core:window:allow-set-decorations",
16 |     "core:window:allow-set-position",
17 |     "custom-window:default",
18 |     "os:default",
19 |     "process:default",
20 |     "opener:default",
21 |     {
22 |       "identifier": "opener:allow-open-path",
23 |       "allow": [
24 |         {
25 |           "path": "**/*"
26 |         }
27 |       ]
28 |     },
29 |     "pinia:default",
30 |     "log:default",
31 |     "updater:default",
32 |     "prevent-default:default",
33 |     "autostart:default",
34 |     "macos-permissions:default",
35 |     "dialog:default",
36 |     "fs:default",
37 |     "fs:read-all",
38 |     "fs:write-all",
39 |     {
40 |       "identifier": "fs:scope",
41 |       "allow": [
42 |         "**/*"
43 |       ]
44 |     },
45 |     "clipboard-manager:allow-write-text",
46 |     "global-shortcut:allow-is-registered",
47 |     "global-shortcut:allow-register",
48 |     "global-shortcut:allow-unregister"
49 |   ]
50 | }
51 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/device/common.rs:
--------------------------------------------------------------------------------
 1 | use crate::core::device::{DeviceEvent, DeviceEventKind};
 2 | use rdev::{Event, EventType, listen};
 3 | use serde_json::json;
 4 | use std::sync::atomic::{AtomicBool, Ordering};
 5 | use tauri::{AppHandle, Emitter, Runtime, command};
 6 | 
 7 | static IS_RUNNING: AtomicBool = AtomicBool::new(false);
 8 | 
 9 | #[command]
10 | pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
11 |     if IS_RUNNING.load(Ordering::SeqCst) {
12 |         return Err("Device is already listening".to_string());
13 |     }
14 | 
15 |     IS_RUNNING.store(true, Ordering::SeqCst);
16 | 
17 |     let callback = move |event: Event| {
18 |         let device_event = match event.event_type {
19 |             EventType::ButtonPress(button) => DeviceEvent {
20 |                 kind: DeviceEventKind::MousePress,
21 |                 value: json!(format!("{:?}", button)),
22 |             },
23 |             EventType::ButtonRelease(button) => DeviceEvent {
24 |                 kind: DeviceEventKind::MouseRelease,
25 |                 value: json!(format!("{:?}", button)),
26 |             },
27 |             EventType::MouseMove { x, y } => DeviceEvent {
28 |                 kind: DeviceEventKind::MouseMove,
29 |                 value: json!({ "x": x, "y": y }),
30 |             },
31 |             EventType::KeyPress(key) => DeviceEvent {
32 |                 kind: DeviceEventKind::KeyboardPress,
33 |                 value: json!(format!("{:?}", key)),
34 |             },
35 |             EventType::KeyRelease(key) => DeviceEvent {
36 |                 kind: DeviceEventKind::KeyboardRelease,
37 |                 value: json!(format!("{:?}", key)),
38 |             },
39 |             _ => return,
40 |         };
41 | 
42 |         let _ = app_handle.emit("device-changed", device_event);
43 |     };
44 | 
45 |     listen(callback).map_err(|err| format!("Failed to listen device: {:?}", err))?;
46 | 
47 |     Ok(())
48 | }
49 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/device/linux.rs:
--------------------------------------------------------------------------------
  1 | use crate::core::device::{DeviceEvent, DeviceEventKind};
  2 | use input::{
  3 |     Event, Libinput, LibinputInterface,
  4 |     event::{
  5 |         PointerEvent,
  6 |         keyboard::{KeyState, KeyboardEventTrait},
  7 |         pointer::ButtonState,
  8 |     },
  9 | };
 10 | use nix::{
 11 |     libc::{O_RDONLY, O_RDWR, O_WRONLY},
 12 |     poll::{PollFd, PollFlags, PollTimeout, poll},
 13 | };
 14 | use serde_json::json;
 15 | use std::{
 16 |     fs::{File, OpenOptions},
 17 |     os::{
 18 |         fd::{AsFd, OwnedFd},
 19 |         unix::prelude::OpenOptionsExt,
 20 |     },
 21 |     path::Path,
 22 |     sync::atomic::{AtomicBool, Ordering},
 23 | };
 24 | use tauri::{AppHandle, Emitter, Runtime, command};
 25 | 
 26 | static IS_RUNNING: AtomicBool = AtomicBool::new(false);
 27 | 
 28 | pub struct Interface;
 29 | 
 30 | impl LibinputInterface for Interface {
 31 |     fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
 32 |         OpenOptions::new()
 33 |             .custom_flags(flags)
 34 |             .read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0))
 35 |             .write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0))
 36 |             .open(path)
 37 |             .map(|file| file.into())
 38 |             .map_err(|err| err.raw_os_error().unwrap())
 39 |     }
 40 | 
 41 |     #[allow(unused_must_use)]
 42 |     fn close_restricted(&mut self, fd: OwnedFd) {
 43 |         File::from(fd);
 44 |     }
 45 | }
 46 | 
 47 | pub fn keyname_from_code(code: u32) -> Option<&'static str> {
 48 |     match code {
 49 |         // Function key
 50 |         1 => Some("Escape"),
 51 |         28 => Some("Return"),
 52 |         14 => Some("Backspace"),
 53 |         15 => Some("Tab"),
 54 |         57 => Some("Space"),
 55 |         58 => Some("CapsLock"),
 56 |         99 => Some("PrintScreen"),
 57 |         70 => Some("ScrollLock"),
 58 |         119 => Some("Pause"),
 59 |         69 => Some("NumLock"),
 60 |         110 => Some("Insert"),
 61 |         102 => Some("Home"),
 62 |         107 => Some("End"),
 63 |         104 => Some("PageUp"),
 64 |         109 => Some("PageDown"),
 65 |         111 => Some("Delete"),
 66 | 
 67 |         // Arrow key
 68 |         103 => Some("UpArrow"),
 69 |         108 => Some("DownArrow"),
 70 |         105 => Some("LeftArrow"),
 71 |         106 => Some("RightArrow"),
 72 | 
 73 |         // F key
 74 |         59 => Some("F1"),
 75 |         60 => Some("F2"),
 76 |         61 => Some("F3"),
 77 |         62 => Some("F4"),
 78 |         63 => Some("F5"),
 79 |         64 => Some("F6"),
 80 |         65 => Some("F7"),
 81 |         66 => Some("F8"),
 82 |         67 => Some("F9"),
 83 |         68 => Some("F10"),
 84 |         87 => Some("F11"),
 85 |         88 => Some("F12"),
 86 | 
 87 |         // Numeric
 88 |         2 => Some("Num1"),
 89 |         3 => Some("Num2"),
 90 |         4 => Some("Num3"),
 91 |         5 => Some("Num4"),
 92 |         6 => Some("Num5"),
 93 |         7 => Some("Num6"),
 94 |         8 => Some("Num7"),
 95 |         9 => Some("Num8"),
 96 |         10 => Some("Num9"),
 97 |         11 => Some("Num0"),
 98 | 
 99 |         // Alphabetic
100 |         16 => Some("KeyQ"),
101 |         17 => Some("KeyW"),
102 |         18 => Some("KeyE"),
103 |         19 => Some("KeyR"),
104 |         20 => Some("KeyT"),
105 |         21 => Some("KeyY"),
106 |         22 => Some("KeyU"),
107 |         23 => Some("KeyI"),
108 |         24 => Some("KeyO"),
109 |         25 => Some("KeyP"),
110 |         30 => Some("KeyA"),
111 |         31 => Some("KeyS"),
112 |         32 => Some("KeyD"),
113 |         33 => Some("KeyF"),
114 |         34 => Some("KeyG"),
115 |         35 => Some("KeyH"),
116 |         36 => Some("KeyJ"),
117 |         37 => Some("KeyK"),
118 |         38 => Some("KeyL"),
119 |         44 => Some("KeyZ"),
120 |         45 => Some("KeyX"),
121 |         46 => Some("KeyC"),
122 |         47 => Some("KeyV"),
123 |         48 => Some("KeyB"),
124 |         49 => Some("KeyN"),
125 |         50 => Some("KeyM"),
126 | 
127 |         // Symbolic
128 |         41 => Some("BackQuote"),
129 |         12 => Some("Minus"),
130 |         13 => Some("Equal"),
131 |         26 => Some("LeftBracket"),
132 |         27 => Some("RightBracket"),
133 |         39 => Some("SemiColon"),
134 |         40 => Some("Quote"),
135 |         43 => Some("BackSlash"),
136 |         86 => Some("IntlBackslash"),
137 |         89 => Some("IntlRo"),
138 |         124 => Some("IntlYen"),
139 |         101 => Some("KanaMode"),
140 |         51 => Some("Comma"),
141 |         52 => Some("Dot"),
142 |         53 => Some("Slash"),
143 | 
144 |         // Control key
145 |         29 => Some("ControlLeft"),
146 |         97 => Some("ControlRight"),
147 |         42 => Some("ShiftLeft"),
148 |         54 => Some("ShiftRight"),
149 |         56 => Some("Alt"),
150 |         100 => Some("AltGr"),
151 |         125 => Some("MetaLeft"),
152 |         126 => Some("MetaRight"),
153 |         127 => Some("Apps"),
154 | 
155 |         // NumPad
156 |         55 => Some("KpMultiply"),
157 |         78 => Some("KpMinus"),
158 |         74 => Some("KpPlus"),
159 |         98 => Some("KpDivide"),
160 |         117 => Some("KpEqual"),
161 |         121 => Some("KpComma"),
162 |         96 => Some("KpReturn"),
163 |         83 => Some("KpDecimal"),
164 |         79 => Some("Kp1"),
165 |         80 => Some("Kp2"),
166 |         81 => Some("Kp3"),
167 |         75 => Some("Kp4"),
168 |         76 => Some("Kp5"),
169 |         77 => Some("Kp6"),
170 |         71 => Some("Kp7"),
171 |         72 => Some("Kp8"),
172 |         73 => Some("Kp9"),
173 |         82 => Some("Kp0"),
174 | 
175 |         // Media key
176 |         115 => Some("VolumeUp"),
177 |         114 => Some("VolumeDown"),
178 |         113 => Some("VolumeMute"),
179 | 
180 |         // Language key
181 |         90 => Some("Lang1"),
182 |         91 => Some("Lang2"),
183 |         92 => Some("Lang3"),
184 |         93 => Some("Lang4"),
185 |         94 => Some("Lang5"),
186 | 
187 |         // Other
188 |         _ => None,
189 |     }
190 | }
191 | 
192 | fn build_device_event(event: &Event) -> Option<DeviceEvent> {
193 |     match event {
194 |         Event::Keyboard(ev) => {
195 |             let key_code = ev.key();
196 |             let key_name = match keyname_from_code(key_code) {
197 |                 Some(name) => name.to_string(),
198 |                 None => format!("Unknown({})", key_code),
199 |             };
200 |             match ev.key_state() {
201 |                 KeyState::Pressed => Some(DeviceEvent {
202 |                     kind: DeviceEventKind::KeyboardPress,
203 |                     value: json!(key_name),
204 |                 }),
205 |                 KeyState::Released => Some(DeviceEvent {
206 |                     kind: DeviceEventKind::KeyboardRelease,
207 |                     value: json!(key_name),
208 |                 }),
209 |             }
210 |         }
211 |         Event::Pointer(ev) => match ev {
212 |             PointerEvent::Button(e) => {
213 |                 let btn_code = e.button();
214 |                 let btn_name = match btn_code {
215 |                     0x110 => String::from("Left"),
216 |                     0x111 => String::from("Right"),
217 |                     0x112 => String::from("Middle"),
218 |                     _ => format!("Unknown({})", btn_code as u8),
219 |                 };
220 |                 match e.button_state() {
221 |                     ButtonState::Pressed => Some(DeviceEvent {
222 |                         kind: DeviceEventKind::MousePress,
223 |                         value: json!(btn_name),
224 |                     }),
225 |                     ButtonState::Released => Some(DeviceEvent {
226 |                         kind: DeviceEventKind::MouseRelease,
227 |                         value: json!(btn_name),
228 |                     }),
229 |                 }
230 |             }
231 |             PointerEvent::Motion(e) => Some(DeviceEvent {
232 |                 kind: DeviceEventKind::MouseMove,
233 |                 value: json!({
234 |                     "x": e.dx_unaccelerated(),
235 |                     "y": e.dy_unaccelerated()
236 |                 }),
237 |             }),
238 |             _ => None,
239 |         },
240 |         _ => None,
241 |     }
242 | }
243 | 
244 | #[command]
245 | pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
246 |     if IS_RUNNING.load(Ordering::SeqCst) {
247 |         return Err("Device is already listening".to_string());
248 |     }
249 | 
250 |     IS_RUNNING.store(true, Ordering::SeqCst);
251 | 
252 |     let mut input = Libinput::new_with_udev(Interface);
253 |     match input.udev_assign_seat("seat0") {
254 |         Ok(_) => {
255 |             let input_clone = &input.clone();
256 |             let mut pollfds = [PollFd::new(input_clone.as_fd(), PollFlags::POLLIN)];
257 |             while poll(&mut pollfds, PollTimeout::NONE).is_ok() {
258 |                 input.dispatch().unwrap();
259 |                 for event in &mut input {
260 |                     let device_event = build_device_event(&event);
261 |                     if let Some(e) = device_event {
262 |                         app_handle.emit("device-changed", e).unwrap();
263 |                     }
264 |                 }
265 |             }
266 |         }
267 |         Err(err) => return Err(format!("Failed to assign seat: {:?}", err)),
268 |     }
269 | 
270 |     Ok(())
271 | }
272 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/device/mod.rs:
--------------------------------------------------------------------------------
 1 | use serde::Serialize;
 2 | use serde_json::Value;
 3 | 
 4 | #[cfg(not(target_os = "linux"))]
 5 | pub mod common;
 6 | 
 7 | #[cfg(not(target_os = "linux"))]
 8 | pub use common::*;
 9 | 
10 | #[cfg(target_os = "linux")]
11 | pub mod linux;
12 | 
13 | #[cfg(target_os = "linux")]
14 | pub use linux::*;
15 | 
16 | #[derive(Debug, Clone, Serialize)]
17 | pub enum DeviceEventKind {
18 |     MousePress,
19 |     MouseRelease,
20 |     MouseMove,
21 |     KeyboardPress,
22 |     KeyboardRelease,
23 | }
24 | 
25 | #[derive(Debug, Clone, Serialize)]
26 | pub struct DeviceEvent {
27 |     kind: DeviceEventKind,
28 |     value: Value,
29 | }
30 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod device;
2 | pub mod prevent_default;
3 | pub mod setup;
4 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/prevent_default.rs:
--------------------------------------------------------------------------------
 1 | pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {
 2 |     #[cfg(debug_assertions)]
 3 |     {
 4 |         use tauri_plugin_prevent_default::Flags;
 5 | 
 6 |         tauri_plugin_prevent_default::Builder::new()
 7 |             .with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
 8 |             .build()
 9 |     }
10 | 
11 |     #[cfg(not(debug_assertions))]
12 |     tauri_plugin_prevent_default::init()
13 | }
14 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/setup/common.rs:
--------------------------------------------------------------------------------
1 | use tauri::{AppHandle, WebviewWindow};
2 | 
3 | pub fn platform(
4 |     _app_handle: &AppHandle,
5 |     _main_window: WebviewWindow,
6 |     _preference_window: WebviewWindow,
7 | ) {
8 | }
9 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/setup/macos.rs:
--------------------------------------------------------------------------------
 1 | #![allow(deprecated)]
 2 | use tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};
 3 | use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
 4 | use tauri_plugin_custom_window::MAIN_WINDOW_LABEL;
 5 | 
 6 | #[allow(non_upper_case_globals)]
 7 | const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
 8 | #[allow(non_upper_case_globals)]
 9 | const NSResizableWindowMask: i32 = 1 << 3;
10 | const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
11 | const WINDOW_BLUR_EVENT: &str = "tauri://blur";
12 | const WINDOW_MOVED_EVENT: &str = "tauri://move";
13 | const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
14 | 
15 | pub fn platform(
16 |     app_handle: &AppHandle,
17 |     main_window: WebviewWindow,
18 |     _preference_window: WebviewWindow,
19 | ) {
20 |     let _ = app_handle.plugin(tauri_nspanel::init());
21 | 
22 |     let _ = app_handle.set_dock_visibility(false);
23 | 
24 |     let panel = main_window.to_panel().unwrap();
25 | 
26 |     panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel | NSResizableWindowMask);
27 | 
28 |     panel.set_collection_behaviour(
29 |         NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
30 |             | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
31 |             | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
32 |     );
33 | 
34 |     let delegate = panel_delegate!(EcoPanelDelegate {
35 |         window_did_become_key,
36 |         window_did_resign_key,
37 |         window_did_resize,
38 |         window_did_move
39 |     });
40 | 
41 |     delegate.set_listener(Box::new(move |delegate_name: String| {
42 |         let target = EventTarget::labeled(MAIN_WINDOW_LABEL);
43 | 
44 |         let window_move_event = || {
45 |             if let Ok(position) = main_window.outer_position() {
46 |                 let _ = main_window.emit_to(target.clone(), WINDOW_MOVED_EVENT, position);
47 |             }
48 |         };
49 | 
50 |         match delegate_name.as_str() {
51 |             "window_did_become_key" => {
52 |                 let _ = main_window.emit_to(target, WINDOW_FOCUS_EVENT, true);
53 |             }
54 |             "window_did_resign_key" => {
55 |                 let _ = main_window.emit_to(target, WINDOW_BLUR_EVENT, true);
56 |             }
57 |             "window_did_resize" => {
58 |                 window_move_event();
59 | 
60 |                 if let Ok(size) = main_window.inner_size() {
61 |                     let _ = main_window.emit_to(target, WINDOW_RESIZED_EVENT, size);
62 |                 }
63 |             }
64 |             "window_did_move" => window_move_event(),
65 |             _ => (),
66 |         }
67 |     }));
68 | 
69 |     panel.set_delegate(delegate);
70 | }
71 | 


--------------------------------------------------------------------------------
/src-tauri/src/core/setup/mod.rs:
--------------------------------------------------------------------------------
 1 | use tauri::{AppHandle, WebviewWindow};
 2 | 
 3 | #[cfg(target_os = "macos")]
 4 | mod macos;
 5 | 
 6 | #[cfg(not(target_os = "macos"))]
 7 | pub mod common;
 8 | 
 9 | #[cfg(target_os = "macos")]
10 | pub use macos::*;
11 | 
12 | #[cfg(not(target_os = "macos"))]
13 | pub use common::*;
14 | 
15 | pub fn default(
16 |     app_handle: &AppHandle,
17 |     main_window: WebviewWindow,
18 |     preference_window: WebviewWindow,
19 | ) {
20 |     #[cfg(debug_assertions)]
21 |     main_window.open_devtools();
22 | 
23 |     platform(app_handle, main_window.clone(), preference_window.clone());
24 | }
25 | 


--------------------------------------------------------------------------------
/src-tauri/src/lib.rs:
--------------------------------------------------------------------------------
 1 | mod core;
 2 | mod utils;
 3 | 
 4 | use core::{device::start_device_listening, prevent_default, setup};
 5 | use tauri::{Manager, WindowEvent, generate_handler};
 6 | use tauri_plugin_autostart::MacosLauncher;
 7 | use tauri_plugin_custom_window::{
 8 |     MAIN_WINDOW_LABEL, PREFERENCE_WINDOW_LABEL, show_preference_window,
 9 | };
10 | use utils::fs_extra::copy_dir;
11 | 
12 | #[cfg_attr(mobile, tauri::mobile_entry_point)]
13 | pub fn run() {
14 |     let app = tauri::Builder::default()
15 |         .setup(|app| {
16 |             let app_handle = app.handle();
17 | 
18 |             let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
19 | 
20 |             let preference_window = app.get_webview_window(PREFERENCE_WINDOW_LABEL).unwrap();
21 | 
22 |             setup::default(&app_handle, main_window.clone(), preference_window.clone());
23 | 
24 |             Ok(())
25 |         })
26 |         .invoke_handler(generate_handler![copy_dir, start_device_listening])
27 |         .plugin(tauri_plugin_custom_window::init())
28 |         .plugin(tauri_plugin_os::init())
29 |         .plugin(tauri_plugin_process::init())
30 |         .plugin(tauri_plugin_opener::init())
31 |         .plugin(tauri_plugin_pinia::init())
32 |         .plugin(tauri_plugin_updater::Builder::new().build())
33 |         .plugin(prevent_default::init())
34 |         .plugin(tauri_plugin_single_instance::init(
35 |             |app_handle, _argv, _cwd| {
36 |                 show_preference_window(app_handle);
37 |             },
38 |         ))
39 |         .plugin(tauri_plugin_log::Builder::new().build())
40 |         .plugin(tauri_plugin_autostart::init(
41 |             MacosLauncher::LaunchAgent,
42 |             None,
43 |         ))
44 |         .plugin(tauri_plugin_macos_permissions::init())
45 |         .plugin(tauri_plugin_dialog::init())
46 |         .plugin(tauri_plugin_fs::init())
47 |         .plugin(tauri_plugin_clipboard_manager::init())
48 |         .plugin(tauri_plugin_global_shortcut::Builder::new().build())
49 |         .on_window_event(|window, event| match event {
50 |             WindowEvent::CloseRequested { api, .. } => {
51 |                 let _ = window.hide();
52 | 
53 |                 api.prevent_close();
54 |             }
55 |             _ => {}
56 |         })
57 |         .build(tauri::generate_context!())
58 |         .expect("error while running tauri application");
59 | 
60 |     app.run(|app_handle, event| match event {
61 |         #[cfg(target_os = "macos")]
62 |         tauri::RunEvent::Reopen { .. } => {
63 |             show_preference_window(app_handle);
64 |         }
65 |         _ => {
66 |             let _ = app_handle;
67 |         }
68 |     });
69 | }
70 | 


--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
2 | 
3 | fn main() {
4 |     bongo_cat_lib::run()
5 | }
6 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/Cargo.toml:
--------------------------------------------------------------------------------
 1 | [package]
 2 | name = "tauri-plugin-custom-window"
 3 | version = "0.1.0"
 4 | authors = []
 5 | description = ""
 6 | edition = "2024"
 7 | rust-version = "1.85.0"
 8 | links = "tauri-plugin-custom-window"
 9 | 
10 | [dependencies]
11 | tauri.workspace = true
12 | serde.workspace = true
13 | 
14 | [build-dependencies]
15 | tauri-plugin.workspace = true
16 | 
17 | [target."cfg(target_os = \"macos\")".dependencies]
18 | tauri-nspanel.workspace = true
19 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/build.rs:
--------------------------------------------------------------------------------
1 | const COMMANDS: &[&str] = &["show_window", "hide_window", "set_always_on_top"];
2 | 
3 | fn main() {
4 |     tauri_plugin::Builder::new(COMMANDS).build();
5 | }
6 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/permissions/default.toml:
--------------------------------------------------------------------------------
1 | "$schema" = "schemas/schema.json"
2 | 
3 | [default]
4 | description = "Default permissions for the plugin"
5 | permissions = ["allow-show-window", "allow-hide-window", "allow-set-always-on-top"]
6 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/src/commands/common.rs:
--------------------------------------------------------------------------------
 1 | use super::{shared_hide_window, shared_set_always_on_top, shared_show_window};
 2 | use tauri::{AppHandle, Runtime, WebviewWindow, command};
 3 | 
 4 | #[command]
 5 | pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
 6 |     shared_show_window(&app_handle, &window);
 7 | }
 8 | 
 9 | #[command]
10 | pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
11 |     shared_hide_window(&app_handle, &window);
12 | }
13 | 
14 | #[command]
15 | pub async fn set_always_on_top<R: Runtime>(
16 |     app_handle: AppHandle<R>,
17 |     window: WebviewWindow<R>,
18 |     always_on_top: bool,
19 | ) {
20 |     shared_set_always_on_top(&app_handle, &window, always_on_top);
21 | }
22 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/src/commands/macos.rs:
--------------------------------------------------------------------------------
 1 | #![allow(deprecated)]
 2 | use super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window};
 3 | use crate::MAIN_WINDOW_LABEL;
 4 | use tauri::{AppHandle, Runtime, WebviewWindow, command};
 5 | use tauri_nspanel::{ManagerExt, cocoa::appkit::NSMainMenuWindowLevel};
 6 | 
 7 | pub enum MacOSPanelStatus {
 8 |     Show,
 9 |     Hide,
10 |     SetAlwaysOnTop(bool),
11 | }
12 | 
13 | #[command]
14 | pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
15 |     if is_main_window(&window) {
16 |         set_macos_panel(&app_handle, &window, MacOSPanelStatus::Show);
17 |     } else {
18 |         shared_show_window(&app_handle, &window);
19 |     }
20 | }
21 | 
22 | #[command]
23 | pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
24 |     if is_main_window(&window) {
25 |         set_macos_panel(&app_handle, &window, MacOSPanelStatus::Hide);
26 |     } else {
27 |         shared_hide_window(&app_handle, &window);
28 |     }
29 | }
30 | 
31 | #[command]
32 | pub async fn set_always_on_top<R: Runtime>(
33 |     app_handle: AppHandle<R>,
34 |     window: WebviewWindow<R>,
35 |     always_on_top: bool,
36 | ) {
37 |     if is_main_window(&window) {
38 |         set_macos_panel(
39 |             &app_handle,
40 |             &window,
41 |             MacOSPanelStatus::SetAlwaysOnTop(always_on_top),
42 |         );
43 |     } else {
44 |         shared_set_always_on_top(&app_handle, &window, always_on_top);
45 |     }
46 | }
47 | 
48 | pub fn set_macos_panel<R: Runtime>(
49 |     app_handle: &AppHandle<R>,
50 |     window: &WebviewWindow<R>,
51 |     status: MacOSPanelStatus,
52 | ) {
53 |     if is_main_window(window) {
54 |         let app_handle_clone = app_handle.clone();
55 | 
56 |         let _ = app_handle.run_on_main_thread(move || {
57 |             if let Ok(panel) = app_handle_clone.get_webview_panel(MAIN_WINDOW_LABEL) {
58 |                 match status {
59 |                     MacOSPanelStatus::Show => {
60 |                         panel.show();
61 |                     }
62 |                     MacOSPanelStatus::Hide => {
63 |                         panel.order_out(None);
64 |                     }
65 |                     MacOSPanelStatus::SetAlwaysOnTop(always_on_top) => {
66 |                         if always_on_top {
67 |                             panel.set_level(NSMainMenuWindowLevel);
68 |                         } else {
69 |                             panel.set_level(-1);
70 |                         };
71 |                     }
72 |                 }
73 |             }
74 |         });
75 |     }
76 | }
77 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/src/commands/mod.rs:
--------------------------------------------------------------------------------
 1 | use tauri::{AppHandle, Manager, Runtime, WebviewWindow, async_runtime::spawn};
 2 | 
 3 | pub static MAIN_WINDOW_LABEL: &str = "main";
 4 | pub static PREFERENCE_WINDOW_LABEL: &str = "preference";
 5 | 
 6 | #[cfg(target_os = "macos")]
 7 | mod macos;
 8 | 
 9 | #[cfg(not(target_os = "macos"))]
10 | mod common;
11 | 
12 | #[cfg(target_os = "macos")]
13 | pub use macos::*;
14 | 
15 | #[cfg(not(target_os = "macos"))]
16 | pub use common::*;
17 | 
18 | pub fn is_main_window<R: Runtime>(window: &WebviewWindow<R>) -> bool {
19 |     window.label() == MAIN_WINDOW_LABEL
20 | }
21 | 
22 | fn shared_show_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) {
23 |     let _ = window.show();
24 |     let _ = window.unminimize();
25 |     let _ = window.set_focus();
26 | }
27 | 
28 | fn shared_hide_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) {
29 |     let _ = window.hide();
30 | }
31 | 
32 | fn shared_set_always_on_top<R: Runtime>(
33 |     _app_handle: &AppHandle<R>,
34 |     window: &WebviewWindow<R>,
35 |     always_on_top: bool,
36 | ) {
37 |     if always_on_top {
38 |         let _ = window.set_always_on_bottom(false);
39 |         let _ = window.set_always_on_top(true);
40 |     } else {
41 |         let _ = window.set_always_on_top(false);
42 |         let _ = window.set_always_on_bottom(true);
43 |     }
44 | }
45 | 
46 | pub fn show_main_window(app_handle: &AppHandle) {
47 |     show_window_by_label(app_handle, MAIN_WINDOW_LABEL);
48 | }
49 | 
50 | pub fn show_preference_window(app_handle: &AppHandle) {
51 |     show_window_by_label(app_handle, PREFERENCE_WINDOW_LABEL);
52 | }
53 | 
54 | fn show_window_by_label(app_handle: &AppHandle, label: &str) {
55 |     if let Some(window) = app_handle.get_webview_window(label) {
56 |         let app_handle_clone = app_handle.clone();
57 | 
58 |         spawn(async move {
59 |             show_window(app_handle_clone, window).await;
60 |         });
61 |     }
62 | }
63 | 


--------------------------------------------------------------------------------
/src-tauri/src/plugins/window/src/lib.rs:
--------------------------------------------------------------------------------
 1 | use tauri::{
 2 |     Runtime, generate_handler,
 3 |     plugin::{Builder, TauriPlugin},
 4 | };
 5 | 
 6 | mod commands;
 7 | 
 8 | pub use commands::*;
 9 | 
10 | pub fn init<R: Runtime>() -> TauriPlugin<R> {
11 |     Builder::new("custom-window")
12 |         .invoke_handler(generate_handler![
13 |             commands::show_window,
14 |             commands::hide_window,
15 |             commands::set_always_on_top
16 |         ])
17 |         .build()
18 | }
19 | 


--------------------------------------------------------------------------------
/src-tauri/src/utils/fs_extra.rs:
--------------------------------------------------------------------------------
 1 | use fs_extra::dir::{CopyOptions, copy};
 2 | use std::fs::create_dir_all;
 3 | use tauri::command;
 4 | 
 5 | #[command]
 6 | pub async fn copy_dir(from_path: String, to_path: String) -> Result<(), String> {
 7 |     let mut options = CopyOptions::new();
 8 |     options.content_only = true;
 9 | 
10 |     create_dir_all(&to_path).map_err(|err| err.to_string())?;
11 | 
12 |     copy(from_path, to_path, &options).map_err(|err| err.to_string())?;
13 | 
14 |     Ok(())
15 | }
16 | 


--------------------------------------------------------------------------------
/src-tauri/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod fs_extra;
2 | 


--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "$schema": "https://schema.tauri.app/config/2",
 3 |   "productName": "BongoCat",
 4 |   "version": "../package.json",
 5 |   "identifier": "com.ayangweb.BongoCat",
 6 |   "build": {
 7 |     "beforeDevCommand": "pnpm dev",
 8 |     "devUrl": "http://localhost:1420",
 9 |     "beforeBuildCommand": "pnpm build",
10 |     "frontendDist": "../dist"
11 |   },
12 |   "app": {
13 |     "macOSPrivateApi": true,
14 |     "windows": [
15 |       {
16 |         "label": "main",
17 |         "title": "BongoCat",
18 |         "url": "index.html/#/",
19 |         "shadow": false,
20 |         "alwaysOnTop": true,
21 |         "transparent": true,
22 |         "decorations": false,
23 |         "acceptFirstMouse": true,
24 |         "skipTaskbar": true
25 |       },
26 |       {
27 |         "label": "preference",
28 |         "title": "偏好设置",
29 |         "url": "index.html/#/preference",
30 |         "visible": false,
31 |         "titleBarStyle": "Overlay",
32 |         "hiddenTitle": true,
33 |         "minWidth": 800,
34 |         "minHeight": 600
35 |       }
36 |     ],
37 |     "security": {
38 |       "csp": null,
39 |       "dangerousDisableAssetCspModification": true,
40 |       "assetProtocol": {
41 |         "enable": true,
42 |         "scope": {
43 |           "allow": ["**/*"],
44 |           "requireLiteralLeadingDot": false
45 |         }
46 |       }
47 |     }
48 |   },
49 |   "bundle": {
50 |     "active": true,
51 |     "category": "Game",
52 |     "createUpdaterArtifacts": true,
53 |     "targets": ["nsis", "dmg", "app", "appimage", "deb", "rpm"],
54 |     "shortDescription": "BongoCat",
55 |     "icon": [
56 |       "icons/32x32.png",
57 |       "icons/128x128.png",
58 |       "icons/128x128@2x.png",
59 |       "icons/icon.icns",
60 |       "icons/icon.ico"
61 |     ],
62 |     "resources": ["assets/tray.png", "assets/models"]
63 |   },
64 |   "plugins": {
65 |     "updater": {
66 |       "dangerousInsecureTransportProtocol": true,
67 |       "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRjJFMzE3MjEwMUZEMTAKUldRUS9RRWhGK1B5NmdkemhKcUFrVjZBQXlzdExpakdWVEJDeU9XckVsbzV2cFIycVJOempWa2UK",
68 |       "endpoints": [
69 |         "http://api.upgrade.toolsetlink.com/v1/tauri/upgrade?tauriKey=KtGlsZUVXmWfjkRKCuqpfw&versionName={{current_version}}&target={{target}}&arch={{arch}}&appointVersionName=&devModelKey=&devKey=",
70 |         "https://gh-proxy.com/github.com/ayangweb/BongoCat/releases/latest/download/latest.json"
71 |       ]
72 |     },
73 |     "fs": {
74 |       "requireLiteralLeadingDot": false
75 |     }
76 |   }
77 | }
78 | 


--------------------------------------------------------------------------------
/src-tauri/tauri.linux.conf.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "identifier": "com.ayangweb.BongoCat",
 3 |   "bundle": {
 4 |     "linux": {
 5 |       "deb": {
 6 |         "depends": ["gstreamer1.0-plugins-good"],
 7 |         "desktopTemplate": "./BongoCat.desktop"
 8 |       },
 9 |       "rpm": {
10 |         "depends": ["gstreamer1-plugins-good"],
11 |         "desktopTemplate": "./BongoCat.desktop"
12 |       }
13 |     }
14 |   }
15 | }
16 | 


--------------------------------------------------------------------------------
/src-tauri/tauri.macos.conf.json:
--------------------------------------------------------------------------------
1 | {
2 |   "identifier": "com.ayangweb.BongoCat",
3 |   "bundle": {
4 |     "resources": ["assets/tray.png", "assets/models"]
5 |   }
6 | }
7 | 


--------------------------------------------------------------------------------
/src-tauri/tauri.windows.conf.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "identifier": "com.ayangweb.BongoCat",
 3 |   "bundle": {
 4 |     "windows": {
 5 |       "digestAlgorithm": "sha256",
 6 |       "nsis": {
 7 |         "languages": ["SimpChinese"],
 8 |         "installMode": "both"
 9 |       }
10 |     }
11 |   }
12 | }
13 | 


--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
 3 | import { error } from '@tauri-apps/plugin-log'
 4 | import { openUrl } from '@tauri-apps/plugin-opener'
 5 | import { useEventListener } from '@vueuse/core'
 6 | import { ConfigProvider } from 'ant-design-vue'
 7 | import zhCN from 'ant-design-vue/es/locale/zh_CN'
 8 | import { isString } from 'es-toolkit'
 9 | import isURL from 'is-url'
10 | import { onMounted } from 'vue'
11 | import { RouterView } from 'vue-router'
12 | 
13 | import { useTauriListen } from './composables/useTauriListen'
14 | import { useThemeVars } from './composables/useThemeVars'
15 | import { useWindowState } from './composables/useWindowState'
16 | import { LISTEN_KEY } from './constants'
17 | import { hideWindow, showWindow } from './plugins/window'
18 | import { useAppStore } from './stores/app'
19 | import { useCatStore } from './stores/cat'
20 | import { useGeneralStore } from './stores/general'
21 | import { useModelStore } from './stores/model'
22 | import { useShortcutStore } from './stores/shortcut.ts'
23 | 
24 | const { generateColorVars } = useThemeVars()
25 | const appStore = useAppStore()
26 | const modelStore = useModelStore()
27 | const catStore = useCatStore()
28 | const generalStore = useGeneralStore()
29 | const shortcutStore = useShortcutStore()
30 | const appWindow = getCurrentWebviewWindow()
31 | const { isRestored, restoreState } = useWindowState()
32 | 
33 | onMounted(async () => {
34 |   generateColorVars()
35 | 
36 |   await appStore.$tauri.start()
37 |   await modelStore.$tauri.start()
38 |   await modelStore.init()
39 |   await catStore.$tauri.start()
40 |   await generalStore.$tauri.start()
41 |   await shortcutStore.$tauri.start()
42 |   await restoreState()
43 |   catStore.init()
44 | })
45 | 
46 | useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
47 |   if (appWindow.label !== payload) return
48 | 
49 |   showWindow()
50 | })
51 | 
52 | useTauriListen(LISTEN_KEY.HIDE_WINDOW, ({ payload }) => {
53 |   if (appWindow.label !== payload) return
54 | 
55 |   hideWindow()
56 | })
57 | 
58 | useEventListener('unhandledrejection', ({ reason }) => {
59 |   const message = isString(reason) ? reason : JSON.stringify(reason)
60 | 
61 |   error(message)
62 | })
63 | 
64 | useEventListener('click', (event) => {
65 |   const link = (event.target as HTMLElement).closest('a')
66 | 
67 |   if (!link) return
68 | 
69 |   const { href, target } = link
70 | 
71 |   if (target === '_blank') return
72 | 
73 |   event.preventDefault()
74 | 
75 |   if (!isURL(href)) return
76 | 
77 |   openUrl(href)
78 | })
79 | </script>
80 | 
81 | <template>
82 |   <ConfigProvider :locale="zhCN">
83 |     <RouterView v-if="isRestored" />
84 |   </ConfigProvider>
85 | </template>
86 | 


--------------------------------------------------------------------------------
/src/assets/css/global.scss:
--------------------------------------------------------------------------------
 1 | html {
 2 |   --uno: select-none overscroll-none antialiased;
 3 | 
 4 |   color-scheme: light;
 5 | 
 6 |   &.dark {
 7 |     color-scheme: dark;
 8 |   }
 9 | 
10 |   img {
11 |     -webkit-user-drag: none;
12 |   }
13 | 
14 |   button {
15 |     outline: none !important;
16 |   }
17 | 
18 |   .ant-card {
19 |     .ant-card-actions {
20 |       > li {
21 |         --uno: flex items-center justify-center;
22 |         > span {
23 |           --uno: inline-flex items-center justify-center min-w-unset;
24 |         }
25 |       }
26 |     }
27 |   }
28 | }
29 | 


--------------------------------------------------------------------------------
/src/components/pro-list-item/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { Flex } from 'ant-design-vue'
 3 | import { computed, useSlots } from 'vue'
 4 | 
 5 | const { title, description, vertical } = defineProps<{
 6 |   title: string
 7 |   description?: string
 8 |   vertical?: boolean
 9 | }>()
10 | 
11 | const slots = useSlots()
12 | 
13 | const hasDescription = computed(() => {
14 |   return description || slots.description
15 | })
16 | </script>
17 | 
18 | <template>
19 |   <Flex
20 |     :align="vertical ? void 0 : 'center'"
21 |     class="b b-color-2 rounded-lg b-solid bg-white p-4"
22 |     gap="middle"
23 |     justify="space-between"
24 |     :vertical="vertical"
25 |   >
26 |     <Flex align="center">
27 |       <Flex vertical>
28 |         <div class="text-sm font-medium">
29 |           {{ title }}
30 |         </div>
31 | 
32 |         <div
33 |           class="break-all text-xs [&_a]:(active:text-color-primary-7 hover:text-color-primary-5 text-color-3) text-color-3"
34 |           :class="{ 'mt-2': hasDescription }"
35 |         >
36 |           <slot name="description">
37 |             {{ description }}
38 |           </slot>
39 |         </div>
40 |       </Flex>
41 |     </Flex>
42 | 
43 |     <slot />
44 |   </Flex>
45 | </template>
46 | 


--------------------------------------------------------------------------------
/src/components/pro-list/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { Flex } from 'ant-design-vue'
 3 | 
 4 | const { title } = defineProps<{
 5 |   title: string
 6 | }>()
 7 | </script>
 8 | 
 9 | <template>
10 |   <Flex
11 |     class="not-last:mb-4"
12 |     gap="small"
13 |     vertical
14 |   >
15 |     <div
16 |       class="text-4 font-medium"
17 |       data-tauri-drag-region
18 |     >
19 |       {{ title }}
20 |     </div>
21 | 
22 |     <Flex
23 |       gap="middle"
24 |       vertical
25 |     >
26 |       <slot />
27 |     </Flex>
28 |   </FLex>
29 | </template>
30 | 


--------------------------------------------------------------------------------
/src/components/pro-shortcut/index.vue:
--------------------------------------------------------------------------------
  1 | <script setup lang="ts">
  2 | import type { Key } from '@/utils/keyboard'
  3 | 
  4 | import { find, map, remove, some, split } from 'es-toolkit/compat'
  5 | import { ref, useTemplateRef, watch } from 'vue'
  6 | 
  7 | import ProListItem from '@/components/pro-list-item/index.vue'
  8 | import { keys, modifierKeys, standardKeys } from '@/utils/keyboard'
  9 | 
 10 | const props = defineProps<{
 11 |   title: string
 12 |   description?: string
 13 | }>()
 14 | 
 15 | const modelValue = defineModel<string>()
 16 | const shortcutInputRef = useTemplateRef('shortcutInput')
 17 | const isFocusing = ref(false)
 18 | const isHovering = ref(false)
 19 | const pressedKeys = ref<Key[]>([])
 20 | 
 21 | watch(modelValue, () => {
 22 |   parseModelValue()
 23 | }, { immediate: true })
 24 | 
 25 | function parseModelValue() {
 26 |   if (!modelValue.value) {
 27 |     return pressedKeys.value = []
 28 |   }
 29 | 
 30 |   pressedKeys.value = split(modelValue.value, '+').map((tauriKey) => {
 31 |     return find(keys, { tauriKey })!
 32 |   })
 33 | }
 34 | 
 35 | function getEventKey(event: KeyboardEvent) {
 36 |   const { key, code } = event
 37 | 
 38 |   const eventKey = key.replace('Meta', 'Command')
 39 | 
 40 |   const isModifierKey = some(modifierKeys, { eventKey })
 41 | 
 42 |   return isModifierKey ? eventKey : code
 43 | }
 44 | 
 45 | function isValidShortcut() {
 46 |   if (pressedKeys.value?.[0]?.eventKey?.startsWith('F')) {
 47 |     return true
 48 |   }
 49 | 
 50 |   const hasModifierKey = some(pressedKeys.value, ({ eventKey }) => {
 51 |     return some(modifierKeys, { eventKey })
 52 |   })
 53 |   const hasStandardKey = some(pressedKeys.value, ({ eventKey }) => {
 54 |     return some(standardKeys, { eventKey })
 55 |   })
 56 | 
 57 |   return hasModifierKey && hasStandardKey
 58 | }
 59 | 
 60 | function handleFocus() {
 61 |   isFocusing.value = true
 62 | 
 63 |   pressedKeys.value = []
 64 | }
 65 | 
 66 | function handleBlur() {
 67 |   isFocusing.value = false
 68 | 
 69 |   if (!isValidShortcut()) {
 70 |     return parseModelValue()
 71 |   }
 72 | 
 73 |   modelValue.value = map(pressedKeys.value, 'tauriKey').join('+')
 74 | }
 75 | 
 76 | function handleKeyDown(event: KeyboardEvent) {
 77 |   const eventKey = getEventKey(event)
 78 | 
 79 |   const matched = find(keys, { eventKey })
 80 |   const isInvalid = !matched
 81 |   const isDuplicate = some(pressedKeys.value, { eventKey })
 82 | 
 83 |   if (isInvalid || isDuplicate) return
 84 | 
 85 |   pressedKeys.value.push(matched)
 86 | 
 87 |   if (isValidShortcut()) {
 88 |     shortcutInputRef.value?.blur()
 89 |   }
 90 | }
 91 | 
 92 | function handleKeyUp(event: KeyboardEvent) {
 93 |   remove(pressedKeys.value, { eventKey: getEventKey(event) })
 94 | }
 95 | </script>
 96 | 
 97 | <template>
 98 |   <ProListItem v-bind="props">
 99 |     <div
100 |       ref="shortcutInput"
101 |       align="center"
102 |       class="relative h-8 min-w-32 flex cursor-text items-center justify-center b b-color-1 hover:b-primary-5 rounded-md b-solid px-2.5 text-color-3 outline-none transition focus:(b-primary shadow-[0_0_0_2px_rgba(5,145,255,0.1)])"
103 |       justify="center"
104 |       :tabindex="0"
105 |       @blur="handleBlur"
106 |       @focus="handleFocus"
107 |       @keydown="handleKeyDown"
108 |       @keyup="handleKeyUp"
109 |       @mouseout="isHovering = false"
110 |       @mouseover="isHovering = true"
111 |     >
112 |       <span v-if="pressedKeys.length === 0">
113 |         {{ isFocusing ? '按下录制快捷键' : '点击录制快捷键' }}
114 |       </span>
115 | 
116 |       <span class="text-primary font-bold">
117 |         {{ map(pressedKeys, 'symbol').join(' ') }}
118 |       </span>
119 | 
120 |       <div
121 |         class="i-iconamoon:close-circle-1 absolute right-2 cursor-pointer text-4 transition hover:text-primary"
122 |         :hidden="isFocusing || !isHovering || pressedKeys.length === 0"
123 |         @mousedown.prevent="modelValue = ''"
124 |       />
125 |     </div>
126 |   </ProListItem>
127 | </template>
128 | 


--------------------------------------------------------------------------------
/src/components/update-app/index.vue:
--------------------------------------------------------------------------------
  1 | <script setup lang="ts">
  2 | import type { Update } from '@tauri-apps/plugin-updater'
  3 | 
  4 | import { relaunch } from '@tauri-apps/plugin-process'
  5 | import { check } from '@tauri-apps/plugin-updater'
  6 | import { useIntervalFn } from '@vueuse/core'
  7 | import { Flex, message, Modal } from 'ant-design-vue'
  8 | import dayjs from 'dayjs'
  9 | import utc from 'dayjs/plugin/utc'
 10 | import { computed, reactive, watch } from 'vue'
 11 | import VueMarkdown from 'vue-markdown-render'
 12 | 
 13 | import { useTauriListen } from '@/composables/useTauriListen'
 14 | import { GITHUB_LINK, LISTEN_KEY, UPGRADE_LINK_ACCESS_KEY } from '@/constants'
 15 | import { showWindow } from '@/plugins/window'
 16 | import { useGeneralStore } from '@/stores/general'
 17 | 
 18 | dayjs.extend(utc)
 19 | 
 20 | interface State {
 21 |   open: boolean
 22 |   update?: Update
 23 |   downloading: boolean
 24 |   totalProgress?: number
 25 |   downloadProgress: number
 26 | }
 27 | 
 28 | const generalStore = useGeneralStore()
 29 | const state = reactive<State>({
 30 |   open: false,
 31 |   downloading: false,
 32 |   downloadProgress: 0,
 33 | })
 34 | const MESSAGE_KEY = 'updatable'
 35 | 
 36 | const { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24)
 37 | 
 38 | watch(() => generalStore.autoCheckUpdate, (value) => {
 39 |   pause()
 40 | 
 41 |   if (!value) return
 42 | 
 43 |   checkUpdate()
 44 | 
 45 |   resume()
 46 | }, { immediate: true })
 47 | 
 48 | useTauriListen<boolean>(LISTEN_KEY.UPDATE_APP, () => {
 49 |   checkUpdate(true)
 50 | 
 51 |   message.loading({
 52 |     key: MESSAGE_KEY,
 53 |     duration: 0,
 54 |     content: '正在检查更新...',
 55 |   })
 56 | })
 57 | 
 58 | const downloadProgress = computed(() => {
 59 |   const { downloadProgress, totalProgress } = state
 60 | 
 61 |   if (!totalProgress) return '0%'
 62 | 
 63 |   const progress = ((downloadProgress / totalProgress) * 100).toFixed(2)
 64 | 
 65 |   return `${progress}%`
 66 | })
 67 | 
 68 | async function checkUpdate(visibleMessage = false) {
 69 |   try {
 70 |     const update = await check({
 71 |       timeout: 5000,
 72 |       headers: {
 73 |         'X-AccessKey': UPGRADE_LINK_ACCESS_KEY,
 74 |       },
 75 |     })
 76 | 
 77 |     if (update) {
 78 |       const { version, currentVersion, body = '', date, downloadAndInstall } = update
 79 | 
 80 |       state.update = Object.assign(update, {
 81 |         version: `v${version}`,
 82 |         currentVersion: `v${currentVersion}`,
 83 |         body: replaceBody(body),
 84 |         date: dayjs.utc(date?.split('.')[0]).local().format('YYYY-MM-DD HH:mm:ss'),
 85 |         downloadAndInstall: downloadAndInstall.bind(update),
 86 |       })
 87 | 
 88 |       showWindow()
 89 | 
 90 |       state.open = true
 91 | 
 92 |       message.destroy(MESSAGE_KEY)
 93 |     } else if (visibleMessage) {
 94 |       message.success({ key: MESSAGE_KEY, content: '当前已是最新版本🎉' })
 95 |     }
 96 |   } catch (error) {
 97 |     if (!visibleMessage) return
 98 | 
 99 |     message.error({ key: MESSAGE_KEY, content: String(error) })
100 |   }
101 | }
102 | 
103 | function replaceBody(body: string) {
104 |   return body
105 |     .replace(/&nbsp;/g, '')
106 |     .split('\n')
107 |     .map(line => line.replace(/\s*-\s+by\s+@.*/, ''))
108 |     .join('\n')
109 | }
110 | 
111 | async function handleOk() {
112 |   try {
113 |     state.downloading = true
114 | 
115 |     await state.update?.downloadAndInstall((progress) => {
116 |       switch (progress.event) {
117 |         case 'Started':
118 |           state.totalProgress = progress.data.contentLength ?? 0
119 |           break
120 |         case 'Progress':
121 |           state.downloadProgress += progress.data.chunkLength
122 |           break
123 |       }
124 |     })
125 | 
126 |     relaunch()
127 |   } catch (error) {
128 |     message.error(String(error))
129 |   } finally {
130 |     state.downloading = false
131 |   }
132 | }
133 | </script>
134 | 
135 | <template>
136 |   <Modal
137 |     v-model:open="state.open"
138 |     cancel-text="稍后更新"
139 |     centered
140 |     :closable="false"
141 |     :mask-closable="false"
142 |     title="发现新版本🥳"
143 |     @ok="handleOk"
144 |   >
145 |     <template #okText>
146 |       {{ state.downloading ? downloadProgress : "立即更新" }}
147 |     </template>
148 | 
149 |     <Flex
150 |       class="pt-1"
151 |       gap="small"
152 |       vertical
153 |     >
154 |       <Flex align="center">
155 |         <span>更新版本:</span>
156 |         <span>
157 |           <span>{{ state.update?.currentVersion }} 👉 </span>
158 |           <a
159 |             :href="`${GITHUB_LINK}/releases/tag/${state.update?.version}`"
160 |           >
161 |             {{ state.update?.version }}
162 |           </a>
163 |         </span>
164 |       </Flex>
165 | 
166 |       <Flex align="center">
167 |         <span>更新时间:</span>
168 |         <span>{{ state.update?.date }}</span>
169 |       </Flex>
170 | 
171 |       <Flex vertical>
172 |         <span>更新日志:</span>
173 | 
174 |         <VueMarkdown
175 |           class="update-note max-h-40 overflow-auto"
176 |           :source="state.update?.body ?? ''"
177 |         />
178 |       </Flex>
179 |     </Flex>
180 |   </Modal>
181 | </template>
182 | 
183 | <style lang="scss" scoped>
184 | .update-note {
185 |   :not(a) {
186 |     all: revert;
187 |   }
188 | }
189 | </style>
190 | 


--------------------------------------------------------------------------------
/src/composables/useDevice.ts:
--------------------------------------------------------------------------------
  1 | import type { Ref } from 'vue'
  2 | 
  3 | import { readDir } from '@tauri-apps/plugin-fs'
  4 | import { uniq } from 'es-toolkit'
  5 | import { reactive, ref, watch } from 'vue'
  6 | 
  7 | import { LISTEN_KEY } from '../constants'
  8 | 
  9 | import { useTauriListen } from './useTauriListen'
 10 | 
 11 | import { useCatStore } from '@/stores/cat'
 12 | import { useModelStore } from '@/stores/model'
 13 | import { isImage } from '@/utils/is'
 14 | import { join } from '@/utils/path'
 15 | import { isWindows } from '@/utils/platform'
 16 | 
 17 | interface MouseButtonEvent {
 18 |   kind: 'MousePress' | 'MouseRelease'
 19 |   value: string
 20 | }
 21 | 
 22 | export interface MouseMoveValue {
 23 |   x: number
 24 |   y: number
 25 | }
 26 | 
 27 | interface MouseMoveEvent {
 28 |   kind: 'MouseMove'
 29 |   value: MouseMoveValue
 30 | }
 31 | 
 32 | interface KeyboardEvent {
 33 |   kind: 'KeyboardPress' | 'KeyboardRelease'
 34 |   value: string
 35 | }
 36 | 
 37 | type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent
 38 | 
 39 | export function useDevice() {
 40 |   const supportLeftKeys = ref<string[]>([])
 41 |   const supportRightKeys = ref<string[]>([])
 42 |   const pressedMouses = ref<string[]>([])
 43 |   const mousePosition = reactive<MouseMoveValue>({ x: 0, y: 0 })
 44 |   const pressedLeftKeys = ref<string[]>([])
 45 |   const pressedRightKeys = ref<string[]>([])
 46 |   const catStore = useCatStore()
 47 |   const modelStore = useModelStore()
 48 |   const releaseTimers = new Map<string, NodeJS.Timeout>()
 49 | 
 50 |   watch(() => modelStore.currentModel, async (model) => {
 51 |     if (!model) return
 52 | 
 53 |     const keySides = [
 54 |       {
 55 |         side: 'left',
 56 |         supportKeys: supportLeftKeys,
 57 |         pressedKeys: pressedLeftKeys,
 58 |       },
 59 |       {
 60 |         side: 'right',
 61 |         supportKeys: supportRightKeys,
 62 |         pressedKeys: pressedRightKeys,
 63 |       },
 64 |     ]
 65 | 
 66 |     for await (const item of keySides) {
 67 |       const { side, supportKeys, pressedKeys } = item
 68 | 
 69 |       try {
 70 |         const files = await readDir(join(model.path, 'resources', `${side}-keys`))
 71 | 
 72 |         const imageFiles = files.filter(file => isImage(file.name))
 73 | 
 74 |         supportKeys.value = imageFiles.map((item) => {
 75 |           return item.name.split('.')[0]
 76 |         })
 77 | 
 78 |         pressedKeys.value = pressedKeys.value.filter((key) => {
 79 |           return supportKeys.value.includes(key)
 80 |         })
 81 |       } catch {
 82 |         supportKeys.value = []
 83 |         pressedKeys.value = []
 84 |       }
 85 |     }
 86 |   }, { deep: true, immediate: true })
 87 | 
 88 |   const handlePress = (array: Ref<string[]>, value?: string) => {
 89 |     if (!value) return
 90 | 
 91 |     if (catStore.singleMode) {
 92 |       array.value = [value]
 93 |     } else {
 94 |       array.value = uniq(array.value.concat(value))
 95 |     }
 96 |   }
 97 | 
 98 |   const handleRelease = (array: Ref<string[]>, value?: string) => {
 99 |     if (!value) return
100 | 
101 |     array.value = array.value.filter(item => item !== value)
102 |   }
103 | 
104 |   const getSupportedKey = (key: string) => {
105 |     for (const side of ['left', 'right']) {
106 |       let nextKey = key
107 | 
108 |       const supportKeys = side === 'left' ? supportLeftKeys.value : supportRightKeys.value
109 | 
110 |       const unsupportedKeys = !supportKeys.includes(key)
111 | 
112 |       if (key.startsWith('F') && unsupportedKeys) {
113 |         nextKey = key.replace(/F(\d+)/, 'Fn')
114 |       }
115 | 
116 |       for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
117 |         if (key.startsWith(item) && unsupportedKeys) {
118 |           const regex = new RegExp(`^(${item}).*`)
119 |           nextKey = key.replace(regex, '$1')
120 |         }
121 |       }
122 | 
123 |       if (!supportKeys.includes(nextKey)) continue
124 | 
125 |       return nextKey
126 |     }
127 |   }
128 | 
129 |   const handleScheduleRelease = (keys: Ref<string[]>, key: string, delay = 500) => {
130 |     if (releaseTimers.has(key)) {
131 |       clearTimeout(releaseTimers.get(key))
132 |     }
133 | 
134 |     const timer = setTimeout(() => {
135 |       handleRelease(keys, key)
136 | 
137 |       releaseTimers.delete(key)
138 |     }, delay)
139 | 
140 |     releaseTimers.set(key, timer)
141 |   }
142 | 
143 |   useTauriListen<DeviceEvent>(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => {
144 |     const { kind, value } = payload
145 | 
146 |     if (kind === 'KeyboardPress' || kind === 'KeyboardRelease') {
147 |       const nextValue = getSupportedKey(value)
148 | 
149 |       if (!nextValue) return
150 | 
151 |       const isLeftSide = supportLeftKeys.value.includes(nextValue)
152 | 
153 |       const pressedKeys = isLeftSide ? pressedLeftKeys : pressedRightKeys
154 | 
155 |       if (nextValue === 'CapsLock') {
156 |         handlePress(pressedKeys, nextValue)
157 | 
158 |         return handleScheduleRelease(pressedKeys, nextValue, 100)
159 |       }
160 | 
161 |       if (kind === 'KeyboardPress') {
162 |         if (isWindows) {
163 |           handleScheduleRelease(pressedKeys, nextValue)
164 |         }
165 | 
166 |         return handlePress(pressedKeys, nextValue)
167 |       }
168 | 
169 |       return handleRelease(pressedKeys, nextValue)
170 |     }
171 | 
172 |     switch (kind) {
173 |       case 'MousePress':
174 |         return handlePress(pressedMouses, value)
175 |       case 'MouseRelease':
176 |         return handleRelease(pressedMouses, value)
177 |       case 'MouseMove':
178 |         return Object.assign(mousePosition, value)
179 |     }
180 |   })
181 | 
182 |   return {
183 |     pressedMouses,
184 |     mousePosition,
185 |     pressedLeftKeys,
186 |     pressedRightKeys,
187 |   }
188 | }
189 | 


--------------------------------------------------------------------------------
/src/composables/useModel.ts:
--------------------------------------------------------------------------------
  1 | import type { MouseMoveValue } from './useDevice.ts'
  2 | import type { Monitor } from '@tauri-apps/api/window'
  3 | 
  4 | import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'
  5 | import { resolveResource } from '@tauri-apps/api/path'
  6 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
  7 | import { availableMonitors as getAvailableMonitors } from '@tauri-apps/api/window'
  8 | import { message } from 'ant-design-vue'
  9 | import { isNil, round } from 'es-toolkit'
 10 | import { computed, onBeforeMount, ref, watch } from 'vue'
 11 | 
 12 | import live2d from '../utils/live2d'
 13 | import { getCursorMonitor } from '../utils/monitor'
 14 | 
 15 | import { useCatStore } from '@/stores/cat'
 16 | import { useModelStore } from '@/stores/model'
 17 | 
 18 | const appWindow = getCurrentWebviewWindow()
 19 | 
 20 | interface ModelSize {
 21 |   width: number
 22 |   height: number
 23 | }
 24 | 
 25 | export function useModel() {
 26 |   const modelStore = useModelStore()
 27 |   const catStore = useCatStore()
 28 |   const modelSize = ref<ModelSize>()
 29 |   const availableMonitors = ref<Monitor[]>([])
 30 | 
 31 |   const isOnlySingleMonitor = computed(() => availableMonitors.value.length === 1)
 32 | 
 33 |   onBeforeMount(async () => {
 34 |     availableMonitors.value = await getAvailableMonitors()
 35 |   })
 36 | 
 37 |   watch(() => modelStore.currentModel, handleLoad, { deep: true, immediate: true })
 38 | 
 39 |   watch([() => catStore.scale, modelSize], async () => {
 40 |     if (!modelSize.value) return
 41 | 
 42 |     const { width, height } = modelSize.value
 43 | 
 44 |     appWindow.setSize(
 45 |       new PhysicalSize({
 46 |         width: round(width * (catStore.scale / 100)),
 47 |         height: round(height * (catStore.scale / 100)),
 48 |       }),
 49 |     )
 50 |   }, { immediate: true })
 51 | 
 52 |   async function handleLoad() {
 53 |     try {
 54 |       if (!modelStore.currentModel) return
 55 | 
 56 |       const { path } = modelStore.currentModel
 57 | 
 58 |       await resolveResource(path)
 59 | 
 60 |       const { width, height, ...rest } = await live2d.load(path)
 61 | 
 62 |       modelSize.value = { width, height }
 63 | 
 64 |       handleResize()
 65 | 
 66 |       Object.assign(modelStore, rest)
 67 |     } catch (error) {
 68 |       message.error(String(error))
 69 |     }
 70 |   }
 71 | 
 72 |   function handleDestroy() {
 73 |     live2d.destroy()
 74 |   }
 75 | 
 76 |   async function handleResize() {
 77 |     if (!modelSize.value) return
 78 | 
 79 |     live2d.fitModel()
 80 | 
 81 |     const { width, height } = modelSize.value
 82 | 
 83 |     if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) {
 84 |       await appWindow.setSize(
 85 |         new LogicalSize({
 86 |           width: innerWidth,
 87 |           height: Math.ceil(innerWidth * (height / width)),
 88 |         }),
 89 |       )
 90 |     }
 91 | 
 92 |     const size = await appWindow.size()
 93 | 
 94 |     catStore.scale = round((size.width / width) * 100)
 95 |   }
 96 | 
 97 |   function handleKeyDown(side: 'left' | 'right', pressed: boolean) {
 98 |     const id = side === 'left' ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'
 99 | 
100 |     const { min, max } = live2d.getParameterRange(id)
101 | 
102 |     live2d.setParameterValue(id, pressed ? max : min)
103 |   }
104 | 
105 |   async function _getCursorMonitor(mousePosition: MouseMoveValue) {
106 |     return isOnlySingleMonitor.value
107 |       ? { ...availableMonitors.value[0], cursorPosition: mousePosition }
108 |       : await getCursorMonitor()
109 |   }
110 | 
111 |   async function handleMouseMove(mousePosition: MouseMoveValue) {
112 |     const monitor = await _getCursorMonitor(mousePosition)
113 | 
114 |     if (!monitor) return
115 | 
116 |     const { size, position, cursorPosition } = monitor
117 | 
118 |     const xRatio = (cursorPosition.x - position.x) / size.width
119 |     const yRatio = (cursorPosition.y - position.y) / size.height
120 | 
121 |     for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) {
122 |       const { min, max } = live2d.getParameterRange(id)
123 | 
124 |       if (isNil(min) || isNil(max)) continue
125 | 
126 |       const isXAxis = id.endsWith('X')
127 | 
128 |       const ratio = isXAxis ? xRatio : yRatio
129 |       let value = max - (ratio * (max - min))
130 | 
131 |       if (isXAxis && catStore.mouseMirror) {
132 |         value *= -1
133 |       }
134 | 
135 |       live2d.setParameterValue(id, value)
136 |     }
137 |   }
138 | 
139 |   function handleMouseDown(value: string[]) {
140 |     const params = {
141 |       ParamMouseLeftDown: value.includes('Left'),
142 |       ParamMouseRightDown: value.includes('Right'),
143 |     }
144 | 
145 |     for (const [id, pressed] of Object.entries(params)) {
146 |       const { min, max } = live2d.getParameterRange(id)
147 | 
148 |       live2d.setParameterValue(id, pressed ? max : min)
149 |     }
150 |   }
151 | 
152 |   return {
153 |     handleLoad,
154 |     handleDestroy,
155 |     handleResize,
156 |     handleKeyDown,
157 |     handleMouseMove,
158 |     handleMouseDown,
159 |   }
160 | }
161 | 


--------------------------------------------------------------------------------
/src/composables/useSharedMenu.ts:
--------------------------------------------------------------------------------
 1 | import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
 2 | import { range } from 'es-toolkit'
 3 | 
 4 | import { showWindow } from '@/plugins/window'
 5 | import { useCatStore } from '@/stores/cat'
 6 | import { isMac } from '@/utils/platform'
 7 | 
 8 | export function useSharedMenu() {
 9 |   const catStore = useCatStore()
10 | 
11 |   const getScaleMenuItems = async () => {
12 |     const options = range(50, 151, 25)
13 | 
14 |     const items = options.map((item) => {
15 |       return CheckMenuItem.new({
16 |         text: item === 100 ? '默认' : `${item}%`,
17 |         checked: catStore.scale === item,
18 |         action: () => {
19 |           catStore.scale = item
20 |         },
21 |       })
22 |     })
23 | 
24 |     if (!options.includes(catStore.scale)) {
25 |       items.unshift(CheckMenuItem.new({
26 |         text: `${catStore.scale}%`,
27 |         checked: true,
28 |         enabled: false,
29 |       }))
30 |     }
31 | 
32 |     return Promise.all(items)
33 |   }
34 | 
35 |   const getOpacityMenuItems = async () => {
36 |     const options = range(25, 101, 25)
37 | 
38 |     const items = options.map((item) => {
39 |       return CheckMenuItem.new({
40 |         text: `${item}%`,
41 |         checked: catStore.opacity === item,
42 |         action: () => {
43 |           catStore.opacity = item
44 |         },
45 |       })
46 |     })
47 | 
48 |     if (!options.includes(catStore.opacity)) {
49 |       items.unshift(CheckMenuItem.new({
50 |         text: `${catStore.opacity}%`,
51 |         checked: true,
52 |         enabled: false,
53 |       }))
54 |     }
55 | 
56 |     return Promise.all(items)
57 |   }
58 | 
59 |   const getSharedMenu = async () => {
60 |     return await Promise.all([
61 |       MenuItem.new({
62 |         text: '偏好设置...',
63 |         accelerator: isMac ? 'Cmd+,' : '',
64 |         action: () => showWindow('preference'),
65 |       }),
66 |       MenuItem.new({
67 |         text: catStore.visible ? '隐藏猫咪' : '显示猫咪',
68 |         action: () => {
69 |           catStore.visible = !catStore.visible
70 |         },
71 |       }),
72 |       PredefinedMenuItem.new({ item: 'Separator' }),
73 |       CheckMenuItem.new({
74 |         text: '窗口穿透',
75 |         checked: catStore.penetrable,
76 |         action: () => {
77 |           catStore.penetrable = !catStore.penetrable
78 |         },
79 |       }),
80 |       Submenu.new({
81 |         text: '窗口尺寸',
82 |         items: await getScaleMenuItems(),
83 |       }),
84 |       Submenu.new({
85 |         text: '不透明度',
86 |         items: await getOpacityMenuItems(),
87 |       }),
88 |     ])
89 |   }
90 | 
91 |   return {
92 |     getSharedMenu,
93 |   }
94 | }
95 | 


--------------------------------------------------------------------------------
/src/composables/useTauriKeyPress.ts:
--------------------------------------------------------------------------------
 1 | import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut'
 2 | import type { Ref } from 'vue'
 3 | 
 4 | import {
 5 |   isRegistered,
 6 |   register,
 7 |   unregister,
 8 | } from '@tauri-apps/plugin-global-shortcut'
 9 | import { ref, watch } from 'vue'
10 | 
11 | export function useTauriKeyPress(shortcut: Ref<string, string>, callback: ShortcutHandler) {
12 |   const oldShortcut = ref(shortcut.value)
13 | 
14 |   watch(shortcut, async (value) => {
15 |     if (oldShortcut.value) {
16 |       const registered = await isRegistered(oldShortcut.value)
17 | 
18 |       if (registered) {
19 |         await unregister(oldShortcut.value)
20 |       }
21 |     }
22 | 
23 |     if (!value) return
24 | 
25 |     await register(value, (event) => {
26 |       if (event.state === 'Released') return
27 | 
28 |       callback(event)
29 |     })
30 | 
31 |     oldShortcut.value = value
32 |   }, { immediate: true })
33 | }
34 | 


--------------------------------------------------------------------------------
/src/composables/useTauriListen.ts:
--------------------------------------------------------------------------------
 1 | import { listen } from '@tauri-apps/api/event'
 2 | import { noop } from '@vueuse/core'
 3 | import { onMounted, onUnmounted, ref } from 'vue'
 4 | 
 5 | export function useTauriListen<T>(...args: Parameters<typeof listen<T>>) {
 6 |   const unlisten = ref(noop)
 7 | 
 8 |   onMounted(async () => {
 9 |     unlisten.value = await listen<T>(...args)
10 |   })
11 | 
12 |   onUnmounted(() => {
13 |     unlisten.value()
14 |   })
15 | }
16 | 


--------------------------------------------------------------------------------
/src/composables/useThemeVars.ts:
--------------------------------------------------------------------------------
 1 | import { theme } from 'ant-design-vue'
 2 | import { kebabCase } from 'es-toolkit'
 3 | 
 4 | export function useThemeVars() {
 5 |   const { defaultAlgorithm, darkAlgorithm, defaultConfig } = theme
 6 | 
 7 |   const generateColorVars = () => {
 8 |     const { token } = defaultConfig
 9 | 
10 |     const colors = [
11 |       defaultAlgorithm(token),
12 |       darkAlgorithm(token),
13 |     ]
14 | 
15 |     for (const [index, item] of colors.entries()) {
16 |       const isDark = index !== 0
17 |       const vars: Record<string, any> = {}
18 | 
19 |       for (const [key, value] of Object.entries(item)) {
20 |         vars[`--ant-${kebabCase(key)}`] = value
21 |       }
22 | 
23 |       const style = document.createElement('style')
24 |       style.dataset.theme = isDark ? 'dark' : 'light'
25 |       const selector = isDark ? 'html.dark' : ':root'
26 |       const values = Object.entries(vars).map(([key, value]) => `${key}: ${value};`)
27 | 
28 |       style.innerHTML = `${selector}{\n${values.join('\n')}\n}`
29 |       document.head.appendChild(style)
30 |     }
31 |   }
32 | 
33 |   return {
34 |     generateColorVars,
35 |   }
36 | }
37 | 


--------------------------------------------------------------------------------
/src/composables/useTray.ts:
--------------------------------------------------------------------------------
  1 | import type { TrayIconOptions } from '@tauri-apps/api/tray'
  2 | 
  3 | import { getName, getVersion } from '@tauri-apps/api/app'
  4 | import { emit } from '@tauri-apps/api/event'
  5 | import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
  6 | import { resolveResource } from '@tauri-apps/api/path'
  7 | import { TrayIcon } from '@tauri-apps/api/tray'
  8 | import { openUrl } from '@tauri-apps/plugin-opener'
  9 | import { exit, relaunch } from '@tauri-apps/plugin-process'
 10 | import { watchDebounced } from '@vueuse/core'
 11 | import { watch } from 'vue'
 12 | 
 13 | import { GITHUB_LINK, LISTEN_KEY } from '../constants'
 14 | import { showWindow } from '../plugins/window'
 15 | import { isMac } from '../utils/platform'
 16 | 
 17 | import { useSharedMenu } from './useSharedMenu'
 18 | 
 19 | import { useCatStore } from '@/stores/cat'
 20 | 
 21 | const TRAY_ID = 'BONGO_CAT_TRAY'
 22 | 
 23 | export function useTray() {
 24 |   const catStore = useCatStore()
 25 |   const { getSharedMenu } = useSharedMenu()
 26 | 
 27 |   watch([() => catStore.visible, () => catStore.penetrable], () => {
 28 |     updateTrayMenu()
 29 |   })
 30 | 
 31 |   watchDebounced([() => catStore.scale, () => catStore.opacity], () => {
 32 |     updateTrayMenu()
 33 |   }, { debounce: 200 })
 34 | 
 35 |   const createTray = async () => {
 36 |     const tray = await getTrayById()
 37 | 
 38 |     if (tray) return
 39 | 
 40 |     const appName = await getName()
 41 |     const appVersion = await getVersion()
 42 | 
 43 |     const menu = await getTrayMenu()
 44 | 
 45 |     const icon = await resolveResource('assets/tray.png')
 46 | 
 47 |     const options: TrayIconOptions = {
 48 |       menu,
 49 |       icon,
 50 |       id: TRAY_ID,
 51 |       tooltip: `${appName} v${appVersion}`,
 52 |       iconAsTemplate: false,
 53 |       menuOnLeftClick: true,
 54 |     }
 55 | 
 56 |     return TrayIcon.new(options)
 57 |   }
 58 | 
 59 |   const getTrayById = () => {
 60 |     return TrayIcon.getById(TRAY_ID)
 61 |   }
 62 | 
 63 |   const getTrayMenu = async () => {
 64 |     const appVersion = await getVersion()
 65 | 
 66 |     const items = await Promise.all([
 67 |       ...await getSharedMenu(),
 68 |       PredefinedMenuItem.new({ item: 'Separator' }),
 69 |       MenuItem.new({
 70 |         text: '检查更新',
 71 |         action: () => {
 72 |           showWindow()
 73 | 
 74 |           emit(LISTEN_KEY.UPDATE_APP)
 75 |         },
 76 |       }),
 77 |       MenuItem.new({
 78 |         text: '开源地址',
 79 |         action: () => openUrl(GITHUB_LINK),
 80 |       }),
 81 |       PredefinedMenuItem.new({ item: 'Separator' }),
 82 |       MenuItem.new({
 83 |         text: `版本 ${appVersion}`,
 84 |         enabled: false,
 85 |       }),
 86 |       MenuItem.new({
 87 |         text: '重启应用',
 88 |         action: relaunch,
 89 |       }),
 90 |       MenuItem.new({
 91 |         text: '退出应用',
 92 |         accelerator: isMac ? 'Cmd+Q' : '',
 93 |         action: () => exit(0),
 94 |       }),
 95 |     ])
 96 | 
 97 |     return Menu.new({ items })
 98 |   }
 99 | 
100 |   const updateTrayMenu = async () => {
101 |     const tray = await getTrayById()
102 | 
103 |     if (!tray) return
104 | 
105 |     const menu = await getTrayMenu()
106 | 
107 |     tray.setMenu(menu)
108 |   }
109 | 
110 |   return {
111 |     createTray,
112 |   }
113 | }
114 | 


--------------------------------------------------------------------------------
/src/composables/useWindowState.ts:
--------------------------------------------------------------------------------
 1 | import type { Event } from '@tauri-apps/api/event'
 2 | 
 3 | import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
 4 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
 5 | import { availableMonitors } from '@tauri-apps/api/window'
 6 | import { isNumber } from 'es-toolkit/compat'
 7 | import { onMounted, ref } from 'vue'
 8 | 
 9 | import { useAppStore } from '@/stores/app'
10 | 
11 | export type WindowState = Record<string, Partial<PhysicalPosition & PhysicalSize> | undefined>
12 | 
13 | const appWindow = getCurrentWebviewWindow()
14 | const { label } = appWindow
15 | 
16 | export function useWindowState() {
17 |   const appStore = useAppStore()
18 |   const isRestored = ref(false)
19 | 
20 |   onMounted(() => {
21 |     appWindow.onMoved(onChange)
22 | 
23 |     appWindow.onResized(onChange)
24 |   })
25 | 
26 |   const onChange = async (event: Event<PhysicalPosition | PhysicalSize>) => {
27 |     const minimized = await appWindow.isMinimized()
28 | 
29 |     if (minimized) return
30 | 
31 |     appStore.windowState[label] ??= {}
32 | 
33 |     Object.assign(appStore.windowState[label], event.payload)
34 |   }
35 | 
36 |   const restoreState = async () => {
37 |     const { x, y, width, height } = appStore.windowState[label] ?? {}
38 | 
39 |     if (isNumber(x) && isNumber(y)) {
40 |       const monitors = await availableMonitors()
41 | 
42 |       const monitor = monitors.find((monitor) => {
43 |         const { position, size } = monitor
44 | 
45 |         const inBoundsX = x >= position.x && x <= position.x + size.width
46 |         const inBoundsY = y >= position.y && y <= position.y + size.height
47 | 
48 |         return inBoundsX && inBoundsY
49 |       })
50 | 
51 |       if (monitor) {
52 |         await appWindow.setPosition(new PhysicalPosition(x, y))
53 |       }
54 |     }
55 | 
56 |     if (width && height) {
57 |       await appWindow.setSize(new PhysicalSize(width, height))
58 |     }
59 | 
60 |     isRestored.value = true
61 |   }
62 | 
63 |   return {
64 |     isRestored,
65 |     restoreState,
66 |   }
67 | }
68 | 


--------------------------------------------------------------------------------
/src/constants/index.ts:
--------------------------------------------------------------------------------
 1 | export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'
 2 | 
 3 | export const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'
 4 | 
 5 | export const LISTEN_KEY = {
 6 |   SHOW_WINDOW: 'show-window',
 7 |   HIDE_WINDOW: 'hide-window',
 8 |   DEVICE_CHANGED: 'device-changed',
 9 |   UPDATE_APP: 'update-app',
10 | }
11 | 
12 | export const INVOKE_KEY = {
13 |   COPY_DIR: 'copy_dir',
14 |   START_DEVICE_LISTENING: 'start_device_listening',
15 | }
16 | 


--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
 1 | import { createPlugin } from '@tauri-store/pinia'
 2 | import { createPinia } from 'pinia'
 3 | import { createApp } from 'vue'
 4 | 
 5 | import App from './App.vue'
 6 | import router from './router'
 7 | import 'virtual:uno.css'
 8 | import 'ant-design-vue/dist/reset.css'
 9 | import './assets/css/global.scss'
10 | 
11 | const pinia = createPinia()
12 | pinia.use(createPlugin({ saveOnChange: true }))
13 | 
14 | createApp(App).use(router).use(pinia).mount('#app')
15 | 


--------------------------------------------------------------------------------
/src/pages/main/index.vue:
--------------------------------------------------------------------------------
  1 | <script setup lang="ts">
  2 | import type { MouseMoveValue } from '@/composables/useDevice'
  3 | 
  4 | import { convertFileSrc, invoke } from '@tauri-apps/api/core'
  5 | import { Menu } from '@tauri-apps/api/menu'
  6 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
  7 | import { exists } from '@tauri-apps/plugin-fs'
  8 | import { useDebounceFn, useEventListener, useRafFn } from '@vueuse/core'
  9 | import { isEqual } from 'es-toolkit'
 10 | import { onMounted, onUnmounted, ref, watch } from 'vue'
 11 | 
 12 | import { useDevice } from '@/composables/useDevice'
 13 | import { useModel } from '@/composables/useModel'
 14 | import { useSharedMenu } from '@/composables/useSharedMenu'
 15 | import { INVOKE_KEY } from '@/constants'
 16 | import { hideWindow, setAlwaysOnTop, showWindow } from '@/plugins/window'
 17 | import { useCatStore } from '@/stores/cat'
 18 | import { useModelStore } from '@/stores/model'
 19 | import { join } from '@/utils/path'
 20 | 
 21 | const appWindow = getCurrentWebviewWindow()
 22 | const { pressedMouses, mousePosition, pressedLeftKeys, pressedRightKeys } = useDevice()
 23 | const { handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel()
 24 | const catStore = useCatStore()
 25 | const { getSharedMenu } = useSharedMenu()
 26 | const modelStore = useModelStore()
 27 | const resizing = ref(false)
 28 | const backgroundImagePath = ref<string>()
 29 | 
 30 | onMounted(() => {
 31 |   invoke(INVOKE_KEY.START_DEVICE_LISTENING)
 32 | })
 33 | 
 34 | onUnmounted(handleDestroy)
 35 | 
 36 | const debouncedResize = useDebounceFn(async () => {
 37 |   await handleResize()
 38 | 
 39 |   resizing.value = false
 40 | }, 100)
 41 | 
 42 | useEventListener('resize', () => {
 43 |   resizing.value = true
 44 | 
 45 |   debouncedResize()
 46 | })
 47 | 
 48 | watch(pressedMouses, handleMouseDown)
 49 | 
 50 | useRafFn((() => {
 51 |   const cached: MouseMoveValue = { x: 0, y: 0 }
 52 |   return () => {
 53 |     if (isEqual(cached, mousePosition)) return
 54 |     Object.assign(cached, mousePosition)
 55 |     handleMouseMove(mousePosition)
 56 |   }
 57 | })())
 58 | 
 59 | watch(pressedLeftKeys, (keys) => {
 60 |   handleKeyDown('left', keys.length > 0)
 61 | })
 62 | 
 63 | watch(pressedRightKeys, (keys) => {
 64 |   handleKeyDown('right', keys.length > 0)
 65 | })
 66 | 
 67 | watch(() => catStore.visible, async (value) => {
 68 |   value ? showWindow() : hideWindow()
 69 | })
 70 | 
 71 | watch(() => catStore.penetrable, (value) => {
 72 |   appWindow.setIgnoreCursorEvents(value)
 73 | }, { immediate: true })
 74 | 
 75 | watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
 76 | 
 77 | watch(() => modelStore.currentModel, async (model) => {
 78 |   if (!model) return
 79 | 
 80 |   const path = join(model.path, 'resources', 'background.png')
 81 | 
 82 |   const existed = await exists(path)
 83 | 
 84 |   backgroundImagePath.value = existed ? convertFileSrc(path) : void 0
 85 | }, { deep: true, immediate: true })
 86 | 
 87 | function handleWindowDrag() {
 88 |   appWindow.startDragging()
 89 | }
 90 | 
 91 | async function handleContextmenu(event: MouseEvent) {
 92 |   event.preventDefault()
 93 | 
 94 |   const menu = await Menu.new({
 95 |     items: await getSharedMenu(),
 96 |   })
 97 | 
 98 |   menu.popup()
 99 | }
100 | 
101 | function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') {
102 |   return convertFileSrc(join(modelStore.currentModel!.path, 'resources', `${side}-keys`, `${key}.png`))
103 | }
104 | </script>
105 | 
106 | <template>
107 |   <div
108 |     class="relative size-screen overflow-hidden children:(absolute size-full)"
109 |     :class="{ '-scale-x-100': catStore.mirrorMode }"
110 |     :style="{ opacity: catStore.opacity / 100 }"
111 |     @contextmenu="handleContextmenu"
112 |     @mousedown="handleWindowDrag"
113 |   >
114 |     <img
115 |       v-if="backgroundImagePath"
116 |       :src="backgroundImagePath"
117 |     >
118 | 
119 |     <canvas id="live2dCanvas" />
120 | 
121 |     <img
122 |       v-for="key in pressedLeftKeys"
123 |       :key="key"
124 |       :src="resolveKeyImagePath(key)"
125 |     >
126 | 
127 |     <img
128 |       v-for="key in pressedRightKeys"
129 |       :key="key"
130 |       :src="resolveKeyImagePath(key, 'right')"
131 |     >
132 | 
133 |     <div
134 |       v-show="resizing"
135 |       class="flex items-center justify-center bg-black"
136 |     >
137 |       <span class="text-center text-5xl text-white">
138 |         重绘中...
139 |       </span>
140 |     </div>
141 |   </div>
142 | </template>
143 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/about/index.vue:
--------------------------------------------------------------------------------
  1 | <script setup lang="ts">
  2 | import { getTauriVersion } from '@tauri-apps/api/app'
  3 | import { emit } from '@tauri-apps/api/event'
  4 | import { appLogDir } from '@tauri-apps/api/path'
  5 | import { writeText } from '@tauri-apps/plugin-clipboard-manager'
  6 | import { openPath, openUrl } from '@tauri-apps/plugin-opener'
  7 | import { arch, platform, version } from '@tauri-apps/plugin-os'
  8 | import { Button, message } from 'ant-design-vue'
  9 | import { onMounted, ref } from 'vue'
 10 | 
 11 | import ProList from '@/components/pro-list/index.vue'
 12 | import ProListItem from '@/components/pro-list-item/index.vue'
 13 | import { GITHUB_LINK, LISTEN_KEY } from '@/constants'
 14 | import { useAppStore } from '@/stores/app'
 15 | 
 16 | const appStore = useAppStore()
 17 | const logDir = ref('')
 18 | 
 19 | onMounted(async () => {
 20 |   logDir.value = await appLogDir()
 21 | })
 22 | 
 23 | function handleUpdate() {
 24 |   emit(LISTEN_KEY.UPDATE_APP)
 25 | }
 26 | 
 27 | async function copyInfo() {
 28 |   const info = {
 29 |     appName: appStore.name,
 30 |     appVersion: appStore.version,
 31 |     tauriVersion: await getTauriVersion(),
 32 |     platform: platform(),
 33 |     platformArch: arch(),
 34 |     platformVersion: version(),
 35 |   }
 36 | 
 37 |   await writeText(JSON.stringify(info, null, 2))
 38 | 
 39 |   message.success('复制成功')
 40 | }
 41 | 
 42 | function feedbackIssue() {
 43 |   openUrl(`${GITHUB_LINK}/issues/new/choose`)
 44 | }
 45 | </script>
 46 | 
 47 | <template>
 48 |   <ProList title="关于软件">
 49 |     <ProListItem
 50 |       :description="`版本:v${appStore.version}`"
 51 |       :title="appStore.name"
 52 |     >
 53 |       <Button
 54 |         type="primary"
 55 |         @click="handleUpdate"
 56 |       >
 57 |         检查更新
 58 |       </Button>
 59 | 
 60 |       <template #icon>
 61 |         <div class="b b-color-2 rounded-xl b-solid">
 62 |           <img
 63 |             class="size-12"
 64 |             src="/logo.png"
 65 |           >
 66 |         </div>
 67 |       </template>
 68 |     </ProListItem>
 69 | 
 70 |     <ProListItem
 71 |       description="复制软件信息并提供给 Bug Issue"
 72 |       title="软件信息"
 73 |     >
 74 |       <Button @click="copyInfo">
 75 |         复制
 76 |       </Button>
 77 |     </ProListItem>
 78 | 
 79 |     <ProListItem title="开源地址">
 80 |       <Button
 81 |         danger
 82 |         @click="feedbackIssue"
 83 |       >
 84 |         反馈问题
 85 |       </Button>
 86 | 
 87 |       <template #description>
 88 |         <a :href="GITHUB_LINK">
 89 |           {{ GITHUB_LINK }}
 90 |         </a>
 91 |       </template>
 92 |     </ProListItem>
 93 | 
 94 |     <ProListItem
 95 |       :description="logDir"
 96 |       title="软件日志"
 97 |     >
 98 |       <Button @click="openPath(logDir)">
 99 |         查看日志
100 |       </Button>
101 |     </ProListItem>
102 |   </ProList>
103 | </template>
104 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/cat/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { InputNumber, Slider, Switch } from 'ant-design-vue'
 3 | 
 4 | import ProList from '@/components/pro-list/index.vue'
 5 | import ProListItem from '@/components/pro-list-item/index.vue'
 6 | import { useCatStore } from '@/stores/cat'
 7 | 
 8 | const catStore = useCatStore()
 9 | 
10 | function opacityFormatter(value?: number) {
11 |   return `${value}%`
12 | }
13 | </script>
14 | 
15 | <template>
16 |   <ProList title="模型设置">
17 |     <ProListItem
18 |       description="启用后,模型将水平镜像翻转"
19 |       title="镜像模式"
20 |     >
21 |       <Switch v-model:checked="catStore.mirrorMode" />
22 |     </ProListItem>
23 | 
24 |     <ProListItem
25 |       description="启用后,每只手只显示最后按下的一个按键"
26 |       title="单键模式"
27 |     >
28 |       <Switch v-model:checked="catStore.singleMode" />
29 |     </ProListItem>
30 | 
31 |     <ProListItem
32 |       description="启用后,鼠标将镜像跟随手部移动"
33 |       title="鼠标镜像"
34 |     >
35 |       <Switch v-model:checked="catStore.mouseMirror" />
36 |     </ProListItem>
37 |   </ProList>
38 | 
39 |   <ProList title="窗口设置">
40 |     <ProListItem
41 |       description="启用后,窗口不影响对其他应用程序的操作"
42 |       title="窗口穿透"
43 |     >
44 |       <Switch v-model:checked="catStore.penetrable" />
45 |     </ProListItem>
46 | 
47 |     <ProListItem
48 |       description="启用后,窗口始终显示在其他应用程序上方"
49 |       title="窗口置顶"
50 |     >
51 |       <Switch v-model:checked="catStore.alwaysOnTop" />
52 |     </ProListItem>
53 | 
54 |     <ProListItem
55 |       description="将鼠标移动到窗口边缘后,也可以拖动调整窗口尺寸"
56 |       title="窗口尺寸"
57 |     >
58 |       <InputNumber
59 |         v-model:value="catStore.scale"
60 |         class="w-28"
61 |         :min="1"
62 |       >
63 |         <template #addonAfter>
64 |           %
65 |         </template>
66 |       </InputNumber>
67 |     </ProListItem>
68 | 
69 |     <ProListItem
70 |       title="不透明度"
71 |       vertical
72 |     >
73 |       <Slider
74 |         v-model:value="catStore.opacity"
75 |         class="m-0!"
76 |         :max="100"
77 |         :min="10"
78 |         :tip-formatter="opacityFormatter"
79 |       />
80 |     </ProListItem>
81 |   </ProList>
82 | </template>
83 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/general/components/macos-permissions/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
 3 | import { message } from '@tauri-apps/plugin-dialog'
 4 | import { Space } from 'ant-design-vue'
 5 | import { checkInputMonitoringPermission, requestInputMonitoringPermission } from 'tauri-plugin-macos-permissions-api'
 6 | import { onMounted, ref } from 'vue'
 7 | 
 8 | import ProList from '@/components/pro-list/index.vue'
 9 | import ProListItem from '@/components/pro-list-item/index.vue'
10 | import { isMac } from '@/utils/platform'
11 | 
12 | const authorized = ref(false)
13 | 
14 | onMounted(async () => {
15 |   authorized.value = await checkInputMonitoringPermission()
16 | 
17 |   if (authorized.value) return
18 | 
19 |   const appWindow = getCurrentWebviewWindow()
20 | 
21 |   await appWindow.setAlwaysOnTop(true)
22 | 
23 |   await message('如果权限已开启,先选中后点击“-”按钮将其删除,再重新手动添加,并重启应用以确保权限生效。', {
24 |     title: '输入监控权限',
25 |     okLabel: '前往开启',
26 |     kind: 'warning',
27 |   })
28 | 
29 |   await appWindow.setAlwaysOnTop(false)
30 | 
31 |   requestInputMonitoringPermission()
32 | })
33 | </script>
34 | 
35 | <template>
36 |   <ProList
37 |     v-if="isMac"
38 |     title="权限设置"
39 |   >
40 |     <ProListItem
41 |       description="开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。"
42 |       title="输入监控权限"
43 |     >
44 |       <Space
45 |         v-if="authorized"
46 |         class="text-success font-bold"
47 |         :size="4"
48 |       >
49 |         <div class="i-solar:verified-check-bold text-4.5" />
50 | 
51 |         <span>已授权</span>
52 |       </Space>
53 | 
54 |       <Space
55 |         v-else
56 |         class="cursor-pointer text-danger font-bold"
57 |         :size="4"
58 |         @click="requestInputMonitoringPermission"
59 |       >
60 |         <div class="i-solar:round-arrow-right-bold text-4.5" />
61 | 
62 |         <span>去授权</span>
63 |       </Space>
64 |     </ProListItem>
65 |   </ProList>
66 | </template>
67 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/general/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
 3 | import { Switch } from 'ant-design-vue'
 4 | import { watch } from 'vue'
 5 | 
 6 | import MacosPermissions from './components/macos-permissions/index.vue'
 7 | 
 8 | import ProList from '@/components/pro-list/index.vue'
 9 | import ProListItem from '@/components/pro-list-item/index.vue'
10 | import { useGeneralStore } from '@/stores/general'
11 | 
12 | const generalStore = useGeneralStore()
13 | 
14 | watch(() => generalStore.autostart, async (value) => {
15 |   const enabled = await isEnabled()
16 | 
17 |   if (value && !enabled) {
18 |     return enable()
19 |   }
20 | 
21 |   if (!value && enabled) {
22 |     disable()
23 |   }
24 | }, { immediate: true })
25 | </script>
26 | 
27 | <template>
28 |   <MacosPermissions />
29 | 
30 |   <ProList title="应用设置">
31 |     <ProListItem title="开机自启动">
32 |       <Switch v-model:checked="generalStore.autostart" />
33 |     </ProListItem>
34 |   </ProList>
35 | 
36 |   <ProList title="更新设置">
37 |     <ProListItem title="自动检查更新">
38 |       <Switch v-model:checked="generalStore.autoCheckUpdate" />
39 |     </ProListItem>
40 |   </ProList>
41 | </template>
42 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/model/components/float-menu/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { EditOutlined, MenuOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
 3 | import { openUrl } from '@tauri-apps/plugin-opener'
 4 | import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
 5 | </script>
 6 | 
 7 | <template>
 8 |   <FloatButtonGroup
 9 |     class="bottom-4 right-4"
10 |     trigger="click"
11 |     type="primary"
12 |   >
13 |     <template #icon>
14 |       <MenuOutlined />
15 |     </template>
16 | 
17 |     <FloatButton
18 |       tooltip="如何制作模型?"
19 |       @click="openUrl('https://juejin.cn/post/7509872655802269731')"
20 |     >
21 |       <template #icon>
22 |         <EditOutlined />
23 |       </template>
24 |     </FloatButton>
25 | 
26 |     <FloatButton
27 |       tooltip="更多模型"
28 |       @click="openUrl('https://github.com/ayangweb/Awesome-BongoCat')"
29 |     >
30 |       <template #icon>
31 |         <UnorderedListOutlined />
32 |       </template>
33 |     </FloatButton>
34 |   </FloatButtonGroup>
35 | </template>
36 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/model/components/upload/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { invoke } from '@tauri-apps/api/core'
 3 | import { appDataDir } from '@tauri-apps/api/path'
 4 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
 5 | import { open } from '@tauri-apps/plugin-dialog'
 6 | import { readDir } from '@tauri-apps/plugin-fs'
 7 | import { message } from 'ant-design-vue'
 8 | import { nanoid } from 'nanoid'
 9 | import { onMounted, ref, useTemplateRef, watch } from 'vue'
10 | 
11 | import { INVOKE_KEY } from '@/constants'
12 | import { useModelStore } from '@/stores/model'
13 | import { join } from '@/utils/path'
14 | 
15 | const dropRef = useTemplateRef('drop')
16 | const dragenter = ref(false)
17 | const selectPaths = ref<string[]>([])
18 | const modelStore = useModelStore()
19 | 
20 | onMounted(() => {
21 |   const appWindow = getCurrentWebviewWindow()
22 | 
23 |   appWindow.onDragDropEvent(({ payload }) => {
24 |     const { type } = payload
25 | 
26 |     if (type === 'over') {
27 |       const { x, y } = payload.position
28 | 
29 |       if (dropRef.value) {
30 |         const { left, right, top, bottom } = dropRef.value.getBoundingClientRect()
31 | 
32 |         const inBoundsX = x >= left && x <= right
33 |         const inBoundsY = y >= top && y <= bottom
34 | 
35 |         dragenter.value = inBoundsX && inBoundsY
36 |       }
37 |     } else if (type === 'drop' && dragenter.value) {
38 |       dragenter.value = false
39 | 
40 |       selectPaths.value = payload.paths
41 |     } else {
42 |       dragenter.value = false
43 |     }
44 |   })
45 | })
46 | 
47 | async function handleUpload() {
48 |   const selected = await open({ directory: true, multiple: true })
49 | 
50 |   if (!selected) return
51 | 
52 |   selectPaths.value = selected
53 | }
54 | 
55 | watch(selectPaths, async (paths) => {
56 |   for await (const path of paths) {
57 |     try {
58 |       const id = nanoid()
59 | 
60 |       const files = await readDir(join(path, 'resources'))
61 | 
62 |       const isKeyboardMode = files.some(file => file.name === 'right-keys')
63 | 
64 |       const toPath = join(await appDataDir(), 'custom-models', id)
65 | 
66 |       await invoke(INVOKE_KEY.COPY_DIR, {
67 |         fromPath: path,
68 |         toPath,
69 |       })
70 | 
71 |       modelStore.models.push({
72 |         id,
73 |         path: toPath,
74 |         mode: isKeyboardMode ? 'keyboard' : 'standard',
75 |         isPreset: false,
76 |       })
77 | 
78 |       message.success('导入成功')
79 |     } catch (error) {
80 |       message.error(String(error))
81 |     }
82 |   }
83 | })
84 | </script>
85 | 
86 | <template>
87 |   <div
88 |     ref="drop"
89 |     class="w-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary"
90 |     :class="{ 'border-primary': dragenter }"
91 |     @click="handleUpload"
92 |   >
93 |     <div class="i-solar:upload-square-outline text-12 text-primary" />
94 | 
95 |     <span>点击或拖动至此区域导入</span>
96 |   </div>
97 | </template>
98 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/model/index.vue:
--------------------------------------------------------------------------------
  1 | <script setup lang="ts">
  2 | import type { Model } from '@/stores/model'
  3 | import type { ComponentPublicInstance } from 'vue'
  4 | 
  5 | import { convertFileSrc } from '@tauri-apps/api/core'
  6 | import { remove } from '@tauri-apps/plugin-fs'
  7 | import { revealItemInDir } from '@tauri-apps/plugin-opener'
  8 | import { useElementSize } from '@vueuse/core'
  9 | import { Card, message, Popconfirm } from 'ant-design-vue'
 10 | import { ref } from 'vue'
 11 | import { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'
 12 | 
 13 | import FloatMenu from './components/float-menu/index.vue'
 14 | import Upload from './components/upload/index.vue'
 15 | 
 16 | import { useModelStore } from '@/stores/model'
 17 | import { join } from '@/utils/path'
 18 | 
 19 | const modelStore = useModelStore()
 20 | const firstItemRef = ref<HTMLElement>()
 21 | 
 22 | const { height } = useElementSize(firstItemRef)
 23 | 
 24 | function setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) {
 25 |   if (!el || index > 0) return
 26 | 
 27 |   if ('$el' in el) {
 28 |     return firstItemRef.value = el.$el
 29 |   }
 30 | 
 31 |   if (el instanceof HTMLElement) {
 32 |     firstItemRef.value = el
 33 |   }
 34 | }
 35 | 
 36 | async function handleDelete(item: Model) {
 37 |   const { id, path } = item
 38 | 
 39 |   try {
 40 |     await remove(path, { recursive: true })
 41 | 
 42 |     message.success('删除成功')
 43 |   } catch (error) {
 44 |     message.error(String(error))
 45 |   } finally {
 46 |     modelStore.models = modelStore.models.filter(item => item.id !== id)
 47 | 
 48 |     if (id === modelStore.currentModel?.id) {
 49 |       modelStore.currentModel = modelStore.models[0]
 50 |     }
 51 |   }
 52 | }
 53 | </script>
 54 | 
 55 | <template>
 56 |   <MasonryGrid
 57 |     :columns="{ 992: 3, 1200: 4, 1600: 6, default: 8 }"
 58 |     :gutter="16"
 59 |   >
 60 |     <MasonryGridItem>
 61 |       <Upload :style="{ height: `${height}px` }" />
 62 |     </MasonryGridItem>
 63 | 
 64 |     <MasonryGridItem
 65 |       v-for="(item, index) in modelStore.models"
 66 |       :key="item.id"
 67 |     >
 68 |       <Card
 69 |         :ref="(el) => setFirstItemRef(el, index)"
 70 |         hoverable
 71 |         size="small"
 72 |         @click="modelStore.currentModel = item"
 73 |       >
 74 |         <template #cover>
 75 |           <img
 76 |             alt="example"
 77 |             :src="convertFileSrc(join(item.path, 'resources', 'cover.png'))"
 78 |           >
 79 |         </template>
 80 | 
 81 |         <template #actions>
 82 |           <i
 83 |             class="i-iconamoon:check-circle-1-bold text-4"
 84 |             :class="{ 'text-success': item.id === modelStore.currentModel?.id }"
 85 |           />
 86 | 
 87 |           <i
 88 |             class="i-iconamoon:link-external-bold text-4"
 89 |             @click.stop="revealItemInDir(item.path)"
 90 |           />
 91 | 
 92 |           <template v-if="!item.isPreset">
 93 |             <Popconfirm
 94 |               description="你确定要删除此模型吗?"
 95 |               placement="topRight"
 96 |               title="删除模型"
 97 |               @confirm="handleDelete(item)"
 98 |             >
 99 |               <i
100 |                 class="i-iconamoon:trash-simple-bold text-4"
101 |                 @click.stop
102 |               />
103 |             </Popconfirm>
104 |           </template>
105 |         </template>
106 |       </Card>
107 |     </MasonryGridItem>
108 |   </MasonryGrid>
109 | 
110 |   <FloatMenu />
111 | </template>
112 | 


--------------------------------------------------------------------------------
/src/pages/preference/components/shortcut/index.vue:
--------------------------------------------------------------------------------
 1 | <script setup lang="ts">
 2 | import { storeToRefs } from 'pinia'
 3 | 
 4 | import ProList from '@/components/pro-list/index.vue'
 5 | import ProShortcut from '@/components/pro-shortcut/index.vue'
 6 | import { useTauriKeyPress } from '@/composables/useTauriKeyPress'
 7 | import { toggleWindowVisible } from '@/plugins/window'
 8 | import { useCatStore } from '@/stores/cat'
 9 | import { useShortcutStore } from '@/stores/shortcut.ts'
10 | 
11 | const shortcutStore = useShortcutStore()
12 | const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore)
13 | const catStore = useCatStore()
14 | 
15 | useTauriKeyPress(visibleCat, () => {
16 |   catStore.visible = !catStore.visible
17 | })
18 | 
19 | useTauriKeyPress(visiblePreference, () => {
20 |   toggleWindowVisible('preference')
21 | })
22 | 
23 | useTauriKeyPress(mirrorMode, () => {
24 |   catStore.mirrorMode = !catStore.mirrorMode
25 | })
26 | 
27 | useTauriKeyPress(penetrable, () => {
28 |   catStore.penetrable = !catStore.penetrable
29 | })
30 | 
31 | useTauriKeyPress(alwaysOnTop, () => {
32 |   catStore.alwaysOnTop = !catStore.alwaysOnTop
33 | })
34 | </script>
35 | 
36 | <template>
37 |   <ProList title="快捷键">
38 |     <ProShortcut
39 |       v-model="shortcutStore.visibleCat"
40 |       description="切换猫咪窗口的显示与隐藏"
41 |       title="打开猫咪"
42 |     />
43 | 
44 |     <ProShortcut
45 |       v-model="shortcutStore.visiblePreference"
46 |       description="切换偏好设置窗口的显示与隐藏"
47 |       title="打开偏好设置"
48 |     />
49 | 
50 |     <ProShortcut
51 |       v-model="shortcutStore.mirrorMode"
52 |       description="切换猫咪的镜像模式"
53 |       title="镜像模式"
54 |     />
55 | 
56 |     <ProShortcut
57 |       v-model="shortcutStore.penetrable"
58 |       description="切换猫咪窗口是否可穿透"
59 |       title="窗口穿透"
60 |     />
61 | 
62 |     <ProShortcut
63 |       v-model="shortcutStore.alwaysOnTop"
64 |       description="切换猫咪窗口是否置顶"
65 |       title="窗口置顶"
66 |     />
67 |   </ProList>
68 | </template>
69 | 


--------------------------------------------------------------------------------
/src/pages/preference/index.vue:
--------------------------------------------------------------------------------
  1 | <script setup lang="ts">
  2 | import { Flex } from 'ant-design-vue'
  3 | import { onMounted, ref } from 'vue'
  4 | 
  5 | import About from './components/about/index.vue'
  6 | import Cat from './components/cat/index.vue'
  7 | import General from './components/general/index.vue'
  8 | import Model from './components/model/index.vue'
  9 | import Shortcut from './components/shortcut/index.vue'
 10 | 
 11 | import UpdateApp from '@/components/update-app/index.vue'
 12 | import { useTray } from '@/composables/useTray'
 13 | import { useAppStore } from '@/stores/app'
 14 | import { isMac } from '@/utils/platform'
 15 | 
 16 | const { createTray } = useTray()
 17 | const appStore = useAppStore()
 18 | const current = ref(0)
 19 | 
 20 | onMounted(async () => {
 21 |   createTray()
 22 | })
 23 | 
 24 | const menus = [
 25 |   {
 26 |     label: '猫咪设置',
 27 |     icon: 'i-solar:cat-bold',
 28 |     component: Cat,
 29 |   },
 30 |   {
 31 |     label: '通用设置',
 32 |     icon: 'i-solar:settings-minimalistic-bold',
 33 |     component: General,
 34 |   },
 35 |   {
 36 |     label: '模型管理',
 37 |     icon: 'i-solar:magic-stick-3-bold',
 38 |     component: Model,
 39 |   },
 40 |   {
 41 |     label: '快捷键',
 42 |     icon: 'i-solar:keyboard-bold',
 43 |     component: Shortcut,
 44 |   },
 45 |   {
 46 |     label: '关于',
 47 |     icon: 'i-solar:info-circle-bold',
 48 |     component: About,
 49 |   },
 50 | ]
 51 | </script>
 52 | 
 53 | <template>
 54 |   <Flex class="h-screen">
 55 |     <div
 56 |       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"
 57 |       :class="[isMac ? 'pt-8' : 'pt-4']"
 58 |       data-tauri-drag-region
 59 |     >
 60 |       <div class="flex flex-col items-center gap-2">
 61 |         <div class="b b-color-2 rounded-2xl b-solid">
 62 |           <img
 63 |             class="size-15"
 64 |             data-tauri-drag-region
 65 |             src="/logo.png"
 66 |           >
 67 |         </div>
 68 | 
 69 |         <span class="font-bold">{{ appStore.name }}</span>
 70 |       </div>
 71 | 
 72 |       <div class="flex flex-col gap-2">
 73 |         <div
 74 |           v-for="(item, index) in menus"
 75 |           :key="item.label"
 76 |           class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 text-color-3 transition"
 77 |           :class="{ 'bg-white! text-primary-5 font-bold': current === index }"
 78 |           @click="current = index"
 79 |         >
 80 |           <div
 81 |             class="size-8"
 82 |             :class="item.icon"
 83 |           />
 84 | 
 85 |           <span>{{ item.label }}</span>
 86 |         </div>
 87 |       </div>
 88 |     </div>
 89 | 
 90 |     <div
 91 |       v-for="(item, index) in menus"
 92 |       v-show="current === index"
 93 |       :key="item.label"
 94 |       class="flex-1 overflow-auto bg-color-8 p-4"
 95 |       data-tauri-drag-region
 96 |     >
 97 |       <component :is="item.component" />
 98 |     </div>
 99 |   </Flex>
100 | 
101 |   <UpdateApp />
102 | </template>
103 | 


--------------------------------------------------------------------------------
/src/plugins/window.ts:
--------------------------------------------------------------------------------
 1 | import { invoke } from '@tauri-apps/api/core'
 2 | import { emit } from '@tauri-apps/api/event'
 3 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
 4 | 
 5 | import { LISTEN_KEY } from '../constants'
 6 | 
 7 | type WindowLabel = 'main' | 'preference'
 8 | 
 9 | const COMMAND = {
10 |   SHOW_WINDOW: 'plugin:custom-window|show_window',
11 |   HIDE_WINDOW: 'plugin:custom-window|hide_window',
12 |   SET_ALWAYS_ON_TOP: 'plugin:custom-window|set_always_on_top',
13 | }
14 | 
15 | export function showWindow(label?: WindowLabel) {
16 |   if (label) {
17 |     emit(LISTEN_KEY.SHOW_WINDOW, label)
18 |   } else {
19 |     invoke(COMMAND.SHOW_WINDOW)
20 |   }
21 | }
22 | 
23 | export function hideWindow(label?: WindowLabel) {
24 |   if (label) {
25 |     emit(LISTEN_KEY.HIDE_WINDOW, label)
26 |   } else {
27 |     invoke(COMMAND.HIDE_WINDOW)
28 |   }
29 | }
30 | 
31 | export function setAlwaysOnTop(alwaysOnTop: boolean) {
32 |   invoke(COMMAND.SET_ALWAYS_ON_TOP, { alwaysOnTop })
33 | }
34 | 
35 | export async function toggleWindowVisible(label?: WindowLabel) {
36 |   const appWindow = getCurrentWebviewWindow()
37 | 
38 |   if (appWindow.label !== label) return
39 | 
40 |   const visible = await appWindow.isVisible()
41 | 
42 |   if (visible) {
43 |     return hideWindow(label)
44 |   }
45 | 
46 |   return showWindow(label)
47 | }
48 | 


--------------------------------------------------------------------------------
/src/router/index.ts:
--------------------------------------------------------------------------------
 1 | import type { RouteRecordRaw } from 'vue-router'
 2 | 
 3 | import { createRouter, createWebHashHistory } from 'vue-router'
 4 | 
 5 | import Main from '../pages/main/index.vue'
 6 | import Preference from '../pages/preference/index.vue'
 7 | 
 8 | const routes: Readonly<RouteRecordRaw[]> = [
 9 |   {
10 |     path: '/',
11 |     component: Main,
12 |   },
13 |   {
14 |     path: '/preference',
15 |     component: Preference,
16 |   },
17 | ]
18 | 
19 | const router = createRouter({
20 |   history: createWebHashHistory(),
21 |   routes,
22 | })
23 | 
24 | export default router
25 | 


--------------------------------------------------------------------------------
/src/stores/app.ts:
--------------------------------------------------------------------------------
 1 | import type { WindowState } from '@/composables/useWindowState'
 2 | 
 3 | import { getName, getVersion } from '@tauri-apps/api/app'
 4 | import { defineStore } from 'pinia'
 5 | import { onMounted, reactive, ref } from 'vue'
 6 | 
 7 | export const useAppStore = defineStore('app', () => {
 8 |   const name = ref('')
 9 |   const version = ref('')
10 |   const windowState = reactive<WindowState>({})
11 | 
12 |   onMounted(async () => {
13 |     name.value = await getName()
14 |     version.value = await getVersion()
15 |   })
16 | 
17 |   return {
18 |     name,
19 |     version,
20 |     windowState,
21 |   }
22 | })
23 | 


--------------------------------------------------------------------------------
/src/stores/cat.ts:
--------------------------------------------------------------------------------
 1 | import { defineStore } from 'pinia'
 2 | import { ref } from 'vue'
 3 | 
 4 | export const useCatStore = defineStore('cat', () => {
 5 |   const visible = ref(false)
 6 |   const mirrorMode = ref(false)
 7 |   const singleMode = ref(false)
 8 |   const mouseMirror = ref(false)
 9 |   const penetrable = ref(false)
10 |   const alwaysOnTop = ref(true)
11 |   const scale = ref(100)
12 |   const opacity = ref(100)
13 | 
14 |   const init = () => {
15 |     visible.value = true
16 |   }
17 | 
18 |   return {
19 |     visible,
20 |     mirrorMode,
21 |     singleMode,
22 |     mouseMirror,
23 |     penetrable,
24 |     alwaysOnTop,
25 |     scale,
26 |     opacity,
27 |     init,
28 |   }
29 | })
30 | 


--------------------------------------------------------------------------------
/src/stores/general.ts:
--------------------------------------------------------------------------------
 1 | import { defineStore } from 'pinia'
 2 | import { ref } from 'vue'
 3 | 
 4 | export const useGeneralStore = defineStore('general', () => {
 5 |   const autoCheckUpdate = ref(false)
 6 |   const autostart = ref(false)
 7 | 
 8 |   return {
 9 |     autoCheckUpdate,
10 |     autostart,
11 |   }
12 | })
13 | 


--------------------------------------------------------------------------------
/src/stores/model.ts:
--------------------------------------------------------------------------------
 1 | import { resolveResource } from '@tauri-apps/api/path'
 2 | import { nanoid } from 'nanoid'
 3 | import { defineStore } from 'pinia'
 4 | import { ref } from 'vue'
 5 | 
 6 | import { join } from '@/utils/path'
 7 | 
 8 | export type ModelMode = 'standard' | 'keyboard' | 'handle'
 9 | 
10 | export interface Model {
11 |   id: string
12 |   path: string
13 |   mode: ModelMode
14 |   isPreset: boolean
15 | }
16 | 
17 | interface Motion {
18 |   Name: string
19 |   File: string
20 |   Sound?: string
21 |   FadeInTime: number
22 |   FadeOutTime: number
23 |   Description?: string
24 | }
25 | 
26 | type MotionGroup = Record<string, Motion[]>
27 | 
28 | interface Expression {
29 |   Name: string
30 |   File: string
31 |   Description?: string
32 | }
33 | 
34 | export const useModelStore = defineStore('model', () => {
35 |   const models = ref<Model[]>([])
36 |   const currentModel = ref<Model>()
37 |   const motions = ref<MotionGroup>({})
38 |   const expressions = ref<Expression[]>([])
39 | 
40 |   const init = async () => {
41 |     const presetModelsPath = await resolveResource('assets/models')
42 | 
43 |     if (models.value.length === 0) {
44 |       const modes: ModelMode[] = ['standard', 'keyboard']
45 | 
46 |       for (const mode of modes) {
47 |         models.value.push({
48 |           mode,
49 |           id: nanoid(),
50 |           isPreset: true,
51 |           path: join(presetModelsPath, mode),
52 |         })
53 |       }
54 |     } else {
55 |       models.value = models.value.map((item) => {
56 |         const { isPreset, mode } = item
57 | 
58 |         if (!isPreset) return item
59 | 
60 |         return {
61 |           ...item,
62 |           path: join(presetModelsPath, mode),
63 |         }
64 |       })
65 |     }
66 | 
67 |     const matched = models.value.find(item => item.id === currentModel.value?.id)
68 | 
69 |     if (matched) {
70 |       return currentModel.value = matched
71 |     }
72 | 
73 |     currentModel.value = models.value[0]
74 |   }
75 | 
76 |   return {
77 |     models,
78 |     currentModel,
79 |     motions,
80 |     expressions,
81 |     init,
82 |   }
83 | })
84 | 


--------------------------------------------------------------------------------
/src/stores/shortcut.ts:
--------------------------------------------------------------------------------
 1 | import { defineStore } from 'pinia'
 2 | import { ref } from 'vue'
 3 | 
 4 | export type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop'
 5 | 
 6 | export const useShortcutStore = defineStore('shortcut', () => {
 7 |   const visibleCat = ref('')
 8 |   const visiblePreference = ref('')
 9 |   const mirrorMode = ref('')
10 |   const penetrable = ref('')
11 |   const alwaysOnTop = ref('')
12 | 
13 |   return {
14 |     visibleCat,
15 |     visiblePreference,
16 |     mirrorMode,
17 |     penetrable,
18 |     alwaysOnTop,
19 |   }
20 | })
21 | 


--------------------------------------------------------------------------------
/src/utils/is.ts:
--------------------------------------------------------------------------------
1 | export function isImage(value: string) {
2 |   const regex = /\.(?:jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i
3 | 
4 |   return regex.test(value)
5 | }
6 | 


--------------------------------------------------------------------------------
/src/utils/keyboard.ts:
--------------------------------------------------------------------------------
  1 | import { isMac } from './platform'
  2 | 
  3 | export interface Key {
  4 |   eventKey: string
  5 |   tauriKey?: string
  6 |   symbol?: string
  7 | }
  8 | 
  9 | export const modifierKeys: Key[] = [
 10 |   {
 11 |     eventKey: 'Shift',
 12 |     symbol: isMac ? '⇧' : 'Shift',
 13 |   },
 14 |   {
 15 |     eventKey: 'Control',
 16 |     symbol: isMac ? '⌃' : 'Ctrl',
 17 |   },
 18 |   {
 19 |     eventKey: 'Alt',
 20 |     symbol: isMac ? '⌥' : 'Alt',
 21 |   },
 22 |   {
 23 |     eventKey: 'Command',
 24 |     symbol: isMac ? '⌘' : 'Super',
 25 |   },
 26 | ].map((item) => {
 27 |   return { ...item, tauriKey: item.eventKey }
 28 | })
 29 | 
 30 | export const standardKeys: Key[] = [
 31 |   // 第一排
 32 |   {
 33 |     eventKey: 'Escape',
 34 |     symbol: isMac ? '⎋' : 'Esc',
 35 |   },
 36 |   {
 37 |     eventKey: 'F1',
 38 |   },
 39 |   {
 40 |     eventKey: 'F2',
 41 |   },
 42 |   {
 43 |     eventKey: 'F3',
 44 |   },
 45 |   {
 46 |     eventKey: 'F4',
 47 |   },
 48 |   {
 49 |     eventKey: 'F5',
 50 |   },
 51 |   {
 52 |     eventKey: 'F6',
 53 |   },
 54 |   {
 55 |     eventKey: 'F7',
 56 |   },
 57 |   {
 58 |     eventKey: 'F8',
 59 |   },
 60 |   {
 61 |     eventKey: 'F9',
 62 |   },
 63 |   {
 64 |     eventKey: 'F10',
 65 |   },
 66 |   {
 67 |     eventKey: 'F11',
 68 |   },
 69 |   {
 70 |     eventKey: 'F12',
 71 |   }, // 第二排
 72 |   {
 73 |     eventKey: 'Backquote',
 74 |     symbol: '`',
 75 |   },
 76 |   {
 77 |     eventKey: 'Digit1',
 78 |   },
 79 |   {
 80 |     eventKey: 'Digit2',
 81 |   },
 82 |   {
 83 |     eventKey: 'Digit3',
 84 |   },
 85 |   {
 86 |     eventKey: 'Digit4',
 87 |   },
 88 |   {
 89 |     eventKey: 'Digit5',
 90 |   },
 91 |   {
 92 |     eventKey: 'Digit6',
 93 |   },
 94 |   {
 95 |     eventKey: 'Digit7',
 96 |   },
 97 |   {
 98 |     eventKey: 'Digit8',
 99 |   },
100 |   {
101 |     eventKey: 'Digit9',
102 |   },
103 |   {
104 |     eventKey: 'Digit0',
105 |   },
106 |   {
107 |     eventKey: 'Minus',
108 |     tauriKey: '-',
109 |     symbol: '-',
110 |   },
111 |   {
112 |     eventKey: 'Equal',
113 |     tauriKey: '=',
114 |     symbol: '=',
115 |   },
116 |   {
117 |     eventKey: 'Backspace',
118 |     symbol: isMac ? '⌫' : void 0,
119 |   },
120 |   // 第三排
121 |   {
122 |     eventKey: 'Tab',
123 |     symbol: isMac ? '⇥' : void 0,
124 |   },
125 |   {
126 |     eventKey: 'KeyQ',
127 |   },
128 |   {
129 |     eventKey: 'KeyW',
130 |   },
131 |   {
132 |     eventKey: 'KeyE',
133 |   },
134 |   {
135 |     eventKey: 'KeyR',
136 |   },
137 |   {
138 |     eventKey: 'KeyT',
139 |   },
140 |   {
141 |     eventKey: 'KeyY',
142 |   },
143 |   {
144 |     eventKey: 'KeyU',
145 |   },
146 |   {
147 |     eventKey: 'KeyI',
148 |   },
149 |   {
150 |     eventKey: 'KeyO',
151 |   },
152 |   {
153 |     eventKey: 'KeyP',
154 |   },
155 |   {
156 |     eventKey: 'BracketLeft',
157 |     symbol: '[',
158 |   },
159 |   {
160 |     eventKey: 'BracketRight',
161 |     symbol: ']',
162 |   },
163 |   {
164 |     eventKey: 'Backslash',
165 |     symbol: '\\',
166 |   },
167 |   // 第四排
168 |   {
169 |     eventKey: 'KeyA',
170 |   },
171 |   {
172 |     eventKey: 'KeyS',
173 |   },
174 |   {
175 |     eventKey: 'KeyD',
176 |   },
177 |   {
178 |     eventKey: 'KeyF',
179 |   },
180 |   {
181 |     eventKey: 'KeyG',
182 |   },
183 |   {
184 |     eventKey: 'KeyH',
185 |   },
186 |   {
187 |     eventKey: 'KeyJ',
188 |   },
189 |   {
190 |     eventKey: 'KeyK',
191 |   },
192 |   {
193 |     eventKey: 'KeyL',
194 |   },
195 |   {
196 |     eventKey: 'Semicolon',
197 |     symbol: ';',
198 |   },
199 |   {
200 |     eventKey: 'Quote',
201 |     symbol: '\'',
202 |   },
203 |   {
204 |     eventKey: 'Enter',
205 |     symbol: isMac ? '↩︎' : void 0,
206 |   },
207 |   // 第五排
208 |   {
209 |     eventKey: 'KeyZ',
210 |   },
211 |   {
212 |     eventKey: 'KeyX',
213 |   },
214 |   {
215 |     eventKey: 'KeyC',
216 |   },
217 |   {
218 |     eventKey: 'KeyV',
219 |   },
220 |   {
221 |     eventKey: 'KeyB',
222 |   },
223 |   {
224 |     eventKey: 'KeyN',
225 |   },
226 |   {
227 |     eventKey: 'KeyM',
228 |   },
229 |   {
230 |     eventKey: 'Comma',
231 |     symbol: ',',
232 |   },
233 |   {
234 |     eventKey: 'Period',
235 |     symbol: '.',
236 |   },
237 |   {
238 |     eventKey: 'Slash',
239 |     symbol: '/',
240 |   },
241 |   // 第六排
242 |   {
243 |     eventKey: 'Space',
244 |     symbol: isMac ? '␣' : void 0,
245 |   },
246 |   // 方向键
247 |   {
248 |     eventKey: 'ArrowUp',
249 |     symbol: '↑',
250 |   },
251 |   {
252 |     eventKey: 'ArrowDown',
253 |     symbol: '↓',
254 |   },
255 |   {
256 |     eventKey: 'ArrowLeft',
257 |     symbol: '←',
258 |   },
259 |   {
260 |     eventKey: 'ArrowRight',
261 |     symbol: '→',
262 |   },
263 | ].map((item) => {
264 |   const { eventKey } = item
265 | 
266 |   item.symbol ??= eventKey
267 |   item.tauriKey ??= eventKey
268 | 
269 |   if (eventKey.startsWith('Digit') || eventKey.startsWith('Key')) {
270 |     item.tauriKey = item.symbol = eventKey.slice(-1)
271 |   }
272 | 
273 |   return item
274 | })
275 | 
276 | export const keys = modifierKeys.concat(standardKeys)
277 | 


--------------------------------------------------------------------------------
/src/utils/live2d.ts:
--------------------------------------------------------------------------------
  1 | import type { Cubism4InternalModel } from 'pixi-live2d-display'
  2 | 
  3 | import { convertFileSrc } from '@tauri-apps/api/core'
  4 | import { readDir, readTextFile } from '@tauri-apps/plugin-fs'
  5 | import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'
  6 | import { Application, Ticker } from 'pixi.js'
  7 | 
  8 | import { join } from './path'
  9 | 
 10 | Live2DModel.registerTicker(Ticker)
 11 | 
 12 | class Live2d {
 13 |   private app: Application | null = null
 14 |   public model: Live2DModel | null = null
 15 |   private modelWidth = 0
 16 |   private modelHeight = 0
 17 | 
 18 |   constructor() { }
 19 | 
 20 |   private initApp() {
 21 |     if (this.app) return
 22 | 
 23 |     const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
 24 | 
 25 |     this.app = new Application({
 26 |       view,
 27 |       resizeTo: window,
 28 |       backgroundAlpha: 0,
 29 |       resolution: devicePixelRatio,
 30 |     })
 31 |   }
 32 | 
 33 |   public async load(path: string) {
 34 |     this.initApp()
 35 | 
 36 |     this.destroy()
 37 | 
 38 |     const files = await readDir(path)
 39 | 
 40 |     const modelFile = files.find(file => file.name.endsWith('.model3.json'))
 41 | 
 42 |     if (!modelFile) {
 43 |       throw new Error('未找到模型主配置文件,请确认模型文件是否完整。')
 44 |     }
 45 | 
 46 |     const modelPath = join(path, modelFile.name)
 47 | 
 48 |     const modelJSON = JSON.parse(await readTextFile(modelPath))
 49 | 
 50 |     const modelSettings = new Cubism4ModelSettings({
 51 |       ...modelJSON,
 52 |       url: convertFileSrc(modelPath),
 53 |     })
 54 | 
 55 |     modelSettings.replaceFiles((file) => {
 56 |       return convertFileSrc(join(path, file))
 57 |     })
 58 | 
 59 |     this.model = await Live2DModel.from(modelSettings)
 60 | 
 61 |     const { width, height } = this.model
 62 |     this.modelWidth = width
 63 |     this.modelHeight = height
 64 | 
 65 |     this.app?.stage.addChild(this.model)
 66 | 
 67 |     const { motions, expressions } = modelSettings
 68 | 
 69 |     return {
 70 |       width,
 71 |       height,
 72 |       motions,
 73 |       expressions,
 74 |     }
 75 |   }
 76 | 
 77 |   public destroy() {
 78 |     this.model?.destroy()
 79 |   }
 80 | 
 81 |   public fitModel() {
 82 |     if (!this.model) return
 83 | 
 84 |     const scaleX = innerWidth / this.modelWidth
 85 |     const scaleY = innerHeight / this.modelHeight
 86 |     const scale = Math.min(scaleX, scaleY)
 87 | 
 88 |     this.model.scale.set(scale)
 89 |     this.model.x = innerWidth / 2
 90 |     this.model.y = innerHeight / 2
 91 |     this.model.anchor.set(0.5)
 92 |   }
 93 | 
 94 |   public playMotion(group: string, index: number) {
 95 |     return this.model?.motion(group, index)
 96 |   }
 97 | 
 98 |   public playExpressions(index: number) {
 99 |     return this.model?.expression(index)
100 |   }
101 | 
102 |   public getCoreModel() {
103 |     const internalModel = this.model?.internalModel as Cubism4InternalModel
104 | 
105 |     return internalModel?.coreModel
106 |   }
107 | 
108 |   public getParameterRange(id: string) {
109 |     const coreModel = this.getCoreModel()
110 | 
111 |     const index = coreModel?.getParameterIndex(id)
112 |     const min = coreModel?.getParameterMinimumValue(index)
113 |     const max = coreModel?.getParameterMaximumValue(index)
114 | 
115 |     return {
116 |       min,
117 |       max,
118 |     }
119 |   }
120 | 
121 |   public setParameterValue(id: string, value: number) {
122 |     const coreModel = this.getCoreModel()
123 | 
124 |     return coreModel?.setParameterValueById?.(id, Number(value))
125 |   }
126 | }
127 | 
128 | const live2d = new Live2d()
129 | 
130 | export default live2d
131 | 


--------------------------------------------------------------------------------
/src/utils/monitor.ts:
--------------------------------------------------------------------------------
 1 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
 2 | import {
 3 |   cursorPosition,
 4 |   monitorFromPoint,
 5 | } from '@tauri-apps/api/window'
 6 | 
 7 | export async function getCursorMonitor() {
 8 |   const appWindow = getCurrentWebviewWindow()
 9 | 
10 |   const scaleFactor = await appWindow.scaleFactor()
11 | 
12 |   const point = await cursorPosition()
13 | 
14 |   const { x, y } = point.toLogical(scaleFactor)
15 | 
16 |   const monitor = await monitorFromPoint(x, y)
17 | 
18 |   if (!monitor) return
19 | 
20 |   return {
21 |     ...monitor,
22 |     cursorPosition: point,
23 |   }
24 | }
25 | 


--------------------------------------------------------------------------------
/src/utils/path.ts:
--------------------------------------------------------------------------------
 1 | import type { LiteralUnion } from 'ant-design-vue/es/_util/type'
 2 | 
 3 | import { sep } from '@tauri-apps/api/path'
 4 | 
 5 | export function join(...paths: LiteralUnion<'resources' | 'left-keys' | 'right-keys' | 'background.png' | 'cover.png'>[]) {
 6 |   const joinPaths = paths.map((path) => {
 7 |     if (path.endsWith(sep())) {
 8 |       return path.slice(0, -1)
 9 |     }
10 | 
11 |     return path
12 |   })
13 | 
14 |   return joinPaths.join(sep())
15 | }
16 | 


--------------------------------------------------------------------------------
/src/utils/platform.ts:
--------------------------------------------------------------------------------
1 | import { platform } from '@tauri-apps/plugin-os'
2 | 
3 | export const isMac = platform() === 'macos'
4 | 
5 | export const isWindows = platform() === 'windows'
6 | 
7 | export const isLinux = platform() === 'linux'
8 | 


--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | /// <reference types="vite/client" />
2 | 
3 | declare module '*.vue' {
4 |   import type { DefineComponent } from 'vue'
5 | 
6 |   const component: DefineComponent<object, object, any>
7 |   export default component
8 | }
9 | 


--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2020",
 4 |     "jsx": "preserve",
 5 |     "lib": ["ES2020", "DOM", "DOM.Iterable"],
 6 |     "useDefineForClassFields": true,
 7 | 
 8 |     "baseUrl": ".",
 9 |     "module": "ESNext",
10 |     /* Bundler mode */
11 |     "moduleResolution": "bundler",
12 |     "paths": {
13 |       "@/*": ["src/*"]
14 |     },
15 |     "resolveJsonModule": true,
16 |     "allowImportingTsExtensions": true,
17 | 
18 |     /* Linting */
19 |     "strict": true,
20 |     "noFallthroughCasesInSwitch": true,
21 |     "noUnusedLocals": true,
22 |     "noUnusedParameters": true,
23 |     "noEmit": true,
24 |     "isolatedModules": true,
25 |     "skipLibCheck": true
26 |   },
27 |   "references": [{ "path": "./tsconfig.node.json" }],
28 |   "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
29 | }
30 | 


--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "compilerOptions": {
 3 |     "composite": true,
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "bundler",
 6 |     "allowSyntheticDefaultImports": true,
 7 |     "skipLibCheck": true
 8 |   },
 9 |   "include": ["vite.config.ts"]
10 | }
11 | 


--------------------------------------------------------------------------------
/uno.config.ts:
--------------------------------------------------------------------------------
 1 | import {
 2 |   defineConfig,
 3 |   presetIcons,
 4 |   presetWind3,
 5 |   transformerDirectives,
 6 |   transformerVariantGroup,
 7 | } from 'unocss'
 8 | 
 9 | export default defineConfig({
10 |   presets: [
11 |     presetWind3(),
12 |     presetIcons(),
13 |   ],
14 |   transformers: [
15 |     transformerVariantGroup(),
16 |     transformerDirectives({
17 |       applyVariable: ['--uno'],
18 |     }),
19 |   ],
20 |   shortcuts: [
21 |     [/^bg-color-(\d+)$/, ([, d]) => `bg-bg-${d}`],
22 |     [/^text-color-(\d+)$/, ([, d]) => `text-text-${d}`],
23 |     [/^b-color-(\d+)$/, ([, d]) => `b-border-${d}`],
24 |     [/^(.*)-primary-(\d+)$/, ([, s, d]) => `${s}-[var(--ant-blue-${d})]`],
25 |   ],
26 |   theme: {
27 |     colors: {
28 |       'bg-1': 'var(--ant-color-bg-layout)',
29 |       'bg-2': 'var(--ant-color-bg-container)',
30 |       'bg-3': 'var(--ant-color-bg-elevated)',
31 |       'bg-4': 'var(--ant-color-bg-spotlight)',
32 |       'bg-5': 'var(--ant-color-fill)',
33 |       'bg-6': 'var(--ant-color-fill-secondary)',
34 |       'bg-7': 'var(--ant-color-fill-tertiary)',
35 |       'bg-8': 'var(--ant-color-fill-quaternary)',
36 |       'text-1': 'var(--ant-color-text)',
37 |       'text-2': 'var(--ant-color-text-secondary)',
38 |       'text-3': 'var(--ant-color-text-tertiary)',
39 |       'text-4': 'var(--ant-color-text-quaternary)',
40 |       'border-1': 'var(--ant-color-border)',
41 |       'border-2': 'var(--ant-color-border-secondary)',
42 |       'primary': 'var(--ant-blue)',
43 |       'success': 'var(--ant-green)',
44 |       'danger': 'var(--ant-red)',
45 |     },
46 |   },
47 | })
48 | 


--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
 1 | import { resolve } from 'node:path'
 2 | import { env } from 'node:process'
 3 | 
 4 | import vue from '@vitejs/plugin-vue'
 5 | import UnoCSS from 'unocss/vite'
 6 | import { defineConfig } from 'vite'
 7 | 
 8 | const host = env.TAURI_DEV_HOST
 9 | 
10 | // https://vitejs.dev/config/
11 | export default defineConfig(async () => ({
12 |   plugins: [vue(), UnoCSS()],
13 |   resolve: {
14 |     alias: {
15 |       '@': resolve(__dirname, 'src'),
16 |     },
17 |   },
18 |   // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`
19 |   //
20 |   // 1. prevent vite from obscuring rust errors
21 |   clearScreen: false,
22 |   // 2. tauri expects a fixed port, fail if that port is not available
23 |   server: {
24 |     port: 1420,
25 |     strictPort: true,
26 |     host: host || false,
27 |     hmr: host
28 |       ? {
29 |           protocol: 'ws',
30 |           host,
31 |           port: 1421,
32 |         }
33 |       : undefined,
34 |     watch: {
35 |       // 3. tell vite to ignore watching `src-tauri`
36 |       ignored: ['**/src-tauri/**'],
37 |     },
38 |   },
39 | }))
40 | 


--------------------------------------------------------------------------------