Full Code of ayangweb/BongoCat for AI

master d9fb19aa2f45 cached
134 files
181.2 KB
56.6k tokens
103 symbols
1 requests
Download .txt
Showing preview only (209K chars total). Download the full file or copy to clipboard to get everything.
Repository: ayangweb/BongoCat
Branch: master
Commit: d9fb19aa2f45
Files: 134
Total size: 181.2 KB

Directory structure:
gitextract__za4gjq7/

├── .commitlintrc
├── .github/
│   ├── CONTRIBUTING.md
│   ├── DOWNLOAD_GUIDE.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── release.yml
│       ├── sync-to-gitee.yml
│       └── upgradelink.yml
├── .gitignore
├── .release-it.ts
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── Cargo.toml
├── LICENSE
├── README.md
├── eslint.config.ts
├── index.html
├── package.json
├── scripts/
│   ├── buildIcon.ts
│   └── release.ts
├── src/
│   ├── App.vue
│   ├── assets/
│   │   └── css/
│   │       └── global.scss
│   ├── components/
│   │   ├── pro-list/
│   │   │   └── index.vue
│   │   ├── pro-list-item/
│   │   │   └── index.vue
│   │   ├── pro-shortcut/
│   │   │   └── index.vue
│   │   └── update-app/
│   │       └── index.vue
│   ├── composables/
│   │   ├── useDevice.ts
│   │   ├── useGamepad.ts
│   │   ├── useModel.ts
│   │   ├── useSharedMenu.ts
│   │   ├── useTauriListen.ts
│   │   ├── useTauriShortcut.ts
│   │   ├── useThemeVars.ts
│   │   ├── useTray.ts
│   │   ├── useWindowPosition.ts
│   │   └── useWindowState.ts
│   ├── constants/
│   │   └── index.ts
│   ├── locales/
│   │   ├── en-US.json
│   │   ├── index.ts
│   │   ├── pt-BR.json
│   │   ├── vi-VN.json
│   │   └── zh-CN.json
│   ├── main.ts
│   ├── pages/
│   │   ├── main/
│   │   │   └── index.vue
│   │   └── preference/
│   │       ├── components/
│   │       │   ├── about/
│   │       │   │   └── index.vue
│   │       │   ├── cat/
│   │       │   │   ├── components/
│   │       │   │   │   └── position/
│   │       │   │   │       └── index.vue
│   │       │   │   └── index.vue
│   │       │   ├── general/
│   │       │   │   ├── components/
│   │       │   │   │   ├── macos-permissions/
│   │       │   │   │   │   └── index.vue
│   │       │   │   │   └── theme-mode/
│   │       │   │   │       └── index.vue
│   │       │   │   └── index.vue
│   │       │   ├── model/
│   │       │   │   ├── components/
│   │       │   │   │   ├── float-menu/
│   │       │   │   │   │   └── index.vue
│   │       │   │   │   └── upload/
│   │       │   │   │       └── index.vue
│   │       │   │   └── index.vue
│   │       │   └── shortcut/
│   │       │       └── index.vue
│   │       └── index.vue
│   ├── plugins/
│   │   └── window.ts
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   ├── app.ts
│   │   ├── cat.ts
│   │   ├── general.ts
│   │   ├── model.ts
│   │   └── shortcut.ts
│   ├── utils/
│   │   ├── is.ts
│   │   ├── keyboard.ts
│   │   ├── live2d.ts
│   │   ├── monitor.ts
│   │   ├── path.ts
│   │   ├── platform.ts
│   │   └── shared.ts
│   └── vite-env.d.ts
├── src-tauri/
│   ├── .gitignore
│   ├── BongoCat.desktop
│   ├── Cargo.toml
│   ├── assets/
│   │   └── models/
│   │       ├── gamepad/
│   │       │   ├── cat.model3.json
│   │       │   ├── demomodel3.cdi3.json
│   │       │   ├── demomodel3.moc3
│   │       │   ├── exp_1.exp3.json
│   │       │   ├── exp_2.exp3.json
│   │       │   ├── live2d_expression0.exp3.json
│   │       │   ├── live2d_expression1.exp3.json
│   │       │   ├── live2d_expression2.exp3.json
│   │       │   ├── live2d_motion1.flac
│   │       │   ├── live2d_motion1.motion3.json
│   │       │   └── live2d_motion2.motion3.json
│   │       ├── keyboard/
│   │       │   ├── cat.model3.json
│   │       │   ├── demomodel2.cdi3.json
│   │       │   ├── demomodel2.moc3
│   │       │   ├── exp_1.exp3.json
│   │       │   ├── exp_2.exp3.json
│   │       │   ├── live2d_expression0.exp3.json
│   │       │   ├── live2d_expression1.exp3.json
│   │       │   ├── live2d_expression2.exp3.json
│   │       │   ├── live2d_motion1.flac
│   │       │   ├── live2d_motion1.motion3.json
│   │       │   └── live2d_motion2.motion3.json
│   │       └── standard/
│   │           ├── cat.model3.json
│   │           ├── demomodel.cdi3.json
│   │           ├── demomodel.moc3
│   │           ├── exp_1.exp3.json
│   │           ├── exp_2.exp3.json
│   │           ├── live2d_expression0.exp3.json
│   │           ├── live2d_expression1.exp3.json
│   │           ├── live2d_expression2.exp3.json
│   │           ├── live2d_motion1.flac
│   │           ├── live2d_motion1.motion3.json
│   │           └── live2d_motion2.motion3.json
│   ├── build.rs
│   ├── capabilities/
│   │   └── default.json
│   ├── src/
│   │   ├── core/
│   │   │   ├── device.rs
│   │   │   ├── gamepad.rs
│   │   │   ├── mod.rs
│   │   │   ├── prevent_default.rs
│   │   │   └── setup/
│   │   │       ├── common.rs
│   │   │       ├── macos.rs
│   │   │       └── mod.rs
│   │   ├── lib.rs
│   │   ├── main.rs
│   │   ├── plugins/
│   │   │   └── window/
│   │   │       ├── Cargo.toml
│   │   │       ├── build.rs
│   │   │       ├── permissions/
│   │   │       │   └── default.toml
│   │   │       └── src/
│   │   │           ├── commands/
│   │   │           │   ├── common.rs
│   │   │           │   ├── macos.rs
│   │   │           │   └── mod.rs
│   │   │           └── lib.rs
│   │   └── utils/
│   │       ├── fs_extra.rs
│   │       └── mod.rs
│   ├── tauri.conf.json
│   ├── tauri.linux.conf.json
│   ├── tauri.macos.conf.json
│   └── tauri.windows.conf.json
├── tsconfig.json
├── tsconfig.node.json
├── uno.config.ts
└── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .commitlintrc
================================================
{
	"extends": "@commitlint/config-conventional"
}


================================================
FILE: .github/CONTRIBUTING.md
================================================
# 贡献指南

非常感谢您对 BongoCat 的关注和贡献!在您提交贡献之前,请先花一些时间阅读以下指南,以确保您的贡献能够顺利进行。

## 透明的开发

所有工作都在 GitHub 上公开进行。无论是核心团队成员还是外部贡献者的 Pull Request,都需要经过相同的 review 流程。

## 提交 Issue

我们使用 [Github Issues](https://github.com/ayangweb/BongoCat/issues) 进行 Bug 报告和新 Feature 建议。在提交 Issue 之前,请确保已经搜索过类似的问题,因为它们可能已经得到解答或正在被修复。对于 Bug 报告,请包含可用于重现问题的完整步骤。对于新 Feature 建议,请指出你想要的更改以及期望的行为。

## 提交 Pull Request

### 共建流程

- 认领 issue:在 Github 建立 Issue 并认领(或直接认领已有 Issue),告知大家自己正在修复,避免重复工作。
- 项目开发:在完成准备工作后,进行 Bug 修复或功能开发。
- 提交 PR。

### 准备工作

- [Rust](https://v2.tauri.app/start/prerequisites/): 请自行根据官网步骤安装 rust 环境。
- [Node.js](https://nodejs.org/en/): 用于运行项目。
- [Pnpm](https://pnpm.io/):本项目使用 Pnpm 进行包管理。

### 下载依赖

```shell
pnpm install
```

### 启动应用

```shell
pnpm tauri dev
```

### 打包应用

> 如果需要打包后进行调试,请在以下命令后面加上 `--debug`

```shell
pnpm tauri build
```

## Commit 指南

Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)。

### Commit 类型

以下是 commit 类型列表:

- feat: 新特性或功能
- fix: 缺陷修复
- docs: 文档更新
- style: 代码风格更新
- refactor: 代码重构,不引入新功能和缺陷修复
- perf: 性能优化
- chore: 其他提交

期待您的参与,让我们一起使 BongoCat 变得更好!


================================================
FILE: .github/DOWNLOAD_GUIDE.md
================================================
# 下载指南

## 系统要求

- macOS 12 或更高版本。
- Windows 10 或更高版本。
- Linux 带有 X11 环境。

## macOS

### 手动下载

- Apple Silicon:下载 `BongoCat_aarch64.dmg`
- Intel Chip:下载 `BongoCat_x64.dmg`

### Homebrew 下载

1. 添加 BongoCat 的 tap 源:

```bash
brew tap ayangweb/BongoCat
```

2. 安装:

```bash
brew install --no-quarantine bongo-cat
```

3. 更新:

```bash
brew upgrade bongo-cat
```

4. 卸载:

```bash
brew uninstall --cask bongo-cat

brew untap ayangweb/BongoCat
```

## Windows

- 64 位系统:下载 `BongoCat_x64.exe`
- 32 位系统:下载 `BongoCat_x86.exe`
- ARM64 架构:下载 `BongoCat_arm64.exe`

## Linux(X11)

### 手动下载

- 64 位系统:
  - Debian / Ubuntu:下载 `BongoCat_amd64.deb`
  - Fedora / RHEL:下载 `BongoCat_x86_64.rpm`
  - 通用版本:下载 `BongoCat_amd64.AppImage`
- ARM64 架构:
  - Debian / Ubuntu:下载 `BongoCat_arm64.deb`
  - Fedora / RHEL:下载 `BongoCat_aarch64.rpm`
  - 通用版本:下载 `BongoCat_aarch64.AppImage`

### AUR 下载

- Manjaro / ArchLinux: `yay -S bongo-cat`


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐞 Bug 报告
title: '[bug] '
description: 报告一个 Bug
labels: bug
body:
  - type: markdown
    attributes:
      value: |
        ## 温馨提示
        1. 请先查阅现有的 [issues](https://github.com/ayangweb/BongoCat/issues)。
        2. 请确保你使用的是[最新版本](https://github.com/ayangweb/BongoCat/releases/latest)。
        3. 请确保该问题不是由其他软件引起的。
        4. 请始终保持友好与尊重,感谢你的理解与配合。

  - type: textarea
    id: description
    attributes:
      label: 描述 Bug
      description: 请详细描述 Bug 并提供截图或视频以帮助我们更好地理解问题。
    validations:
      required: true

  - type: textarea
    id: reproduction
    attributes:
      label: 重现步骤
      description: 请详细列出重现问题的步骤,并附带截图或视频。

  - type: textarea
    id: expected-behavior
    attributes:
      label: 预期行为
      description: 请描述你期望发生的行为。

  - type: textarea
    id: info
    attributes:
      render: text
      label: 软件信息
      description: 请前往偏好设置窗口的「关于 > 关于软件 > 软件信息」复制软件信息。
    validations:
      required: true

  - type: textarea
    id: context
    attributes:
      label: 附加信息
      description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 💡 功能请求
title: '[feat] '
description: 提出一个想法
labels: feature request
body:
  - type: textarea
    id: problem
    attributes:
      label: 描述问题
      description: 请清晰地描述此功能将解决的具体问题。
    validations:
      required: true

  - type: textarea
    id: solution
    attributes:
      label: 描述您希望的解决方案
      description: 请清晰地描述您期望的变更或改进。
    validations:
      required: true

  - type: textarea
    id: alternatives
    attributes:
      label: 考虑的替代方案
      description: 提供您考虑过的其他替代解决方案。

  - type: textarea
    id: context
    attributes:
      label: 附加信息
      description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。


================================================
FILE: .github/workflows/release.yml
================================================
name: BongoCat Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:

jobs:
  create-release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set output
        id: vars
        run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT

      - name: Setup node
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Generate changelog
        id: create_release
        run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }}
        env:
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

  build-app:
    needs: create-release
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - platform: macos-latest
            target: aarch64-apple-darwin
          - platform: macos-latest
            target: x86_64-apple-darwin

          - platform: windows-latest
            target: x86_64-pc-windows-msvc
          - platform: windows-latest
            target: i686-pc-windows-msvc
          - platform: windows-latest
            target: aarch64-pc-windows-msvc

          - platform: ubuntu-22.04
            target: x86_64-unknown-linux-gnu
          - platform: ubuntu-22.04-arm
            target: aarch64-unknown-linux-gnu

    runs-on: ${{ matrix.platform }}
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Setup node
        uses: actions/setup-node@v4
        with:
          node-version: 20
      - uses: pnpm/action-setup@v3
        with:
          version: latest

      - name: Install rust target
        run: rustup target add ${{ matrix.target }}

      - name: Install dependencies (ubuntu only)
        if: startsWith(matrix.platform, 'ubuntu')
        run: |
          sudo apt-get update
          sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libudev-dev patchelf xdg-utils

      - name: Install Rust stable
        uses: dtolnay/rust-toolchain@stable

      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: target

      - name: Sync node version and setup cache
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: pnpm

      - name: Install front-end dependencies
        run: pnpm install --frozen-lockfile

      - name: Build the app
        uses: tauri-apps/tauri-action@v0
        env:
          CI: false
          PLATFORM: ${{ matrix.platform }}
          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
        with:
          tagName: ${{ github.ref_name }}
          releaseName: BongoCat ${{ needs.create-release.outputs.APP_VERSION }}
          releaseBody: ''
          releaseDraft: true
          prerelease: false
          args: --target ${{ matrix.target }}


================================================
FILE: .github/workflows/sync-to-gitee.yml
================================================
name: Sync Github Repos To Gitee
on:
  push:
    branches:
      - master

jobs:
  repo-sync:
    runs-on: ubuntu-latest
    steps:
      - name: Sync Github Repos To Gitee
        uses: Yikun/hub-mirror-action@master
        with:
          src: github/ayangweb
          dst: gitee/ayangweb
          dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
          dst_token: ${{ secrets.GITEE_TOKEN }}
          static_list: BongoCat
          force_update: true


================================================
FILE: .github/workflows/upgradelink.yml
================================================
name: Upload Release to UpgradeLink

on:
  release:
    types: [published]
  workflow_dispatch:

jobs:
  upgradeLink-upload:
    permissions:
      contents: write
    runs-on: ubuntu-latest
    steps:
      - name: Send a request to UpgradeLink
        uses: toolsetlink/upgradelink-action@v5
        with:
          source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json'
          access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
          tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }}
          github-token: ${{ secrets.RELEASE_TOKEN }}


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local
target

# Editor directories and files
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?


================================================
FILE: .release-it.ts
================================================
/* eslint-disable no-template-curly-in-string */
import type { Config } from 'release-it'

export default {
  git: {
    commitMessage: 'v${version}',
    tagName: 'v${version}',
  },
  npm: {
    publish: false,
  },
  hooks: {
    'after:bump': 'tsx scripts/release.ts',
  },
} satisfies Config


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "tauri-apps.tauri-vscode",
    "rust-lang.rust-analyzer",
    "antfu.unocss",
    "dbaeumer.vscode-eslint"
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  // Disable the default formatter, use eslint instead
  "prettier.enable": false,

  "eslint.format.enable": true,

  // Auto fix
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": "explicit",
    "source.organizeImports": "never"
  },

  // Silent the stylistic rules in you IDE, but still auto fix them
  "eslint.rules.customizations": [
    { "rule": "style/*", "severity": "off", "fixable": true },
    { "rule": "format/*", "severity": "off", "fixable": true },
    { "rule": "*-indent", "severity": "off", "fixable": true },
    { "rule": "*-spacing", "severity": "off", "fixable": true },
    { "rule": "*-spaces", "severity": "off", "fixable": true },
    { "rule": "*-order", "severity": "off", "fixable": true },
    { "rule": "*-dangle", "severity": "off", "fixable": true },
    { "rule": "*-newline", "severity": "off", "fixable": true },
    { "rule": "*quotes", "severity": "off", "fixable": true },
    { "rule": "*semi", "severity": "off", "fixable": true }
  ],

  // Enable eslint for all supported languages
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue",
    "html",
    "markdown",
    "json",
    "json5",
    "jsonc",
    "yaml",
    "toml",
    "xml",
    "gql",
    "graphql",
    "astro",
    "svelte",
    "css",
    "less",
    "scss",
    "pcss",
    "postcss"
  ],

  "typescript.enablePromptUseWorkspaceTsdk": true,
  "typescript.tsdk": "./node_modules/typescript/lib",

  "i18n-ally.localesPaths": ["src/locales"],
  "i18n-ally.keystyle": "nested",
  "i18n-ally.displayLanguage": "zh-CN"
}


================================================
FILE: Cargo.toml
================================================
[workspace]
resolver = "2"
members = [ "src-tauri" ]

[profile.release]
strip = true
opt-level = 3
codegen-units = 1
panic = "abort"
debug-assertions = false
overflow-checks = false
lto = true

[workspace.dependencies]
tauri = "2"
serde = "1"
serde_json = "1"
fs_extra = "1"
tauri-plugin = { version = "2", features = [ "build" ] }
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-plugin-custom-window = { path = "./src-tauri/src/plugins/window" }


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2025 ayangweb

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
![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)

<div align="center">
  <div>
    <a href="https://github.com/ayangweb/BongoCat/releases"><img alt="Windows" src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg==" /></a>
    <a href="https://github.com/ayangweb/BongoCat/releases"><img alt="MacOS" src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white" /></a>
    <a href="https://github.com/ayangweb/BongoCat/releases"><img alt="Linux" src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white" /></a>
  </div>

  <p>
    <a href="./LICENSE"><img src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square" /></a>
    <a href="https://github.com/ayangweb/BongoCat/releases/latest"><img src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square"/></a>
    <a href="https://github.com/ayangweb/BongoCat/releases"><img src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square"/></a>
  </p>

  <p>
    <a href="https://trendshift.io/developers/8507" target="_blank"><img src="https://trendshift.io/api/badge/developers/8507" alt="ayangweb | Trendshift" width="250" height="55" /></a>
    <a href="https://trendshift.io/repositories/14605" target="_blank"><img src="https://trendshift.io/api/badge/repositories/14605" alt="ayangweb%2FBongoCat | Trendshift" width="250" height="55" /></a>
    <a href="https://hellogithub.com/repository/7d23863fd4be47b39e816193ded385c9" target="_blank">
      <picture>
        <source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=dark" />
        <source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" />
        <img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" width="250" height="55" />
      </picture>
    </a>
  </p>
</div>

| macOS                                                                                        | Windows                                                                                        | Linux(x11)                                                                                   |
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| ![macOS](https://i0.hdslb.com/bfs/openplatform/dff276b96d49c5d6c431b74b531aab72191b3d87.png) | ![Windows](https://i0.hdslb.com/bfs/openplatform/a4149b753856ee7f401989da902cf3b5ad35b39e.png) | ![Linux](https://i0.hdslb.com/bfs/openplatform/3b49f961819d3ff63b2b80251c1cc13c27e986b0.png) |

## 赞助商

<a href="https://www.toolsetlink.com">
  <img height="54" alt="UpgradeLink" src="https://github.com/user-attachments/assets/6b84fb0f-3f1d-44b5-9932-2298bc999d8d" />
</a>

## 开发背景

本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱,但仅支持 Windows 平台。作为一名深度 macOS 用户,我特别希望在自己的设备上也能使用这款可爱的猫咪,于是我决定开发一个适配 macOS 的版本。

同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux(x11),让更多的用户都能与这只可爱的猫咪互动!

## 下载

- [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1)
- [GitHub Releases](https://github.com/ayangweb/BongoCat/releases)

不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。

## 功能介绍

- 适配 macOS、Windows 和 Linux(x11)。
- 根据键盘、鼠标或手柄的操作,同步对应的动作。
- 支持导入自定义模型,自由打造专属猫咪形象。
- 完全开源,代码公开透明,绝不收集任何用户数据。
- 支持离线运行,无需联网,保护用户隐私。

## 模型转换

如果你想将 Bongo-Cat-Mver 应用中的模型转换为兼容 BongoCat 的格式,可以使用以下工具:

🔗 [在线转换](https://bongocat.vteamer.cc)

## 更多模型

你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:

📦 [Awesome-BongoCat](https://github.com/ayangweb/Awesome-BongoCat)

## 社区交流

<table>
  <thead>
    <tr>
      <th>QQ 群 1</th>
      <th>QQ 群 2</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <a href="https://qm.qq.com/q/AS3gNv2Vzy">
          <picture>
            <source
              media="(prefers-color-scheme: dark)"
              srcset="https://i0.hdslb.com/bfs/openplatform/8ecdc4982ab01b59d7731fcca3ec26631a274560.png"
            />
            <source
              media="(prefers-color-scheme: light)"
              srcset="https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png"
            />
            <img
              alt="QQ Group 1"
              src="https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png"
              height="250"
            />
          </picture>
        </a>
      </td>
      <td>
        <a href="https://qm.qq.com/q/TmltLAod2O">
          <picture>
            <source
              media="(prefers-color-scheme: dark)"
              srcset="https://i0.hdslb.com/bfs/openplatform/473c522487ff33e0f32b15466aeb0734f17161c8.png"
            />
            <source
              media="(prefers-color-scheme: light)"
              srcset="https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png"
            />
            <img
              alt="QQ Group 2"
              src="https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png"
              height="250"
            />
          </picture>
        </a>
      </td>
    </tr>
  </tbody>
</table>

## 赞赏

每一份认可都值得被珍视!赞赏随缘,心意无价,谢谢你的支持 ❤️

<picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/e7438bff14cdfb6bfd0feacbb482f99ea4093294.png" />
  <source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png" />
  <img alt="微信赞赏码" src="https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png" height="250" />
</picture>

## 贡献指南

感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。

<a href="https://openomy.com/ayangweb/BongoCat" target="_blank" style="display: block; width: 100%;" align="center">
  <img src="https://openomy.com/svg?repo=ayangweb/BongoCat&chart=bubble" alt="Contribution Leaderboard" style="display: block; width: 100%;" />
</a>

## 历史星标

<a href="https://www.star-history.com/#ayangweb/BongoCat&Date">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date&theme=dark" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
   <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
 </picture>
</a>


================================================
FILE: eslint.config.ts
================================================
import antfu from '@antfu/eslint-config'

export default antfu({
  formatters: true,
  unocss: true,
  rules: {
    'antfu/if-newline': 'off',
    'style/brace-style': ['error', '1tbs'],
    'ts/no-use-before-define': 'off',
    'unused-imports/no-unused-imports': 'error',
    'perfectionist/sort-imports': 'off',
    'import/order': [
      'error',
      {
        'newlines-between': 'always',
        'groups': ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],
        'alphabetize': {
          order: 'asc',
          caseInsensitive: true,
        },
      },
    ],
    'vue/attributes-order': ['error', { alphabetical: true }],
    'vue/max-attributes-per-line': 'error',
  },
  ignores: ['**/*.toml'],
})


================================================
FILE: index.html
================================================
<!doctype html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BongoCat</title>
    <script src="/js/live2dcubismcore.min.js"></script>
    <script src="/js/live2d.min.js"></script>
  </head>

  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>


================================================
FILE: package.json
================================================
{
  "name": "bongo-cat",
  "type": "module",
  "version": "0.9.0",
  "private": true,
  "author": {
    "name": "ayangweb",
    "email": "ayangweb@foxmail.com"
  },
  "scripts": {
    "dev": "run-s build:icon dev:vite",
    "build": "run-s build:*",
    "dev:vite": "vite",
    "build:vite": "vite build",
    "build:icon": "tsx scripts/buildIcon.ts",
    "preview": "vite preview",
    "tauri": "tauri",
    "lint": "eslint --fix src",
    "preinstall": "npx only-allow pnpm",
    "prepare": "simple-git-hooks",
    "release": "release-it"
  },
  "dependencies": {
    "@ant-design/icons-vue": "^7.0.1",
    "@tauri-apps/api": "^2.5.0",
    "@tauri-apps/plugin-autostart": "~2.3.0",
    "@tauri-apps/plugin-clipboard-manager": "~2.2.2",
    "@tauri-apps/plugin-dialog": "~2.2.2",
    "@tauri-apps/plugin-fs": "~2.3.0",
    "@tauri-apps/plugin-global-shortcut": "~2.2.1",
    "@tauri-apps/plugin-log": "~2.3.1",
    "@tauri-apps/plugin-opener": "~2.2.7",
    "@tauri-apps/plugin-os": "^2.2.1",
    "@tauri-apps/plugin-process": "^2.2.1",
    "@tauri-apps/plugin-updater": "~2.7.1",
    "@tauri-store/pinia": "^3.7.0",
    "@vueuse/core": "^13.3.0",
    "ant-design-vue": "^4.2.6",
    "dayjs": "^1.11.13",
    "es-toolkit": "^1.38.0",
    "is-url": "^1.2.4",
    "json5": "^2.2.3",
    "nanoid": "^5.1.5",
    "pinia": "^3.0.3",
    "pixi-live2d-display": "^0.4.0",
    "pixi.js": "^6.5.10",
    "tauri-plugin-locale-api": "^2.0.1",
    "tauri-plugin-macos-permissions-api": "^2.3.0",
    "vue": "^3.5.16",
    "vue-i18n": "^11.1.12",
    "vue-markdown-render": "^2.2.1",
    "vue-router": "^4.5.1",
    "vue3-masonry-css": "^1.0.7"
  },
  "devDependencies": {
    "@antfu/eslint-config": "^4.13.3",
    "@commitlint/cli": "^19.8.1",
    "@commitlint/config-conventional": "^19.8.1",
    "@iconify-json/iconamoon": "^1.2.2",
    "@iconify-json/solar": "^1.2.2",
    "@tauri-apps/cli": "^2.5.0",
    "@types/is-url": "^1.2.32",
    "@types/node": "^22.15.29",
    "@unocss/eslint-plugin": "^66.1.3",
    "@vitejs/plugin-vue": "^5.2.4",
    "eslint": "^9.28.0",
    "eslint-plugin-format": "^1.0.1",
    "lint-staged": "^15.5.2",
    "npm-run-all": "^4.1.5",
    "release-it": "^18.1.2",
    "sass": "^1.89.1",
    "simple-git-hooks": "^2.13.0",
    "tsx": "^4.19.4",
    "typescript": "~5.6.3",
    "unocss": "66.1.0-beta.7",
    "vite": "^6.3.5"
  },
  "simple-git-hooks": {
    "commit-msg": "npx --no-install commitlint -e",
    "pre-commit": "npx lint-staged"
  },
  "lint-staged": {
    "*": "eslint --fix"
  }
}


================================================
FILE: scripts/buildIcon.ts
================================================
import { execSync } from 'node:child_process'
import { env, platform } from 'node:process'

(() => {
  const isMac = env.PLATFORM?.startsWith('macos') ?? platform === 'darwin'

  const logoName = isMac ? 'logo-mac' : 'logo'

  const command = `tauri icon src-tauri/assets/${logoName}.png`

  execSync(command, { stdio: 'inherit' })
})()


================================================
FILE: scripts/release.ts
================================================
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'

import { name, version } from '../package.json'

const __dirname = dirname(fileURLToPath(import.meta.url));

(() => {
  const tomlPath = resolve(__dirname, '..', 'src-tauri', 'Cargo.toml')
  const lockPath = resolve(__dirname, '..', 'Cargo.lock')

  for (const path of [tomlPath, lockPath]) {
    let content = readFileSync(path, 'utf-8')

    const regexp = new RegExp(
      `(name\\s*=\\s*"${name}"\\s*version\\s*=\\s*)"(\\d+\\.\\d+\\.\\d+(-\\w+\\.\\d+)?)"`,
    )

    content = content.replace(regexp, `$1"${version}"`)

    writeFileSync(path, content)
  }
})()


================================================
FILE: src/App.vue
================================================
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { error } from '@tauri-apps/plugin-log'
import { openUrl } from '@tauri-apps/plugin-opener'
import { useEventListener } from '@vueuse/core'
import { ConfigProvider, theme } from 'ant-design-vue'
import { isString } from 'es-toolkit'
import isURL from 'is-url'
import { onMounted, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'

import { useTauriListen } from './composables/useTauriListen'
import { useThemeVars } from './composables/useThemeVars'
import { useWindowState } from './composables/useWindowState'
import { LANGUAGE, LISTEN_KEY } from './constants'
import { getAntdLocale } from './locales/index.ts'
import { hideWindow, showWindow } from './plugins/window'
import { useAppStore } from './stores/app'
import { useCatStore } from './stores/cat'
import { useGeneralStore } from './stores/general'
import { useModelStore } from './stores/model'
import { useShortcutStore } from './stores/shortcut.ts'

const { generateColorVars } = useThemeVars()
const appStore = useAppStore()
const modelStore = useModelStore()
const catStore = useCatStore()
const generalStore = useGeneralStore()
const shortcutStore = useShortcutStore()
const appWindow = getCurrentWebviewWindow()
const { isRestored, restoreState } = useWindowState()
const { darkAlgorithm, defaultAlgorithm } = theme
const { locale } = useI18n()

onMounted(async () => {
  generateColorVars()

  await appStore.$tauri.start()
  await appStore.init()
  await modelStore.$tauri.start()
  await modelStore.init()
  await catStore.$tauri.start()
  catStore.init()
  await generalStore.$tauri.start()
  await generalStore.init()
  await shortcutStore.$tauri.start()
  await restoreState()
})

watch(() => generalStore.appearance.language, (value) => {
  locale.value = value ?? LANGUAGE.EN_US
})

useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
  if (appWindow.label !== payload) return

  showWindow()
})

useTauriListen(LISTEN_KEY.HIDE_WINDOW, ({ payload }) => {
  if (appWindow.label !== payload) return

  hideWindow()
})

useEventListener('unhandledrejection', ({ reason }) => {
  const message = isString(reason) ? reason : JSON.stringify(reason)

  error(message)
})

useEventListener('click', (event) => {
  const link = (event.target as HTMLElement).closest('a')

  if (!link) return

  const { href, target } = link

  if (target === '_blank') return

  event.preventDefault()

  if (!isURL(href)) return

  openUrl(href)
})
</script>

<template>
  <ConfigProvider
    :locale="getAntdLocale(generalStore.appearance.language)"
    :theme="{
      algorithm: generalStore.appearance.isDark ? darkAlgorithm : defaultAlgorithm,
    }"
  >
    <RouterView v-if="isRestored" />
  </ConfigProvider>
</template>


================================================
FILE: src/assets/css/global.scss
================================================
html {
  --uno: select-none overscroll-none antialiased;

  color-scheme: light;

  body {
    --uno: transition-opacity-300;
  }

  &.dark {
    color-scheme: dark;
  }

  img {
    -webkit-user-drag: none;
  }

  button {
    outline: none !important;
  }

  .ant-card {
    .ant-card-actions {
      > li {
        --uno: flex items-center justify-center;
        > span {
          --uno: inline-flex items-center justify-center min-w-unset;
        }
      }
    }
  }
}


================================================
FILE: src/components/pro-list/index.vue
================================================
<script setup lang="ts">
import { Flex } from 'ant-design-vue'

const { title } = defineProps<{
  title: string
}>()
</script>

<template>
  <Flex
    class="not-last:mb-4"
    gap="small"
    vertical
  >
    <div
      class="text-4 font-medium"
      data-tauri-drag-region
    >
      {{ title }}
    </div>

    <Flex
      gap="middle"
      vertical
    >
      <slot />
    </Flex>
  </FLex>
</template>


================================================
FILE: src/components/pro-list-item/index.vue
================================================
<script setup lang="ts">
import { Flex } from 'ant-design-vue'
import { computed, useSlots } from 'vue'

const { title, description, vertical } = defineProps<{
  title: string
  description?: string
  vertical?: boolean
}>()

const slots = useSlots()

const hasDescription = computed(() => {
  return description || slots.description
})
</script>

<template>
  <Flex
    :align="vertical ? void 0 : 'center'"
    class="b b-color-2 rounded-lg b-solid bg-color-3 p-4"
    :gap="vertical ? 'middle' : 'large'"
    justify="space-between"
    :vertical="vertical"
  >
    <Flex
      align="center"
      class="flex-1"
    >
      <Flex vertical>
        <div class="text-sm font-medium">
          {{ title }}
        </div>

        <div
          class="break-all text-xs [&_a]:(active:text-color-primary-7 hover:text-color-primary-5 text-color-3) text-color-3"
          :class="{ 'mt-2': hasDescription }"
        >
          <slot name="description">
            {{ description }}
          </slot>
        </div>
      </Flex>
    </Flex>

    <slot />
  </Flex>
</template>


================================================
FILE: src/components/pro-shortcut/index.vue
================================================
<script setup lang="ts">
import type { Key } from '@/utils/keyboard'

import { find, map, remove, some, split } from 'es-toolkit/compat'
import { ref, useTemplateRef, watch } from 'vue'

import ProListItem from '@/components/pro-list-item/index.vue'
import { keys, modifierKeys, standardKeys } from '@/utils/keyboard'

const props = defineProps<{
  title: string
  description?: string
}>()

const modelValue = defineModel<string>()
const shortcutInputRef = useTemplateRef('shortcutInput')
const isFocusing = ref(false)
const isHovering = ref(false)
const pressedKeys = ref<Key[]>([])

watch(modelValue, () => {
  parseModelValue()
}, { immediate: true })

function parseModelValue() {
  if (!modelValue.value) {
    return pressedKeys.value = []
  }

  pressedKeys.value = split(modelValue.value, '+').map((tauriKey) => {
    return find(keys, { tauriKey })!
  })
}

function getEventKey(event: KeyboardEvent) {
  const { key, code } = event

  const eventKey = key.replace('Meta', 'Command')

  const isModifierKey = some(modifierKeys, { eventKey })

  return isModifierKey ? eventKey : code
}

function isValidShortcut() {
  if (pressedKeys.value?.[0]?.eventKey?.startsWith('F')) {
    return true
  }

  const hasModifierKey = some(pressedKeys.value, ({ eventKey }) => {
    return some(modifierKeys, { eventKey })
  })
  const hasStandardKey = some(pressedKeys.value, ({ eventKey }) => {
    return some(standardKeys, { eventKey })
  })

  return hasModifierKey && hasStandardKey
}

function handleFocus() {
  isFocusing.value = true

  pressedKeys.value = []
}

function handleBlur() {
  isFocusing.value = false

  if (!isValidShortcut()) {
    return parseModelValue()
  }

  modelValue.value = map(pressedKeys.value, 'tauriKey').join('+')
}

function handleKeyDown(event: KeyboardEvent) {
  const eventKey = getEventKey(event)

  const matched = find(keys, { eventKey })
  const isInvalid = !matched
  const isDuplicate = some(pressedKeys.value, { eventKey })

  if (isInvalid || isDuplicate) return

  pressedKeys.value.push(matched)

  if (isValidShortcut()) {
    shortcutInputRef.value?.blur()
  }
}

function handleKeyUp(event: KeyboardEvent) {
  remove(pressedKeys.value, { eventKey: getEventKey(event) })
}
</script>

<template>
  <ProListItem v-bind="props">
    <div
      ref="shortcutInput"
      align="center"
      class="relative h-8 min-w-32 flex cursor-text items-center justify-center b b-color-1 hover:b-primary-5 rounded-md b-solid px-2.5 text-color-3 outline-none transition focus:(b-primary shadow-[0_0_0_2px_rgba(5,145,255,0.1)])"
      justify="center"
      :tabindex="0"
      @blur="handleBlur"
      @focus="handleFocus"
      @keydown="handleKeyDown"
      @keyup="handleKeyUp"
      @mouseout="isHovering = false"
      @mouseover="isHovering = true"
    >
      <span v-if="pressedKeys.length === 0">
        {{ isFocusing ? $t('components.proShortcut.hints.pressRecordShortcut') : $t('components.proShortcut.hints.clickRecordShortcut') }}
      </span>

      <span class="text-primary font-bold">
        {{ map(pressedKeys, 'symbol').join(' ') }}
      </span>

      <div
        class="i-iconamoon:close-circle-1 absolute right-2 cursor-pointer text-4 transition hover:text-primary"
        :hidden="isFocusing || !isHovering || pressedKeys.length === 0"
        @mousedown.prevent="modelValue = ''"
      />
    </div>
  </ProListItem>
</template>


================================================
FILE: src/components/update-app/index.vue
================================================
<script setup lang="ts">
import type { Update } from '@tauri-apps/plugin-updater'

import { relaunch } from '@tauri-apps/plugin-process'
import { check } from '@tauri-apps/plugin-updater'
import { useIntervalFn } from '@vueuse/core'
import { Flex, message, Modal } from 'ant-design-vue'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import { computed, reactive, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import VueMarkdown from 'vue-markdown-render'

import { useTauriListen } from '@/composables/useTauriListen'
import { GITHUB_LINK, LISTEN_KEY, UPGRADE_LINK_ACCESS_KEY } from '@/constants'
import { showWindow } from '@/plugins/window'
import { useGeneralStore } from '@/stores/general'

dayjs.extend(utc)

interface State {
  open: boolean
  update?: Update
  downloading: boolean
  totalProgress?: number
  downloadProgress: number
}

const generalStore = useGeneralStore()
const state = reactive<State>({
  open: false,
  downloading: false,
  downloadProgress: 0,
})
const MESSAGE_KEY = 'updatable'
const { t } = useI18n()

const { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24)

watch(() => generalStore.update.autoCheck, (value) => {
  pause()

  if (!value) return

  checkUpdate()

  resume()
}, { immediate: true })

useTauriListen<boolean>(LISTEN_KEY.UPDATE_APP, () => {
  checkUpdate(true)

  message.loading({
    key: MESSAGE_KEY,
    duration: 0,
    content: t('components.updateApp.hints.checkingUpdates'),
  })
})

const downloadProgress = computed(() => {
  const { downloadProgress, totalProgress } = state

  if (!totalProgress) return '0%'

  const progress = ((downloadProgress / totalProgress) * 100).toFixed(2)

  return `${progress}%`
})

async function checkUpdate(visibleMessage = false) {
  try {
    const update = await check({
      timeout: 5000,
      headers: {
        'X-AccessKey': UPGRADE_LINK_ACCESS_KEY,
      },
    })

    if (update) {
      const { version, currentVersion, body = '', date, downloadAndInstall } = update

      state.update = Object.assign(update, {
        version: `v${version}`,
        currentVersion: `v${currentVersion}`,
        body: replaceBody(body),
        date: dayjs.utc(date?.split('.')[0]).local().format('YYYY-MM-DD HH:mm:ss'),
        downloadAndInstall: downloadAndInstall.bind(update),
      })

      showWindow()

      state.open = true

      message.destroy(MESSAGE_KEY)
    } else if (visibleMessage) {
      message.success({ key: MESSAGE_KEY, content: t('components.updateApp.hints.alreadyLatest') })
    }
  } catch (error) {
    if (!visibleMessage) return

    message.error({ key: MESSAGE_KEY, content: String(error) })
  }
}

function replaceBody(body: string) {
  return body
    .replace(/&nbsp;/g, '')
    .split('\n')
    .map(line => line.replace(/\s*-\s+by\s+@.*/, ''))
    .join('\n')
}

async function handleOk() {
  try {
    state.downloading = true

    await state.update?.downloadAndInstall((progress) => {
      switch (progress.event) {
        case 'Started':
          state.totalProgress = progress.data.contentLength ?? 0
          break
        case 'Progress':
          state.downloadProgress += progress.data.chunkLength
          break
      }
    })

    relaunch()
  } catch (error) {
    message.error(String(error))
  } finally {
    Object.assign(state, {
      downloading: false,
      downloadProgress: 0,
    })
  }
}
</script>

<template>
  <Modal
    v-model:open="state.open"
    :cancel-text="$t('components.updateApp.buttons.updateLater')"
    centered
    :closable="false"
    :mask-closable="false"
    :title="$t('components.updateApp.title')"
    @ok="handleOk"
  >
    <template #okText>
      {{ state.downloading ? downloadProgress : $t('components.updateApp.buttons.updateNow') }}
    </template>

    <Flex
      class="pt-1"
      gap="small"
      vertical
    >
      <Flex align="center">
        <span>{{ $t('components.updateApp.labels.updateVersion') }}</span>
        <span>
          <span>{{ state.update?.currentVersion }} 👉 </span>
          <a
            :href="`${GITHUB_LINK}/releases/tag/${state.update?.version}`"
          >
            {{ state.update?.version }}
          </a>
        </span>
      </Flex>

      <Flex align="center">
        <span>{{ $t('components.updateApp.labels.updateTime') }}</span>
        <span>{{ state.update?.date }}</span>
      </Flex>

      <Flex vertical>
        <span>{{ $t('components.updateApp.labels.changelog') }}</span>

        <VueMarkdown
          class="update-note max-h-40 overflow-auto"
          :source="state.update?.body ?? ''"
        />
      </Flex>
    </Flex>
  </Modal>
</template>

<style lang="scss" scoped>
.update-note {
  :not(a) {
    all: revert;
  }
}
</style>


================================================
FILE: src/composables/useDevice.ts
================================================
import { invoke } from '@tauri-apps/api/core'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { cursorPosition } from '@tauri-apps/api/window'

import { INVOKE_KEY, LISTEN_KEY } from '../constants'

import { useModel } from './useModel'
import { useTauriListen } from './useTauriListen'

import { useCatStore } from '@/stores/cat'
import { useModelStore } from '@/stores/model'
import { inBetween } from '@/utils/is'
import { isWindows } from '@/utils/platform'

interface MouseButtonEvent {
  kind: 'MousePress' | 'MouseRelease'
  value: string
}

export interface CursorPoint {
  x: number
  y: number
}

interface MouseMoveEvent {
  kind: 'MouseMove'
  value: CursorPoint
}

interface KeyboardEvent {
  kind: 'KeyboardPress' | 'KeyboardRelease'
  value: string
}

type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent

export function useDevice() {
  const modelStore = useModelStore()
  const releaseTimers = new Map<string, NodeJS.Timeout>()
  const catStore = useCatStore()
  const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()

  const startListening = () => {
    invoke(INVOKE_KEY.START_DEVICE_LISTENING)
  }

  const getSupportedKey = (key: string) => {
    let nextKey = key

    const unsupportedKey = !modelStore.supportKeys[nextKey]

    if (key.startsWith('F') && unsupportedKey) {
      nextKey = key.replace(/F(\d+)/, 'Fn')
    }

    for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {
      if (key.startsWith(item) && unsupportedKey) {
        const regex = new RegExp(`^(${item}).*`)
        nextKey = key.replace(regex, '$1')
      }
    }

    return nextKey
  }

  const handleCursorMove = async () => {
    const cursorPoint = await cursorPosition()

    handleMouseMove(cursorPoint)

    if (catStore.window.hideOnHover) {
      const appWindow = getCurrentWebviewWindow()
      const position = await appWindow.outerPosition()
      const { width, height } = await appWindow.innerSize()

      const isInWindow = inBetween(cursorPoint.x, position.x, position.x + width)
        && inBetween(cursorPoint.y, position.y, position.y + height)

      document.body.style.setProperty('opacity', isInWindow ? '0' : 'unset')

      if (!catStore.window.passThrough) {
        appWindow.setIgnoreCursorEvents(isInWindow)
      }
    }
  }

  const handleAutoRelease = (key: string, delay = 100) => {
    handlePress(key)

    if (releaseTimers.has(key)) {
      clearTimeout(releaseTimers.get(key))
    }

    const timer = setTimeout(() => {
      handleRelease(key)

      releaseTimers.delete(key)
    }, delay)

    releaseTimers.set(key, timer)
  }

  useTauriListen<DeviceEvent>(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => {
    const { kind, value } = payload

    if (kind === 'KeyboardPress' || kind === 'KeyboardRelease') {
      const nextValue = getSupportedKey(value)

      if (!nextValue) return

      if (nextValue === 'CapsLock') {
        return handleAutoRelease(nextValue)
      }

      if (kind === 'KeyboardPress') {
        if (isWindows) {
          const delay = catStore.model.autoReleaseDelay * 1000

          return handleAutoRelease(nextValue, delay)
        }

        return handlePress(nextValue)
      }

      return handleRelease(nextValue)
    }

    switch (kind) {
      case 'MousePress':
        return handleMouseChange(value)
      case 'MouseRelease':
        return handleMouseChange(value, false)
      case 'MouseMove':
        return handleCursorMove()
    }
  })

  return {
    startListening,
  }
}


================================================
FILE: src/composables/useGamepad.ts
================================================
import type { LiteralUnion } from 'ant-design-vue/es/_util/type'

import { invoke } from '@tauri-apps/api/core'
import { computed, reactive, watch } from 'vue'

import { useModel } from './useModel'
import { useTauriListen } from './useTauriListen'

import { INVOKE_KEY, LISTEN_KEY } from '@/constants'
import { useModelStore } from '@/stores/model'
import live2d from '@/utils/live2d'

type GamepadEventName = LiteralUnion<'LeftStickX' | 'LeftStickY' | 'RightStickX' | 'RightStickY' | 'LeftThumb' | 'RightThumb'>

interface GamepadEvent {
  kind: 'ButtonChanged' | 'AxisChanged'
  name: GamepadEventName
  value: number
}

interface StickState {
  x: number
  y: number
  moved: boolean
  pressed: boolean
}

interface Sticks {
  left: StickState
  right: StickState
}

const INITIAL_STICK_STATE: StickState = { x: 0, y: 0, moved: false, pressed: false }

export function useGamepad() {
  const { currentModel } = useModelStore()
  const { handlePress, handleRelease, handleAxisChange } = useModel()
  const sticks = reactive<Sticks>({
    left: { ...INITIAL_STICK_STATE },
    right: { ...INITIAL_STICK_STATE },
  })

  const stickActive = computed(() => ({
    left: sticks.left.moved || sticks.left.pressed,
    right: sticks.right.moved || sticks.right.pressed,
  }))

  watch(() => currentModel?.mode, (mode) => {
    if (mode === 'gamepad') {
      return invoke(INVOKE_KEY.START_GAMEPAD_LISTING)
    }

    invoke(INVOKE_KEY.STOP_GAMEPAD_LISTING)
  }, { immediate: true })

  watch(sticks.left, ({ x, y, moved, pressed }) => {
    sticks.left.moved = x !== 0 || y !== 0

    live2d.setParameterValue('CatParamStickShowLeftHand', moved || pressed)
  }, { deep: true })

  watch(sticks.right, ({ x, y, moved, pressed }) => {
    sticks.right.moved = x !== 0 || y !== 0

    live2d.setParameterValue('CatParamStickShowRightHand', moved || pressed)
  }, { deep: true })

  useTauriListen<GamepadEvent>(LISTEN_KEY.GAMEPAD_CHANGED, ({ payload }) => {
    const { name, value } = payload

    switch (name) {
      case 'LeftStickX':
        sticks.left.x = value

        return handleAxisChange('CatParamStickLX', value)
      case 'LeftStickY':
        sticks.left.y = value

        return handleAxisChange('CatParamStickLY', value)
      case 'RightStickX':
        sticks.right.x = value

        return handleAxisChange('CatParamStickRX', value)
      case 'RightStickY':
        sticks.right.y = value

        return handleAxisChange('CatParamStickRY', value)
      case 'LeftThumb':
        sticks.left.pressed = value !== 0

        return live2d.setParameterValue('CatParamStickLeftDown', value !== 0)
      case 'RightThumb':
        sticks.right.pressed = value !== 0

        return live2d.setParameterValue('CatParamStickRightDown', value !== 0)
      default:
        return value > 0 ? handlePress(name) : handleRelease(name)
    }
  })

  return {
    stickActive,
  }
}


================================================
FILE: src/composables/useModel.ts
================================================
import type { PhysicalPosition } from '@tauri-apps/api/dpi'

import { LogicalSize } from '@tauri-apps/api/dpi'
import { resolveResource, sep } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { message } from 'ant-design-vue'
import { isNil, round } from 'es-toolkit'
import { nth } from 'es-toolkit/compat'
import { ref } from 'vue'

import live2d from '../utils/live2d'

import { useCatStore } from '@/stores/cat'
import { useModelStore } from '@/stores/model'
import { getCursorMonitor } from '@/utils/monitor'

const appWindow = getCurrentWebviewWindow()

export interface ModelSize {
  width: number
  height: number
}

export function useModel() {
  const modelStore = useModelStore()
  const catStore = useCatStore()
  const modelSize = ref<ModelSize>()

  async function handleLoad() {
    try {
      if (!modelStore.currentModel) return

      const { path } = modelStore.currentModel

      await resolveResource(path)

      const { width, height, ...rest } = await live2d.load(path)

      modelSize.value = { width, height }

      handleResize()

      Object.assign(modelStore, rest)
    } catch (error) {
      message.error(String(error))
    }
  }

  function handleDestroy() {
    live2d.destroy()
  }

  async function handleResize() {
    if (!modelSize.value) return

    live2d.resizeModel(modelSize.value)

    const { width, height } = modelSize.value

    if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) {
      await appWindow.setSize(
        new LogicalSize({
          width: innerWidth,
          height: Math.ceil(innerWidth * (height / width)),
        }),
      )
    }

    const size = await appWindow.size()

    catStore.window.scale = round((size.width / width) * 100)
  }

  const handlePress = (key: string) => {
    const path = modelStore.supportKeys[key]

    if (!path) return

    if (catStore.model.single) {
      const dirName = nth(path.split(sep()), -2)!

      const filterKeys = Object.entries(modelStore.pressedKeys).filter(([, value]) => {
        return value.includes(dirName)
      })

      for (const [key] of filterKeys) {
        handleRelease(key)
      }
    }

    modelStore.pressedKeys[key] = path
  }

  const handleRelease = (key: string) => {
    delete modelStore.pressedKeys[key]
  }

  function handleKeyChange(isLeft = true, pressed = true) {
    const id = isLeft ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'

    live2d.setParameterValue(id, pressed)
  }

  function handleMouseChange(key: string, pressed = true) {
    const id = key === 'Left' ? 'ParamMouseLeftDown' : 'ParamMouseRightDown'

    live2d.setParameterValue(id, pressed)
  }

  async function handleMouseMove(cursorPoint: PhysicalPosition) {
    const monitor = await getCursorMonitor(cursorPoint)

    if (!monitor) return

    const { size, position } = monitor

    const xRatio = (cursorPoint.x - position.x) / size.width
    const yRatio = (cursorPoint.y - position.y) / size.height

    for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) {
      const { min, max } = live2d.getParameterRange(id)

      if (isNil(min) || isNil(max)) continue

      const isXAxis = id.endsWith('X')

      const ratio = isXAxis ? xRatio : yRatio
      let value = max - (ratio * (max - min))

      if (isXAxis && catStore.model.mouseMirror) {
        value *= -1
      }

      live2d.setParameterValue(id, value)
    }
  }

  async function handleAxisChange(id: string, value: number) {
    const { min, max } = live2d.getParameterRange(id)

    live2d.setParameterValue(id, Math.max(min, value * max))
  }

  return {
    modelSize,
    handlePress,
    handleRelease,
    handleLoad,
    handleDestroy,
    handleResize,
    handleKeyChange,
    handleMouseChange,
    handleMouseMove,
    handleAxisChange,
  }
}


================================================
FILE: src/composables/useSharedMenu.ts
================================================
import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
import { range } from 'es-toolkit'
import { useI18n } from 'vue-i18n'

import { showWindow } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
import { isMac } from '@/utils/platform'

export function useSharedMenu() {
  const catStore = useCatStore()
  const { t } = useI18n()

  const getScaleMenuItems = async () => {
    const options = range(50, 151, 25)

    const items = options.map((item) => {
      return CheckMenuItem.new({
        text: `${item}%`,
        checked: catStore.window.scale === item,
        action: () => {
          catStore.window.scale = item
        },
      })
    })

    if (!options.includes(catStore.window.scale)) {
      items.unshift(CheckMenuItem.new({
        text: `${catStore.window.scale}%`,
        checked: true,
        enabled: false,
      }))
    }

    return Promise.all(items)
  }

  const getOpacityMenuItems = async () => {
    const options = range(25, 101, 25)

    const items = options.map((item) => {
      return CheckMenuItem.new({
        text: `${item}%`,
        checked: catStore.window.opacity === item,
        action: () => {
          catStore.window.opacity = item
        },
      })
    })

    if (!options.includes(catStore.window.opacity)) {
      items.unshift(CheckMenuItem.new({
        text: `${catStore.window.opacity}%`,
        checked: true,
        enabled: false,
      }))
    }

    return Promise.all(items)
  }

  const getSharedMenu = async () => {
    return await Promise.all([
      MenuItem.new({
        text: t('composables.useSharedMenu.labels.preference'),
        accelerator: isMac ? 'Cmd+,' : '',
        action: () => showWindow('preference'),
      }),
      MenuItem.new({
        text: catStore.window.visible ? t('composables.useSharedMenu.labels.hideCat') : t('composables.useSharedMenu.labels.showCat'),
        action: () => {
          catStore.window.visible = !catStore.window.visible
        },
      }),
      PredefinedMenuItem.new({ item: 'Separator' }),
      CheckMenuItem.new({
        text: t('composables.useSharedMenu.labels.passThrough'),
        checked: catStore.window.passThrough,
        action: () => {
          catStore.window.passThrough = !catStore.window.passThrough
        },
      }),
      Submenu.new({
        text: t('composables.useSharedMenu.labels.windowSize'),
        items: await getScaleMenuItems(),
      }),
      Submenu.new({
        text: t('composables.useSharedMenu.labels.opacity'),
        items: await getOpacityMenuItems(),
      }),
    ])
  }

  return {
    getSharedMenu,
  }
}


================================================
FILE: src/composables/useTauriListen.ts
================================================
import { listen } from '@tauri-apps/api/event'
import { noop } from '@vueuse/core'
import { onMounted, onUnmounted, ref } from 'vue'

export function useTauriListen<T>(...args: Parameters<typeof listen<T>>) {
  const unlisten = ref(noop)

  onMounted(async () => {
    unlisten.value = await listen<T>(...args)
  })

  onUnmounted(() => {
    unlisten.value()
  })
}


================================================
FILE: src/composables/useTauriShortcut.ts
================================================
import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut'
import type { Ref } from 'vue'

import {
  isRegistered,
  register,
  unregister,
} from '@tauri-apps/plugin-global-shortcut'
import { ref, watch } from 'vue'

export function useTauriShortcut(shortcut: Ref<string, string>, callback: ShortcutHandler) {
  const oldShortcut = ref(shortcut.value)

  watch(shortcut, async (value) => {
    if (oldShortcut.value) {
      const registered = await isRegistered(oldShortcut.value)

      if (registered) {
        await unregister(oldShortcut.value)
      }
    }

    if (!value) return

    await register(value, (event) => {
      if (event.state === 'Released') return

      callback(event)
    })

    oldShortcut.value = value
  }, { immediate: true })
}


================================================
FILE: src/composables/useThemeVars.ts
================================================
import { theme } from 'ant-design-vue'
import { kebabCase } from 'es-toolkit'

export function useThemeVars() {
  const { defaultAlgorithm, darkAlgorithm, defaultConfig } = theme

  const generateColorVars = () => {
    const { token } = defaultConfig

    const colors = [
      defaultAlgorithm(token),
      darkAlgorithm(token),
    ]

    for (const [index, item] of colors.entries()) {
      const isDark = index !== 0
      const vars: Record<string, any> = {}

      for (const [key, value] of Object.entries(item)) {
        vars[`--ant-${kebabCase(key)}`] = value
      }

      const style = document.createElement('style')
      style.dataset.theme = isDark ? 'dark' : 'light'
      const selector = isDark ? 'html.dark' : ':root'
      const values = Object.entries(vars).map(([key, value]) => `${key}: ${value};`)

      style.innerHTML = `${selector}{\n${values.join('\n')}\n}`
      document.head.appendChild(style)
    }
  }

  return {
    generateColorVars,
  }
}


================================================
FILE: src/composables/useTray.ts
================================================
import type { TrayIconOptions } from '@tauri-apps/api/tray'

import { getName, getVersion } from '@tauri-apps/api/app'
import { emit } from '@tauri-apps/api/event'
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
import { resolveResource } from '@tauri-apps/api/path'
import { TrayIcon } from '@tauri-apps/api/tray'
import { openUrl } from '@tauri-apps/plugin-opener'
import { exit, relaunch } from '@tauri-apps/plugin-process'
import { watchDebounced } from '@vueuse/core'
import { watch } from 'vue'
import { useI18n } from 'vue-i18n'

import { GITHUB_LINK, LISTEN_KEY } from '../constants'
import { showWindow } from '../plugins/window'
import { isMac } from '../utils/platform'

import { useSharedMenu } from './useSharedMenu'

import { useCatStore } from '@/stores/cat'
import { useGeneralStore } from '@/stores/general'

const TRAY_ID = 'BONGO_CAT_TRAY'

export function useTray() {
  const catStore = useCatStore()
  const generalStore = useGeneralStore()
  const { getSharedMenu } = useSharedMenu()
  const { t } = useI18n()

  watch([() => catStore.window.visible, () => catStore.window.passThrough, () => generalStore.appearance.language], () => {
    updateTrayMenu()
  })

  watchDebounced([() => catStore.window.scale, () => catStore.window.opacity], () => {
    updateTrayMenu()
  }, { debounce: 200 })

  const createTray = async () => {
    const tray = await getTrayById()

    if (tray) return

    const appName = await getName()
    const appVersion = await getVersion()

    const menu = await getTrayMenu()

    const path = isMac ? 'assets/tray-mac.png' : 'assets/tray.png'
    const icon = await resolveResource(path)

    const options: TrayIconOptions = {
      menu,
      icon,
      id: TRAY_ID,
      tooltip: `${appName} v${appVersion}`,
      iconAsTemplate: true,
      menuOnLeftClick: true,
    }

    return TrayIcon.new(options)
  }

  const getTrayById = () => {
    return TrayIcon.getById(TRAY_ID)
  }

  const getTrayMenu = async () => {
    const appVersion = await getVersion()

    const items = await Promise.all([
      ...await getSharedMenu(),
      PredefinedMenuItem.new({ item: 'Separator' }),
      MenuItem.new({
        text: t('composables.useTray.checkUpdate'),
        action: () => {
          showWindow()

          emit(LISTEN_KEY.UPDATE_APP)
        },
      }),
      MenuItem.new({
        text: t('composables.useTray.openSource'),
        action: () => openUrl(GITHUB_LINK),
      }),
      PredefinedMenuItem.new({ item: 'Separator' }),
      MenuItem.new({
        text: `v${appVersion}`,
        enabled: false,
      }),
      MenuItem.new({
        text: t('composables.useTray.restartApp'),
        action: relaunch,
      }),
      MenuItem.new({
        text: t('composables.useTray.quitApp'),
        accelerator: isMac ? 'Cmd+Q' : '',
        action: () => exit(0),
      }),
    ])

    return Menu.new({ items })
  }

  const updateTrayMenu = async () => {
    const tray = await getTrayById()

    if (!tray) return

    const menu = await getTrayMenu()

    tray.setMenu(menu)
  }

  return {
    createTray,
  }
}


================================================
FILE: src/composables/useWindowPosition.ts
================================================
import { PhysicalPosition } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { onMounted, ref, watch } from 'vue'

import { useCatStore } from '@/stores/cat'
import { getCursorMonitor } from '@/utils/monitor'

const appWindow = getCurrentWebviewWindow()

export function useWindowPosition() {
  const catStore = useCatStore()
  const isMounted = ref(false)

  const setWindowPosition = async () => {
    const monitor = await getCursorMonitor()

    if (!monitor) return

    const windowSize = await appWindow.outerSize()

    switch (catStore.window.position) {
      case 'topLeft':
        return appWindow.setPosition(new PhysicalPosition(0, 0))
      case 'topRight':
        return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, 0))
      case 'bottomLeft':
        return appWindow.setPosition(new PhysicalPosition(0, monitor.size.height - windowSize.height))
      default:
        return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, monitor.size.height - windowSize.height))
    }
  }

  onMounted(async () => {
    await setWindowPosition()

    isMounted.value = true

    appWindow.onScaleChanged(setWindowPosition)
  })

  watch(() => catStore.window.position, setWindowPosition)

  return {
    isMounted,
    setWindowPosition,
  }
}


================================================
FILE: src/composables/useWindowState.ts
================================================
import type { Event } from '@tauri-apps/api/event'

import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { availableMonitors } from '@tauri-apps/api/window'
import { isNumber } from 'es-toolkit/compat'
import { onMounted, ref } from 'vue'

import { useAppStore } from '@/stores/app'

export type WindowState = Record<string, Partial<PhysicalPosition & PhysicalSize> | undefined>

const appWindow = getCurrentWebviewWindow()
const { label } = appWindow

export function useWindowState() {
  const appStore = useAppStore()
  const isRestored = ref(false)

  onMounted(() => {
    appWindow.onMoved(onChange)

    appWindow.onResized(onChange)
  })

  const onChange = async (event: Event<PhysicalPosition | PhysicalSize>) => {
    const minimized = await appWindow.isMinimized()

    if (minimized) return

    appStore.windowState[label] ??= {}

    Object.assign(appStore.windowState[label], event.payload)
  }

  const restoreState = async () => {
    const { x, y, width, height } = appStore.windowState[label] ?? {}

    if (isNumber(x) && isNumber(y)) {
      const monitors = await availableMonitors()

      const monitor = monitors.find((monitor) => {
        const { position, size } = monitor

        const inBoundsX = x >= position.x && x <= position.x + size.width
        const inBoundsY = y >= position.y && y <= position.y + size.height

        return inBoundsX && inBoundsY
      })

      if (monitor) {
        await appWindow.setPosition(new PhysicalPosition(x, y))
      }
    }

    if (width && height) {
      await appWindow.setSize(new PhysicalSize(width, height))
    }

    isRestored.value = true
  }

  return {
    isRestored,
    restoreState,
  }
}


================================================
FILE: src/constants/index.ts
================================================
export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'

export const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'

export const LISTEN_KEY = {
  SHOW_WINDOW: 'show-window',
  HIDE_WINDOW: 'hide-window',
  DEVICE_CHANGED: 'device-changed',
  UPDATE_APP: 'update-app',
  GAMEPAD_CHANGED: 'gamepad-changed',
}

export const INVOKE_KEY = {
  COPY_DIR: 'copy_dir',
  START_DEVICE_LISTENING: 'start_device_listening',
  START_GAMEPAD_LISTING: 'start_gamepad_listing',
  STOP_GAMEPAD_LISTING: 'stop_gamepad_listing',
}

export const LANGUAGE = {
  ZH_CN: 'zh-CN',
  EN_US: 'en-US',
  VI_VN: 'vi-VN',
  PT_BR: 'pt-BR',
} as const


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


================================================
FILE: src/locales/index.ts
================================================
import type { Language } from '@/stores/general'
import type { Locale as AntdLocale } from 'ant-design-vue/es/locale'

import antdEnUS from 'ant-design-vue/locale/en_US'
import antdPtBR from 'ant-design-vue/locale/pt_BR'
import antdViVN from 'ant-design-vue/locale/vi_VN'
import antdZhCN from 'ant-design-vue/locale/zh_CN'
import { createI18n } from 'vue-i18n'

import enUS from './en-US.json'
import ptBR from './pt-BR.json'
import viVN from './vi-VN.json'
import zhCN from './zh-CN.json'

import { LANGUAGE } from '@/constants'

export const i18n = createI18n({
  legacy: false,
  locale: LANGUAGE.EN_US,
  fallbackLocale: LANGUAGE.EN_US,
  messages: {
    [LANGUAGE.ZH_CN]: zhCN,
    [LANGUAGE.EN_US]: enUS,
    [LANGUAGE.VI_VN]: viVN,
    [LANGUAGE.PT_BR]: ptBR,
  },
})

export function getAntdLocale(language: Language = LANGUAGE.EN_US) {
  const antdLanguage: Record<Language, AntdLocale> = {
    [LANGUAGE.ZH_CN]: antdZhCN,
    [LANGUAGE.EN_US]: antdEnUS,
    [LANGUAGE.VI_VN]: antdViVN,
    [LANGUAGE.PT_BR]: antdPtBR,
  }

  return antdLanguage[language]
}


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


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


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


================================================
FILE: src/main.ts
================================================
import { createPlugin } from '@tauri-store/pinia'
import { createPinia } from 'pinia'
import { createApp } from 'vue'

import App from './App.vue'
import { i18n } from './locales'
import router from './router'

import 'virtual:uno.css'
import 'ant-design-vue/dist/reset.css'
import './assets/css/global.scss'

const pinia = createPinia()
pinia.use(createPlugin({ saveOnChange: true }))

createApp(App).use(router).use(pinia).use(i18n).mount('#app')


================================================
FILE: src/pages/main/index.vue
================================================
<script setup lang="ts">
import { convertFileSrc } from '@tauri-apps/api/core'
import { PhysicalSize } from '@tauri-apps/api/dpi'
import { Menu } from '@tauri-apps/api/menu'
import { sep } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { exists, readDir } from '@tauri-apps/plugin-fs'
import { useDebounceFn, useEventListener } from '@vueuse/core'
import { round } from 'es-toolkit'
import { nth } from 'es-toolkit/compat'
import { onMounted, onUnmounted, ref, watch } from 'vue'

import { useDevice } from '@/composables/useDevice'
import { useGamepad } from '@/composables/useGamepad'
import { useModel } from '@/composables/useModel'
import { useSharedMenu } from '@/composables/useSharedMenu'
import { useWindowPosition } from '@/composables/useWindowPosition'
import { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
import { useGeneralStore } from '@/stores/general.ts'
import { useModelStore } from '@/stores/model'
import { isImage } from '@/utils/is'
import { join } from '@/utils/path'
import { clearObject } from '@/utils/shared'

const { startListening } = useDevice()
const appWindow = getCurrentWebviewWindow()
const { modelSize, handleLoad, handleDestroy, handleResize, handleKeyChange } = useModel()
const catStore = useCatStore()
const { getSharedMenu } = useSharedMenu()
const modelStore = useModelStore()
const generalStore = useGeneralStore()
const resizing = ref(false)
const backgroundImagePath = ref<string>()
const { stickActive } = useGamepad()
const { isMounted, setWindowPosition } = useWindowPosition()

onMounted(startListening)

onUnmounted(handleDestroy)

const debouncedResize = useDebounceFn(async () => {
  await handleResize()

  await setWindowPosition()

  resizing.value = false
}, 100)

useEventListener('resize', () => {
  resizing.value = true

  debouncedResize()
})

watch(() => modelStore.currentModel, async (model) => {
  if (!model) return

  await handleLoad()

  const path = join(model.path, 'resources', 'background.png')

  const existed = await exists(path)

  backgroundImagePath.value = existed ? convertFileSrc(path) : void 0

  clearObject([modelStore.supportKeys, modelStore.pressedKeys])

  const resourcePath = join(model.path, 'resources')
  const groups = ['left-keys', 'right-keys']

  for await (const groupName of groups) {
    const groupDir = join(resourcePath, groupName)
    const files = await readDir(groupDir).catch(() => [])
    const imageFiles = files.filter(file => isImage(file.name))

    for (const file of imageFiles) {
      const fileName = file.name.split('.')[0]

      modelStore.supportKeys[fileName] = join(groupDir, file.name)
    }
  }

  setWindowPosition()
}, { deep: true, immediate: true })

watch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => {
  if (!modelSize) return

  const { width, height } = modelSize

  appWindow.setSize(
    new PhysicalSize({
      width: Math.round(width * (scale / 100)),
      height: Math.round(height * (scale / 100)),
    }),
  )
}, { immediate: true })

watch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => {
  const dirs = Object.values(keys).map((path) => {
    return nth(path.split(sep()), -2)!
  })

  const hasLeft = dirs.some(dir => dir.startsWith('left'))
  const hasRight = dirs.some(dir => dir.startsWith('right'))

  handleKeyChange(true, stickActive.left || hasLeft)
  handleKeyChange(false, stickActive.right || hasRight)
}, { deep: true })

watch(() => catStore.window.visible, async (value) => {
  value ? showWindow() : hideWindow()
})

watch(() => catStore.window.passThrough, (value) => {
  appWindow.setIgnoreCursorEvents(value)
}, { immediate: true })

watch(() => catStore.window.alwaysOnTop, setAlwaysOnTop, { immediate: true })

watch(() => generalStore.app.taskbarVisible, setTaskbarVisibility, { immediate: true })

function handleMouseDown() {
  appWindow.startDragging()
}

async function handleContextmenu(event: MouseEvent) {
  event.preventDefault()

  if (event.shiftKey) return

  const menu = await Menu.new({
    items: await getSharedMenu(),
  })

  menu.popup()
}

function handleMouseMove(event: MouseEvent) {
  const { buttons, shiftKey, movementX, movementY } = event

  if (buttons !== 2 || !shiftKey) return

  const delta = (movementX + movementY) * 0.5
  const nextScale = Math.max(10, Math.min(catStore.window.scale + delta, 500))

  catStore.window.scale = round(nextScale)
}
</script>

<template>
  <div
    v-show="isMounted"
    class="relative size-screen overflow-hidden children:(absolute size-full)"
    :class="{ '-scale-x-100': catStore.model.mirror }"
    :style="{
      opacity: catStore.window.opacity / 100,
      borderRadius: `${catStore.window.radius}%`,
    }"
    @contextmenu="handleContextmenu"
    @mousedown="handleMouseDown"
    @mousemove="handleMouseMove"
  >
    <img
      v-if="backgroundImagePath"
      class="object-cover"
      :src="backgroundImagePath"
    >

    <canvas id="live2dCanvas" />

    <img
      v-for="path in modelStore.pressedKeys"
      :key="path"
      class="object-cover"
      :src="convertFileSrc(path)"
    >

    <div
      v-show="resizing"
      class="flex items-center justify-center bg-black"
    >
      <span class="text-center text-[10vw] text-white">
        {{ $t('pages.main.hints.redrawing') }}
      </span>
    </div>
  </div>
</template>


================================================
FILE: src/pages/preference/components/about/index.vue
================================================
<script setup lang="ts">
import { getTauriVersion } from '@tauri-apps/api/app'
import { emit } from '@tauri-apps/api/event'
import { appLogDir } from '@tauri-apps/api/path'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { openPath, openUrl } from '@tauri-apps/plugin-opener'
import { arch, platform, version } from '@tauri-apps/plugin-os'
import { Button, message } from 'ant-design-vue'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { GITHUB_LINK, LISTEN_KEY } from '@/constants'
import { useAppStore } from '@/stores/app'

const appStore = useAppStore()
const logDir = ref('')
const { t } = useI18n()

onMounted(async () => {
  logDir.value = await appLogDir()
})

function handleUpdate() {
  emit(LISTEN_KEY.UPDATE_APP)
}

async function copyInfo() {
  const info = {
    appName: appStore.name,
    appVersion: appStore.version,
    tauriVersion: await getTauriVersion(),
    platform: platform(),
    platformArch: arch(),
    platformVersion: version(),
  }

  await writeText(JSON.stringify(info, null, 2))

  message.success(t('pages.preference.about.hints.copySuccess'))
}

function feedbackIssue() {
  openUrl(`${GITHUB_LINK}/issues/new/choose`)
}
</script>

<template>
  <ProList :title="$t('pages.preference.about.labels.aboutApp')">
    <ProListItem
      :description="`v${appStore.version}`"
      :title="appStore.name"
    >
      <Button
        type="primary"
        @click="handleUpdate"
      >
        {{ $t('pages.preference.about.buttons.checkUpdate') }}
      </Button>

      <template #icon>
        <div class="b b-color-2 rounded-xl b-solid">
          <img
            class="size-12"
            src="/logo.png"
          >
        </div>
      </template>
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.about.hints.appInfo')"
      :title="$t('pages.preference.about.labels.appInfo')"
    >
      <Button @click="copyInfo">
        {{ $t('pages.preference.about.buttons.copy') }}
      </Button>
    </ProListItem>

    <ProListItem :title="$t('pages.preference.about.labels.openSource')">
      <Button
        danger
        @click="feedbackIssue"
      >
        {{ $t('pages.preference.about.buttons.feedbackIssues') }}
      </Button>

      <template #description>
        <a :href="GITHUB_LINK">
          {{ GITHUB_LINK }}
        </a>
      </template>
    </ProListItem>

    <ProListItem
      :description="logDir"
      :title="$t('pages.preference.about.labels.appLog')"
    >
      <Button @click="openPath(logDir)">
        {{ $t('pages.preference.about.buttons.viewLog') }}
      </Button>
    </ProListItem>
  </ProList>
</template>


================================================
FILE: src/pages/preference/components/cat/components/position/index.vue
================================================
<script setup lang="ts">
import { Select, SelectOption } from 'ant-design-vue'

import ProListItem from '@/components/pro-list-item/index.vue'
import { useCatStore } from '@/stores/cat'

const catStore = useCatStore()
</script>

<template>
  <ProListItem
    :description="$t('pages.preference.cat.hints.position')"
    :title="$t('pages.preference.cat.labels.position')"
  >
    <Select v-model:value="catStore.window.position">
      <SelectOption value="bottomRight">
        {{ $t('pages.preference.cat.options.bottomRight') }}
      </SelectOption>
      <SelectOption value="bottomLeft">
        {{ $t('pages.preference.cat.options.bottomLeft') }}
      </SelectOption>
      <SelectOption value="topLeft">
        {{ $t('pages.preference.cat.options.topLeft') }}
      </SelectOption>
      <SelectOption value="topRight">
        {{ $t('pages.preference.cat.options.topRight') }}
      </SelectOption>
    </Select>
  </ProListItem>
</template>


================================================
FILE: src/pages/preference/components/cat/index.vue
================================================
<script setup lang="ts">
import { InputNumber, Slider, Switch } from 'ant-design-vue'

import Position from './components/position/index.vue'

import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useCatStore } from '@/stores/cat'
import { isWindows } from '@/utils/platform'

const catStore = useCatStore()
</script>

<template>
  <ProList :title="$t('pages.preference.cat.labels.modelSettings')">
    <ProListItem
      :description="$t('pages.preference.cat.hints.mirrorMode')"
      :title="$t('pages.preference.cat.labels.mirrorMode')"
    >
      <Switch v-model:checked="catStore.model.mirror" />
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.cat.hints.singleMode')"
      :title="$t('pages.preference.cat.labels.singleMode')"
    >
      <Switch v-model:checked="catStore.model.single" />
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.cat.hints.mouseMirror')"
      :title="$t('pages.preference.cat.labels.mouseMirror')"
    >
      <Switch v-model:checked="catStore.model.mouseMirror" />
    </ProListItem>

    <ProListItem
      v-if="isWindows"
      :description="$t('pages.preference.cat.hints.autoReleaseDelay')"
      :title="$t('pages.preference.cat.labels.autoReleaseDelay')"
    >
      <InputNumber
        v-model:value="catStore.model.autoReleaseDelay"
        addon-after="s"
        class="w-28"
      />
    </ProListItem>
  </ProList>

  <ProList :title="$t('pages.preference.cat.labels.windowSettings')">
    <Position />

    <ProListItem
      :description="$t('pages.preference.cat.hints.passThrough')"
      :title="$t('pages.preference.cat.labels.passThrough')"
    >
      <Switch v-model:checked="catStore.window.passThrough" />
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.cat.hints.alwaysOnTop')"
      :title="$t('pages.preference.cat.labels.alwaysOnTop')"
    >
      <Switch v-model:checked="catStore.window.alwaysOnTop" />
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.cat.hints.hideOnHover')"
      :title="$t('pages.preference.cat.labels.hideOnHover')"
    >
      <Switch v-model:checked="catStore.window.hideOnHover" />
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.cat.hints.windowSize')"
      :title="$t('pages.preference.cat.labels.windowSize')"
    >
      <InputNumber
        v-model:value="catStore.window.scale"
        addon-after="%"
        class="w-28"
        :max="500"
        :min="1"
      />
    </ProListItem>

    <ProListItem :title="$t('pages.preference.cat.labels.windowRadius')">
      <InputNumber
        v-model:value="catStore.window.radius"
        addon-after="%"
        class="w-28"
        :min="0"
      />
    </ProListItem>

    <ProListItem
      :title="$t('pages.preference.cat.labels.opacity')"
      vertical
    >
      <Slider
        v-model:value="catStore.window.opacity"
        class="m-[0]!"
        :max="100"
        :min="10"
        :tip-formatter="(value) => `${value}%`"
      />
    </ProListItem>
  </ProList>
</template>


================================================
FILE: src/pages/preference/components/general/components/macos-permissions/index.vue
================================================
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { confirm } from '@tauri-apps/plugin-dialog'
import { Space } from 'ant-design-vue'
import { checkInputMonitoringPermission, requestInputMonitoringPermission } from 'tauri-plugin-macos-permissions-api'
import { onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { isMac } from '@/utils/platform'

const authorized = ref(false)
const { t } = useI18n()

onMounted(async () => {
  authorized.value = await checkInputMonitoringPermission()

  if (authorized.value) return

  const appWindow = getCurrentWebviewWindow()

  await appWindow.setAlwaysOnTop(true)

  const confirmed = await confirm(t('pages.preference.general.hints.inputMonitoringPermissionGuide'), {
    title: t('pages.preference.general.labels.inputMonitoringPermission'),
    okLabel: t('pages.preference.general.buttons.openNow'),
    cancelLabel: t('pages.preference.general.buttons.openLater'),
    kind: 'warning',
  })

  if (!confirmed) return

  await appWindow.setAlwaysOnTop(false)

  requestInputMonitoringPermission()
})
</script>

<template>
  <ProList
    v-if="isMac"
    :title="$t('pages.preference.general.labels.permissionsSettings')"
  >
    <ProListItem
      :description="$t('pages.preference.general.hints.inputMonitoringPermission')"
      :title="$t('pages.preference.general.labels.inputMonitoringPermission')"
    >
      <Space
        v-if="authorized"
        class="text-success font-bold"
        :size="4"
      >
        <div class="i-solar:verified-check-bold text-4.5" />

        <span class="whitespace-nowrap">{{ $t('pages.preference.general.status.authorized') }}</span>
      </Space>

      <Space
        v-else
        class="cursor-pointer text-danger font-bold"
        :size="4"
        @click="requestInputMonitoringPermission"
      >
        <div class="i-solar:round-arrow-right-bold text-4.5" />

        <span class="whitespace-nowrap">{{ $t('pages.preference.general.status.authorize') }}</span>
      </Space>
    </ProListItem>
  </ProList>
</template>


================================================
FILE: src/pages/preference/components/general/components/theme-mode/index.vue
================================================
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Select, SelectOption } from 'ant-design-vue'
import { onMounted, watch } from 'vue'

import ProListItem from '@/components/pro-list-item/index.vue'
import { useGeneralStore } from '@/stores/general'

const generalStore = useGeneralStore()
const appWindow = getCurrentWebviewWindow()

onMounted(() => {
  appWindow.onThemeChanged(async ({ payload }) => {
    if (generalStore.appearance.theme !== 'auto') return

    generalStore.appearance.isDark = payload === 'dark'
  })
})

watch(() => generalStore.appearance.theme, async (value) => {
  let nextTheme = value === 'auto' ? null : value

  await appWindow.setTheme(nextTheme)

  nextTheme = nextTheme ?? (await appWindow.theme())

  generalStore.appearance.isDark = nextTheme === 'dark'
}, { immediate: true })

watch(() => generalStore.appearance.isDark, (value) => {
  if (value) {
    document.documentElement.classList.add('dark')
  } else {
    document.documentElement.classList.remove('dark')
  }
}, { immediate: true })
</script>

<template>
  <ProListItem :title="$t('pages.preference.general.labels.themeMode')">
    <Select v-model:value="generalStore.appearance.theme">
      <SelectOption value="auto">
        {{ $t('pages.preference.general.options.auto') }}
      </SelectOption>
      <SelectOption value="light">
        {{ $t('pages.preference.general.options.lightMode') }}
      </SelectOption>
      <SelectOption value="dark">
        {{ $t('pages.preference.general.options.darkMode') }}
      </SelectOption>
    </Select>
  </ProListItem>
</template>


================================================
FILE: src/pages/preference/components/general/index.vue
================================================
<script setup lang="ts">
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
import { Select, Switch } from 'ant-design-vue'
import { watch } from 'vue'

import MacosPermissions from './components/macos-permissions/index.vue'
import ThemeMode from './components/theme-mode/index.vue'

import ProList from '@/components/pro-list/index.vue'
import ProListItem from '@/components/pro-list-item/index.vue'
import { useGeneralStore } from '@/stores/general'

const generalStore = useGeneralStore()

watch(() => generalStore.app.autostart, async (value) => {
  const enabled = await isEnabled()

  if (value && !enabled) {
    return enable()
  }

  if (!value && enabled) {
    disable()
  }
}, { immediate: true })
</script>

<template>
  <MacosPermissions />

  <ProList :title="$t('pages.preference.general.labels.appSettings')">
    <ProListItem :title="$t('pages.preference.general.labels.launchOnStartup')">
      <Switch v-model:checked="generalStore.app.autostart" />
    </ProListItem>

    <ProListItem
      :description="$t('pages.preference.general.hints.showTaskbarIcon')"
      :title="$t('pages.preference.general.labels.showTaskbarIcon')"
    >
      <Switch v-model:checked="generalStore.app.taskbarVisible" />
    </ProListItem>
  </ProList>

  <ProList :title="$t('pages.preference.general.labels.appearanceSettings')">
    <ThemeMode />

    <ProListItem :title="$t('pages.preference.general.labels.language')">
      <Select v-model:value="generalStore.appearance.language">
        <Select.Option value="zh-CN">
          简体中文
        </Select.Option>
        <Select.Option value="en-US">
          English
        </Select.Option>
        <Select.Option value="vi-VN">
          Tiếng Việt
        </Select.Option>
        <Select.Option value="pt-BR">
          Português
        </Select.Option>
      </Select>
    </ProListItem>
  </ProList>

  <ProList :title="$t('pages.preference.general.labels.updateSettings')">
    <ProListItem :title="$t('pages.preference.general.labels.autoCheckUpdate')">
      <Switch v-model:checked="generalStore.update.autoCheck" />
    </ProListItem>
  </ProList>
</template>


================================================
FILE: src/pages/preference/components/model/components/float-menu/index.vue
================================================
<script setup lang="ts">
import { EditOutlined, MenuOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'
import { openUrl } from '@tauri-apps/plugin-opener'
import { FloatButton, FloatButtonGroup } from 'ant-design-vue'
</script>

<template>
  <FloatButtonGroup
    class="bottom-4 right-4"
    trigger="click"
    type="primary"
  >
    <template #icon>
      <MenuOutlined />
    </template>

    <FloatButton
      :tooltip="$t('pages.preference.model.tooltips.createModel')"
      @click="openUrl('https://juejin.cn/post/7509872655802269731')"
    >
      <template #icon>
        <EditOutlined />
      </template>
    </FloatButton>

    <FloatButton
      :tooltip="$t('pages.preference.model.tooltips.convertModel')"
      @click="openUrl('https://bongocat.vteamer.cc')"
    >
      <template #icon>
        <SyncOutlined />
      </template>
    </FloatButton>

    <FloatButton
      :tooltip="$t('pages.preference.model.tooltips.moreModels')"
      @click="openUrl('https://github.com/ayangweb/Awesome-BongoCat')"
    >
      <template #icon>
        <UnorderedListOutlined />
      </template>
    </FloatButton>
  </FloatButtonGroup>
</template>


================================================
FILE: src/pages/preference/components/model/components/upload/index.vue
================================================
<script setup lang="ts">
import type { ModelMode } from '@/stores/model'

import { invoke } from '@tauri-apps/api/core'
import { appDataDir } from '@tauri-apps/api/path'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { open } from '@tauri-apps/plugin-dialog'
import { readDir } from '@tauri-apps/plugin-fs'
import { message } from 'ant-design-vue'
import { nanoid } from 'nanoid'
import { onMounted, ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'

import { INVOKE_KEY } from '@/constants'
import { useModelStore } from '@/stores/model'
import { join } from '@/utils/path'

const dropRef = useTemplateRef('drop')
const dragenter = ref(false)
const selectPaths = ref<string[]>([])
const modelStore = useModelStore()
const { t } = useI18n()

onMounted(() => {
  const appWindow = getCurrentWebviewWindow()

  appWindow.onDragDropEvent(({ payload }) => {
    const { type } = payload

    if (type === 'over') {
      const { x, y } = payload.position

      if (dropRef.value) {
        const { left, right, top, bottom } = dropRef.value.getBoundingClientRect()

        const inBoundsX = x >= left && x <= right
        const inBoundsY = y >= top && y <= bottom

        dragenter.value = inBoundsX && inBoundsY
      }
    } else if (type === 'drop' && dragenter.value) {
      dragenter.value = false

      selectPaths.value = payload.paths
    } else {
      dragenter.value = false
    }
  })
})

async function handleUpload() {
  const selected = await open({ directory: true, multiple: true })

  if (!selected) return

  selectPaths.value = selected
}

watch(selectPaths, async (paths) => {
  for await (const fromPath of paths) {
    try {
      const id = nanoid()

      let mode: ModelMode = 'standard'

      const files = await readDir(join(fromPath, 'resources', 'right-keys')).catch(() => [])

      if (files.length > 0) {
        const fileNames = files.map(file => file.name.split('.')[0])

        if (fileNames.includes('East')) {
          mode = 'gamepad'
        } else {
          mode = 'keyboard'
        }
      }

      const toPath = join(await appDataDir(), 'custom-models', id)

      await invoke(INVOKE_KEY.COPY_DIR, {
        fromPath,
        toPath,
      })

      modelStore.models.push({
        id,
        path: toPath,
        mode,
        isPreset: false,
      })

      message.success(t('pages.preference.model.hints.importSuccess'))
    } catch (error) {
      message.error(String(error))
    }
  }
})
</script>

<template>
  <div
    ref="drop"
    class="w-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary"
    :class="{ 'border-primary': dragenter }"
    @click="handleUpload"
  >
    <div class="i-solar:upload-square-outline text-12 text-primary" />

    <span>{{ $t('pages.preference.model.hints.clickOrDragToImport') }}</span>
  </div>
</template>


================================================
FILE: src/pages/preference/components/model/index.vue
================================================
<script setup lang="ts">
import type { Model } from '@/stores/model'
import type { ComponentPublicInstance } from 'vue'

import { convertFileSrc } from '@tauri-apps/api/core'
import { remove } from '@tauri-apps/plugin-fs'
import { revealItemInDir } from '@tauri-apps/plugin-opener'
import { useElementSize } from '@vueuse/core'
import { Card, message, Popconfirm } from 'ant-design-vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'

import FloatMenu from './components/float-menu/index.vue'
import Upload from './components/upload/index.vue'

import { useModelStore } from '@/stores/model'
import { join } from '@/utils/path'

const modelStore = useModelStore()
const firstItemRef = ref<HTMLElement>()
const { height } = useElementSize(firstItemRef)
const { t } = useI18n()

function setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) {
  if (!el || index > 0) return

  if ('$el' in el) {
    return firstItemRef.value = el.$el
  }

  if (el instanceof HTMLElement) {
    firstItemRef.value = el
  }
}

async function handleDelete(item: Model) {
  const { id, path } = item

  try {
    await remove(path, { recursive: true })

    message.success(t('pages.preference.model.hints.deleteSuccess'))
  } catch (error) {
    message.error(String(error))
  } finally {
    modelStore.models = modelStore.models.filter(item => item.id !== id)

    if (id === modelStore.currentModel?.id) {
      modelStore.currentModel = modelStore.models[0]
    }
  }
}
</script>

<template>
  <MasonryGrid
    :columns="{ 992: 3, 1200: 4, 1600: 6, default: 8 }"
    :gutter="16"
  >
    <MasonryGridItem>
      <Upload :style="{ height: `${height}px` }" />
    </MasonryGridItem>

    <MasonryGridItem
      v-for="(item, index) in modelStore.models"
      :key="item.id"
    >
      <Card
        :ref="(el) => setFirstItemRef(el, index)"
        hoverable
        size="small"
        @click="modelStore.currentModel = item"
      >
        <template #cover>
          <img
            alt="example"
            :src="convertFileSrc(join(item.path, 'resources', 'cover.png'))"
          >
        </template>

        <template #actions>
          <i
            class="i-iconamoon:check-circle-1-bold text-4"
            :class="{ 'text-success': item.id === modelStore.currentModel?.id }"
          />

          <i
            class="i-iconamoon:link-external-bold text-4"
            @click.stop="revealItemInDir(item.path)"
          />

          <template v-if="!item.isPreset">
            <Popconfirm
              :description="$t('pages.preference.model.hints.deleteModel')"
              placement="topRight"
              :title="$t('pages.preference.model.labels.deleteModel')"
              @confirm="handleDelete(item)"
            >
              <i
                class="i-iconamoon:trash-simple-bold text-4"
                @click.stop
              />
            </Popconfirm>
          </template>
        </template>
      </Card>
    </MasonryGridItem>
  </MasonryGrid>

  <FloatMenu />
</template>


================================================
FILE: src/pages/preference/components/shortcut/index.vue
================================================
<script setup lang="ts">
import { storeToRefs } from 'pinia'

import ProList from '@/components/pro-list/index.vue'
import ProShortcut from '@/components/pro-shortcut/index.vue'
import { useTauriShortcut } from '@/composables/useTauriShortcut'
import { toggleWindowVisible } from '@/plugins/window'
import { useCatStore } from '@/stores/cat'
import { useShortcutStore } from '@/stores/shortcut.ts'

const shortcutStore = useShortcutStore()
const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore)
const catStore = useCatStore()

useTauriShortcut(visibleCat, () => {
  catStore.window.visible = !catStore.window.visible
})

useTauriShortcut(visiblePreference, () => {
  toggleWindowVisible('preference')
})

useTauriShortcut(mirrorMode, () => {
  catStore.model.mirror = !catStore.model.mirror
})

useTauriShortcut(penetrable, () => {
  catStore.window.passThrough = !catStore.window.passThrough
})

useTauriShortcut(alwaysOnTop, () => {
  catStore.window.alwaysOnTop = !catStore.window.alwaysOnTop
})
</script>

<template>
  <ProList :title="$t('pages.preference.shortcut.title')">
    <ProShortcut
      v-model="shortcutStore.visibleCat"
      :description="$t('pages.preference.shortcut.hints.toggleCat')"
      :title="$t('pages.preference.shortcut.labels.toggleCat')"
    />

    <ProShortcut
      v-model="shortcutStore.visiblePreference"
      :description="$t('pages.preference.shortcut.hints.togglePreferences')"
      :title="$t('pages.preference.shortcut.labels.togglePreferences')"
    />

    <ProShortcut
      v-model="shortcutStore.mirrorMode"
      :description="$t('pages.preference.shortcut.hints.mirrorMode')"
      :title="$t('pages.preference.shortcut.labels.mirrorMode')"
    />

    <ProShortcut
      v-model="shortcutStore.penetrable"
      :description="$t('pages.preference.shortcut.hints.passThrough')"
      :title="$t('pages.preference.shortcut.labels.passThrough')"
    />

    <ProShortcut
      v-model="shortcutStore.alwaysOnTop"
      :description="$t('pages.preference.shortcut.hints.alwaysOnTop')"
      :title="$t('pages.preference.shortcut.labels.alwaysOnTop')"
    />
  </ProList>
</template>


================================================
FILE: src/pages/preference/index.vue
================================================
<script setup lang="ts">
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { Flex } from 'ant-design-vue'
import { computed, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'

import About from './components/about/index.vue'
import Cat from './components/cat/index.vue'
import General from './components/general/index.vue'
import Model from './components/model/index.vue'
import Shortcut from './components/shortcut/index.vue'

import UpdateApp from '@/components/update-app/index.vue'
import { useTray } from '@/composables/useTray'
import { useAppStore } from '@/stores/app'
import { useGeneralStore } from '@/stores/general'
import { isMac } from '@/utils/platform'

const { createTray } = useTray()
const appStore = useAppStore()
const current = ref(0)
const { t } = useI18n()
const generalStore = useGeneralStore()
const appWindow = getCurrentWebviewWindow()

onMounted(async () => {
  createTray()
})

watch(() => generalStore.appearance.language, () => {
  appWindow.setTitle(t('pages.preference.title'))
}, { immediate: true })

const menus = computed(() => [
  {
    label: t('pages.preference.cat.title'),
    icon: 'i-solar:cat-bold',
    component: Cat,
  },
  {
    label: t('pages.preference.general.title'),
    icon: 'i-solar:settings-minimalistic-bold',
    component: General,
  },
  {
    label: t('pages.preference.model.title'),
    icon: 'i-solar:magic-stick-3-bold',
    component: Model,
  },
  {
    label: t('pages.preference.shortcut.title'),
    icon: 'i-solar:keyboard-bold',
    component: Shortcut,
  },
  {
    label: t('pages.preference.about.title'),
    icon: 'i-solar:info-circle-bold',
    component: About,
  },
])
</script>

<template>
  <Flex class="h-screen">
    <div
      class="h-full w-30 flex flex-col items-center gap-4 overflow-auto dark:(bg-color-3 bg-none) bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear"
      :class="[isMac ? 'pt-8' : 'pt-4']"
      data-tauri-drag-region
    >
      <div class="flex flex-col items-center gap-2">
        <div class="b b-color-2 rounded-2xl b-solid">
          <img
            class="size-15"
            data-tauri-drag-region
            src="/logo.png"
          >
        </div>

        <span class="font-bold">{{ appStore.name }}</span>
      </div>

      <div class="flex flex-col gap-2">
        <div
          v-for="(item, index) in menus"
          :key="item.label"
          class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 dark:text-color-2 text-color-3 transition"
          :class="{ 'bg-color-2! text-primary-5 dark:text-primary-7 font-bold dark:bg-color-8!': current === index }"
          @click="current = index"
        >
          <div
            class="size-8"
            :class="item.icon"
          />

          <span>{{ item.label }}</span>
        </div>
      </div>
    </div>

    <div
      v-for="(item, index) in menus"
      v-show="current === index"
      :key="item.label"
      class="flex-1 overflow-auto bg-color-8 dark:bg-color-2 p-4"
      data-tauri-drag-region
    >
      <component :is="item.component" />
    </div>
  </Flex>

  <UpdateApp />
</template>


================================================
FILE: src/plugins/window.ts
================================================
import { invoke } from '@tauri-apps/api/core'
import { emit } from '@tauri-apps/api/event'
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'

import { LISTEN_KEY } from '../constants'

type WindowLabel = 'main' | 'preference'

const COMMAND = {
  SHOW_WINDOW: 'plugin:custom-window|show_window',
  HIDE_WINDOW: 'plugin:custom-window|hide_window',
  SET_ALWAYS_ON_TOP: 'plugin:custom-window|set_always_on_top',
  SET_TASKBAR_VISIBILITY: 'plugin:custom-window|set_taskbar_visibility',
}

export function showWindow(label?: WindowLabel) {
  if (label) {
    emit(LISTEN_KEY.SHOW_WINDOW, label)
  } else {
    invoke(COMMAND.SHOW_WINDOW)
  }
}

export function hideWindow(label?: WindowLabel) {
  if (label) {
    emit(LISTEN_KEY.HIDE_WINDOW, label)
  } else {
    invoke(COMMAND.HIDE_WINDOW)
  }
}

export function setAlwaysOnTop(alwaysOnTop: boolean) {
  invoke(COMMAND.SET_ALWAYS_ON_TOP, { alwaysOnTop })
}

export async function toggleWindowVisible(label?: WindowLabel) {
  const appWindow = getCurrentWebviewWindow()

  if (appWindow.label !== label) return

  const visible = await appWindow.isVisible()

  if (visible) {
    return hideWindow(label)
  }

  return showWindow(label)
}

export async function setTaskbarVisibility(visible: boolean) {
  invoke(COMMAND.SET_TASKBAR_VISIBILITY, { visible })
}


================================================
FILE: src/router/index.ts
================================================
import type { RouteRecordRaw } from 'vue-router'

import { createRouter, createWebHashHistory } from 'vue-router'

import Main from '../pages/main/index.vue'
import Preference from '../pages/preference/index.vue'

const routes: Readonly<RouteRecordRaw[]> = [
  {
    path: '/',
    component: Main,
  },
  {
    path: '/preference',
    component: Preference,
  },
]

const router = createRouter({
  history: createWebHashHistory(),
  routes,
})

export default router


================================================
FILE: src/stores/app.ts
================================================
import type { WindowState } from '@/composables/useWindowState'

import { getName, getVersion } from '@tauri-apps/api/app'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'

export const useAppStore = defineStore('app', () => {
  const name = ref('')
  const version = ref('')
  const windowState = reactive<WindowState>({})

  const init = async () => {
    name.value = await getName()
    version.value = await getVersion()
  }

  return {
    name,
    version,
    windowState,
    init,
  }
})


================================================
FILE: src/stores/cat.ts
================================================
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'

export interface CatStore {
  model: {
    mirror: boolean
    single: boolean
    mouseMirror: boolean
    autoReleaseDelay: number
  }
  window: {
    visible: boolean
    passThrough: boolean
    alwaysOnTop: boolean
    scale: number
    opacity: number
    radius: number
    hideOnHover: boolean
    position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'
  }
}

export const useCatStore = defineStore('cat', () => {
  /* ------------ 废弃字段(后续删除) ------------ */

  /** @deprecated 请使用 `model.mirror` */
  const mirrorMode = ref(false)

  /** @deprecated 请使用 `model.single` */
  const singleMode = ref(false)

  /** @deprecated 请使用 `model.mouseMirror` */
  const mouseMirror = ref(false)

  /** @deprecated 请使用 `window.passThrough` */
  const penetrable = ref(false)

  /** @deprecated 请使用 `window.alwaysOnTop` */
  const alwaysOnTop = ref(true)

  /** @deprecated 请使用 `window.scale` */
  const scale = ref(100)

  /** @deprecated 请使用 `window.opacity` */
  const opacity = ref(100)

  /** @deprecated 用于标识数据是否已迁移,后续版本将删除 */
  const migrated = ref(false)

  const model = reactive<CatStore['model']>({
    mirror: false,
    single: false,
    mouseMirror: false,
    autoReleaseDelay: 3,
  })

  const window = reactive<CatStore['window']>({
    visible: true,
    passThrough: false,
    alwaysOnTop: false,
    scale: 100,
    opacity: 100,
    radius: 0,
    hideOnHover: false,
    position: 'bottomRight',
  })

  const init = () => {
    if (migrated.value) return

    model.mirror = mirrorMode.value
    model.single = singleMode.value
    model.mouseMirror = mouseMirror.value

    window.visible = true
    window.passThrough = penetrable.value
    window.alwaysOnTop = alwaysOnTop.value
    window.scale = scale.value
    window.opacity = opacity.value

    migrated.value = true
  }

  return {
    migrated,
    model,
    window,
    init,
  }
})


================================================
FILE: src/stores/general.ts
================================================
import type { Theme } from '@tauri-apps/api/window'

import { defineStore } from 'pinia'
import { getLocale } from 'tauri-plugin-locale-api'
import { reactive, ref } from 'vue'

import { LANGUAGE } from '@/constants'

export type Language = typeof LANGUAGE[keyof typeof LANGUAGE]

export interface GeneralStore {
  app: {
    autostart: boolean
    taskbarVisible: boolean
  }
  appearance: {
    theme: 'auto' | Theme
    isDark: boolean
    language?: Language
  }
  update: {
    autoCheck: boolean
  }
}

export const useGeneralStore = defineStore('general', () => {
  /* ------------ 废弃字段(后续删除) ------------ */

  /** @deprecated 请使用 `update.autoCheck` */
  const autoCheckUpdate = ref(false)

  /** @deprecated 请使用 `app.autostart` */
  const autostart = ref(false)

  /** @deprecated 请使用 `app.taskbarVisible` */
  const taskbarVisibility = ref(false)

  /** @deprecated 请使用 `appearance.theme` */
  const theme = ref<'auto' | Theme>('auto')

  /** @deprecated 请使用 `appearance.isDark` */
  const isDark = ref(false)

  /** @deprecated 用于标识数据是否已迁移,后续版本将删除 */
  const migrated = ref(false)

  const app = reactive<GeneralStore['app']>({
    autostart: false,
    taskbarVisible: false,
  })

  const appearance = reactive<GeneralStore['appearance']>({
    theme: 'auto',
    isDark: false,
  })

  const update = reactive<GeneralStore['update']>({
    autoCheck: false,
  })

  const getLanguage = async () => {
    const locale = await getLocale<Language>()

    if (Object.values(LANGUAGE).includes(locale)) {
      return locale
    }

    return LANGUAGE.EN_US
  }

  const init = async () => {
    appearance.language ??= await getLanguage()

    if (migrated.value) return

    app.autostart = autostart.value
    app.taskbarVisible = taskbarVisibility.value

    appearance.theme = theme.value
    appearance.isDark = isDark.value

    update.autoCheck = autoCheckUpdate.value

    migrated.value = true
  }

  return {
    migrated,
    app,
    appearance,
    update,
    init,
  }
})


================================================
FILE: src/stores/model.ts
================================================
import { resolveResource } from '@tauri-apps/api/path'
import { filter, find } from 'es-toolkit/compat'
import { nanoid } from 'nanoid'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'

import { join } from '@/utils/path'

export type ModelMode = 'standard' | 'keyboard' | 'gamepad'

export interface Model {
  id: string
  path: string
  mode: ModelMode
  isPreset: boolean
}

interface Motion {
  Name: string
  File: string
  Sound?: string
  FadeInTime: number
  FadeOutTime: number
  Description?: string
}

type MotionGroup = Record<string, Motion[]>

interface Expression {
  Name: string
  File: string
  Description?: string
}

export const useModelStore = defineStore('model', () => {
  const models = ref<Model[]>([])
  const currentModel = ref<Model>()
  const motions = ref<MotionGroup>({})
  const expressions = ref<Expression[]>([])
  const supportKeys = reactive<Record<string, string>>({})
  const pressedKeys = reactive<Record<string, string>>({})

  const init = async () => {
    const modelsPath = await resolveResource('assets/models')

    const nextModels = filter(models.value, { isPreset: false })
    const presetModels = filter(models.value, { isPreset: true })

    const modes: ModelMode[] = ['gamepad', 'keyboard', 'standard']

    for (const mode of modes) {
      const matched = find(presetModels, { mode })

      nextModels.unshift({
        id: matched?.id ?? nanoid(),
        mode,
        isPreset: true,
        path: join(modelsPath, mode),
      })
    }

    const matched = find(nextModels, { id: currentModel.value?.id })

    currentModel.value = matched ?? nextModels[0]

    models.value = nextModels
  }

  return {
    models,
    currentModel,
    motions,
    expressions,
    supportKeys,
    pressedKeys,
    init,
  }
}, {
  tauri: {
    filterKeys: ['models', 'currentModel'],
    filterKeysStrategy: 'pick',
  },
})


================================================
FILE: src/stores/shortcut.ts
================================================
import { defineStore } from 'pinia'
import { ref } from 'vue'

export type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop'

export const useShortcutStore = defineStore('shortcut', () => {
  const visibleCat = ref('')
  const visiblePreference = ref('')
  const mirrorMode = ref('')
  const penetrable = ref('')
  const alwaysOnTop = ref('')

  return {
    visibleCat,
    visiblePreference,
    mirrorMode,
    penetrable,
    alwaysOnTop,
  }
})


================================================
FILE: src/utils/is.ts
================================================
export function isImage(value: string) {
  const regex = /\.(?:jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i

  return regex.test(value)
}

export function inBetween(value: number, minimum: number, maximum: number) {
  return value >= minimum && value <= maximum
}


================================================
FILE: src/utils/keyboard.ts
================================================
import { isMac } from './platform'

export interface Key {
  eventKey: string
  tauriKey?: string
  symbol?: string
}

export const modifierKeys: Key[] = [
  {
    eventKey: 'Shift',
    symbol: isMac ? '⇧' : 'Shift',
  },
  {
    eventKey: 'Control',
    symbol: isMac ? '⌃' : 'Ctrl',
  },
  {
    eventKey: 'Alt',
    symbol: isMac ? '⌥' : 'Alt',
  },
  {
    eventKey: 'Command',
    symbol: isMac ? '⌘' : 'Super',
  },
].map((item) => {
  return { ...item, tauriKey: item.eventKey }
})

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

  item.symbol ??= eventKey
  item.tauriKey ??= eventKey

  if (eventKey.startsWith('Digit') || eventKey.startsWith('Key')) {
    item.tauriKey = item.symbol = eventKey.slice(-1)
  }

  return item
})

export const keys = modifierKeys.concat(standardKeys)


================================================
FILE: src/utils/live2d.ts
================================================
import type { ModelSize } from '@/composables/useModel'
import type { Cubism4InternalModel } from 'pixi-live2d-display'

import { convertFileSrc } from '@tauri-apps/api/core'
import { readDir, readTextFile } from '@tauri-apps/plugin-fs'
import JSON5 from 'json5'
import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'
import { Application, Ticker } from 'pixi.js'

import { join } from './path'

import { i18n } from '@/locales'

Live2DModel.registerTicker(Ticker)

class Live2d {
  private app: Application | null = null
  public model: Live2DModel | null = null

  constructor() { }

  private initApp() {
    if (this.app) return

    const view = document.getElementById('live2dCanvas') as HTMLCanvasElement

    this.app = new Application({
      view,
      resizeTo: window,
      backgroundAlpha: 0,
      resolution: devicePixelRatio,
    })
  }

  public async load(path: string) {
    this.initApp()

    this.destroy()

    const files = await readDir(path)

    const modelFile = files.find(file => file.name.endsWith('.model3.json'))

    if (!modelFile) {
      throw new Error(i18n.global.t('utils.live2d.hints.notFound'))
    }

    const modelPath = join(path, modelFile.name)

    const modelJSON = JSON5.parse(await readTextFile(modelPath))

    const modelSettings = new Cubism4ModelSettings({
      ...modelJSON,
      url: convertFileSrc(modelPath),
    })

    modelSettings.replaceFiles((file) => {
      return convertFileSrc(join(path, file))
    })

    this.model = await Live2DModel.from(modelSettings)

    this.app?.stage.addChild(this.model)

    const { width, height } = this.model
    const { motions, expressions } = modelSettings

    return {
      width,
      height,
      motions,
      expressions,
    }
  }

  public destroy() {
    if (!this.model) return

    this.model?.destroy()

    this.model = null
  }

  public resizeModel(modelSize: ModelSize) {
    if (!this.model) return

    const { width, height } = modelSize

    const scaleX = innerWidth / width
    const scaleY = innerHeight / height
    const scale = Math.min(scaleX, scaleY)

    this.model.scale.set(scale)
    this.model.x = innerWidth / 2
    this.model.y = innerHeight / 2
    this.model.anchor.set(0.5)
  }

  public playMotion(group: string, index: number) {
    return this.model?.motion(group, index)
  }

  public playExpressions(index: number) {
    return this.model?.expression(index)
  }

  public getCoreModel() {
    const internalModel = this.model?.internalModel as Cubism4InternalModel

    return internalModel?.coreModel
  }

  public getParameterRange(id: string) {
    const coreModel = this.getCoreModel()

    const index = coreModel?.getParameterIndex(id)
    const min = coreModel?.getParameterMinimumValue(index)
    const max = coreModel?.getParameterMaximumValue(index)

    return {
      min,
      max,
    }
  }

  public setParameterValue(id: string, value: number | boolean) {
    const coreModel = this.getCoreModel()

    return coreModel?.setParameterValueById?.(id, Number(value))
  }
}

const live2d = new Live2d()

export default live2d


================================================
FILE: src/utils/monitor.ts
================================================
import type { PhysicalPosition } from '@tauri-apps/api/window'

import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
import { cursorPosition, monitorFromPoint } from '@tauri-apps/api/window'

export async function getCursorMonitor(cursorPoint?: PhysicalPosition) {
  cursorPoint ??= await cursorPosition()

  const appWindow = getCurrentWebviewWindow()

  const scaleFactor = await appWindow.scaleFactor()

  const { x, y } = cursorPoint.toLogical(scaleFactor)

  const monitor = await monitorFromPoint(x, y)

  if (!monitor) return

  return monitor
}


================================================
FILE: src/utils/path.ts
================================================
import { sep } from '@tauri-apps/api/path'

export function join(...paths: string[]) {
  const joinPaths = paths.map((path, index) => {
    if (index === 0) {
      return path.replace(new RegExp(`${sep()}+$`), '')
    } else {
      return path.replace(new RegExp(`^${sep()}+|${sep()}+$`, 'g'), '')
    }
  })

  return joinPaths.join(sep())
}


================================================
FILE: src/utils/platform.ts
================================================
import { platform } from '@tauri-apps/plugin-os'

export const isMac = platform() === 'macos'

export const isWindows = platform() === 'windows'

export const isLinux = platform() === 'linux'


================================================
FILE: src/utils/shared.ts
================================================
import { castArray } from 'es-toolkit/compat'

export function clearObject<T extends Record<string, unknown>>(targets: T | T[]) {
  for (const target of castArray<T>(targets)) {
    for (const key of Object.keys(target)) {
      delete target[key]
    }
  }
}


================================================
FILE: src/vite-env.d.ts
================================================
/// <reference types="vite/client" />

declare module '*.vue' {
  import type { DefineComponent } from 'vue'

  const component: DefineComponent<object, object, any>
  export default component
}


================================================
FILE: src-tauri/.gitignore
================================================
# Generated by Cargo
# will have compiled files and executables
/target/

# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas

icons
autogenerated
schemas

================================================
FILE: src-tauri/BongoCat.desktop
================================================
[Desktop Entry]
Type=Application
Name={{{name}}}
Exec={{{exec}}}
Icon={{{icon}}}
Categories={{{categories}}}
Comment={{{comment}}}
Terminal=false


================================================
FILE: src-tauri/Cargo.toml
================================================
[package]
name = "bongo-cat"
version = "0.9.0"
description = "A Tauri App"
authors = [ "ayangweb" ]
edition = "2024"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
# The `_lib` suffix may seem redundant but it is necessary
# to make the lib name unique and wouldn't conflict with the bin name.
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
name = "bongo_cat_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-private-api", "image-png"] }
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
tauri-plugin-custom-window.workspace = true
tauri-plugin-os = "2"
tauri-plugin-process = "2"
tauri-plugin-opener = "2"
tauri-plugin-pinia = "3"
tauri-plugin-log = "2"
tauri-plugin-updater = "2"
tauri-plugin-prevent-default = "1"
tauri-plugin-single-instance = "2"
tauri-plugin-autostart = "2"
tauri-plugin-macos-permissions = "2"
tauri-plugin-dialog = "2"
tauri-plugin-fs = "2"
fs_extra = "1"
tauri-plugin-clipboard-manager = "2"
tauri-plugin-global-shortcut = "2"
tauri-plugin-locale = "2"
rdev = { git = "https://github.com/kunkunsh/rdev" }
gilrs = { git = "https://github.com/ayangweb/gilrs", default-features = false, features = ["xinput"] }

[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel.workspace = true

[features]
cargo-clippy = []


================================================
FILE: src-tauri/assets/models/gamepad/cat.model3.json
================================================
{
  "Version": 3,
  "FileReferences": {
    "Moc": "demomodel3.moc3",
    "Textures": [
      "demomodel3.1024/texture_00.png",
      "demomodel3.1024/texture_01.png",
      "demomodel3.1024/texture_02.png"
    ],
    "DisplayInfo": "demomodel3.cdi3.json",
    "Expressions": [
      {
        "Name": "live2d_expression0.exp3.json",
        "File": "live2d_expression0.exp3.json"
      },
      {
        "Name": "live2d_expression1.exp3.json",
        "File": "live2d_expression1.exp3.json"
      },
      {
        "Name": "live2d_expression2.exp3.json",
        "File": "live2d_expression2.exp3.json"
      }
    ],
    "Motions": {
      "CAT_motion": [
        {
          "File": "live2d_motion1.motion3.json",
          "Sound": "live2d_motion1.flac",
          "FadeInTime": 0,
          "FadeOutTime": 0
        },
        {
          "File": "live2d_motion2.motion3.json",
          "FadeInTime": 0,
          "FadeOutTime": 0
        }
      ],
      "CAT_motion_lock": [
        {
          "File": "live2d_motion1.motion3.json",
          "Sound": "live2d_motion1.flac",
          "FadeInTime": 0,
          "FadeOutTime": 0
        },
        {
          "File": "live2d_motion2.motion3.json",
          "FadeInTime": 0,
          "FadeOutTime": 0
        }
      ]
    }
  },
  "Groups": [
    {
      "Target": "Parameter",
      "Name": "LipSync",
      "Ids": []
    },
    {
      "Target": "Parameter",
      "Name": "EyeBlink",
      "Ids": [
        "ParamEyeLOpen",
        "ParamEyeROpen"
      ]
    }
  ]
}


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


================================================
FILE: src-tauri/assets/models/gamepad/exp_1.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": [
    {
      "Id": "ParamEyeLOpen",
      "Value": 0.321,
      "Blend": "Multiply"
    },
    {
      "Id": "ParamEyeROpen",
      "Value": 0.313,
      "Blend": "Multiply"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/gamepad/exp_2.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": [
    {
      "Id": "ParamEyeLOpen",
      "Value": -1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/gamepad/live2d_expression0.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": []
}


================================================
FILE: src-tauri/assets/models/gamepad/live2d_expression1.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "FadeInTime": 0.8,
  "Parameters": [
    {
      "Id": "Param4",
      "Value": 1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/gamepad/live2d_expression2.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "FadeInTime": 0.5,
  "Parameters": [
    {
      "Id": "Param5",
      "Value": 1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/gamepad/live2d_motion1.motion3.json
================================================
{
  "Version": 3,
  "Meta": {
    "Duration": 1.633,
    "Fps": 30.0,
    "Loop": true,
    "AreBeziersRestricted": false,
    "CurveCount": 2,
    "TotalSegmentCount": 8,
    "TotalPointCount": 20,
    "UserDataCount": 0,
    "TotalUserDataSize": 0
  },
  "Curves": [
    {
      "Target": "Parameter",
      "Id": "Param",
      "Segments": [
        0,
        0,
        1,
        0.033,
        0,
        0.067,
        1,
        0.1,
        1,
        1,
        0.411,
        1,
        0.722,
        1,
        1.033,
        1,
        1,
        1.189,
        1,
        1.344,
        0,
        1.5,
        0,
        0,
        1.633,
        0
      ]
    },
    {
      "Target": "Parameter",
      "Id": "Param2",
      "Segments": [
        0,
        0,
        0,
        0.067,
        0,
        1,
        0.1,
        0,
        0.133,
        0.142,
        0.167,
        0.2,
        1,
        0.489,
        0.764,
        0.811,
        1,
        1.133,
        1,
        0,
        1.633,
        1
      ]
    }
  ]
}


================================================
FILE: src-tauri/assets/models/gamepad/live2d_motion2.motion3.json
================================================
{
  "Version": 3,
  "Meta": {
    "Duration": 2.333,
    "Fps": 30.0,
    "Loop": true,
    "AreBeziersRestricted": true,
    "CurveCount": 2,
    "TotalSegmentCount": 7,
    "TotalPointCount": 21,
    "UserDataCount": 0,
    "TotalUserDataSize": 0
  },
  "Curves": [
    {
      "Target": "Parameter",
      "Id": "CatParamLeftHandDown",
      "Segments": [
        0,
        0,
        0,
        2.333,
        0
      ]
    },
    {
      "Target": "Parameter",
      "Id": "Param3",
      "Segments": [
        0,
        0,
        1,
        0.133,
        0,
        0.267,
        30,
        0.4,
        30,
        1,
        0.522,
        30,
        0.644,
        0,
        0.767,
        0,
        1,
        0.9,
        0,
        1.033,
        30,
        1.167,
        30,
        1,
        1.3,
        30,
        1.433,
        0,
        1.567,
        0,
        1,
        1.7,
        0,
        1.833,
        30,
        1.967,
        30,
        1,
        2.089,
        30,
        2.211,
        0,
        2.333,
        0
      ]
    }
  ]
}


================================================
FILE: src-tauri/assets/models/keyboard/cat.model3.json
================================================
{
  "Version": 3,
  "FileReferences": {
    "Moc": "demomodel2.moc3",
    "Textures": [
      "demomodel2.1024/texture_00.png",
      "demomodel2.1024/texture_01.png",
      "demomodel2.1024/texture_02.png"
    ],
    "DisplayInfo": "demomodel2.cdi3.json",
    "Expressions": [
      {
        "Name": "live2d_expression0.exp3.json",
        "File": "live2d_expression0.exp3.json"
      },
      {
        "Name": "live2d_expression1.exp3.json",
        "File": "live2d_expression1.exp3.json"
      },
      {
        "Name": "live2d_expression2.exp3.json",
        "File": "live2d_expression2.exp3.json"
      }
    ],
    "Motions": {
      "CAT_motion": [
        {
          "File": "live2d_motion1.motion3.json",
          "Sound": "live2d_motion1.flac",
          "FadeInTime": 0,
          "FadeOutTime": 0
        },
        {
          "File": "live2d_motion2.motion3.json",
          "FadeInTime": 0,
          "FadeOutTime": 0
        }
      ],
      "CAT_motion_lock": [
        {
          "File": "live2d_motion1.motion3.json",
          "Sound": "live2d_motion1.flac",
          "FadeInTime": 0,
          "FadeOutTime": 0
        },
        {
          "File": "live2d_motion2.motion3.json",
          "FadeInTime": 0,
          "FadeOutTime": 0
        }
      ]
    }
  },
  "Groups": [
    {
      "Target": "Parameter",
      "Name": "EyeBlink",
      "Ids": [
        "ParamEyeLOpen",
        "ParamEyeROpen"
      ]
    },
    {
      "Target": "Parameter",
      "Name": "LipSync",
      "Ids": []
    }
  ]
}


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


================================================
FILE: src-tauri/assets/models/keyboard/exp_1.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": [
    {
      "Id": "ParamEyeLOpen",
      "Value": 0.321,
      "Blend": "Multiply"
    },
    {
      "Id": "ParamEyeROpen",
      "Value": 0.313,
      "Blend": "Multiply"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/keyboard/exp_2.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": [
    {
      "Id": "ParamEyeLOpen",
      "Value": -1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/keyboard/live2d_expression0.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": []
}


================================================
FILE: src-tauri/assets/models/keyboard/live2d_expression1.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "FadeInTime": 0.8,
  "Parameters": [
    {
      "Id": "Param4",
      "Value": 1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/keyboard/live2d_expression2.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "FadeInTime": 0.5,
  "Parameters": [
    {
      "Id": "Param5",
      "Value": 1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/keyboard/live2d_motion1.motion3.json
================================================
{
  "Version": 3,
  "Meta": {
    "Duration": 1.633,
    "Fps": 30.0,
    "Loop": true,
    "AreBeziersRestricted": false,
    "CurveCount": 2,
    "TotalSegmentCount": 8,
    "TotalPointCount": 20,
    "UserDataCount": 0,
    "TotalUserDataSize": 0
  },
  "Curves": [
    {
      "Target": "Parameter",
      "Id": "Param",
      "Segments": [
        0,
        0,
        1,
        0.033,
        0,
        0.067,
        1,
        0.1,
        1,
        1,
        0.411,
        1,
        0.722,
        1,
        1.033,
        1,
        1,
        1.189,
        1,
        1.344,
        0,
        1.5,
        0,
        0,
        1.633,
        0
      ]
    },
    {
      "Target": "Parameter",
      "Id": "Param2",
      "Segments": [
        0,
        0,
        0,
        0.067,
        0,
        1,
        0.1,
        0,
        0.133,
        0.142,
        0.167,
        0.2,
        1,
        0.489,
        0.764,
        0.811,
        1,
        1.133,
        1,
        0,
        1.633,
        1
      ]
    }
  ]
}


================================================
FILE: src-tauri/assets/models/keyboard/live2d_motion2.motion3.json
================================================
{
  "Version": 3,
  "Meta": {
    "Duration": 2.333,
    "Fps": 30.0,
    "Loop": true,
    "AreBeziersRestricted": true,
    "CurveCount": 2,
    "TotalSegmentCount": 7,
    "TotalPointCount": 21,
    "UserDataCount": 0,
    "TotalUserDataSize": 0
  },
  "Curves": [
    {
      "Target": "Parameter",
      "Id": "CatParamLeftHandDown",
      "Segments": [
        0,
        0,
        0,
        2.333,
        0
      ]
    },
    {
      "Target": "Parameter",
      "Id": "Param3",
      "Segments": [
        0,
        0,
        1,
        0.133,
        0,
        0.267,
        30,
        0.4,
        30,
        1,
        0.522,
        30,
        0.644,
        0,
        0.767,
        0,
        1,
        0.9,
        0,
        1.033,
        30,
        1.167,
        30,
        1,
        1.3,
        30,
        1.433,
        0,
        1.567,
        0,
        1,
        1.7,
        0,
        1.833,
        30,
        1.967,
        30,
        1,
        2.089,
        30,
        2.211,
        0,
        2.333,
        0
      ]
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/cat.model3.json
================================================
{
  "Version": 3,
  "FileReferences": {
    "Moc": "demomodel.moc3",
    "Textures": [
      "demomodel.1024/texture_00.png",
      "demomodel.1024/texture_01.png",
      "demomodel.1024/texture_02.png"
    ],
    "DisplayInfo": "demomodel.cdi3.json",
    "Expressions": [
      {
        "Name": "live2d_expression0.exp3.json",
        "File": "live2d_expression0.exp3.json"
      },
      {
        "Name": "live2d_expression1.exp3.json",
        "File": "live2d_expression1.exp3.json"
      },
      {
        "Name": "live2d_expression2.exp3.json",
        "File": "live2d_expression2.exp3.json"
      }
    ],
    "Motions": {
      "CAT_motion": [
        {
          "File": "live2d_motion1.motion3.json",
          "Sound": "live2d_motion1.flac",
          "FadeInTime": 0,
          "FadeOutTime": 0
        },
        {
          "File": "live2d_motion2.motion3.json",
          "FadeInTime": 0,
          "FadeOutTime": 0
        }
      ],
      "CAT_motion_lock": [
        {
          "File": "live2d_motion1.motion3.json",
          "Sound": "live2d_motion1.flac",
          "FadeInTime": 0,
          "FadeOutTime": 0
        },
        {
          "File": "live2d_motion2.motion3.json",
          "FadeInTime": 0,
          "FadeOutTime": 0
        }
      ]
    }
  },
  "Groups": [
    {
      "Target": "Parameter",
      "Name": "EyeBlink",
      "Ids": [
        "ParamEyeLOpen",
        "ParamEyeROpen"
      ]
    },
    {
      "Target": "Parameter",
      "Name": "LipSync",
      "Ids": []
    }
  ],
  "HitAreas": []
}


================================================
FILE: src-tauri/assets/models/standard/demomodel.cdi3.json
================================================
{
  "Version": 3,
  "Parameters": [
    {
      "Id": "ParamAngleX",
      "GroupId": "",
      "Name": "角度 X"
    },
    {
      "Id": "ParamAngleY",
      "GroupId": "",
      "Name": "角度 Y"
    },
    {
      "Id": "ParamMouseX",
      "GroupId": "",
      "Name": "鼠标X"
    },
    {
      "Id": "ParamMouseY",
      "GroupId": "",
      "Name": "鼠标Y"
    },
    {
      "Id": "ParamMouseLeftDown",
      "GroupId": "",
      "Name": "鼠标左键按下"
    },
    {
      "Id": "ParamMouseRightDown",
      "GroupId": "",
      "Name": "鼠标右键按下"
    },
    {
      "Id": "CatParamLeftHandDown",
      "GroupId": "",
      "Name": "键盘按下"
    },
    {
      "Id": "ParamAngleZ",
      "GroupId": "",
      "Name": "角度 Z"
    },
    {
      "Id": "ParamEyeLOpen",
      "GroupId": "",
      "Name": "左眼 开闭"
    },
    {
      "Id": "ParamEyeLSmile",
      "GroupId": "",
      "Name": "左眼 微笑"
    },
    {
      "Id": "ParamEyeROpen",
      "GroupId": "",
      "Name": "右眼"
    },
    {
      "Id": "ParamEyeRSmile",
      "GroupId": "",
      "Name": "右眼 微笑"
    },
    {
      "Id": "Param3",
      "GroupId": "",
      "Name": "挥手"
    },
    {
      "Id": "Param",
      "GroupId": "ParamGroup",
      "Name": "开启闪电"
    },
    {
      "Id": "Param2",
      "GroupId": "ParamGroup",
      "Name": "闪电划过"
    },
    {
      "Id": "Param4",
      "GroupId": "ParamGroup2",
      "Name": "表情:thuglife"
    },
    {
      "Id": "Param5",
      "GroupId": "ParamGroup2",
      "Name": "表情:升天"
    },
    {
      "Id": "ParamEyeBallX",
      "GroupId": "",
      "Name": "眼球 X"
    },
    {
      "Id": "ParamEyeBallY",
      "GroupId": "",
      "Name": "眼球 Y"
    },
    {
      "Id": "ParamBrowLY",
      "GroupId": "",
      "Name": "左眉上下"
    },
    {
      "Id": "ParamBrowRY",
      "GroupId": "",
      "Name": "右眉 上下"
    },
    {
      "Id": "ParamBrowLX",
      "GroupId": "",
      "Name": "左眉 左右"
    },
    {
      "Id": "ParamBrowRX",
      "GroupId": "",
      "Name": "右眉 左右"
    },
    {
      "Id": "ParamBrowLAngle",
      "GroupId": "",
      "Name": "左眉 角度"
    },
    {
      "Id": "ParamBrowRAngle",
      "GroupId": "",
      "Name": "右眉 角度"
    },
    {
      "Id": "ParamBrowLForm",
      "GroupId": "",
      "Name": "左眉 変形"
    },
    {
      "Id": "ParamBrowRForm",
      "GroupId": "",
      "Name": "右眉 変形"
    },
    {
      "Id": "ParamMouthForm",
      "GroupId": "",
      "Name": "嘴部 变形"
    },
    {
      "Id": "ParamMouthOpenY",
      "GroupId": "",
      "Name": "嘴巴 张开和闭合"
    },
    {
      "Id": "ParamCheek",
      "GroupId": "",
      "Name": "脸颊"
    },
    {
      "Id": "ParamBodyAngleX",
      "GroupId": "",
      "Name": "身体旋转 X"
    },
    {
      "Id": "ParamBodyAngleY",
      "GroupId": "",
      "Name": "身体旋转 Y"
    },
    {
      "Id": "ParamBodyAngleZ",
      "GroupId": "",
      "Name": "身体旋转 Z"
    },
    {
      "Id": "ParamBreath",
      "GroupId": "",
      "Name": "呼吸"
    },
    {
      "Id": "ParamHairFront",
      "GroupId": "",
      "Name": "摇动 前发"
    },
    {
      "Id": "ParamHairSide",
      "GroupId": "",
      "Name": "摇动 侧发"
    },
    {
      "Id": "ParamHairBack",
      "GroupId": "",
      "Name": "摇动 后发"
    }
  ],
  "ParameterGroups": [
    {
      "Id": "ParamGroup",
      "GroupId": "",
      "Name": "闪电"
    },
    {
      "Id": "ParamGroup2",
      "GroupId": "",
      "Name": "表情"
    }
  ],
  "Parts": [
    {
      "Id": "Part11",
      "Name": "demomodel.psd(未找到对应图层)"
    },
    {
      "Id": "Part7",
      "Name": "demomodel.psd(未找到对应图层)"
    },
    {
      "Id": "Part3",
      "Name": "demomodel.psd(未找到对应图层)"
    },
    {
      "Id": "Part2",
      "Name": "demomodel.psd(未找到对应图层)"
    },
    {
      "Id": "Part",
      "Name": "demomodel.psd(未找到对应图层)"
    },
    {
      "Id": "Part10",
      "Name": "天使环"
    },
    {
      "Id": "Part5",
      "Name": "demomodel.psd(未找到对应图层)"
    },
    {
      "Id": "Part8",
      "Name": "thug life"
    },
    {
      "Id": "Part6",
      "Name": "闪电"
    },
    {
      "Id": "Part4",
      "Name": "闪电"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/exp_1.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": [
    {
      "Id": "ParamEyeLOpen",
      "Value": 0.321,
      "Blend": "Multiply"
    },
    {
      "Id": "ParamEyeROpen",
      "Value": 0.313,
      "Blend": "Multiply"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/exp_2.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": [
    {
      "Id": "ParamEyeLOpen",
      "Value": -1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/live2d_expression0.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "Parameters": []
}


================================================
FILE: src-tauri/assets/models/standard/live2d_expression1.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "FadeInTime": 0.8,
  "Parameters": [
    {
      "Id": "Param4",
      "Value": 1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/live2d_expression2.exp3.json
================================================
{
  "Type": "Live2D Expression",
  "FadeInTime": 0.5,
  "Parameters": [
    {
      "Id": "Param5",
      "Value": 1,
      "Blend": "Add"
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/live2d_motion1.motion3.json
================================================
{
  "Version": 3,
  "Meta": {
    "Duration": 1.633,
    "Fps": 30.0,
    "Loop": true,
    "AreBeziersRestricted": false,
    "CurveCount": 2,
    "TotalSegmentCount": 8,
    "TotalPointCount": 20,
    "UserDataCount": 0,
    "TotalUserDataSize": 0
  },
  "Curves": [
    {
      "Target": "Parameter",
      "Id": "Param",
      "Segments": [
        0,
        0,
        1,
        0.033,
        0,
        0.067,
        1,
        0.1,
        1,
        1,
        0.411,
        1,
        0.722,
        1,
        1.033,
        1,
        1,
        1.189,
        1,
        1.344,
        0,
        1.5,
        0,
        0,
        1.633,
        0
      ]
    },
    {
      "Target": "Parameter",
      "Id": "Param2",
      "Segments": [
        0,
        0,
        0,
        0.067,
        0,
        1,
        0.1,
        0,
        0.133,
        0.142,
        0.167,
        0.2,
        1,
        0.489,
        0.764,
        0.811,
        1,
        1.133,
        1,
        0,
        1.633,
        1
      ]
    }
  ]
}


================================================
FILE: src-tauri/assets/models/standard/live2d_motion2.motion3.json
================================================
{
  "Version": 3,
  "Meta": {
    "Duration": 2.333,
    "Fps": 30.0,
    "Loop": true,
    "AreBeziersRestricted": true,
    "CurveCount": 2,
    "TotalSegmentCount": 7,
    "TotalPointCount": 21,
    "UserDataCount": 0,
    "TotalUserDataSize": 0
  },
  "Curves": [
    {
      "Target": "Parameter",
      "Id": "CatParamLeftHandDown",
      "Segments": [
        0,
        0,
        0,
        2.333,
        0
      ]
    },
    {
      "Target": "Parameter",
      "Id": "Param3",
      "Segments": [
        0,
        0,
        1,
        0.133,
        0,
        0.267,
        30,
        0.4,
        30,
        1,
        0.522,
        30,
        0.644,
        0,
        0.767,
        0,
        1,
        0.9,
        0,
        1.033,
        30,
        1.167,
        30,
        1,
        1.3,
        30,
        1.433,
        0,
        1.567,
        0,
        1,
        1.7,
        0,
        1.833,
        30,
        1.967,
        30,
        1,
        2.089,
        30,
        2.211,
        0,
        2.333,
        0
      ]
    }
  ]
}


================================================
FILE: src-tauri/build.rs
================================================
fn main() {
    tauri_build::build()
}


================================================
FILE: src-tauri/capabilities/default.json
================================================
{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": [
    "*"
  ],
  "permissions": [
    "core:default",
    "core:window:allow-start-dragging",
    "core:window:allow-set-size",
    "core:window:deny-internal-toggle-maximize",
    "core:window:allow-set-always-on-top",
    "core:window:allow-set-ignore-cursor-events",
    "core:window:allow-set-decorations",
    "core:window:allow-set-position",
    "core:window:allow-set-theme",
    "core:window:allow-set-title",
    "custom-window:default",
    "os:default",
    "process:default",
    "opener:default",
    {
      "identifier": "opener:allow-open-path",
      "allow": [
        {
          "path": "**/*"
        }
      ]
    },
    "pinia:default",
    "log:default",
    "updater:default",
    "prevent-default:default",
    "autostart:default",
    "macos-permissions:default",
    "dialog:default",
    "fs:default",
    "fs:read-all",
    "fs:write-all",
    {
      "identifier": "fs:scope",
      "allow": [
        "**/*"
      ]
    },
    "clipboard-manager:allow-write-text",
    "global-shortcut:allow-is-registered",
    "global-shortcut:allow-register",
    "global-shortcut:allow-unregister",
    "locale:default"
  ]
}


================================================
FILE: src-tauri/src/core/device.rs
================================================
use rdev::{Event, EventType, listen};
use serde::Serialize;
use serde_json::{Value, json};
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{AppHandle, Emitter, Runtime, command};

#[derive(Debug, Clone, Serialize)]
pub enum DeviceEventKind {
    MousePress,
    MouseRelease,
    MouseMove,
    KeyboardPress,
    KeyboardRelease,
}

#[derive(Debug, Clone, Serialize)]
pub struct DeviceEvent {
    kind: DeviceEventKind,
    value: Value,
}

static IS_LISTENING: AtomicBool = AtomicBool::new(false);

#[command]
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
    if IS_LISTENING.load(Ordering::SeqCst) {
        return Ok(());
    }

    IS_LISTENING.store(true, Ordering::SeqCst);

    let callback = move |event: Event| {
        let device_event = match event.event_type {
            EventType::ButtonPress(button) => DeviceEvent {
                kind: DeviceEventKind::MousePress,
                value: json!(format!("{:?}", button)),
            },
            EventType::ButtonRelease(button) => DeviceEvent {
                kind: DeviceEventKind::MouseRelease,
                value: json!(format!("{:?}", button)),
            },
            EventType::MouseMove { x, y } => DeviceEvent {
                kind: DeviceEventKind::MouseMove,
                value: json!({ "x": x, "y": y }),
            },
            EventType::KeyPress(key) => DeviceEvent {
                kind: DeviceEventKind::KeyboardPress,
                value: json!(format!("{:?}", key)),
            },
            EventType::KeyRelease(key) => DeviceEvent {
                kind: DeviceEventKind::KeyboardRelease,
                value: json!(format!("{:?}", key)),
            },
            _ => return,
        };

        let _ = app_handle.emit("device-changed", device_event);
    };

    listen(callback).map_err(|err| format!("Failed to listen device: {:?}", err))?;

    Ok(())
}


================================================
FILE: src-tauri/src/core/gamepad.rs
================================================
use gilrs::{EventType, Gilrs};
use serde::Serialize;
use std::sync::atomic::{AtomicBool, Ordering};
use tauri::{AppHandle, Emitter, Runtime, command};

static IS_LISTENING: AtomicBool = AtomicBool::new(false);

#[derive(Debug, Clone, Serialize)]
pub enum GamepadEventKind {
    ButtonChanged,
    AxisChanged,
}

#[derive(Debug, Clone, Serialize)]
pub struct GamepadEvent {
    kind: GamepadEventKind,
    name: String,
    value: f32,
}

#[command]
pub async fn start_gamepad_listing<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
    if IS_LISTENING.load(Ordering::SeqCst) {
        return Ok(());
    }

    IS_LISTENING.store(true, Ordering::SeqCst);

    let mut gilrs = Gilrs::new().map_err(|err| err.to_string())?;

    while IS_LISTENING.load(Ordering::SeqCst) {
        while let Some(event) = gilrs.next_event() {
            let gamepad_event = match event.event {
                EventType::ButtonChanged(button, value, ..) => GamepadEvent {
                    kind: GamepadEventKind::ButtonChanged,
                    name: format!("{:?}", button),
                    value,
                },
                EventType::AxisChanged(axis, value, ..) => GamepadEvent {
                    kind: GamepadEventKind::AxisChanged,
                    name: format!("{:?}", axis),
                    value,
                },
                _ => continue,
            };

            let _ = app_handle.emit("gamepad-changed", gamepad_event);
        }
    }

    Ok(())
}

#[command]
pub async fn stop_gamepad_listing() {
    if !IS_LISTENING.load(Ordering::SeqCst) {
        return;
    }

    IS_LISTENING.store(false, Ordering::SeqCst);
}


================================================
FILE: src-tauri/src/core/mod.rs
================================================
pub mod device;
pub mod gamepad;
pub mod prevent_default;
pub mod setup;


================================================
FILE: src-tauri/src/core/prevent_default.rs
================================================
pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {
    #[cfg(debug_assertions)]
    {
        use tauri_plugin_prevent_default::Flags;

        tauri_plugin_prevent_default::Builder::new()
            .with_flags(Flags::all().difference(Flags::CONTEXT_MENU))
            .build()
    }

    #[cfg(not(debug_assertions))]
    tauri_plugin_prevent_default::init()
}


================================================
FILE: src-tauri/src/core/setup/common.rs
================================================
use tauri::{AppHandle, WebviewWindow};

pub fn platform(
    _app_handle: &AppHandle,
    _main_window: WebviewWindow,
    _preference_window: WebviewWindow,
) {
}


================================================
FILE: src-tauri/src/core/setup/macos.rs
================================================
#![allow(deprecated)]
use tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};
use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
use tauri_plugin_custom_window::MAIN_WINDOW_LABEL;

#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
#[allow(non_upper_case_globals)]
const NSResizableWindowMask: i32 = 1 << 3;
const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
const WINDOW_BLUR_EVENT: &str = "tauri://blur";
const WINDOW_MOVED_EVENT: &str = "tauri://move";
const WINDOW_RESIZED_EVENT: &str = "tauri://resize";

pub fn platform(
    app_handle: &AppHandle,
    main_window: WebviewWindow,
    _preference_window: WebviewWindow,
) {
    let _ = app_handle.plugin(tauri_nspanel::init());

    let _ = app_handle.set_dock_visibility(false);

    let panel = main_window.to_panel().unwrap();

    panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel | NSResizableWindowMask);

    panel.set_collection_behaviour(
        NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
            | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
            | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
    );

    let delegate = panel_delegate!(EcoPanelDelegate {
        window_did_become_key,
        window_did_resign_key,
        window_did_resize,
        window_did_move
    });

    delegate.set_listener(Box::new(move |delegate_name: String| {
        let target = EventTarget::labeled(MAIN_WINDOW_LABEL);

        let window_move_event = || {
            if let Ok(position) = main_window.outer_position() {
                let _ = main_window.emit_to(target.clone(), WINDOW_MOVED_EVENT, position);
            }
        };

        match delegate_name.as_str() {
            "window_did_become_key" => {
                let _ = main_window.emit_to(target, WINDOW_FOCUS_EVENT, true);
            }
            "window_did_resign_key" => {
                let _ = main_window.emit_to(target, WINDOW_BLUR_EVENT, true);
            }
            "window_did_resize" => {
                window_move_event();

                if let Ok(size) = main_window.outer_size() {
                    let _ = main_window.emit_to(target, WINDOW_RESIZED_EVENT, size);
                }
            }
            "window_did_move" => window_move_event(),
            _ => (),
        }
    }));

    panel.set_delegate(delegate);
}


================================================
FILE: src-tauri/src/core/setup/mod.rs
================================================
use tauri::{AppHandle, WebviewWindow};

#[cfg(target_os = "macos")]
mod macos;

#[cfg(not(target_os = "macos"))]
pub mod common;

#[cfg(target_os = "macos")]
pub use macos::*;

#[cfg(not(target_os = "macos"))]
pub use common::*;

pub fn default(
    app_handle: &AppHandle,
    main_window: WebviewWindow,
    preference_window: WebviewWindow,
) {
    #[cfg(debug_assertions)]
    main_window.open_devtools();

    platform(app_handle, main_window.clone(), preference_window.clone());
}


================================================
FILE: src-tauri/src/lib.rs
================================================
mod core;
mod utils;

use core::{
    device::start_device_listening,
    gamepad::{start_gamepad_listing, stop_gamepad_listing},
    prevent_default, setup,
};
use tauri::{Manager, WindowEvent, generate_handler};
use tauri_plugin_autostart::MacosLauncher;
use tauri_plugin_custom_window::{
    MAIN_WINDOW_LABEL, PREFERENCE_WINDOW_LABEL, show_preference_window,
};
use utils::fs_extra::copy_dir;

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    let app = tauri::Builder::default()
        .setup(|app| {
            let app_handle = app.handle();

            let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();

            let preference_window = app.get_webview_window(PREFERENCE_WINDOW_LABEL).unwrap();

            setup::default(&app_handle, main_window.clone(), preference_window.clone());

            Ok(())
        })
        .invoke_handler(generate_handler![
            copy_dir,
            start_device_listening,
            start_gamepad_listing,
            stop_gamepad_listing
        ])
        .plugin(tauri_plugin_custom_window::init())
        .plugin(tauri_plugin_os::init())
        .plugin(tauri_plugin_process::init())
        .plugin(tauri_plugin_opener::init())
        .plugin(tauri_plugin_pinia::init())
        .plugin(tauri_plugin_updater::Builder::new().build())
        .plugin(prevent_default::init())
        .plugin(tauri_plugin_single_instance::init(
            |app_handle, _argv, _cwd| {
                show_preference_window(app_handle);
            },
        ))
        .plugin(
            tauri_plugin_log::Builder::new()
                .timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)
                .filter(|metadata| !metadata.target().contains("gilrs"))
                .build(),
        )
        .plugin(tauri_plugin_autostart::init(
            MacosLauncher::LaunchAgent,
            None,
        ))
        .plugin(tauri_plugin_macos_permissions::init())
        .plugin(tauri_plugin_dialog::init())
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_clipboard_manager::init())
        .plugin(tauri_plugin_global_shortcut::Builder::new().build())
        .plugin(tauri_plugin_locale::init())
        .on_window_event(|window, event| match event {
            WindowEvent::CloseRequested { api, .. } => {
                let _ = window.hide();

                api.prevent_close();
            }
            _ => {}
        })
        .build(tauri::generate_context!())
        .expect("error while running tauri application");

    app.run(|app_handle, event| match event {
        #[cfg(target_os = "macos")]
        tauri::RunEvent::Reopen { .. } => {
            show_preference_window(app_handle);
        }
        _ => {
            let _ = app_handle;
        }
    });
}


================================================
FILE: src-tauri/src/main.rs
================================================
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    bongo_cat_lib::run()
}


================================================
FILE: src-tauri/src/plugins/window/Cargo.toml
================================================
[package]
name = "tauri-plugin-custom-window"
version = "0.1.0"
authors = []
description = ""
edition = "2024"
rust-version = "1.85.0"
links = "tauri-plugin-custom-window"

[dependencies]
tauri.workspace = true
serde.workspace = true

[build-dependencies]
tauri-plugin.workspace = true

[target."cfg(target_os = \"macos\")".dependencies]
tauri-nspanel.workspace = true


================================================
FILE: src-tauri/src/plugins/window/build.rs
================================================
const COMMANDS: &[&str] = &[
    "show_window",
    "hide_window",
    "set_always_on_top",
    "set_taskbar_visibility",
];

fn main() {
    tauri_plugin::Builder::new(COMMANDS).build();
}


================================================
FILE: src-tauri/src/plugins/window/permissions/default.toml
================================================
"$schema" = "schemas/schema.json"

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


================================================
FILE: src-tauri/src/plugins/window/src/commands/common.rs
================================================
use super::{shared_hide_window, shared_set_always_on_top, shared_show_window};
use tauri::{AppHandle, Runtime, WebviewWindow, command};

#[command]
pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
    shared_show_window(&app_handle, &window);
}

#[command]
pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
    shared_hide_window(&app_handle, &window);
}

#[command]
pub async fn set_always_on_top<R: Runtime>(
    app_handle: AppHandle<R>,
    window: WebviewWindow<R>,
    always_on_top: bool,
) {
    shared_set_always_on_top(&app_handle, &window, always_on_top);
}

#[command]
pub async fn set_taskbar_visibility<R: Runtime>(window: WebviewWindow<R>, visible: bool) {
    let _ = window.set_skip_taskbar(!visible);
}


================================================
FILE: src-tauri/src/plugins/window/src/commands/macos.rs
================================================
#![allow(deprecated)]
use super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window};
use crate::MAIN_WINDOW_LABEL;
use tauri::{AppHandle, Runtime, WebviewWindow, command};
use tauri_nspanel::{ManagerExt, cocoa::appkit::NSMainMenuWindowLevel};

pub enum MacOSPanelStatus {
    Show,
    Hide,
    SetAlwaysOnTop(bool),
}

#[command]
pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
    if is_main_window(&window) {
        set_macos_panel(&app_handle, &window, MacOSPanelStatus::Show);
    } else {
        shared_show_window(&app_handle, &window);
    }
}

#[command]
pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {
    if is_main_window(&window) {
        set_macos_panel(&app_handle, &window, MacOSPanelStatus::Hide);
    } else {
        shared_hide_window(&app_handle, &window);
    }
}

#[command]
pub async fn set_always_on_top<R: Runtime>(
    app_handle: AppHandle<R>,
    window: WebviewWindow<R>,
    always_on_top: bool,
) {
    if is_main_window(&window) {
        set_macos_panel(
            &app_handle,
            &window,
            MacOSPanelStatus::SetAlwaysOnTop(always_on_top),
        );
    } else {
        shared_set_always_on_top(&app_handle, &window, always_on_top);
    }
}

pub fn set_macos_panel<R: Runtime>(
    app_handle: &AppHandle<R>,
    window: &WebviewWindow<R>,
    status: MacOSPanelStatus,
) {
    if is_main_window(window) {
        let app_handle_clone = app_handle.clone();

        let _ = app_handle.run_on_main_thread(move || {
            if let Ok(panel) = app_handle_clone.get_webview_panel(MAIN_WINDOW_LABEL) {
                match status {
                    MacOSPanelStatus::Show => {
                        panel.show();
                    }
                    MacOSPanelStatus::Hide => {
                        panel.order_out(None);
                    }
                    MacOSPanelStatus::SetAlwaysOnTop(always_on_top) => {
                        if always_on_top {
                            panel.set_level(NSMainMenuWindowLevel);
                        } else {
                            panel.set_level(-1);
                        };
                    }
                }
            }
        });
    }
}

#[command]
pub async fn set_taskbar_visibility<R: Runtime>(app_handle: AppHandle<R>, visible: bool) {
    let _ = app_handle.set_dock_visibility(visible);
}


================================================
FILE: src-tauri/src/plugins/window/src/commands/mod.rs
================================================
use tauri::{AppHandle, Manager, Runtime, WebviewWindow, async_runtime::spawn};

pub static MAIN_WINDOW_LABEL: &str = "main";
pub static PREFERENCE_WINDOW_LABEL: &str = "preference";

#[cfg(target_os = "macos")]
mod macos;

#[cfg(not(target_os = "macos"))]
mod common;

#[cfg(target_os = "macos")]
pub use macos::*;

#[cfg(not(target_os = "macos"))]
pub use common::*;

pub fn is_main_window<R: Runtime>(window: &WebviewWindow<R>) -> bool {
    window.label() == MAIN_WINDOW_LABEL
}

fn shared_show_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) {
    let _ = window.show();
    let _ = window.unminimize();
    let _ = window.set_focus();
}

fn shared_hide_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) {
    let _ = window.hide();
}

fn shared_set_always_on_top<R: Runtime>(
    _app_handle: &AppHandle<R>,
    window: &WebviewWindow<R>,
    always_on_top: bool,
) {
    if always_on_top {
        let _ = window.set_always_on_bottom(false);
        let _ = window.set_always_on_top(true);
    } else {
        let _ = window.set_always_on_top(false);
        let _ = window.set_always_on_bottom(true);
    }
}

pub fn show_main_window(app_handle: &AppHandle) {
    show_window_by_label(app_handle, MAIN_WINDOW_LABEL);
}

pub fn show_preference_window(app_handle: &AppHandle) {
    show_window_by_label(app_handle, PREFERENCE_WINDOW_LABEL);
}

fn show_window_by_label(app_handle: &AppHandle, label: &str) {
    if let Some(window) = app_handle.get_webview_window(label) {
        let app_handle_clone = app_handle.clone();

        spawn(async move {
            show_window(app_handle_clone, window).await;
        });
    }
}


================================================
FILE: src-tauri/src/plugins/window/src/lib.rs
================================================
use tauri::{
    Runtime, generate_handler,
    plugin::{Builder, TauriPlugin},
};

mod commands;

pub use commands::*;

pub fn init<R: Runtime>() -> TauriPlugin<R> {
    Builder::new("custom-window")
        .invoke_handler(generate_handler![
            commands::show_window,
            commands::hide_window,
            commands::set_always_on_top,
            commands::set_taskbar_visibility,
        ])
        .build()
}


================================================
FILE: src-tauri/src/utils/fs_extra.rs
================================================
use fs_extra::dir::{CopyOptions, copy};
use std::fs::create_dir_all;
use tauri::command;

#[command]
pub async fn copy_dir(from_path: String, to_path: String) -> Result<(), String> {
    let mut options = CopyOptions::new();
    options.content_only = true;

    create_dir_all(&to_path).map_err(|err| err.to_string())?;

    copy(from_path, to_path, &options).map_err(|err| err.to_string())?;

    Ok(())
}


================================================
FILE: src-tauri/src/utils/mod.rs
================================================
pub mod fs_extra;


================================================
FILE: src-tauri/tauri.conf.json
================================================
{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "BongoCat",
  "version": "../package.json",
  "identifier": "com.ayangweb.BongoCat",
  "build": {
    "beforeDevCommand": "pnpm dev",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "pnpm build",
    "frontendDist": "../dist"
  },
  "app": {
    "macOSPrivateApi": true,
    "windows": [
      {
        "label": "main",
        "title": "BongoCat",
        "url": "index.html/#/",
        "shadow": false,
        "alwaysOnTop": true,
        "transparent": true,
        "decorations": false,
        "acceptFirstMouse": true,
        "skipTaskbar": true,
        "maximizable": false
      },
      {
        "label": "preference",
        "url": "index.html/#/preference",
        "visible": false,
        "titleBarStyle": "Overlay",
        "hiddenTitle": true,
        "minWidth": 800,
        "minHeight": 600
      }
    ],
    "security": {
      "csp": null,
      "dangerousDisableAssetCspModification": true,
      "assetProtocol": {
        "enable": true,
        "scope": {
          "allow": ["**/*"],
          "requireLiteralLeadingDot": false
        }
      }
    }
  },
  "bundle": {
    "active": true,
    "category": "Entertainment",
    "createUpdaterArtifacts": true,
    "targets": ["nsis", "dmg", "app", "appimage", "deb", "rpm"],
    "shortDescription": "BongoCat",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ],
    "resources": ["assets/tray.png", "assets/models"]
  },
  "plugins": {
    "updater": {
      "dangerousInsecureTransportProtocol": true,
      "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRjJFMzE3MjEwMUZEMTAKUldRUS9RRWhGK1B5NmdkemhKcUFrVjZBQXlzdExpakdWVEJDeU9XckVsbzV2cFIycVJOempWa2UK",
      "endpoints": [
        "http://api.upgrade.toolsetlink.com/v1/tauri/upgrade?tauriKey=KtGlsZUVXmWfjkRKCuqpfw&versionName={{current_version}}&target={{target}}&arch={{arch}}&appointVersionName=&devModelKey=&devKey=",
        "https://gh-proxy.com/github.com/ayangweb/BongoCat/releases/latest/download/latest.json"
      ]
    },
    "fs": {
      "requireLiteralLeadingDot": false
    }
  }
}


================================================
FILE: src-tauri/tauri.linux.conf.json
================================================
{
  "identifier": "com.ayangweb.BongoCat",
  "bundle": {
    "linux": {
      "deb": {
        "depends": ["gstreamer1.0-plugins-good"],
        "desktopTemplate": "./BongoCat.desktop"
      },
      "rpm": {
        "depends": ["gstreamer1-plugins-good"],
        "desktopTemplate": "./BongoCat.desktop"
      }
    }
  }
}


================================================
FILE: src-tauri/tauri.macos.conf.json
================================================
{
  "identifier": "com.ayangweb.BongoCat",
  "bundle": {
    "resources": ["assets/tray-mac.png", "assets/models"]
  }
}


================================================
FILE: src-tauri/tauri.windows.conf.json
================================================
{
  "identifier": "com.ayangweb.BongoCat",
  "bundle": {
    "windows": {
      "digestAlgorithm": "sha256",
      "nsis": {
        "languages": [
          "English",
          "Vietnamese",
          "SimpChinese",
          "PortugueseBR"
        ],
        "installMode": "both",
        "displayLanguageSelector": true
      }
    }
  }
}


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2020",
    "jsx": "prese
Download .txt
gitextract__za4gjq7/

├── .commitlintrc
├── .github/
│   ├── CONTRIBUTING.md
│   ├── DOWNLOAD_GUIDE.md
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── release.yml
│       ├── sync-to-gitee.yml
│       └── upgradelink.yml
├── .gitignore
├── .release-it.ts
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── Cargo.toml
├── LICENSE
├── README.md
├── eslint.config.ts
├── index.html
├── package.json
├── scripts/
│   ├── buildIcon.ts
│   └── release.ts
├── src/
│   ├── App.vue
│   ├── assets/
│   │   └── css/
│   │       └── global.scss
│   ├── components/
│   │   ├── pro-list/
│   │   │   └── index.vue
│   │   ├── pro-list-item/
│   │   │   └── index.vue
│   │   ├── pro-shortcut/
│   │   │   └── index.vue
│   │   └── update-app/
│   │       └── index.vue
│   ├── composables/
│   │   ├── useDevice.ts
│   │   ├── useGamepad.ts
│   │   ├── useModel.ts
│   │   ├── useSharedMenu.ts
│   │   ├── useTauriListen.ts
│   │   ├── useTauriShortcut.ts
│   │   ├── useThemeVars.ts
│   │   ├── useTray.ts
│   │   ├── useWindowPosition.ts
│   │   └── useWindowState.ts
│   ├── constants/
│   │   └── index.ts
│   ├── locales/
│   │   ├── en-US.json
│   │   ├── index.ts
│   │   ├── pt-BR.json
│   │   ├── vi-VN.json
│   │   └── zh-CN.json
│   ├── main.ts
│   ├── pages/
│   │   ├── main/
│   │   │   └── index.vue
│   │   └── preference/
│   │       ├── components/
│   │       │   ├── about/
│   │       │   │   └── index.vue
│   │       │   ├── cat/
│   │       │   │   ├── components/
│   │       │   │   │   └── position/
│   │       │   │   │       └── index.vue
│   │       │   │   └── index.vue
│   │       │   ├── general/
│   │       │   │   ├── components/
│   │       │   │   │   ├── macos-permissions/
│   │       │   │   │   │   └── index.vue
│   │       │   │   │   └── theme-mode/
│   │       │   │   │       └── index.vue
│   │       │   │   └── index.vue
│   │       │   ├── model/
│   │       │   │   ├── components/
│   │       │   │   │   ├── float-menu/
│   │       │   │   │   │   └── index.vue
│   │       │   │   │   └── upload/
│   │       │   │   │       └── index.vue
│   │       │   │   └── index.vue
│   │       │   └── shortcut/
│   │       │       └── index.vue
│   │       └── index.vue
│   ├── plugins/
│   │   └── window.ts
│   ├── router/
│   │   └── index.ts
│   ├── stores/
│   │   ├── app.ts
│   │   ├── cat.ts
│   │   ├── general.ts
│   │   ├── model.ts
│   │   └── shortcut.ts
│   ├── utils/
│   │   ├── is.ts
│   │   ├── keyboard.ts
│   │   ├── live2d.ts
│   │   ├── monitor.ts
│   │   ├── path.ts
│   │   ├── platform.ts
│   │   └── shared.ts
│   └── vite-env.d.ts
├── src-tauri/
│   ├── .gitignore
│   ├── BongoCat.desktop
│   ├── Cargo.toml
│   ├── assets/
│   │   └── models/
│   │       ├── gamepad/
│   │       │   ├── cat.model3.json
│   │       │   ├── demomodel3.cdi3.json
│   │       │   ├── demomodel3.moc3
│   │       │   ├── exp_1.exp3.json
│   │       │   ├── exp_2.exp3.json
│   │       │   ├── live2d_expression0.exp3.json
│   │       │   ├── live2d_expression1.exp3.json
│   │       │   ├── live2d_expression2.exp3.json
│   │       │   ├── live2d_motion1.flac
│   │       │   ├── live2d_motion1.motion3.json
│   │       │   └── live2d_motion2.motion3.json
│   │       ├── keyboard/
│   │       │   ├── cat.model3.json
│   │       │   ├── demomodel2.cdi3.json
│   │       │   ├── demomodel2.moc3
│   │       │   ├── exp_1.exp3.json
│   │       │   ├── exp_2.exp3.json
│   │       │   ├── live2d_expression0.exp3.json
│   │       │   ├── live2d_expression1.exp3.json
│   │       │   ├── live2d_expression2.exp3.json
│   │       │   ├── live2d_motion1.flac
│   │       │   ├── live2d_motion1.motion3.json
│   │       │   └── live2d_motion2.motion3.json
│   │       └── standard/
│   │           ├── cat.model3.json
│   │           ├── demomodel.cdi3.json
│   │           ├── demomodel.moc3
│   │           ├── exp_1.exp3.json
│   │           ├── exp_2.exp3.json
│   │           ├── live2d_expression0.exp3.json
│   │           ├── live2d_expression1.exp3.json
│   │           ├── live2d_expression2.exp3.json
│   │           ├── live2d_motion1.flac
│   │           ├── live2d_motion1.motion3.json
│   │           └── live2d_motion2.motion3.json
│   ├── build.rs
│   ├── capabilities/
│   │   └── default.json
│   ├── src/
│   │   ├── core/
│   │   │   ├── device.rs
│   │   │   ├── gamepad.rs
│   │   │   ├── mod.rs
│   │   │   ├── prevent_default.rs
│   │   │   └── setup/
│   │   │       ├── common.rs
│   │   │       ├── macos.rs
│   │   │       └── mod.rs
│   │   ├── lib.rs
│   │   ├── main.rs
│   │   ├── plugins/
│   │   │   └── window/
│   │   │       ├── Cargo.toml
│   │   │       ├── build.rs
│   │   │       ├── permissions/
│   │   │       │   └── default.toml
│   │   │       └── src/
│   │   │           ├── commands/
│   │   │           │   ├── common.rs
│   │   │           │   ├── macos.rs
│   │   │           │   └── mod.rs
│   │   │           └── lib.rs
│   │   └── utils/
│   │       ├── fs_extra.rs
│   │       └── mod.rs
│   ├── tauri.conf.json
│   ├── tauri.linux.conf.json
│   ├── tauri.macos.conf.json
│   └── tauri.windows.conf.json
├── tsconfig.json
├── tsconfig.node.json
├── uno.config.ts
└── vite.config.ts
Download .txt
SYMBOL INDEX (103 symbols across 38 files)

FILE: src-tauri/build.rs
  function main (line 1) | fn main() {

FILE: src-tauri/src/core/device.rs
  type DeviceEventKind (line 8) | pub enum DeviceEventKind {
  type DeviceEvent (line 17) | pub struct DeviceEvent {
  function start_device_listening (line 25) | pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>...

FILE: src-tauri/src/core/gamepad.rs
  type GamepadEventKind (line 9) | pub enum GamepadEventKind {
  type GamepadEvent (line 15) | pub struct GamepadEvent {
  function start_gamepad_listing (line 22) | pub async fn start_gamepad_listing<R: Runtime>(app_handle: AppHandle<R>)...
  function stop_gamepad_listing (line 55) | pub async fn stop_gamepad_listing() {

FILE: src-tauri/src/core/prevent_default.rs
  function init (line 1) | pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {

FILE: src-tauri/src/core/setup/common.rs
  function platform (line 3) | pub fn platform(

FILE: src-tauri/src/core/setup/macos.rs
  constant NSWindowStyleMaskNonActivatingPanel (line 7) | const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
  constant NSResizableWindowMask (line 9) | const NSResizableWindowMask: i32 = 1 << 3;
  constant WINDOW_FOCUS_EVENT (line 10) | const WINDOW_FOCUS_EVENT: &str = "tauri://focus";
  constant WINDOW_BLUR_EVENT (line 11) | const WINDOW_BLUR_EVENT: &str = "tauri://blur";
  constant WINDOW_MOVED_EVENT (line 12) | const WINDOW_MOVED_EVENT: &str = "tauri://move";
  constant WINDOW_RESIZED_EVENT (line 13) | const WINDOW_RESIZED_EVENT: &str = "tauri://resize";
  function platform (line 15) | pub fn platform(

FILE: src-tauri/src/core/setup/mod.rs
  function default (line 15) | pub fn default(

FILE: src-tauri/src/lib.rs
  function run (line 17) | pub fn run() {

FILE: src-tauri/src/main.rs
  function main (line 3) | fn main() {

FILE: src-tauri/src/plugins/window/build.rs
  constant COMMANDS (line 1) | const COMMANDS: &[&str] = &[
  function main (line 8) | fn main() {

FILE: src-tauri/src/plugins/window/src/commands/common.rs
  function show_window (line 5) | pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: W...
  function hide_window (line 10) | pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: W...
  function set_always_on_top (line 15) | pub async fn set_always_on_top<R: Runtime>(
  function set_taskbar_visibility (line 24) | pub async fn set_taskbar_visibility<R: Runtime>(window: WebviewWindow<R>...

FILE: src-tauri/src/plugins/window/src/commands/macos.rs
  type MacOSPanelStatus (line 7) | pub enum MacOSPanelStatus {
  function show_window (line 14) | pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: W...
  function hide_window (line 23) | pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: W...
  function set_always_on_top (line 32) | pub async fn set_always_on_top<R: Runtime>(
  function set_macos_panel (line 48) | pub fn set_macos_panel<R: Runtime>(
  function set_taskbar_visibility (line 79) | pub async fn set_taskbar_visibility<R: Runtime>(app_handle: AppHandle<R>...

FILE: src-tauri/src/plugins/window/src/commands/mod.rs
  function is_main_window (line 18) | pub fn is_main_window<R: Runtime>(window: &WebviewWindow<R>) -> bool {
  function shared_show_window (line 22) | fn shared_show_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &W...
  function shared_hide_window (line 28) | fn shared_hide_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &W...
  function shared_set_always_on_top (line 32) | fn shared_set_always_on_top<R: Runtime>(
  function show_main_window (line 46) | pub fn show_main_window(app_handle: &AppHandle) {
  function show_preference_window (line 50) | pub fn show_preference_window(app_handle: &AppHandle) {
  function show_window_by_label (line 54) | fn show_window_by_label(app_handle: &AppHandle, label: &str) {

FILE: src-tauri/src/plugins/window/src/lib.rs
  function init (line 10) | pub fn init<R: Runtime>() -> TauriPlugin<R> {

FILE: src-tauri/src/utils/fs_extra.rs
  function copy_dir (line 6) | pub async fn copy_dir(from_path: String, to_path: String) -> Result<(), ...

FILE: src/composables/useDevice.ts
  type MouseButtonEvent (line 15) | interface MouseButtonEvent {
  type CursorPoint (line 20) | interface CursorPoint {
  type MouseMoveEvent (line 25) | interface MouseMoveEvent {
  type KeyboardEvent (line 30) | interface KeyboardEvent {
  type DeviceEvent (line 35) | type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent
  function useDevice (line 37) | function useDevice() {

FILE: src/composables/useGamepad.ts
  type GamepadEventName (line 13) | type GamepadEventName = LiteralUnion<'LeftStickX' | 'LeftStickY' | 'Righ...
  type GamepadEvent (line 15) | interface GamepadEvent {
  type StickState (line 21) | interface StickState {
  type Sticks (line 28) | interface Sticks {
  constant INITIAL_STICK_STATE (line 33) | const INITIAL_STICK_STATE: StickState = { x: 0, y: 0, moved: false, pres...
  function useGamepad (line 35) | function useGamepad() {

FILE: src/composables/useModel.ts
  type ModelSize (line 19) | interface ModelSize {
  function useModel (line 24) | function useModel() {

FILE: src/composables/useSharedMenu.ts
  function useSharedMenu (line 9) | function useSharedMenu() {

FILE: src/composables/useTauriListen.ts
  function useTauriListen (line 5) | function useTauriListen<T>(...args: Parameters<typeof listen<T>>) {

FILE: src/composables/useTauriShortcut.ts
  function useTauriShortcut (line 11) | function useTauriShortcut(shortcut: Ref<string, string>, callback: Short...

FILE: src/composables/useThemeVars.ts
  function useThemeVars (line 4) | function useThemeVars() {

FILE: src/composables/useTray.ts
  constant TRAY_ID (line 23) | const TRAY_ID = 'BONGO_CAT_TRAY'
  function useTray (line 25) | function useTray() {

FILE: src/composables/useWindowPosition.ts
  function useWindowPosition (line 10) | function useWindowPosition() {

FILE: src/composables/useWindowState.ts
  type WindowState (line 11) | type WindowState = Record<string, Partial<PhysicalPosition & PhysicalSiz...
  function useWindowState (line 16) | function useWindowState() {

FILE: src/constants/index.ts
  constant GITHUB_LINK (line 1) | const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'
  constant UPGRADE_LINK_ACCESS_KEY (line 3) | const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'
  constant LISTEN_KEY (line 5) | const LISTEN_KEY = {
  constant INVOKE_KEY (line 13) | const INVOKE_KEY = {
  constant LANGUAGE (line 20) | const LANGUAGE = {

FILE: src/locales/index.ts
  function getAntdLocale (line 29) | function getAntdLocale(language: Language = LANGUAGE.EN_US) {

FILE: src/plugins/window.ts
  type WindowLabel (line 7) | type WindowLabel = 'main' | 'preference'
  constant COMMAND (line 9) | const COMMAND = {
  function showWindow (line 16) | function showWindow(label?: WindowLabel) {
  function hideWindow (line 24) | function hideWindow(label?: WindowLabel) {
  function setAlwaysOnTop (line 32) | function setAlwaysOnTop(alwaysOnTop: boolean) {
  function toggleWindowVisible (line 36) | async function toggleWindowVisible(label?: WindowLabel) {
  function setTaskbarVisibility (line 50) | async function setTaskbarVisibility(visible: boolean) {

FILE: src/stores/cat.ts
  type CatStore (line 4) | interface CatStore {

FILE: src/stores/general.ts
  type Language (line 9) | type Language = typeof LANGUAGE[keyof typeof LANGUAGE]
  type GeneralStore (line 11) | interface GeneralStore {

FILE: src/stores/model.ts
  type ModelMode (line 9) | type ModelMode = 'standard' | 'keyboard' | 'gamepad'
  type Model (line 11) | interface Model {
  type Motion (line 18) | interface Motion {
  type MotionGroup (line 27) | type MotionGroup = Record<string, Motion[]>
  type Expression (line 29) | interface Expression {

FILE: src/stores/shortcut.ts
  type HotKey (line 4) | type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop'

FILE: src/utils/is.ts
  function isImage (line 1) | function isImage(value: string) {
  function inBetween (line 7) | function inBetween(value: number, minimum: number, maximum: number) {

FILE: src/utils/keyboard.ts
  type Key (line 3) | interface Key {

FILE: src/utils/live2d.ts
  class Live2d (line 16) | class Live2d {
    method constructor (line 20) | constructor() { }
    method initApp (line 22) | private initApp() {
    method load (line 35) | public async load(path: string) {
    method destroy (line 76) | public destroy() {
    method resizeModel (line 84) | public resizeModel(modelSize: ModelSize) {
    method playMotion (line 99) | public playMotion(group: string, index: number) {
    method playExpressions (line 103) | public playExpressions(index: number) {
    method getCoreModel (line 107) | public getCoreModel() {
    method getParameterRange (line 113) | public getParameterRange(id: string) {
    method setParameterValue (line 126) | public setParameterValue(id: string, value: number | boolean) {

FILE: src/utils/monitor.ts
  function getCursorMonitor (line 6) | async function getCursorMonitor(cursorPoint?: PhysicalPosition) {

FILE: src/utils/path.ts
  function join (line 3) | function join(...paths: string[]) {

FILE: src/utils/shared.ts
  function clearObject (line 3) | function clearObject<T extends Record<string, unknown>>(targets: T | T[]) {
Condensed preview — 134 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (207K chars).
[
  {
    "path": ".commitlintrc",
    "chars": 50,
    "preview": "{\n\t\"extends\": \"@commitlint/config-conventional\"\n}\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "chars": 1113,
    "preview": "# 贡献指南\n\n非常感谢您对 BongoCat 的关注和贡献!在您提交贡献之前,请先花一些时间阅读以下指南,以确保您的贡献能够顺利进行。\n\n## 透明的开发\n\n所有工作都在 GitHub 上公开进行。无论是核心团队成员还是外部贡献者的 Pu"
  },
  {
    "path": ".github/DOWNLOAD_GUIDE.md",
    "chars": 907,
    "preview": "# 下载指南\n\n## 系统要求\n\n- macOS 12 或更高版本。\n- Windows 10 或更高版本。\n- Linux 带有 X11 环境。\n\n## macOS\n\n### 手动下载\n\n- Apple Silicon:下载 `Bongo"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 1048,
    "preview": "name: 🐞 Bug 报告\ntitle: '[bug] '\ndescription: 报告一个 Bug\nlabels: bug\nbody:\n  - type: markdown\n    attributes:\n      value: |"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 611,
    "preview": "name: 💡 功能请求\ntitle: '[feat] '\ndescription: 提出一个想法\nlabels: feature request\nbody:\n  - type: textarea\n    id: problem\n    a"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 3040,
    "preview": "name: BongoCat Release\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n\njobs:\n  create-release:\n    runs-on: ub"
  },
  {
    "path": ".github/workflows/sync-to-gitee.yml",
    "chars": 454,
    "preview": "name: Sync Github Repos To Gitee\non:\n  push:\n    branches:\n      - master\n\njobs:\n  repo-sync:\n    runs-on: ubuntu-latest"
  },
  {
    "path": ".github/workflows/upgradelink.yml",
    "chars": 579,
    "preview": "name: Upload Release to UpgradeLink\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  upgradeLink-upl"
  },
  {
    "path": ".gitignore",
    "chars": 225,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndis"
  },
  {
    "path": ".release-it.ts",
    "chars": 297,
    "preview": "/* eslint-disable no-template-curly-in-string */\nimport type { Config } from 'release-it'\n\nexport default {\n  git: {\n   "
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 142,
    "preview": "{\n  \"recommendations\": [\n    \"tauri-apps.tauri-vscode\",\n    \"rust-lang.rust-analyzer\",\n    \"antfu.unocss\",\n    \"dbaeumer"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 1603,
    "preview": "{\n  // Disable the default formatter, use eslint instead\n  \"prettier.enable\": false,\n\n  \"eslint.format.enable\": true,\n\n "
  },
  {
    "path": "Cargo.toml",
    "chars": 488,
    "preview": "[workspace]\nresolver = \"2\"\nmembers = [ \"src-tauri\" ]\n\n[profile.release]\nstrip = true\nopt-level = 3\ncodegen-units = 1\npan"
  },
  {
    "path": "LICENSE",
    "chars": 1065,
    "preview": "MIT License\n\nCopyright (c) 2025 ayangweb\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\no"
  },
  {
    "path": "README.md",
    "chars": 7951,
    "preview": "![BongoCat](https://socialify.git.ci/ayangweb/BongoCat/image?custom_description=&description=1&font=Source+Code+Pro&fork"
  },
  {
    "path": "eslint.config.ts",
    "chars": 751,
    "preview": "import antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  formatters: true,\n  unocss: true,\n  rules: {\n    'ant"
  },
  {
    "path": "index.html",
    "chars": 396,
    "preview": "<!doctype html>\n<html lang=\"zh\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "package.json",
    "chars": 2517,
    "preview": "{\n  \"name\": \"bongo-cat\",\n  \"type\": \"module\",\n  \"version\": \"0.9.0\",\n  \"private\": true,\n  \"author\": {\n    \"name\": \"ayangwe"
  },
  {
    "path": "scripts/buildIcon.ts",
    "chars": 337,
    "preview": "import { execSync } from 'node:child_process'\nimport { env, platform } from 'node:process'\n\n(() => {\n  const isMac = env"
  },
  {
    "path": "scripts/release.ts",
    "chars": 709,
    "preview": "import { readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPat"
  },
  {
    "path": "src/App.vue",
    "chars": 2835,
    "preview": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { error } from '"
  },
  {
    "path": "src/assets/css/global.scss",
    "chars": 476,
    "preview": "html {\n  --uno: select-none overscroll-none antialiased;\n\n  color-scheme: light;\n\n  body {\n    --uno: transition-opacity"
  },
  {
    "path": "src/components/pro-list/index.vue",
    "chars": 412,
    "preview": "<script setup lang=\"ts\">\nimport { Flex } from 'ant-design-vue'\n\nconst { title } = defineProps<{\n  title: string\n}>()\n</s"
  },
  {
    "path": "src/components/pro-list-item/index.vue",
    "chars": 1080,
    "preview": "<script setup lang=\"ts\">\nimport { Flex } from 'ant-design-vue'\nimport { computed, useSlots } from 'vue'\n\nconst { title, "
  },
  {
    "path": "src/components/pro-shortcut/index.vue",
    "chars": 3394,
    "preview": "<script setup lang=\"ts\">\nimport type { Key } from '@/utils/keyboard'\n\nimport { find, map, remove, some, split } from 'es"
  },
  {
    "path": "src/components/update-app/index.vue",
    "chars": 4741,
    "preview": "<script setup lang=\"ts\">\nimport type { Update } from '@tauri-apps/plugin-updater'\n\nimport { relaunch } from '@tauri-apps"
  },
  {
    "path": "src/composables/useDevice.ts",
    "chars": 3556,
    "preview": "import { invoke } from '@tauri-apps/api/core'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nim"
  },
  {
    "path": "src/composables/useGamepad.ts",
    "chars": 2891,
    "preview": "import type { LiteralUnion } from 'ant-design-vue/es/_util/type'\n\nimport { invoke } from '@tauri-apps/api/core'\nimport {"
  },
  {
    "path": "src/composables/useModel.ts",
    "chars": 3862,
    "preview": "import type { PhysicalPosition } from '@tauri-apps/api/dpi'\n\nimport { LogicalSize } from '@tauri-apps/api/dpi'\nimport { "
  },
  {
    "path": "src/composables/useSharedMenu.ts",
    "chars": 2652,
    "preview": "import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'\nimport { range } from 'es-to"
  },
  {
    "path": "src/composables/useTauriListen.ts",
    "chars": 367,
    "preview": "import { listen } from '@tauri-apps/api/event'\nimport { noop } from '@vueuse/core'\nimport { onMounted, onUnmounted, ref "
  },
  {
    "path": "src/composables/useTauriShortcut.ts",
    "chars": 781,
    "preview": "import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut'\nimport type { Ref } from 'vue'\n\nimport {\n  isR"
  },
  {
    "path": "src/composables/useThemeVars.ts",
    "chars": 983,
    "preview": "import { theme } from 'ant-design-vue'\nimport { kebabCase } from 'es-toolkit'\n\nexport function useThemeVars() {\n  const "
  },
  {
    "path": "src/composables/useTray.ts",
    "chars": 3120,
    "preview": "import type { TrayIconOptions } from '@tauri-apps/api/tray'\n\nimport { getName, getVersion } from '@tauri-apps/api/app'\ni"
  },
  {
    "path": "src/composables/useWindowPosition.ts",
    "chars": 1379,
    "preview": "import { PhysicalPosition } from '@tauri-apps/api/dpi'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewW"
  },
  {
    "path": "src/composables/useWindowState.ts",
    "chars": 1777,
    "preview": "import type { Event } from '@tauri-apps/api/event'\n\nimport { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'"
  },
  {
    "path": "src/constants/index.ts",
    "chars": 639,
    "preview": "export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'\n\nexport const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThD"
  },
  {
    "path": "src/locales/en-US.json",
    "chars": 6261,
    "preview": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"Redrawing...\"\n      }\n    },\n    \"preference\": {\n   "
  },
  {
    "path": "src/locales/index.ts",
    "chars": 1067,
    "preview": "import type { Language } from '@/stores/general'\nimport type { Locale as AntdLocale } from 'ant-design-vue/es/locale'\n\ni"
  },
  {
    "path": "src/locales/pt-BR.json",
    "chars": 7056,
    "preview": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"Redimensionando...\"\n      }\n    },\n    \"preference\":"
  },
  {
    "path": "src/locales/vi-VN.json",
    "chars": 6380,
    "preview": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"Đang đổi kích thước...\"\n      }\n    },\n    \"preferen"
  },
  {
    "path": "src/locales/zh-CN.json",
    "chars": 4727,
    "preview": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"重绘中...\"\n      }\n    },\n    \"preference\": {\n      \"ti"
  },
  {
    "path": "src/main.ts",
    "chars": 449,
    "preview": "import { createPlugin } from '@tauri-store/pinia'\nimport { createPinia } from 'pinia'\nimport { createApp } from 'vue'\n\ni"
  },
  {
    "path": "src/pages/main/index.vue",
    "chars": 5472,
    "preview": "<script setup lang=\"ts\">\nimport { convertFileSrc } from '@tauri-apps/api/core'\nimport { PhysicalSize } from '@tauri-apps"
  },
  {
    "path": "src/pages/preference/components/about/index.vue",
    "chars": 2791,
    "preview": "<script setup lang=\"ts\">\nimport { getTauriVersion } from '@tauri-apps/api/app'\nimport { emit } from '@tauri-apps/api/eve"
  },
  {
    "path": "src/pages/preference/components/cat/components/position/index.vue",
    "chars": 953,
    "preview": "<script setup lang=\"ts\">\nimport { Select, SelectOption } from 'ant-design-vue'\n\nimport ProListItem from '@/components/pr"
  },
  {
    "path": "src/pages/preference/components/cat/index.vue",
    "chars": 3157,
    "preview": "<script setup lang=\"ts\">\nimport { InputNumber, Slider, Switch } from 'ant-design-vue'\n\nimport Position from './component"
  },
  {
    "path": "src/pages/preference/components/general/components/macos-permissions/index.vue",
    "chars": 2213,
    "preview": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { confirm } from"
  },
  {
    "path": "src/pages/preference/components/general/components/theme-mode/index.vue",
    "chars": 1635,
    "preview": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { Select, Select"
  },
  {
    "path": "src/pages/preference/components/general/index.vue",
    "chars": 2153,
    "preview": "<script setup lang=\"ts\">\nimport { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'\nimport { Select, Swit"
  },
  {
    "path": "src/pages/preference/components/model/components/float-menu/index.vue",
    "chars": 1183,
    "preview": "<script setup lang=\"ts\">\nimport { EditOutlined, MenuOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/ic"
  },
  {
    "path": "src/pages/preference/components/model/components/upload/index.vue",
    "chars": 2957,
    "preview": "<script setup lang=\"ts\">\nimport type { ModelMode } from '@/stores/model'\n\nimport { invoke } from '@tauri-apps/api/core'\n"
  },
  {
    "path": "src/pages/preference/components/model/index.vue",
    "chars": 3113,
    "preview": "<script setup lang=\"ts\">\nimport type { Model } from '@/stores/model'\nimport type { ComponentPublicInstance } from 'vue'\n"
  },
  {
    "path": "src/pages/preference/components/shortcut/index.vue",
    "chars": 2184,
    "preview": "<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\n\nimport ProList from '@/components/pro-list/index.vue'\nimpo"
  },
  {
    "path": "src/pages/preference/index.vue",
    "chars": 3223,
    "preview": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { Flex } from 'a"
  },
  {
    "path": "src/plugins/window.ts",
    "chars": 1329,
    "preview": "import { invoke } from '@tauri-apps/api/core'\nimport { emit } from '@tauri-apps/api/event'\nimport { getCurrentWebviewWin"
  },
  {
    "path": "src/router/index.ts",
    "chars": 469,
    "preview": "import type { RouteRecordRaw } from 'vue-router'\n\nimport { createRouter, createWebHashHistory } from 'vue-router'\n\nimpor"
  },
  {
    "path": "src/stores/app.ts",
    "chars": 522,
    "preview": "import type { WindowState } from '@/composables/useWindowState'\n\nimport { getName, getVersion } from '@tauri-apps/api/ap"
  },
  {
    "path": "src/stores/cat.ts",
    "chars": 1947,
    "preview": "import { defineStore } from 'pinia'\nimport { reactive, ref } from 'vue'\n\nexport interface CatStore {\n  model: {\n    mirr"
  },
  {
    "path": "src/stores/general.ts",
    "chars": 1997,
    "preview": "import type { Theme } from '@tauri-apps/api/window'\n\nimport { defineStore } from 'pinia'\nimport { getLocale } from 'taur"
  },
  {
    "path": "src/stores/model.ts",
    "chars": 1897,
    "preview": "import { resolveResource } from '@tauri-apps/api/path'\nimport { filter, find } from 'es-toolkit/compat'\nimport { nanoid "
  },
  {
    "path": "src/stores/shortcut.ts",
    "chars": 468,
    "preview": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport type HotKey = 'visibleCat' | 'mirrorMode' | 'penet"
  },
  {
    "path": "src/utils/is.ts",
    "chars": 275,
    "preview": "export function isImage(value: string) {\n  const regex = /\\.(?:jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i\n\n"
  },
  {
    "path": "src/utils/keyboard.ts",
    "chars": 3527,
    "preview": "import { isMac } from './platform'\n\nexport interface Key {\n  eventKey: string\n  tauriKey?: string\n  symbol?: string\n}\n\ne"
  },
  {
    "path": "src/utils/live2d.ts",
    "chars": 3114,
    "preview": "import type { ModelSize } from '@/composables/useModel'\nimport type { Cubism4InternalModel } from 'pixi-live2d-display'\n"
  },
  {
    "path": "src/utils/monitor.ts",
    "chars": 572,
    "preview": "import type { PhysicalPosition } from '@tauri-apps/api/window'\n\nimport { getCurrentWebviewWindow } from '@tauri-apps/api"
  },
  {
    "path": "src/utils/path.ts",
    "chars": 345,
    "preview": "import { sep } from '@tauri-apps/api/path'\n\nexport function join(...paths: string[]) {\n  const joinPaths = paths.map((pa"
  },
  {
    "path": "src/utils/platform.ts",
    "chars": 192,
    "preview": "import { platform } from '@tauri-apps/plugin-os'\n\nexport const isMac = platform() === 'macos'\n\nexport const isWindows = "
  },
  {
    "path": "src/utils/shared.ts",
    "chars": 260,
    "preview": "import { castArray } from 'es-toolkit/compat'\n\nexport function clearObject<T extends Record<string, unknown>>(targets: T"
  },
  {
    "path": "src/vite-env.d.ts",
    "chars": 195,
    "preview": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n\n  const co"
  },
  {
    "path": "src-tauri/.gitignore",
    "chars": 194,
    "preview": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Generated by Tauri\n# will have schema files "
  },
  {
    "path": "src-tauri/BongoCat.desktop",
    "chars": 146,
    "preview": "[Desktop Entry]\nType=Application\nName={{{name}}}\nExec={{{exec}}}\nIcon={{{icon}}}\nCategories={{{categories}}}\nComment={{{"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "chars": 1547,
    "preview": "[package]\nname = \"bongo-cat\"\nversion = \"0.9.0\"\ndescription = \"A Tauri App\"\nauthors = [ \"ayangweb\" ]\nedition = \"2024\"\n\n# "
  },
  {
    "path": "src-tauri/assets/models/gamepad/cat.model3.json",
    "chars": 1534,
    "preview": "{\n  \"Version\": 3,\n  \"FileReferences\": {\n    \"Moc\": \"demomodel3.moc3\",\n    \"Textures\": [\n      \"demomodel3.1024/texture_0"
  },
  {
    "path": "src-tauri/assets/models/gamepad/demomodel3.cdi3.json",
    "chars": 4832,
    "preview": "{\n  \"Version\": 3,\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 X\"\n    },\n  "
  },
  {
    "path": "src-tauri/assets/models/gamepad/exp_1.exp3.json",
    "chars": 236,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": 0.321,\n      \"Blend"
  },
  {
    "path": "src-tauri/assets/models/gamepad/exp_2.exp3.json",
    "chars": 138,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": -1,\n      \"Blend\": "
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_expression0.exp3.json",
    "chars": 54,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_expression1.exp3.json",
    "chars": 151,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.8,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param4\",\n      \"Value\": 1,\n  "
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_expression2.exp3.json",
    "chars": 151,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.5,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param5\",\n      \"Value\": 1,\n  "
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_motion1.motion3.json",
    "chars": 1059,
    "preview": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 1.633,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": fals"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_motion2.motion3.json",
    "chars": 1085,
    "preview": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 2.333,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": true"
  },
  {
    "path": "src-tauri/assets/models/keyboard/cat.model3.json",
    "chars": 1534,
    "preview": "{\n  \"Version\": 3,\n  \"FileReferences\": {\n    \"Moc\": \"demomodel2.moc3\",\n    \"Textures\": [\n      \"demomodel2.1024/texture_0"
  },
  {
    "path": "src-tauri/assets/models/keyboard/demomodel2.cdi3.json",
    "chars": 3867,
    "preview": "{\n  \"Version\": 3,\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 X\"\n    },\n  "
  },
  {
    "path": "src-tauri/assets/models/keyboard/exp_1.exp3.json",
    "chars": 236,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": 0.321,\n      \"Blend"
  },
  {
    "path": "src-tauri/assets/models/keyboard/exp_2.exp3.json",
    "chars": 138,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": -1,\n      \"Blend\": "
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_expression0.exp3.json",
    "chars": 54,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_expression1.exp3.json",
    "chars": 151,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.8,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param4\",\n      \"Value\": 1,\n  "
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_expression2.exp3.json",
    "chars": 151,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.5,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param5\",\n      \"Value\": 1,\n  "
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_motion1.motion3.json",
    "chars": 1059,
    "preview": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 1.633,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": fals"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_motion2.motion3.json",
    "chars": 1085,
    "preview": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 2.333,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": true"
  },
  {
    "path": "src-tauri/assets/models/standard/cat.model3.json",
    "chars": 1547,
    "preview": "{\n  \"Version\": 3,\n  \"FileReferences\": {\n    \"Moc\": \"demomodel.moc3\",\n    \"Textures\": [\n      \"demomodel.1024/texture_00."
  },
  {
    "path": "src-tauri/assets/models/standard/demomodel.cdi3.json",
    "chars": 4056,
    "preview": "{\n  \"Version\": 3,\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 X\"\n    },\n  "
  },
  {
    "path": "src-tauri/assets/models/standard/exp_1.exp3.json",
    "chars": 236,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": 0.321,\n      \"Blend"
  },
  {
    "path": "src-tauri/assets/models/standard/exp_2.exp3.json",
    "chars": 138,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": -1,\n      \"Blend\": "
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_expression0.exp3.json",
    "chars": 54,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_expression1.exp3.json",
    "chars": 151,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.8,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param4\",\n      \"Value\": 1,\n  "
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_expression2.exp3.json",
    "chars": 151,
    "preview": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.5,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param5\",\n      \"Value\": 1,\n  "
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_motion1.motion3.json",
    "chars": 1059,
    "preview": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 1.633,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": fals"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_motion2.motion3.json",
    "chars": 1085,
    "preview": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 2.333,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": true"
  },
  {
    "path": "src-tauri/build.rs",
    "chars": 39,
    "preview": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/default.json",
    "chars": 1290,
    "preview": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"default\",\n  \"description\": \"Capability for the mai"
  },
  {
    "path": "src-tauri/src/core/device.rs",
    "chars": 1939,
    "preview": "use rdev::{Event, EventType, listen};\nuse serde::Serialize;\nuse serde_json::{Value, json};\nuse std::sync::atomic::{Atomi"
  },
  {
    "path": "src-tauri/src/core/gamepad.rs",
    "chars": 1669,
    "preview": "use gilrs::{EventType, Gilrs};\nuse serde::Serialize;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse tauri::{AppHandl"
  },
  {
    "path": "src-tauri/src/core/mod.rs",
    "chars": 73,
    "preview": "pub mod device;\npub mod gamepad;\npub mod prevent_default;\npub mod setup;\n"
  },
  {
    "path": "src-tauri/src/core/prevent_default.rs",
    "chars": 371,
    "preview": "pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {\n    #[cfg(debug_assertions)]\n    {\n        use tauri_plugin_pr"
  },
  {
    "path": "src-tauri/src/core/setup/common.rs",
    "chars": 164,
    "preview": "use tauri::{AppHandle, WebviewWindow};\n\npub fn platform(\n    _app_handle: &AppHandle,\n    _main_window: WebviewWindow,\n "
  },
  {
    "path": "src-tauri/src/core/setup/macos.rs",
    "chars": 2470,
    "preview": "#![allow(deprecated)]\nuse tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};\nuse tauri_nspanel::{WebviewWindowExt,"
  },
  {
    "path": "src-tauri/src/core/setup/mod.rs",
    "chars": 487,
    "preview": "use tauri::{AppHandle, WebviewWindow};\n\n#[cfg(target_os = \"macos\")]\nmod macos;\n\n#[cfg(not(target_os = \"macos\"))]\npub mod"
  },
  {
    "path": "src-tauri/src/lib.rs",
    "chars": 2813,
    "preview": "mod core;\nmod utils;\n\nuse core::{\n    device::start_device_listening,\n    gamepad::{start_gamepad_listing, stop_gamepad_"
  },
  {
    "path": "src-tauri/src/main.rs",
    "chars": 107,
    "preview": "#![cfg_attr(not(debug_assertions), windows_subsystem = \"windows\")]\n\nfn main() {\n    bongo_cat_lib::run()\n}\n"
  },
  {
    "path": "src-tauri/src/plugins/window/Cargo.toml",
    "chars": 369,
    "preview": "[package]\nname = \"tauri-plugin-custom-window\"\nversion = \"0.1.0\"\nauthors = []\ndescription = \"\"\nedition = \"2024\"\nrust-vers"
  },
  {
    "path": "src-tauri/src/plugins/window/build.rs",
    "chars": 190,
    "preview": "const COMMANDS: &[&str] = &[\n    \"show_window\",\n    \"hide_window\",\n    \"set_always_on_top\",\n    \"set_taskbar_visibility\""
  },
  {
    "path": "src-tauri/src/plugins/window/permissions/default.toml",
    "chars": 212,
    "preview": "\"$schema\" = \"schemas/schema.json\"\n\n[default]\ndescription = \"Default permissions for the plugin\"\npermissions = [\"allow-sh"
  },
  {
    "path": "src-tauri/src/plugins/window/src/commands/common.rs",
    "chars": 804,
    "preview": "use super::{shared_hide_window, shared_set_always_on_top, shared_show_window};\nuse tauri::{AppHandle, Runtime, WebviewWi"
  },
  {
    "path": "src-tauri/src/plugins/window/src/commands/macos.rs",
    "chars": 2462,
    "preview": "#![allow(deprecated)]\nuse super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window};\nuse"
  },
  {
    "path": "src-tauri/src/plugins/window/src/commands/mod.rs",
    "chars": 1684,
    "preview": "use tauri::{AppHandle, Manager, Runtime, WebviewWindow, async_runtime::spawn};\n\npub static MAIN_WINDOW_LABEL: &str = \"ma"
  },
  {
    "path": "src-tauri/src/plugins/window/src/lib.rs",
    "chars": 431,
    "preview": "use tauri::{\n    Runtime, generate_handler,\n    plugin::{Builder, TauriPlugin},\n};\n\nmod commands;\n\npub use commands::*;\n"
  },
  {
    "path": "src-tauri/src/utils/fs_extra.rs",
    "chars": 408,
    "preview": "use fs_extra::dir::{CopyOptions, copy};\nuse std::fs::create_dir_all;\nuse tauri::command;\n\n#[command]\npub async fn copy_d"
  },
  {
    "path": "src-tauri/src/utils/mod.rs",
    "chars": 18,
    "preview": "pub mod fs_extra;\n"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "chars": 2243,
    "preview": "{\n  \"$schema\": \"https://schema.tauri.app/config/2\",\n  \"productName\": \"BongoCat\",\n  \"version\": \"../package.json\",\n  \"iden"
  },
  {
    "path": "src-tauri/tauri.linux.conf.json",
    "chars": 325,
    "preview": "{\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"bundle\": {\n    \"linux\": {\n      \"deb\": {\n        \"depends\": [\"gstreamer1.0"
  },
  {
    "path": "src-tauri/tauri.macos.conf.json",
    "chars": 121,
    "preview": "{\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"bundle\": {\n    \"resources\": [\"assets/tray-mac.png\", \"assets/models\"]\n  }\n}"
  },
  {
    "path": "src-tauri/tauri.windows.conf.json",
    "chars": 345,
    "preview": "{\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"bundle\": {\n    \"windows\": {\n      \"digestAlgorithm\": \"sha256\",\n      \"nsis"
  },
  {
    "path": "tsconfig.json",
    "chars": 726,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"jsx\": \"preserve\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n  "
  },
  {
    "path": "tsconfig.node.json",
    "chars": 213,
    "preview": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyn"
  },
  {
    "path": "uno.config.ts",
    "chars": 1399,
    "preview": "import {\n  defineConfig,\n  presetIcons,\n  presetWind3,\n  transformerDirectives,\n  transformerVariantGroup,\n} from 'unocs"
  },
  {
    "path": "vite.config.ts",
    "chars": 945,
    "preview": "import { resolve } from 'node:path'\nimport { env } from 'node:process'\n\nimport vue from '@vitejs/plugin-vue'\nimport UnoC"
  }
]

// ... and 6 more files (download for full content)

About this extraction

This page contains the full source code of the ayangweb/BongoCat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 134 files (181.2 KB), approximately 56.6k tokens, and a symbol index with 103 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!