[
  {
    "path": ".commitlintrc",
    "content": "{\n\t\"extends\": \"@commitlint/config-conventional\"\n}\n"
  },
  {
    "path": ".github/CONTRIBUTING.md",
    "content": "# 贡献指南\n\n非常感谢您对 BongoCat 的关注和贡献！在您提交贡献之前，请先花一些时间阅读以下指南，以确保您的贡献能够顺利进行。\n\n## 透明的开发\n\n所有工作都在 GitHub 上公开进行。无论是核心团队成员还是外部贡献者的 Pull Request，都需要经过相同的 review 流程。\n\n## 提交 Issue\n\n我们使用 [Github Issues](https://github.com/ayangweb/BongoCat/issues) 进行 Bug 报告和新 Feature 建议。在提交 Issue 之前，请确保已经搜索过类似的问题，因为它们可能已经得到解答或正在被修复。对于 Bug 报告，请包含可用于重现问题的完整步骤。对于新 Feature 建议，请指出你想要的更改以及期望的行为。\n\n## 提交 Pull Request\n\n### 共建流程\n\n- 认领 issue：在 Github 建立 Issue 并认领（或直接认领已有 Issue），告知大家自己正在修复，避免重复工作。\n- 项目开发：在完成准备工作后，进行 Bug 修复或功能开发。\n- 提交 PR。\n\n### 准备工作\n\n- [Rust](https://v2.tauri.app/start/prerequisites/): 请自行根据官网步骤安装 rust 环境。\n- [Node.js](https://nodejs.org/en/): 用于运行项目。\n- [Pnpm](https://pnpm.io/)：本项目使用 Pnpm 进行包管理。\n\n### 下载依赖\n\n```shell\npnpm install\n```\n\n### 启动应用\n\n```shell\npnpm tauri dev\n```\n\n### 打包应用\n\n> 如果需要打包后进行调试，请在以下命令后面加上 `--debug`\n\n```shell\npnpm tauri build\n```\n\n## Commit 指南\n\nCommit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)。\n\n### Commit 类型\n\n以下是 commit 类型列表:\n\n- feat: 新特性或功能\n- fix: 缺陷修复\n- docs: 文档更新\n- style: 代码风格更新\n- refactor: 代码重构，不引入新功能和缺陷修复\n- perf: 性能优化\n- chore: 其他提交\n\n期待您的参与，让我们一起使 BongoCat 变得更好！\n"
  },
  {
    "path": ".github/DOWNLOAD_GUIDE.md",
    "content": "# 下载指南\n\n## 系统要求\n\n- macOS 12 或更高版本。\n- Windows 10 或更高版本。\n- Linux 带有 X11 环境。\n\n## macOS\n\n### 手动下载\n\n- Apple Silicon：下载 `BongoCat_aarch64.dmg`\n- Intel Chip：下载 `BongoCat_x64.dmg`\n\n### Homebrew 下载\n\n1. 添加 BongoCat 的 tap 源：\n\n```bash\nbrew tap ayangweb/BongoCat\n```\n\n2. 安装：\n\n```bash\nbrew install --no-quarantine bongo-cat\n```\n\n3. 更新：\n\n```bash\nbrew upgrade bongo-cat\n```\n\n4. 卸载：\n\n```bash\nbrew uninstall --cask bongo-cat\n\nbrew untap ayangweb/BongoCat\n```\n\n## Windows\n\n- 64 位系统：下载 `BongoCat_x64.exe`\n- 32 位系统：下载 `BongoCat_x86.exe`\n- ARM64 架构：下载 `BongoCat_arm64.exe`\n\n## Linux(X11)\n\n### 手动下载\n\n- 64 位系统：\n  - Debian / Ubuntu：下载 `BongoCat_amd64.deb`\n  - Fedora / RHEL：下载 `BongoCat_x86_64.rpm`\n  - 通用版本：下载 `BongoCat_amd64.AppImage`\n- ARM64 架构：\n  - Debian / Ubuntu：下载 `BongoCat_arm64.deb`\n  - Fedora / RHEL：下载 `BongoCat_aarch64.rpm`\n  - 通用版本：下载 `BongoCat_aarch64.AppImage`\n\n### AUR 下载\n\n- Manjaro / ArchLinux: `yay -S bongo-cat`\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐞 Bug 报告\ntitle: '[bug] '\ndescription: 报告一个 Bug\nlabels: bug\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## 温馨提示\n        1. 请先查阅现有的 [issues](https://github.com/ayangweb/BongoCat/issues)。\n        2. 请确保你使用的是[最新版本](https://github.com/ayangweb/BongoCat/releases/latest)。\n        3. 请确保该问题不是由其他软件引起的。\n        4. 请始终保持友好与尊重，感谢你的理解与配合。\n\n  - type: textarea\n    id: description\n    attributes:\n      label: 描述 Bug\n      description: 请详细描述 Bug 并提供截图或视频以帮助我们更好地理解问题。\n    validations:\n      required: true\n\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: 重现步骤\n      description: 请详细列出重现问题的步骤，并附带截图或视频。\n\n  - type: textarea\n    id: expected-behavior\n    attributes:\n      label: 预期行为\n      description: 请描述你期望发生的行为。\n\n  - type: textarea\n    id: info\n    attributes:\n      render: text\n      label: 软件信息\n      description: 请前往偏好设置窗口的「关于 > 关于软件 > 软件信息」复制软件信息。\n    validations:\n      required: true\n\n  - type: textarea\n    id: context\n    attributes:\n      label: 附加信息\n      description: 请在此提供有关该问题的其他相关信息，帮助我们更全面地理解问题。\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 💡 功能请求\ntitle: '[feat] '\ndescription: 提出一个想法\nlabels: feature request\nbody:\n  - type: textarea\n    id: problem\n    attributes:\n      label: 描述问题\n      description: 请清晰地描述此功能将解决的具体问题。\n    validations:\n      required: true\n\n  - type: textarea\n    id: solution\n    attributes:\n      label: 描述您希望的解决方案\n      description: 请清晰地描述您期望的变更或改进。\n    validations:\n      required: true\n\n  - type: textarea\n    id: alternatives\n    attributes:\n      label: 考虑的替代方案\n      description: 提供您考虑过的其他替代解决方案。\n\n  - type: textarea\n    id: context\n    attributes:\n      label: 附加信息\n      description: 请在此提供有关该问题的其他相关信息，帮助我们更全面地理解问题。\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: BongoCat Release\n\non:\n  push:\n    tags:\n      - 'v*'\n  workflow_dispatch:\n\njobs:\n  create-release:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - name: Set output\n        id: vars\n        run: echo \"tag=${GITHUB_REF#refs/*/}\" >> $GITHUB_OUTPUT\n\n      - name: Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n\n      - name: Generate changelog\n        id: create_release\n        run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }}\n        env:\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n\n  build-app:\n    needs: create-release\n    permissions:\n      contents: write\n    strategy:\n      fail-fast: false\n      matrix:\n        include:\n          - platform: macos-latest\n            target: aarch64-apple-darwin\n          - platform: macos-latest\n            target: x86_64-apple-darwin\n\n          - platform: windows-latest\n            target: x86_64-pc-windows-msvc\n          - platform: windows-latest\n            target: i686-pc-windows-msvc\n          - platform: windows-latest\n            target: aarch64-pc-windows-msvc\n\n          - platform: ubuntu-22.04\n            target: x86_64-unknown-linux-gnu\n          - platform: ubuntu-22.04-arm\n            target: aarch64-unknown-linux-gnu\n\n    runs-on: ${{ matrix.platform }}\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v4\n      - name: Setup node\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - uses: pnpm/action-setup@v3\n        with:\n          version: latest\n\n      - name: Install rust target\n        run: rustup target add ${{ matrix.target }}\n\n      - name: Install dependencies (ubuntu only)\n        if: startsWith(matrix.platform, 'ubuntu')\n        run: |\n          sudo apt-get update\n          sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev libudev-dev patchelf xdg-utils\n\n      - name: Install Rust stable\n        uses: dtolnay/rust-toolchain@stable\n\n      - name: Rust cache\n        uses: swatinem/rust-cache@v2\n        with:\n          workspaces: target\n\n      - name: Sync node version and setup cache\n        uses: actions/setup-node@v4\n        with:\n          node-version: 20\n          cache: pnpm\n\n      - name: Install front-end dependencies\n        run: pnpm install --frozen-lockfile\n\n      - name: Build the app\n        uses: tauri-apps/tauri-action@v0\n        env:\n          CI: false\n          PLATFORM: ${{ matrix.platform }}\n          GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}\n          TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}\n          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}\n        with:\n          tagName: ${{ github.ref_name }}\n          releaseName: BongoCat ${{ needs.create-release.outputs.APP_VERSION }}\n          releaseBody: ''\n          releaseDraft: true\n          prerelease: false\n          args: --target ${{ matrix.target }}\n"
  },
  {
    "path": ".github/workflows/sync-to-gitee.yml",
    "content": "name: Sync Github Repos To Gitee\non:\n  push:\n    branches:\n      - master\n\njobs:\n  repo-sync:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Sync Github Repos To Gitee\n        uses: Yikun/hub-mirror-action@master\n        with:\n          src: github/ayangweb\n          dst: gitee/ayangweb\n          dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}\n          dst_token: ${{ secrets.GITEE_TOKEN }}\n          static_list: BongoCat\n          force_update: true\n"
  },
  {
    "path": ".github/workflows/upgradelink.yml",
    "content": "name: Upload Release to UpgradeLink\n\non:\n  release:\n    types: [published]\n  workflow_dispatch:\n\njobs:\n  upgradeLink-upload:\n    permissions:\n      contents: write\n    runs-on: ubuntu-latest\n    steps:\n      - name: Send a request to UpgradeLink\n        uses: toolsetlink/upgradelink-action@v5\n        with:\n          source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json'\n          access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}\n          tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }}\n          github-token: ${{ secrets.RELEASE_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\ntarget\n\n# Editor directories and files\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": ".release-it.ts",
    "content": "/* eslint-disable no-template-curly-in-string */\nimport type { Config } from 'release-it'\n\nexport default {\n  git: {\n    commitMessage: 'v${version}',\n    tagName: 'v${version}',\n  },\n  npm: {\n    publish: false,\n  },\n  hooks: {\n    'after:bump': 'tsx scripts/release.ts',\n  },\n} satisfies Config\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"tauri-apps.tauri-vscode\",\n    \"rust-lang.rust-analyzer\",\n    \"antfu.unocss\",\n    \"dbaeumer.vscode-eslint\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  // Disable the default formatter, use eslint instead\n  \"prettier.enable\": false,\n\n  \"eslint.format.enable\": true,\n\n  // Auto fix\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": \"explicit\",\n    \"source.organizeImports\": \"never\"\n  },\n\n  // Silent the stylistic rules in you IDE, but still auto fix them\n  \"eslint.rules.customizations\": [\n    { \"rule\": \"style/*\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"format/*\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*-indent\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*-spacing\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*-spaces\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*-order\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*-dangle\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*-newline\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*quotes\", \"severity\": \"off\", \"fixable\": true },\n    { \"rule\": \"*semi\", \"severity\": \"off\", \"fixable\": true }\n  ],\n\n  // Enable eslint for all supported languages\n  \"eslint.validate\": [\n    \"javascript\",\n    \"javascriptreact\",\n    \"typescript\",\n    \"typescriptreact\",\n    \"vue\",\n    \"html\",\n    \"markdown\",\n    \"json\",\n    \"json5\",\n    \"jsonc\",\n    \"yaml\",\n    \"toml\",\n    \"xml\",\n    \"gql\",\n    \"graphql\",\n    \"astro\",\n    \"svelte\",\n    \"css\",\n    \"less\",\n    \"scss\",\n    \"pcss\",\n    \"postcss\"\n  ],\n\n  \"typescript.enablePromptUseWorkspaceTsdk\": true,\n  \"typescript.tsdk\": \"./node_modules/typescript/lib\",\n\n  \"i18n-ally.localesPaths\": [\"src/locales\"],\n  \"i18n-ally.keystyle\": \"nested\",\n  \"i18n-ally.displayLanguage\": \"zh-CN\"\n}\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[workspace]\nresolver = \"2\"\nmembers = [ \"src-tauri\" ]\n\n[profile.release]\nstrip = true\nopt-level = 3\ncodegen-units = 1\npanic = \"abort\"\ndebug-assertions = false\noverflow-checks = false\nlto = true\n\n[workspace.dependencies]\ntauri = \"2\"\nserde = \"1\"\nserde_json = \"1\"\nfs_extra = \"1\"\ntauri-plugin = { version = \"2\", features = [ \"build\" ] }\ntauri-nspanel = { git = \"https://github.com/ahkohd/tauri-nspanel\", branch = \"v2\" }\ntauri-plugin-custom-window = { path = \"./src-tauri/src/plugins/window\" }\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 ayangweb\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "![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)\n\n<div align=\"center\">\n  <div>\n    <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>\n    <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>\n    <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>\n  </div>\n\n  <p>\n    <a href=\"./LICENSE\"><img src=\"https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square\" /></a>\n    <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>\n    <a href=\"https://github.com/ayangweb/BongoCat/releases\"><img src=\"https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square\"/></a>\n  </p>\n\n  <p>\n    <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>\n    <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>\n    <a href=\"https://hellogithub.com/repository/7d23863fd4be47b39e816193ded385c9\" target=\"_blank\">\n      <picture>\n        <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=dark\" />\n        <source media=\"(prefers-color-scheme: light)\" srcset=\"https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral\" />\n        <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\" />\n      </picture>\n    </a>\n  </p>\n</div>\n\n| macOS                                                                                        | Windows                                                                                        | Linux(x11)                                                                                   |\n| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |\n| ![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) |\n\n## 赞助商\n\n<a href=\"https://www.toolsetlink.com\">\n  <img height=\"54\" alt=\"UpgradeLink\" src=\"https://github.com/user-attachments/assets/6b84fb0f-3f1d-44b5-9932-2298bc999d8d\" />\n</a>\n\n## 开发背景\n\n本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱，但仅支持 Windows 平台。作为一名深度 macOS 用户，我特别希望在自己的设备上也能使用这款可爱的猫咪，于是我决定开发一个适配 macOS 的版本。\n\n同时，得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力，本项目不仅支持 macOS，还兼容 Windows 和 Linux(x11)，让更多的用户都能与这只可爱的猫咪互动！\n\n## 下载\n\n- [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1)\n- [GitHub Releases](https://github.com/ayangweb/BongoCat/releases)\n\n不确定下载哪一个？请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。\n\n## 功能介绍\n\n- 适配 macOS、Windows 和 Linux(x11)。\n- 根据键盘、鼠标或手柄的操作，同步对应的动作。\n- 支持导入自定义模型，自由打造专属猫咪形象。\n- 完全开源，代码公开透明，绝不收集任何用户数据。\n- 支持离线运行，无需联网，保护用户隐私。\n\n## 模型转换\n\n如果你想将 Bongo-Cat-Mver 应用中的模型转换为兼容 BongoCat 的格式，可以使用以下工具：\n\n🔗 [在线转换](https://bongocat.vteamer.cc)\n\n## 更多模型\n\n你可以在这个仓库中探索、下载更多猫咪模型，或提交你的创作，与大家一起分享：\n\n📦 [Awesome-BongoCat](https://github.com/ayangweb/Awesome-BongoCat)\n\n## 社区交流\n\n<table>\n  <thead>\n    <tr>\n      <th>QQ 群 1</th>\n      <th>QQ 群 2</th>\n    </tr>\n  </thead>\n  <tbody>\n    <tr>\n      <td>\n        <a href=\"https://qm.qq.com/q/AS3gNv2Vzy\">\n          <picture>\n            <source\n              media=\"(prefers-color-scheme: dark)\"\n              srcset=\"https://i0.hdslb.com/bfs/openplatform/8ecdc4982ab01b59d7731fcca3ec26631a274560.png\"\n            />\n            <source\n              media=\"(prefers-color-scheme: light)\"\n              srcset=\"https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png\"\n            />\n            <img\n              alt=\"QQ Group 1\"\n              src=\"https://i0.hdslb.com/bfs/openplatform/09f56580397063e1819c4c2ed63d07dee12720e1.png\"\n              height=\"250\"\n            />\n          </picture>\n        </a>\n      </td>\n      <td>\n        <a href=\"https://qm.qq.com/q/TmltLAod2O\">\n          <picture>\n            <source\n              media=\"(prefers-color-scheme: dark)\"\n              srcset=\"https://i0.hdslb.com/bfs/openplatform/473c522487ff33e0f32b15466aeb0734f17161c8.png\"\n            />\n            <source\n              media=\"(prefers-color-scheme: light)\"\n              srcset=\"https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png\"\n            />\n            <img\n              alt=\"QQ Group 2\"\n              src=\"https://i0.hdslb.com/bfs/openplatform/d5ae8c5af6ae1d0a1f066705ee822d1287384cf6.png\"\n              height=\"250\"\n            />\n          </picture>\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n\n## 赞赏\n\n每一份认可都值得被珍视！赞赏随缘，心意无价，谢谢你的支持 ❤️\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://i0.hdslb.com/bfs/openplatform/e7438bff14cdfb6bfd0feacbb482f99ea4093294.png\" />\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png\" />\n  <img alt=\"微信赞赏码\" src=\"https://i0.hdslb.com/bfs/openplatform/da55cc3ec1556580c91e59f589792866c998c7c6.png\" height=\"250\" />\n</picture>\n\n## 贡献指南\n\n感谢大家为 BongoCat 做出的宝贵贡献！如果你也希望为 BongoCat 做出贡献，请查阅[贡献指南](.github/CONTRIBUTING.md)。\n\n<a href=\"https://openomy.com/ayangweb/BongoCat\" target=\"_blank\" style=\"display: block; width: 100%;\" align=\"center\">\n  <img src=\"https://openomy.com/svg?repo=ayangweb/BongoCat&chart=bubble\" alt=\"Contribution Leaderboard\" style=\"display: block; width: 100%;\" />\n</a>\n\n## 历史星标\n\n<a href=\"https://www.star-history.com/#ayangweb/BongoCat&Date\">\n <picture>\n   <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date&theme=dark\" />\n   <source media=\"(prefers-color-scheme: light)\" srcset=\"https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date\" />\n   <img alt=\"Star History Chart\" src=\"https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date\" />\n </picture>\n</a>\n"
  },
  {
    "path": "eslint.config.ts",
    "content": "import antfu from '@antfu/eslint-config'\n\nexport default antfu({\n  formatters: true,\n  unocss: true,\n  rules: {\n    'antfu/if-newline': 'off',\n    'style/brace-style': ['error', '1tbs'],\n    'ts/no-use-before-define': 'off',\n    'unused-imports/no-unused-imports': 'error',\n    'perfectionist/sort-imports': 'off',\n    'import/order': [\n      'error',\n      {\n        'newlines-between': 'always',\n        'groups': ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'],\n        'alphabetize': {\n          order: 'asc',\n          caseInsensitive: true,\n        },\n      },\n    ],\n    'vue/attributes-order': ['error', { alphabetical: true }],\n    'vue/max-attributes-per-line': 'error',\n  },\n  ignores: ['**/*.toml'],\n})\n"
  },
  {
    "path": "index.html",
    "content": "<!doctype html>\n<html lang=\"zh\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>BongoCat</title>\n    <script src=\"/js/live2dcubismcore.min.js\"></script>\n    <script src=\"/js/live2d.min.js\"></script>\n  </head>\n\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.ts\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bongo-cat\",\n  \"type\": \"module\",\n  \"version\": \"0.9.0\",\n  \"private\": true,\n  \"author\": {\n    \"name\": \"ayangweb\",\n    \"email\": \"ayangweb@foxmail.com\"\n  },\n  \"scripts\": {\n    \"dev\": \"run-s build:icon dev:vite\",\n    \"build\": \"run-s build:*\",\n    \"dev:vite\": \"vite\",\n    \"build:vite\": \"vite build\",\n    \"build:icon\": \"tsx scripts/buildIcon.ts\",\n    \"preview\": \"vite preview\",\n    \"tauri\": \"tauri\",\n    \"lint\": \"eslint --fix src\",\n    \"preinstall\": \"npx only-allow pnpm\",\n    \"prepare\": \"simple-git-hooks\",\n    \"release\": \"release-it\"\n  },\n  \"dependencies\": {\n    \"@ant-design/icons-vue\": \"^7.0.1\",\n    \"@tauri-apps/api\": \"^2.5.0\",\n    \"@tauri-apps/plugin-autostart\": \"~2.3.0\",\n    \"@tauri-apps/plugin-clipboard-manager\": \"~2.2.2\",\n    \"@tauri-apps/plugin-dialog\": \"~2.2.2\",\n    \"@tauri-apps/plugin-fs\": \"~2.3.0\",\n    \"@tauri-apps/plugin-global-shortcut\": \"~2.2.1\",\n    \"@tauri-apps/plugin-log\": \"~2.3.1\",\n    \"@tauri-apps/plugin-opener\": \"~2.2.7\",\n    \"@tauri-apps/plugin-os\": \"^2.2.1\",\n    \"@tauri-apps/plugin-process\": \"^2.2.1\",\n    \"@tauri-apps/plugin-updater\": \"~2.7.1\",\n    \"@tauri-store/pinia\": \"^3.7.0\",\n    \"@vueuse/core\": \"^13.3.0\",\n    \"ant-design-vue\": \"^4.2.6\",\n    \"dayjs\": \"^1.11.13\",\n    \"es-toolkit\": \"^1.38.0\",\n    \"is-url\": \"^1.2.4\",\n    \"json5\": \"^2.2.3\",\n    \"nanoid\": \"^5.1.5\",\n    \"pinia\": \"^3.0.3\",\n    \"pixi-live2d-display\": \"^0.4.0\",\n    \"pixi.js\": \"^6.5.10\",\n    \"tauri-plugin-locale-api\": \"^2.0.1\",\n    \"tauri-plugin-macos-permissions-api\": \"^2.3.0\",\n    \"vue\": \"^3.5.16\",\n    \"vue-i18n\": \"^11.1.12\",\n    \"vue-markdown-render\": \"^2.2.1\",\n    \"vue-router\": \"^4.5.1\",\n    \"vue3-masonry-css\": \"^1.0.7\"\n  },\n  \"devDependencies\": {\n    \"@antfu/eslint-config\": \"^4.13.3\",\n    \"@commitlint/cli\": \"^19.8.1\",\n    \"@commitlint/config-conventional\": \"^19.8.1\",\n    \"@iconify-json/iconamoon\": \"^1.2.2\",\n    \"@iconify-json/solar\": \"^1.2.2\",\n    \"@tauri-apps/cli\": \"^2.5.0\",\n    \"@types/is-url\": \"^1.2.32\",\n    \"@types/node\": \"^22.15.29\",\n    \"@unocss/eslint-plugin\": \"^66.1.3\",\n    \"@vitejs/plugin-vue\": \"^5.2.4\",\n    \"eslint\": \"^9.28.0\",\n    \"eslint-plugin-format\": \"^1.0.1\",\n    \"lint-staged\": \"^15.5.2\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"release-it\": \"^18.1.2\",\n    \"sass\": \"^1.89.1\",\n    \"simple-git-hooks\": \"^2.13.0\",\n    \"tsx\": \"^4.19.4\",\n    \"typescript\": \"~5.6.3\",\n    \"unocss\": \"66.1.0-beta.7\",\n    \"vite\": \"^6.3.5\"\n  },\n  \"simple-git-hooks\": {\n    \"commit-msg\": \"npx --no-install commitlint -e\",\n    \"pre-commit\": \"npx lint-staged\"\n  },\n  \"lint-staged\": {\n    \"*\": \"eslint --fix\"\n  }\n}\n"
  },
  {
    "path": "scripts/buildIcon.ts",
    "content": "import { execSync } from 'node:child_process'\nimport { env, platform } from 'node:process'\n\n(() => {\n  const isMac = env.PLATFORM?.startsWith('macos') ?? platform === 'darwin'\n\n  const logoName = isMac ? 'logo-mac' : 'logo'\n\n  const command = `tauri icon src-tauri/assets/${logoName}.png`\n\n  execSync(command, { stdio: 'inherit' })\n})()\n"
  },
  {
    "path": "scripts/release.ts",
    "content": "import { readFileSync, writeFileSync } from 'node:fs'\nimport { dirname, resolve } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport { name, version } from '../package.json'\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\n(() => {\n  const tomlPath = resolve(__dirname, '..', 'src-tauri', 'Cargo.toml')\n  const lockPath = resolve(__dirname, '..', 'Cargo.lock')\n\n  for (const path of [tomlPath, lockPath]) {\n    let content = readFileSync(path, 'utf-8')\n\n    const regexp = new RegExp(\n      `(name\\\\s*=\\\\s*\"${name}\"\\\\s*version\\\\s*=\\\\s*)\"(\\\\d+\\\\.\\\\d+\\\\.\\\\d+(-\\\\w+\\\\.\\\\d+)?)\"`,\n    )\n\n    content = content.replace(regexp, `$1\"${version}\"`)\n\n    writeFileSync(path, content)\n  }\n})()\n"
  },
  {
    "path": "src/App.vue",
    "content": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { error } from '@tauri-apps/plugin-log'\nimport { openUrl } from '@tauri-apps/plugin-opener'\nimport { useEventListener } from '@vueuse/core'\nimport { ConfigProvider, theme } from 'ant-design-vue'\nimport { isString } from 'es-toolkit'\nimport isURL from 'is-url'\nimport { onMounted, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { RouterView } from 'vue-router'\n\nimport { useTauriListen } from './composables/useTauriListen'\nimport { useThemeVars } from './composables/useThemeVars'\nimport { useWindowState } from './composables/useWindowState'\nimport { LANGUAGE, LISTEN_KEY } from './constants'\nimport { getAntdLocale } from './locales/index.ts'\nimport { hideWindow, showWindow } from './plugins/window'\nimport { useAppStore } from './stores/app'\nimport { useCatStore } from './stores/cat'\nimport { useGeneralStore } from './stores/general'\nimport { useModelStore } from './stores/model'\nimport { useShortcutStore } from './stores/shortcut.ts'\n\nconst { generateColorVars } = useThemeVars()\nconst appStore = useAppStore()\nconst modelStore = useModelStore()\nconst catStore = useCatStore()\nconst generalStore = useGeneralStore()\nconst shortcutStore = useShortcutStore()\nconst appWindow = getCurrentWebviewWindow()\nconst { isRestored, restoreState } = useWindowState()\nconst { darkAlgorithm, defaultAlgorithm } = theme\nconst { locale } = useI18n()\n\nonMounted(async () => {\n  generateColorVars()\n\n  await appStore.$tauri.start()\n  await appStore.init()\n  await modelStore.$tauri.start()\n  await modelStore.init()\n  await catStore.$tauri.start()\n  catStore.init()\n  await generalStore.$tauri.start()\n  await generalStore.init()\n  await shortcutStore.$tauri.start()\n  await restoreState()\n})\n\nwatch(() => generalStore.appearance.language, (value) => {\n  locale.value = value ?? LANGUAGE.EN_US\n})\n\nuseTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {\n  if (appWindow.label !== payload) return\n\n  showWindow()\n})\n\nuseTauriListen(LISTEN_KEY.HIDE_WINDOW, ({ payload }) => {\n  if (appWindow.label !== payload) return\n\n  hideWindow()\n})\n\nuseEventListener('unhandledrejection', ({ reason }) => {\n  const message = isString(reason) ? reason : JSON.stringify(reason)\n\n  error(message)\n})\n\nuseEventListener('click', (event) => {\n  const link = (event.target as HTMLElement).closest('a')\n\n  if (!link) return\n\n  const { href, target } = link\n\n  if (target === '_blank') return\n\n  event.preventDefault()\n\n  if (!isURL(href)) return\n\n  openUrl(href)\n})\n</script>\n\n<template>\n  <ConfigProvider\n    :locale=\"getAntdLocale(generalStore.appearance.language)\"\n    :theme=\"{\n      algorithm: generalStore.appearance.isDark ? darkAlgorithm : defaultAlgorithm,\n    }\"\n  >\n    <RouterView v-if=\"isRestored\" />\n  </ConfigProvider>\n</template>\n"
  },
  {
    "path": "src/assets/css/global.scss",
    "content": "html {\n  --uno: select-none overscroll-none antialiased;\n\n  color-scheme: light;\n\n  body {\n    --uno: transition-opacity-300;\n  }\n\n  &.dark {\n    color-scheme: dark;\n  }\n\n  img {\n    -webkit-user-drag: none;\n  }\n\n  button {\n    outline: none !important;\n  }\n\n  .ant-card {\n    .ant-card-actions {\n      > li {\n        --uno: flex items-center justify-center;\n        > span {\n          --uno: inline-flex items-center justify-center min-w-unset;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/components/pro-list/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { Flex } from 'ant-design-vue'\n\nconst { title } = defineProps<{\n  title: string\n}>()\n</script>\n\n<template>\n  <Flex\n    class=\"not-last:mb-4\"\n    gap=\"small\"\n    vertical\n  >\n    <div\n      class=\"text-4 font-medium\"\n      data-tauri-drag-region\n    >\n      {{ title }}\n    </div>\n\n    <Flex\n      gap=\"middle\"\n      vertical\n    >\n      <slot />\n    </Flex>\n  </FLex>\n</template>\n"
  },
  {
    "path": "src/components/pro-list-item/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { Flex } from 'ant-design-vue'\nimport { computed, useSlots } from 'vue'\n\nconst { title, description, vertical } = defineProps<{\n  title: string\n  description?: string\n  vertical?: boolean\n}>()\n\nconst slots = useSlots()\n\nconst hasDescription = computed(() => {\n  return description || slots.description\n})\n</script>\n\n<template>\n  <Flex\n    :align=\"vertical ? void 0 : 'center'\"\n    class=\"b b-color-2 rounded-lg b-solid bg-color-3 p-4\"\n    :gap=\"vertical ? 'middle' : 'large'\"\n    justify=\"space-between\"\n    :vertical=\"vertical\"\n  >\n    <Flex\n      align=\"center\"\n      class=\"flex-1\"\n    >\n      <Flex vertical>\n        <div class=\"text-sm font-medium\">\n          {{ title }}\n        </div>\n\n        <div\n          class=\"break-all text-xs [&_a]:(active:text-color-primary-7 hover:text-color-primary-5 text-color-3) text-color-3\"\n          :class=\"{ 'mt-2': hasDescription }\"\n        >\n          <slot name=\"description\">\n            {{ description }}\n          </slot>\n        </div>\n      </Flex>\n    </Flex>\n\n    <slot />\n  </Flex>\n</template>\n"
  },
  {
    "path": "src/components/pro-shortcut/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Key } from '@/utils/keyboard'\n\nimport { find, map, remove, some, split } from 'es-toolkit/compat'\nimport { ref, useTemplateRef, watch } from 'vue'\n\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { keys, modifierKeys, standardKeys } from '@/utils/keyboard'\n\nconst props = defineProps<{\n  title: string\n  description?: string\n}>()\n\nconst modelValue = defineModel<string>()\nconst shortcutInputRef = useTemplateRef('shortcutInput')\nconst isFocusing = ref(false)\nconst isHovering = ref(false)\nconst pressedKeys = ref<Key[]>([])\n\nwatch(modelValue, () => {\n  parseModelValue()\n}, { immediate: true })\n\nfunction parseModelValue() {\n  if (!modelValue.value) {\n    return pressedKeys.value = []\n  }\n\n  pressedKeys.value = split(modelValue.value, '+').map((tauriKey) => {\n    return find(keys, { tauriKey })!\n  })\n}\n\nfunction getEventKey(event: KeyboardEvent) {\n  const { key, code } = event\n\n  const eventKey = key.replace('Meta', 'Command')\n\n  const isModifierKey = some(modifierKeys, { eventKey })\n\n  return isModifierKey ? eventKey : code\n}\n\nfunction isValidShortcut() {\n  if (pressedKeys.value?.[0]?.eventKey?.startsWith('F')) {\n    return true\n  }\n\n  const hasModifierKey = some(pressedKeys.value, ({ eventKey }) => {\n    return some(modifierKeys, { eventKey })\n  })\n  const hasStandardKey = some(pressedKeys.value, ({ eventKey }) => {\n    return some(standardKeys, { eventKey })\n  })\n\n  return hasModifierKey && hasStandardKey\n}\n\nfunction handleFocus() {\n  isFocusing.value = true\n\n  pressedKeys.value = []\n}\n\nfunction handleBlur() {\n  isFocusing.value = false\n\n  if (!isValidShortcut()) {\n    return parseModelValue()\n  }\n\n  modelValue.value = map(pressedKeys.value, 'tauriKey').join('+')\n}\n\nfunction handleKeyDown(event: KeyboardEvent) {\n  const eventKey = getEventKey(event)\n\n  const matched = find(keys, { eventKey })\n  const isInvalid = !matched\n  const isDuplicate = some(pressedKeys.value, { eventKey })\n\n  if (isInvalid || isDuplicate) return\n\n  pressedKeys.value.push(matched)\n\n  if (isValidShortcut()) {\n    shortcutInputRef.value?.blur()\n  }\n}\n\nfunction handleKeyUp(event: KeyboardEvent) {\n  remove(pressedKeys.value, { eventKey: getEventKey(event) })\n}\n</script>\n\n<template>\n  <ProListItem v-bind=\"props\">\n    <div\n      ref=\"shortcutInput\"\n      align=\"center\"\n      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)])\"\n      justify=\"center\"\n      :tabindex=\"0\"\n      @blur=\"handleBlur\"\n      @focus=\"handleFocus\"\n      @keydown=\"handleKeyDown\"\n      @keyup=\"handleKeyUp\"\n      @mouseout=\"isHovering = false\"\n      @mouseover=\"isHovering = true\"\n    >\n      <span v-if=\"pressedKeys.length === 0\">\n        {{ isFocusing ? $t('components.proShortcut.hints.pressRecordShortcut') : $t('components.proShortcut.hints.clickRecordShortcut') }}\n      </span>\n\n      <span class=\"text-primary font-bold\">\n        {{ map(pressedKeys, 'symbol').join(' ') }}\n      </span>\n\n      <div\n        class=\"i-iconamoon:close-circle-1 absolute right-2 cursor-pointer text-4 transition hover:text-primary\"\n        :hidden=\"isFocusing || !isHovering || pressedKeys.length === 0\"\n        @mousedown.prevent=\"modelValue = ''\"\n      />\n    </div>\n  </ProListItem>\n</template>\n"
  },
  {
    "path": "src/components/update-app/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Update } from '@tauri-apps/plugin-updater'\n\nimport { relaunch } from '@tauri-apps/plugin-process'\nimport { check } from '@tauri-apps/plugin-updater'\nimport { useIntervalFn } from '@vueuse/core'\nimport { Flex, message, Modal } from 'ant-design-vue'\nimport dayjs from 'dayjs'\nimport utc from 'dayjs/plugin/utc'\nimport { computed, reactive, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport VueMarkdown from 'vue-markdown-render'\n\nimport { useTauriListen } from '@/composables/useTauriListen'\nimport { GITHUB_LINK, LISTEN_KEY, UPGRADE_LINK_ACCESS_KEY } from '@/constants'\nimport { showWindow } from '@/plugins/window'\nimport { useGeneralStore } from '@/stores/general'\n\ndayjs.extend(utc)\n\ninterface State {\n  open: boolean\n  update?: Update\n  downloading: boolean\n  totalProgress?: number\n  downloadProgress: number\n}\n\nconst generalStore = useGeneralStore()\nconst state = reactive<State>({\n  open: false,\n  downloading: false,\n  downloadProgress: 0,\n})\nconst MESSAGE_KEY = 'updatable'\nconst { t } = useI18n()\n\nconst { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24)\n\nwatch(() => generalStore.update.autoCheck, (value) => {\n  pause()\n\n  if (!value) return\n\n  checkUpdate()\n\n  resume()\n}, { immediate: true })\n\nuseTauriListen<boolean>(LISTEN_KEY.UPDATE_APP, () => {\n  checkUpdate(true)\n\n  message.loading({\n    key: MESSAGE_KEY,\n    duration: 0,\n    content: t('components.updateApp.hints.checkingUpdates'),\n  })\n})\n\nconst downloadProgress = computed(() => {\n  const { downloadProgress, totalProgress } = state\n\n  if (!totalProgress) return '0%'\n\n  const progress = ((downloadProgress / totalProgress) * 100).toFixed(2)\n\n  return `${progress}%`\n})\n\nasync function checkUpdate(visibleMessage = false) {\n  try {\n    const update = await check({\n      timeout: 5000,\n      headers: {\n        'X-AccessKey': UPGRADE_LINK_ACCESS_KEY,\n      },\n    })\n\n    if (update) {\n      const { version, currentVersion, body = '', date, downloadAndInstall } = update\n\n      state.update = Object.assign(update, {\n        version: `v${version}`,\n        currentVersion: `v${currentVersion}`,\n        body: replaceBody(body),\n        date: dayjs.utc(date?.split('.')[0]).local().format('YYYY-MM-DD HH:mm:ss'),\n        downloadAndInstall: downloadAndInstall.bind(update),\n      })\n\n      showWindow()\n\n      state.open = true\n\n      message.destroy(MESSAGE_KEY)\n    } else if (visibleMessage) {\n      message.success({ key: MESSAGE_KEY, content: t('components.updateApp.hints.alreadyLatest') })\n    }\n  } catch (error) {\n    if (!visibleMessage) return\n\n    message.error({ key: MESSAGE_KEY, content: String(error) })\n  }\n}\n\nfunction replaceBody(body: string) {\n  return body\n    .replace(/&nbsp;/g, '')\n    .split('\\n')\n    .map(line => line.replace(/\\s*-\\s+by\\s+@.*/, ''))\n    .join('\\n')\n}\n\nasync function handleOk() {\n  try {\n    state.downloading = true\n\n    await state.update?.downloadAndInstall((progress) => {\n      switch (progress.event) {\n        case 'Started':\n          state.totalProgress = progress.data.contentLength ?? 0\n          break\n        case 'Progress':\n          state.downloadProgress += progress.data.chunkLength\n          break\n      }\n    })\n\n    relaunch()\n  } catch (error) {\n    message.error(String(error))\n  } finally {\n    Object.assign(state, {\n      downloading: false,\n      downloadProgress: 0,\n    })\n  }\n}\n</script>\n\n<template>\n  <Modal\n    v-model:open=\"state.open\"\n    :cancel-text=\"$t('components.updateApp.buttons.updateLater')\"\n    centered\n    :closable=\"false\"\n    :mask-closable=\"false\"\n    :title=\"$t('components.updateApp.title')\"\n    @ok=\"handleOk\"\n  >\n    <template #okText>\n      {{ state.downloading ? downloadProgress : $t('components.updateApp.buttons.updateNow') }}\n    </template>\n\n    <Flex\n      class=\"pt-1\"\n      gap=\"small\"\n      vertical\n    >\n      <Flex align=\"center\">\n        <span>{{ $t('components.updateApp.labels.updateVersion') }}</span>\n        <span>\n          <span>{{ state.update?.currentVersion }} 👉 </span>\n          <a\n            :href=\"`${GITHUB_LINK}/releases/tag/${state.update?.version}`\"\n          >\n            {{ state.update?.version }}\n          </a>\n        </span>\n      </Flex>\n\n      <Flex align=\"center\">\n        <span>{{ $t('components.updateApp.labels.updateTime') }}</span>\n        <span>{{ state.update?.date }}</span>\n      </Flex>\n\n      <Flex vertical>\n        <span>{{ $t('components.updateApp.labels.changelog') }}</span>\n\n        <VueMarkdown\n          class=\"update-note max-h-40 overflow-auto\"\n          :source=\"state.update?.body ?? ''\"\n        />\n      </Flex>\n    </Flex>\n  </Modal>\n</template>\n\n<style lang=\"scss\" scoped>\n.update-note {\n  :not(a) {\n    all: revert;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/composables/useDevice.ts",
    "content": "import { invoke } from '@tauri-apps/api/core'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { cursorPosition } from '@tauri-apps/api/window'\n\nimport { INVOKE_KEY, LISTEN_KEY } from '../constants'\n\nimport { useModel } from './useModel'\nimport { useTauriListen } from './useTauriListen'\n\nimport { useCatStore } from '@/stores/cat'\nimport { useModelStore } from '@/stores/model'\nimport { inBetween } from '@/utils/is'\nimport { isWindows } from '@/utils/platform'\n\ninterface MouseButtonEvent {\n  kind: 'MousePress' | 'MouseRelease'\n  value: string\n}\n\nexport interface CursorPoint {\n  x: number\n  y: number\n}\n\ninterface MouseMoveEvent {\n  kind: 'MouseMove'\n  value: CursorPoint\n}\n\ninterface KeyboardEvent {\n  kind: 'KeyboardPress' | 'KeyboardRelease'\n  value: string\n}\n\ntype DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent\n\nexport function useDevice() {\n  const modelStore = useModelStore()\n  const releaseTimers = new Map<string, NodeJS.Timeout>()\n  const catStore = useCatStore()\n  const { handlePress, handleRelease, handleMouseChange, handleMouseMove } = useModel()\n\n  const startListening = () => {\n    invoke(INVOKE_KEY.START_DEVICE_LISTENING)\n  }\n\n  const getSupportedKey = (key: string) => {\n    let nextKey = key\n\n    const unsupportedKey = !modelStore.supportKeys[nextKey]\n\n    if (key.startsWith('F') && unsupportedKey) {\n      nextKey = key.replace(/F(\\d+)/, 'Fn')\n    }\n\n    for (const item of ['Meta', 'Shift', 'Alt', 'Control']) {\n      if (key.startsWith(item) && unsupportedKey) {\n        const regex = new RegExp(`^(${item}).*`)\n        nextKey = key.replace(regex, '$1')\n      }\n    }\n\n    return nextKey\n  }\n\n  const handleCursorMove = async () => {\n    const cursorPoint = await cursorPosition()\n\n    handleMouseMove(cursorPoint)\n\n    if (catStore.window.hideOnHover) {\n      const appWindow = getCurrentWebviewWindow()\n      const position = await appWindow.outerPosition()\n      const { width, height } = await appWindow.innerSize()\n\n      const isInWindow = inBetween(cursorPoint.x, position.x, position.x + width)\n        && inBetween(cursorPoint.y, position.y, position.y + height)\n\n      document.body.style.setProperty('opacity', isInWindow ? '0' : 'unset')\n\n      if (!catStore.window.passThrough) {\n        appWindow.setIgnoreCursorEvents(isInWindow)\n      }\n    }\n  }\n\n  const handleAutoRelease = (key: string, delay = 100) => {\n    handlePress(key)\n\n    if (releaseTimers.has(key)) {\n      clearTimeout(releaseTimers.get(key))\n    }\n\n    const timer = setTimeout(() => {\n      handleRelease(key)\n\n      releaseTimers.delete(key)\n    }, delay)\n\n    releaseTimers.set(key, timer)\n  }\n\n  useTauriListen<DeviceEvent>(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => {\n    const { kind, value } = payload\n\n    if (kind === 'KeyboardPress' || kind === 'KeyboardRelease') {\n      const nextValue = getSupportedKey(value)\n\n      if (!nextValue) return\n\n      if (nextValue === 'CapsLock') {\n        return handleAutoRelease(nextValue)\n      }\n\n      if (kind === 'KeyboardPress') {\n        if (isWindows) {\n          const delay = catStore.model.autoReleaseDelay * 1000\n\n          return handleAutoRelease(nextValue, delay)\n        }\n\n        return handlePress(nextValue)\n      }\n\n      return handleRelease(nextValue)\n    }\n\n    switch (kind) {\n      case 'MousePress':\n        return handleMouseChange(value)\n      case 'MouseRelease':\n        return handleMouseChange(value, false)\n      case 'MouseMove':\n        return handleCursorMove()\n    }\n  })\n\n  return {\n    startListening,\n  }\n}\n"
  },
  {
    "path": "src/composables/useGamepad.ts",
    "content": "import type { LiteralUnion } from 'ant-design-vue/es/_util/type'\n\nimport { invoke } from '@tauri-apps/api/core'\nimport { computed, reactive, watch } from 'vue'\n\nimport { useModel } from './useModel'\nimport { useTauriListen } from './useTauriListen'\n\nimport { INVOKE_KEY, LISTEN_KEY } from '@/constants'\nimport { useModelStore } from '@/stores/model'\nimport live2d from '@/utils/live2d'\n\ntype GamepadEventName = LiteralUnion<'LeftStickX' | 'LeftStickY' | 'RightStickX' | 'RightStickY' | 'LeftThumb' | 'RightThumb'>\n\ninterface GamepadEvent {\n  kind: 'ButtonChanged' | 'AxisChanged'\n  name: GamepadEventName\n  value: number\n}\n\ninterface StickState {\n  x: number\n  y: number\n  moved: boolean\n  pressed: boolean\n}\n\ninterface Sticks {\n  left: StickState\n  right: StickState\n}\n\nconst INITIAL_STICK_STATE: StickState = { x: 0, y: 0, moved: false, pressed: false }\n\nexport function useGamepad() {\n  const { currentModel } = useModelStore()\n  const { handlePress, handleRelease, handleAxisChange } = useModel()\n  const sticks = reactive<Sticks>({\n    left: { ...INITIAL_STICK_STATE },\n    right: { ...INITIAL_STICK_STATE },\n  })\n\n  const stickActive = computed(() => ({\n    left: sticks.left.moved || sticks.left.pressed,\n    right: sticks.right.moved || sticks.right.pressed,\n  }))\n\n  watch(() => currentModel?.mode, (mode) => {\n    if (mode === 'gamepad') {\n      return invoke(INVOKE_KEY.START_GAMEPAD_LISTING)\n    }\n\n    invoke(INVOKE_KEY.STOP_GAMEPAD_LISTING)\n  }, { immediate: true })\n\n  watch(sticks.left, ({ x, y, moved, pressed }) => {\n    sticks.left.moved = x !== 0 || y !== 0\n\n    live2d.setParameterValue('CatParamStickShowLeftHand', moved || pressed)\n  }, { deep: true })\n\n  watch(sticks.right, ({ x, y, moved, pressed }) => {\n    sticks.right.moved = x !== 0 || y !== 0\n\n    live2d.setParameterValue('CatParamStickShowRightHand', moved || pressed)\n  }, { deep: true })\n\n  useTauriListen<GamepadEvent>(LISTEN_KEY.GAMEPAD_CHANGED, ({ payload }) => {\n    const { name, value } = payload\n\n    switch (name) {\n      case 'LeftStickX':\n        sticks.left.x = value\n\n        return handleAxisChange('CatParamStickLX', value)\n      case 'LeftStickY':\n        sticks.left.y = value\n\n        return handleAxisChange('CatParamStickLY', value)\n      case 'RightStickX':\n        sticks.right.x = value\n\n        return handleAxisChange('CatParamStickRX', value)\n      case 'RightStickY':\n        sticks.right.y = value\n\n        return handleAxisChange('CatParamStickRY', value)\n      case 'LeftThumb':\n        sticks.left.pressed = value !== 0\n\n        return live2d.setParameterValue('CatParamStickLeftDown', value !== 0)\n      case 'RightThumb':\n        sticks.right.pressed = value !== 0\n\n        return live2d.setParameterValue('CatParamStickRightDown', value !== 0)\n      default:\n        return value > 0 ? handlePress(name) : handleRelease(name)\n    }\n  })\n\n  return {\n    stickActive,\n  }\n}\n"
  },
  {
    "path": "src/composables/useModel.ts",
    "content": "import type { PhysicalPosition } from '@tauri-apps/api/dpi'\n\nimport { LogicalSize } from '@tauri-apps/api/dpi'\nimport { resolveResource, sep } from '@tauri-apps/api/path'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { message } from 'ant-design-vue'\nimport { isNil, round } from 'es-toolkit'\nimport { nth } from 'es-toolkit/compat'\nimport { ref } from 'vue'\n\nimport live2d from '../utils/live2d'\n\nimport { useCatStore } from '@/stores/cat'\nimport { useModelStore } from '@/stores/model'\nimport { getCursorMonitor } from '@/utils/monitor'\n\nconst appWindow = getCurrentWebviewWindow()\n\nexport interface ModelSize {\n  width: number\n  height: number\n}\n\nexport function useModel() {\n  const modelStore = useModelStore()\n  const catStore = useCatStore()\n  const modelSize = ref<ModelSize>()\n\n  async function handleLoad() {\n    try {\n      if (!modelStore.currentModel) return\n\n      const { path } = modelStore.currentModel\n\n      await resolveResource(path)\n\n      const { width, height, ...rest } = await live2d.load(path)\n\n      modelSize.value = { width, height }\n\n      handleResize()\n\n      Object.assign(modelStore, rest)\n    } catch (error) {\n      message.error(String(error))\n    }\n  }\n\n  function handleDestroy() {\n    live2d.destroy()\n  }\n\n  async function handleResize() {\n    if (!modelSize.value) return\n\n    live2d.resizeModel(modelSize.value)\n\n    const { width, height } = modelSize.value\n\n    if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) {\n      await appWindow.setSize(\n        new LogicalSize({\n          width: innerWidth,\n          height: Math.ceil(innerWidth * (height / width)),\n        }),\n      )\n    }\n\n    const size = await appWindow.size()\n\n    catStore.window.scale = round((size.width / width) * 100)\n  }\n\n  const handlePress = (key: string) => {\n    const path = modelStore.supportKeys[key]\n\n    if (!path) return\n\n    if (catStore.model.single) {\n      const dirName = nth(path.split(sep()), -2)!\n\n      const filterKeys = Object.entries(modelStore.pressedKeys).filter(([, value]) => {\n        return value.includes(dirName)\n      })\n\n      for (const [key] of filterKeys) {\n        handleRelease(key)\n      }\n    }\n\n    modelStore.pressedKeys[key] = path\n  }\n\n  const handleRelease = (key: string) => {\n    delete modelStore.pressedKeys[key]\n  }\n\n  function handleKeyChange(isLeft = true, pressed = true) {\n    const id = isLeft ? 'CatParamLeftHandDown' : 'CatParamRightHandDown'\n\n    live2d.setParameterValue(id, pressed)\n  }\n\n  function handleMouseChange(key: string, pressed = true) {\n    const id = key === 'Left' ? 'ParamMouseLeftDown' : 'ParamMouseRightDown'\n\n    live2d.setParameterValue(id, pressed)\n  }\n\n  async function handleMouseMove(cursorPoint: PhysicalPosition) {\n    const monitor = await getCursorMonitor(cursorPoint)\n\n    if (!monitor) return\n\n    const { size, position } = monitor\n\n    const xRatio = (cursorPoint.x - position.x) / size.width\n    const yRatio = (cursorPoint.y - position.y) / size.height\n\n    for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) {\n      const { min, max } = live2d.getParameterRange(id)\n\n      if (isNil(min) || isNil(max)) continue\n\n      const isXAxis = id.endsWith('X')\n\n      const ratio = isXAxis ? xRatio : yRatio\n      let value = max - (ratio * (max - min))\n\n      if (isXAxis && catStore.model.mouseMirror) {\n        value *= -1\n      }\n\n      live2d.setParameterValue(id, value)\n    }\n  }\n\n  async function handleAxisChange(id: string, value: number) {\n    const { min, max } = live2d.getParameterRange(id)\n\n    live2d.setParameterValue(id, Math.max(min, value * max))\n  }\n\n  return {\n    modelSize,\n    handlePress,\n    handleRelease,\n    handleLoad,\n    handleDestroy,\n    handleResize,\n    handleKeyChange,\n    handleMouseChange,\n    handleMouseMove,\n    handleAxisChange,\n  }\n}\n"
  },
  {
    "path": "src/composables/useSharedMenu.ts",
    "content": "import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'\nimport { range } from 'es-toolkit'\nimport { useI18n } from 'vue-i18n'\n\nimport { showWindow } from '@/plugins/window'\nimport { useCatStore } from '@/stores/cat'\nimport { isMac } from '@/utils/platform'\n\nexport function useSharedMenu() {\n  const catStore = useCatStore()\n  const { t } = useI18n()\n\n  const getScaleMenuItems = async () => {\n    const options = range(50, 151, 25)\n\n    const items = options.map((item) => {\n      return CheckMenuItem.new({\n        text: `${item}%`,\n        checked: catStore.window.scale === item,\n        action: () => {\n          catStore.window.scale = item\n        },\n      })\n    })\n\n    if (!options.includes(catStore.window.scale)) {\n      items.unshift(CheckMenuItem.new({\n        text: `${catStore.window.scale}%`,\n        checked: true,\n        enabled: false,\n      }))\n    }\n\n    return Promise.all(items)\n  }\n\n  const getOpacityMenuItems = async () => {\n    const options = range(25, 101, 25)\n\n    const items = options.map((item) => {\n      return CheckMenuItem.new({\n        text: `${item}%`,\n        checked: catStore.window.opacity === item,\n        action: () => {\n          catStore.window.opacity = item\n        },\n      })\n    })\n\n    if (!options.includes(catStore.window.opacity)) {\n      items.unshift(CheckMenuItem.new({\n        text: `${catStore.window.opacity}%`,\n        checked: true,\n        enabled: false,\n      }))\n    }\n\n    return Promise.all(items)\n  }\n\n  const getSharedMenu = async () => {\n    return await Promise.all([\n      MenuItem.new({\n        text: t('composables.useSharedMenu.labels.preference'),\n        accelerator: isMac ? 'Cmd+,' : '',\n        action: () => showWindow('preference'),\n      }),\n      MenuItem.new({\n        text: catStore.window.visible ? t('composables.useSharedMenu.labels.hideCat') : t('composables.useSharedMenu.labels.showCat'),\n        action: () => {\n          catStore.window.visible = !catStore.window.visible\n        },\n      }),\n      PredefinedMenuItem.new({ item: 'Separator' }),\n      CheckMenuItem.new({\n        text: t('composables.useSharedMenu.labels.passThrough'),\n        checked: catStore.window.passThrough,\n        action: () => {\n          catStore.window.passThrough = !catStore.window.passThrough\n        },\n      }),\n      Submenu.new({\n        text: t('composables.useSharedMenu.labels.windowSize'),\n        items: await getScaleMenuItems(),\n      }),\n      Submenu.new({\n        text: t('composables.useSharedMenu.labels.opacity'),\n        items: await getOpacityMenuItems(),\n      }),\n    ])\n  }\n\n  return {\n    getSharedMenu,\n  }\n}\n"
  },
  {
    "path": "src/composables/useTauriListen.ts",
    "content": "import { listen } from '@tauri-apps/api/event'\nimport { noop } from '@vueuse/core'\nimport { onMounted, onUnmounted, ref } from 'vue'\n\nexport function useTauriListen<T>(...args: Parameters<typeof listen<T>>) {\n  const unlisten = ref(noop)\n\n  onMounted(async () => {\n    unlisten.value = await listen<T>(...args)\n  })\n\n  onUnmounted(() => {\n    unlisten.value()\n  })\n}\n"
  },
  {
    "path": "src/composables/useTauriShortcut.ts",
    "content": "import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut'\nimport type { Ref } from 'vue'\n\nimport {\n  isRegistered,\n  register,\n  unregister,\n} from '@tauri-apps/plugin-global-shortcut'\nimport { ref, watch } from 'vue'\n\nexport function useTauriShortcut(shortcut: Ref<string, string>, callback: ShortcutHandler) {\n  const oldShortcut = ref(shortcut.value)\n\n  watch(shortcut, async (value) => {\n    if (oldShortcut.value) {\n      const registered = await isRegistered(oldShortcut.value)\n\n      if (registered) {\n        await unregister(oldShortcut.value)\n      }\n    }\n\n    if (!value) return\n\n    await register(value, (event) => {\n      if (event.state === 'Released') return\n\n      callback(event)\n    })\n\n    oldShortcut.value = value\n  }, { immediate: true })\n}\n"
  },
  {
    "path": "src/composables/useThemeVars.ts",
    "content": "import { theme } from 'ant-design-vue'\nimport { kebabCase } from 'es-toolkit'\n\nexport function useThemeVars() {\n  const { defaultAlgorithm, darkAlgorithm, defaultConfig } = theme\n\n  const generateColorVars = () => {\n    const { token } = defaultConfig\n\n    const colors = [\n      defaultAlgorithm(token),\n      darkAlgorithm(token),\n    ]\n\n    for (const [index, item] of colors.entries()) {\n      const isDark = index !== 0\n      const vars: Record<string, any> = {}\n\n      for (const [key, value] of Object.entries(item)) {\n        vars[`--ant-${kebabCase(key)}`] = value\n      }\n\n      const style = document.createElement('style')\n      style.dataset.theme = isDark ? 'dark' : 'light'\n      const selector = isDark ? 'html.dark' : ':root'\n      const values = Object.entries(vars).map(([key, value]) => `${key}: ${value};`)\n\n      style.innerHTML = `${selector}{\\n${values.join('\\n')}\\n}`\n      document.head.appendChild(style)\n    }\n  }\n\n  return {\n    generateColorVars,\n  }\n}\n"
  },
  {
    "path": "src/composables/useTray.ts",
    "content": "import type { TrayIconOptions } from '@tauri-apps/api/tray'\n\nimport { getName, getVersion } from '@tauri-apps/api/app'\nimport { emit } from '@tauri-apps/api/event'\nimport { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'\nimport { resolveResource } from '@tauri-apps/api/path'\nimport { TrayIcon } from '@tauri-apps/api/tray'\nimport { openUrl } from '@tauri-apps/plugin-opener'\nimport { exit, relaunch } from '@tauri-apps/plugin-process'\nimport { watchDebounced } from '@vueuse/core'\nimport { watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { GITHUB_LINK, LISTEN_KEY } from '../constants'\nimport { showWindow } from '../plugins/window'\nimport { isMac } from '../utils/platform'\n\nimport { useSharedMenu } from './useSharedMenu'\n\nimport { useCatStore } from '@/stores/cat'\nimport { useGeneralStore } from '@/stores/general'\n\nconst TRAY_ID = 'BONGO_CAT_TRAY'\n\nexport function useTray() {\n  const catStore = useCatStore()\n  const generalStore = useGeneralStore()\n  const { getSharedMenu } = useSharedMenu()\n  const { t } = useI18n()\n\n  watch([() => catStore.window.visible, () => catStore.window.passThrough, () => generalStore.appearance.language], () => {\n    updateTrayMenu()\n  })\n\n  watchDebounced([() => catStore.window.scale, () => catStore.window.opacity], () => {\n    updateTrayMenu()\n  }, { debounce: 200 })\n\n  const createTray = async () => {\n    const tray = await getTrayById()\n\n    if (tray) return\n\n    const appName = await getName()\n    const appVersion = await getVersion()\n\n    const menu = await getTrayMenu()\n\n    const path = isMac ? 'assets/tray-mac.png' : 'assets/tray.png'\n    const icon = await resolveResource(path)\n\n    const options: TrayIconOptions = {\n      menu,\n      icon,\n      id: TRAY_ID,\n      tooltip: `${appName} v${appVersion}`,\n      iconAsTemplate: true,\n      menuOnLeftClick: true,\n    }\n\n    return TrayIcon.new(options)\n  }\n\n  const getTrayById = () => {\n    return TrayIcon.getById(TRAY_ID)\n  }\n\n  const getTrayMenu = async () => {\n    const appVersion = await getVersion()\n\n    const items = await Promise.all([\n      ...await getSharedMenu(),\n      PredefinedMenuItem.new({ item: 'Separator' }),\n      MenuItem.new({\n        text: t('composables.useTray.checkUpdate'),\n        action: () => {\n          showWindow()\n\n          emit(LISTEN_KEY.UPDATE_APP)\n        },\n      }),\n      MenuItem.new({\n        text: t('composables.useTray.openSource'),\n        action: () => openUrl(GITHUB_LINK),\n      }),\n      PredefinedMenuItem.new({ item: 'Separator' }),\n      MenuItem.new({\n        text: `v${appVersion}`,\n        enabled: false,\n      }),\n      MenuItem.new({\n        text: t('composables.useTray.restartApp'),\n        action: relaunch,\n      }),\n      MenuItem.new({\n        text: t('composables.useTray.quitApp'),\n        accelerator: isMac ? 'Cmd+Q' : '',\n        action: () => exit(0),\n      }),\n    ])\n\n    return Menu.new({ items })\n  }\n\n  const updateTrayMenu = async () => {\n    const tray = await getTrayById()\n\n    if (!tray) return\n\n    const menu = await getTrayMenu()\n\n    tray.setMenu(menu)\n  }\n\n  return {\n    createTray,\n  }\n}\n"
  },
  {
    "path": "src/composables/useWindowPosition.ts",
    "content": "import { PhysicalPosition } from '@tauri-apps/api/dpi'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { onMounted, ref, watch } from 'vue'\n\nimport { useCatStore } from '@/stores/cat'\nimport { getCursorMonitor } from '@/utils/monitor'\n\nconst appWindow = getCurrentWebviewWindow()\n\nexport function useWindowPosition() {\n  const catStore = useCatStore()\n  const isMounted = ref(false)\n\n  const setWindowPosition = async () => {\n    const monitor = await getCursorMonitor()\n\n    if (!monitor) return\n\n    const windowSize = await appWindow.outerSize()\n\n    switch (catStore.window.position) {\n      case 'topLeft':\n        return appWindow.setPosition(new PhysicalPosition(0, 0))\n      case 'topRight':\n        return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, 0))\n      case 'bottomLeft':\n        return appWindow.setPosition(new PhysicalPosition(0, monitor.size.height - windowSize.height))\n      default:\n        return appWindow.setPosition(new PhysicalPosition(monitor.size.width - windowSize.width, monitor.size.height - windowSize.height))\n    }\n  }\n\n  onMounted(async () => {\n    await setWindowPosition()\n\n    isMounted.value = true\n\n    appWindow.onScaleChanged(setWindowPosition)\n  })\n\n  watch(() => catStore.window.position, setWindowPosition)\n\n  return {\n    isMounted,\n    setWindowPosition,\n  }\n}\n"
  },
  {
    "path": "src/composables/useWindowState.ts",
    "content": "import type { Event } from '@tauri-apps/api/event'\n\nimport { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { availableMonitors } from '@tauri-apps/api/window'\nimport { isNumber } from 'es-toolkit/compat'\nimport { onMounted, ref } from 'vue'\n\nimport { useAppStore } from '@/stores/app'\n\nexport type WindowState = Record<string, Partial<PhysicalPosition & PhysicalSize> | undefined>\n\nconst appWindow = getCurrentWebviewWindow()\nconst { label } = appWindow\n\nexport function useWindowState() {\n  const appStore = useAppStore()\n  const isRestored = ref(false)\n\n  onMounted(() => {\n    appWindow.onMoved(onChange)\n\n    appWindow.onResized(onChange)\n  })\n\n  const onChange = async (event: Event<PhysicalPosition | PhysicalSize>) => {\n    const minimized = await appWindow.isMinimized()\n\n    if (minimized) return\n\n    appStore.windowState[label] ??= {}\n\n    Object.assign(appStore.windowState[label], event.payload)\n  }\n\n  const restoreState = async () => {\n    const { x, y, width, height } = appStore.windowState[label] ?? {}\n\n    if (isNumber(x) && isNumber(y)) {\n      const monitors = await availableMonitors()\n\n      const monitor = monitors.find((monitor) => {\n        const { position, size } = monitor\n\n        const inBoundsX = x >= position.x && x <= position.x + size.width\n        const inBoundsY = y >= position.y && y <= position.y + size.height\n\n        return inBoundsX && inBoundsY\n      })\n\n      if (monitor) {\n        await appWindow.setPosition(new PhysicalPosition(x, y))\n      }\n    }\n\n    if (width && height) {\n      await appWindow.setSize(new PhysicalSize(width, height))\n    }\n\n    isRestored.value = true\n  }\n\n  return {\n    isRestored,\n    restoreState,\n  }\n}\n"
  },
  {
    "path": "src/constants/index.ts",
    "content": "export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'\n\nexport const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'\n\nexport const LISTEN_KEY = {\n  SHOW_WINDOW: 'show-window',\n  HIDE_WINDOW: 'hide-window',\n  DEVICE_CHANGED: 'device-changed',\n  UPDATE_APP: 'update-app',\n  GAMEPAD_CHANGED: 'gamepad-changed',\n}\n\nexport const INVOKE_KEY = {\n  COPY_DIR: 'copy_dir',\n  START_DEVICE_LISTENING: 'start_device_listening',\n  START_GAMEPAD_LISTING: 'start_gamepad_listing',\n  STOP_GAMEPAD_LISTING: 'stop_gamepad_listing',\n}\n\nexport const LANGUAGE = {\n  ZH_CN: 'zh-CN',\n  EN_US: 'en-US',\n  VI_VN: 'vi-VN',\n  PT_BR: 'pt-BR',\n} as const\n"
  },
  {
    "path": "src/locales/en-US.json",
    "content": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"Redrawing...\"\n      }\n    },\n    \"preference\": {\n      \"title\": \"Preferences\",\n      \"cat\": {\n        \"title\": \"Cat\",\n        \"labels\": {\n          \"modelSettings\": \"Model Settings\",\n          \"mirrorMode\": \"Mirror Mode\",\n          \"singleMode\": \"Single Key Mode\",\n          \"mouseMirror\": \"Mouse Mirror\",\n          \"windowSettings\": \"Window Settings\",\n          \"passThrough\": \"Pass Through\",\n          \"alwaysOnTop\": \"Always on Top\",\n          \"windowSize\": \"Window Size\",\n          \"windowRadius\": \"Window Radius\",\n          \"opacity\": \"Opacity\",\n          \"autoReleaseDelay\": \"Auto Release Delay\",\n          \"hideOnHover\": \"Hide on Hover\",\n          \"position\": \"Window Position\"\n        },\n        \"hints\": {\n          \"mirrorMode\": \"When enabled, the model will be mirrored horizontally.\",\n          \"singleMode\": \"When enabled, only the last pressed key is displayed for each hand.\",\n          \"mouseMirror\": \"When enabled, the mouse will mirror the hand movement.\",\n          \"passThrough\": \"When enabled, clicks pass through the window without affecting it.\",\n          \"alwaysOnTop\": \"When enabled, the window stays above all other windows.\",\n          \"windowSize\": \"Move mouse to window edge, or hold Shift and right-drag to resize.\",\n          \"autoReleaseDelay\": \"On Windows, some system keys cannot capture release events and will auto-release after timeout.\",\n          \"hideOnHover\": \"When enabled, the window hides when mouse hovers over it.\",\n          \"position\": \"Takes effect after the app starts, or when this parameter, window size, model, or screen resolution changes.\"\n        },\n        \"options\": {\n          \"topLeft\": \"Top Left\",\n          \"topRight\": \"Top Right\",\n          \"bottomLeft\": \"Bottom Left\",\n          \"bottomRight\": \"Bottom Right\"\n        }\n      },\n      \"general\": {\n        \"title\": \"General\",\n        \"labels\": {\n          \"appSettings\": \"Application Settings\",\n          \"launchOnStartup\": \"Launch on Startup\",\n          \"showTaskbarIcon\": \"Show Taskbar Icon\",\n          \"appearanceSettings\": \"Appearance Settings\",\n          \"themeMode\": \"Theme Mode\",\n          \"language\": \"Language\",\n          \"updateSettings\": \"Update Settings\",\n          \"autoCheckUpdate\": \"Auto Check for Updates\",\n          \"permissionsSettings\": \"Permissions Settings\",\n          \"inputMonitoringPermission\": \"Input Monitoring Permission\"\n        },\n        \"options\": {\n          \"auto\": \"System\",\n          \"lightMode\": \"Light\",\n          \"darkMode\": \"Dark\"\n        },\n        \"hints\": {\n          \"showTaskbarIcon\": \"When enabled, the window can be captured via OBS Studio.\",\n          \"inputMonitoringPermission\": \"Enable input monitoring to receive keyboard and mouse events from the system.\",\n          \"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.\"\n        },\n        \"status\": {\n          \"authorized\": \"Authorized\",\n          \"authorize\": \"Go to Enable\"\n        },\n        \"buttons\": {\n          \"openNow\": \"Open Now\",\n          \"openLater\": \"Open Later\"\n        }\n      },\n      \"model\": {\n        \"title\": \"Model\",\n        \"labels\": {\n          \"deleteModel\": \"Delete Model\"\n        },\n        \"hints\": {\n          \"deleteSuccess\": \"Deleted Successfully\",\n          \"deleteModel\": \"Are you sure you want to delete this model?\",\n          \"importSuccess\": \"Imported Successfully\",\n          \"clickOrDragToImport\": \"Click or drag here to import\"\n        },\n        \"tooltips\": {\n          \"createModel\": \"Create Model\",\n          \"convertModel\": \"Convert Model\",\n          \"moreModels\": \"More Models\"\n        }\n      },\n      \"shortcut\": {\n        \"title\": \"Shortcuts\",\n        \"labels\": {\n          \"toggleCat\": \"Toggle Cat\",\n          \"togglePreferences\": \"Toggle Preferences\",\n          \"mirrorMode\": \"Mirror Mode\",\n          \"passThrough\": \"Pass Through\",\n          \"alwaysOnTop\": \"Always on Top\"\n        },\n        \"hints\": {\n          \"toggleCat\": \"Toggle the visibility of the cat window.\",\n          \"togglePreferences\": \"Toggle the visibility of the preferences window.\",\n          \"mirrorMode\": \"Toggle the cat's mirror mode.\",\n          \"passThrough\": \"Toggle whether the cat window is pass-through.\",\n          \"alwaysOnTop\": \"Toggle whether the cat window stays on top.\"\n        }\n      },\n      \"about\": {\n        \"title\": \"About\",\n        \"labels\": {\n          \"aboutApp\": \"About App\",\n          \"appLog\": \"App Logs\",\n          \"appInfo\": \"App Info\",\n          \"openSource\": \"Open Source\"\n        },\n        \"hints\": {\n          \"appInfo\": \"Copy app information and provide it to bug issue.\",\n          \"copySuccess\": \"Copied Successfully\"\n        },\n        \"buttons\": {\n          \"checkUpdate\": \"Check for Updates\",\n          \"copy\": \"Copy\",\n          \"feedbackIssues\": \"Feedback Issues\",\n          \"viewLog\": \"View Logs\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"proShortcut\": {\n      \"hints\": {\n        \"pressRecordShortcut\": \"Press to record shortcut\",\n        \"clickRecordShortcut\": \"Click to record shortcut\"\n      }\n    },\n    \"updateApp\": {\n      \"title\": \"New Version Found 🥳\",\n      \"labels\": {\n        \"updateVersion\": \"Update Version: \",\n        \"updateTime\": \"Update Time: \",\n        \"changelog\": \"Changelog: \"\n      },\n      \"hints\": {\n        \"checkingUpdates\": \"Checking for updates...\",\n        \"alreadyLatest\": \"Already on the latest version 🎉\"\n      },\n      \"buttons\": {\n        \"updateNow\": \"Update Now\",\n        \"updateLater\": \"Update Later\"\n      }\n    }\n  },\n  \"composables\": {\n    \"useSharedMenu\": {\n      \"labels\": {\n        \"preference\": \"Preferences...\",\n        \"hideCat\": \"Hide Cat\",\n        \"showCat\": \"Show Cat\",\n        \"passThrough\": \"Pass Through\",\n        \"windowSize\": \"Window Size\",\n        \"opacity\": \"Opacity\"\n      }\n    },\n    \"useTray\": {\n      \"checkUpdate\": \"Check for Updates\",\n      \"openSource\": \"Open Source\",\n      \"restartApp\": \"Restart App\",\n      \"quitApp\": \"Quit App\"\n    }\n  },\n  \"utils\": {\n    \"live2d\": {\n      \"hints\": {\n        \"notFound\": \"Model master configuration file not found, please ensure the model files are complete.\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/index.ts",
    "content": "import type { Language } from '@/stores/general'\nimport type { Locale as AntdLocale } from 'ant-design-vue/es/locale'\n\nimport antdEnUS from 'ant-design-vue/locale/en_US'\nimport antdPtBR from 'ant-design-vue/locale/pt_BR'\nimport antdViVN from 'ant-design-vue/locale/vi_VN'\nimport antdZhCN from 'ant-design-vue/locale/zh_CN'\nimport { createI18n } from 'vue-i18n'\n\nimport enUS from './en-US.json'\nimport ptBR from './pt-BR.json'\nimport viVN from './vi-VN.json'\nimport zhCN from './zh-CN.json'\n\nimport { LANGUAGE } from '@/constants'\n\nexport const i18n = createI18n({\n  legacy: false,\n  locale: LANGUAGE.EN_US,\n  fallbackLocale: LANGUAGE.EN_US,\n  messages: {\n    [LANGUAGE.ZH_CN]: zhCN,\n    [LANGUAGE.EN_US]: enUS,\n    [LANGUAGE.VI_VN]: viVN,\n    [LANGUAGE.PT_BR]: ptBR,\n  },\n})\n\nexport function getAntdLocale(language: Language = LANGUAGE.EN_US) {\n  const antdLanguage: Record<Language, AntdLocale> = {\n    [LANGUAGE.ZH_CN]: antdZhCN,\n    [LANGUAGE.EN_US]: antdEnUS,\n    [LANGUAGE.VI_VN]: antdViVN,\n    [LANGUAGE.PT_BR]: antdPtBR,\n  }\n\n  return antdLanguage[language]\n}\n"
  },
  {
    "path": "src/locales/pt-BR.json",
    "content": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"Redimensionando...\"\n      }\n    },\n    \"preference\": {\n      \"title\": \"Preferências\",\n      \"cat\": {\n        \"title\": \"Gato\",\n        \"labels\": {\n          \"modelSettings\": \"Configurações do Modelo\",\n          \"mirrorMode\": \"Modo Espelho\",\n          \"singleMode\": \"Mostrar Apenas Última Tecla\",\n          \"mouseMirror\": \"Espelho do Mouse\",\n          \"windowSettings\": \"Configurações da Janela\",\n          \"passThrough\": \"Janela Transparente\",\n          \"alwaysOnTop\": \"Sempre no Topo\",\n          \"windowSize\": \"Tamanho da Janela\",\n          \"windowRadius\": \"Raio da Janela\",\n          \"opacity\": \"Opacidade\",\n          \"autoReleaseDelay\": \"Atraso de Liberação Automática\",\n          \"hideOnHover\": \"Ocultar ao Passar o Mouse\",\n          \"position\": \"Posição da Janela\"\n        },\n        \"hints\": {\n          \"mirrorMode\": \"Quando ativado, o modelo será invertido horizontalmente.\",\n          \"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).\",\n          \"mouseMirror\": \"Quando ativado, o mouse espelhará o movimento da mão.\",\n          \"passThrough\": \"Quando ativado, a janela não afetará operações em outros aplicativos.\",\n          \"alwaysOnTop\": \"Quando ativado, a janela sempre ficará acima de outros aplicativos.\",\n          \"windowSize\": \"Mova o mouse para a borda da janela ou segure Shift e arraste com o botão direito para redimensionar.\",\n          \"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.\",\n          \"hideOnHover\": \"Quando ativado, a janela será ocultada quando o mouse passar sobre ela.\",\n          \"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.\"\n        },\n        \"options\": {\n          \"topLeft\": \"Canto Superior Esquerdo\",\n          \"topRight\": \"Canto Superior Direito\",\n          \"bottomLeft\": \"Canto Inferior Esquerdo\",\n          \"bottomRight\": \"Canto Inferior Direito\"\n        }\n      },\n      \"general\": {\n        \"title\": \"Geral\",\n        \"labels\": {\n          \"appSettings\": \"Configurações do aplicativo\",\n          \"launchOnStartup\": \"Iniciar na inicialização\",\n          \"showTaskbarIcon\": \"Mostrar ícone na barra de tarefas\",\n          \"appearanceSettings\": \"Configurações de aparência\",\n          \"themeMode\": \"Tema\",\n          \"language\": \"Idiomas\",\n          \"updateSettings\": \"Configurações de atualização\",\n          \"autoCheckUpdate\": \"Verificar atualizações automaticamente\",\n          \"permissionsSettings\": \"Configurações de Permissões\",\n          \"inputMonitoringPermission\": \"Permissão de Monitoramento de Entrada\"\n        },\n        \"options\": {\n          \"auto\": \"Sistema\",\n          \"lightMode\": \"Claro\",\n          \"darkMode\": \"Escuro\"\n        },\n        \"hints\": {\n          \"showTaskbarIcon\": \"Uma vez ativado, você pode capturar a janela via OBS Studio.\",\n          \"inputMonitoringPermission\": \"Ative a permissão de monitoramento de entrada para receber eventos de teclado e mouse do sistema para responder às suas ações.\",\n          \"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.\"\n        },\n        \"status\": {\n          \"authorized\": \"Autorizado\",\n          \"authorize\": \"Ir para Ativar\"\n        },\n        \"buttons\": {\n          \"openNow\": \"Abrir Agora\",\n          \"openLater\": \"Abrir Mais Tarde\"\n        }\n      },\n      \"model\": {\n        \"title\": \"Modelo\",\n        \"labels\": {\n          \"deleteModel\": \"Excluir modelo\"\n        },\n        \"hints\": {\n          \"deleteSuccess\": \"Excluído com sucesso\",\n          \"deleteModel\": \"Tem certeza de que deseja excluir este modelo?\",\n          \"importSuccess\": \"Importação bem-sucedida\",\n          \"clickOrDragToImport\": \"Clique ou arraste para importar\"\n        },\n        \"tooltips\": {\n          \"createModel\": \"Criar modelo\",\n          \"convertModel\": \"Converter modelo\",\n          \"moreModels\": \"Mais modelos\"\n        }\n      },\n      \"shortcut\": {\n        \"title\": \"Atalhos\",\n        \"labels\": {\n          \"toggleCat\": \"Mostrar/Ocultar Gato\",\n          \"togglePreferences\": \"Abrir Preferências\",\n          \"mirrorMode\": \"Modo Espelho\",\n          \"passThrough\": \"Janela Transparente\",\n          \"alwaysOnTop\": \"Sempre no Topo\"\n        },\n        \"hints\": {\n          \"toggleCat\": \"Alternar a visibilidade da janela do gato.\",\n          \"togglePreferences\": \"Alternar a visibilidade da janela de preferências.\",\n          \"mirrorMode\": \"Alternar o modo espelho do gato.\",\n          \"passThrough\": \"Alternar se a janela do gato é clicável.\",\n          \"alwaysOnTop\": \"Alternar se a janela do gato permanece no topo.\"\n        }\n      },\n      \"about\": {\n        \"title\": \"Sobre\",\n        \"labels\": {\n          \"aboutApp\": \"Sobre o Aplicativo\",\n          \"appLog\": \"Logs do Aplicativo\",\n          \"appInfo\": \"Informações do Aplicativo\",\n          \"openSource\": \"Código Aberto\"\n        },\n        \"hints\": {\n          \"appInfo\": \"Copiar informações do aplicativo para incluir em relatórios de bugs.\",\n          \"copySuccess\": \"Copiado com sucesso\"\n        },\n        \"buttons\": {\n          \"checkUpdate\": \"Verificar atualizações\",\n          \"copy\": \"Copiar\",\n          \"feedbackIssues\": \"Reportar Problema\",\n          \"viewLog\": \"Ver Logs\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"proShortcut\": {\n      \"hints\": {\n        \"pressRecordShortcut\": \"Pressione as teclas para gravar atalho\",\n        \"clickRecordShortcut\": \"Clique para gravar atalho\"\n      }\n    },\n    \"updateApp\": {\n      \"title\": \"Nova versão encontrada 🥳\",\n      \"labels\": {\n        \"updateVersion\": \"Versão: \",\n        \"updateTime\": \"Hora da atualização: \",\n        \"changelog\": \"Registro de alterações: \"\n      },\n      \"hints\": {\n        \"checkingUpdates\": \"Verificando atualizações...\",\n        \"alreadyLatest\": \"Você já está na versão mais recente 🎉\"\n      },\n      \"buttons\": {\n        \"updateNow\": \"Atualizar Agora\",\n        \"updateLater\": \"Atualizar mais tarde\"\n      }\n    }\n  },\n  \"composables\": {\n    \"useSharedMenu\": {\n      \"labels\": {\n        \"preference\": \"Preferências...\",\n        \"hideCat\": \"Ocultar Gato\",\n        \"showCat\": \"Mostrar Gato\",\n        \"passThrough\": \"Janela Transparente\",\n        \"windowSize\": \"Tamanho da Janela\",\n        \"opacity\": \"Opacidade\"\n      }\n    },\n    \"useTray\": {\n      \"checkUpdate\": \"Verificar atualizações\",\n      \"openSource\": \"Código Fonte\",\n      \"restartApp\": \"Reiniciar\",\n      \"quitApp\": \"Sair\"\n    }\n  },\n  \"utils\": {\n    \"live2d\": {\n      \"hints\": {\n        \"notFound\": \"Arquivo de configuração principal do modelo não encontrado. Verifique se os arquivos do modelo estão completos.\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/vi-VN.json",
    "content": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"Đang đổi kích thước...\"\n      }\n    },\n    \"preference\": {\n      \"title\": \"Tùy chỉnh\",\n      \"cat\": {\n        \"title\": \"Mèo\",\n        \"labels\": {\n          \"modelSettings\": \"Cài đặt Mô hình\",\n          \"mirrorMode\": \"Chế độ gương\",\n          \"singleMode\": \"Chỉ hiển thị phím cuối cùng\",\n          \"mouseMirror\": \"Phản chiếu chuột\",\n          \"windowSettings\": \"Cài đặt Cửa sổ\",\n          \"passThrough\": \"Click xuyên\",\n          \"alwaysOnTop\": \"Luôn trên cùng\",\n          \"windowSize\": \"Kích thước\",\n          \"windowRadius\": \"Độ bo tròn cửa sổ\",\n          \"opacity\": \"Độ mờ\",\n          \"autoReleaseDelay\": \"Độ trễ tự động nhả phím\",\n          \"hideOnHover\": \"Ẩn khi di chuột\",\n          \"position\": \"Vị trí cửa sổ\"\n        },\n        \"hints\": {\n          \"mirrorMode\": \"Bật để lật ngang mô hình.\",\n          \"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).\",\n          \"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ế.\",\n          \"passThrough\": \"Bật để cửa sổ không ảnh hưởng đến thao tác trên ứng dụng khác.\",\n          \"alwaysOnTop\": \"Bật để cửa sổ luôn nằm trên ứng dụng khác.\",\n          \"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.\",\n          \"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ờ.\",\n          \"hideOnHover\": \"Khi bật, cửa sổ sẽ ẩn khi chuột di chuyển vào.\",\n          \"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.\"\n        },\n        \"options\": {\n          \"topLeft\": \"Góc trên cùng bên trái\",\n          \"topRight\": \"Góc trên cùng bên phải\",\n          \"bottomLeft\": \"Góc dưới cùng bên trái\",\n          \"bottomRight\": \"Góc dưới cùng bên phải\"\n        }\n      },\n      \"general\": {\n        \"title\": \"Chung\",\n        \"labels\": {\n          \"appSettings\": \"Cài đặt ứng dụng\",\n          \"launchOnStartup\": \"Khởi động cùng hệ thống\",\n          \"showTaskbarIcon\": \"Hiện biểu tượng trên thanh tác vụ (icon taskbar)\",\n          \"appearanceSettings\": \"Cài đặt giao diện\",\n          \"themeMode\": \"Giao diện\",\n          \"language\": \"Ngôn ngữ\",\n          \"updateSettings\": \"Cài đặt cập nhật\",\n          \"autoCheckUpdate\": \"Tự động kiểm tra cập nhật\",\n          \"permissionsSettings\": \"Cài đặt quyền\",\n          \"inputMonitoringPermission\": \"Quyền giám sát đầu vào\"\n        },\n        \"options\": {\n          \"auto\": \"Theo hệ thống\",\n          \"lightMode\": \"Sáng\",\n          \"darkMode\": \"Tối\"\n        },\n        \"hints\": {\n          \"showTaskbarIcon\": \"Bật để có thể quay cửa sổ qua OBS.\",\n          \"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.\",\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.\"\n        },\n        \"status\": {\n          \"authorized\": \"Đã cấp quyền\",\n          \"authorize\": \"Đi đến Bật\"\n        },\n        \"buttons\": {\n          \"openNow\": \"Mở ngay\",\n          \"openLater\": \"Mở sau\"\n        }\n      },\n      \"model\": {\n        \"title\": \"Mô hình\",\n        \"labels\": {\n          \"deleteModel\": \"Xóa mô hình\"\n        },\n        \"hints\": {\n          \"deleteSuccess\": \"Xóa thành công\",\n          \"deleteModel\": \"Bạn chắc muốn xóa mô hình này?\",\n          \"importSuccess\": \"Nhập thành công\",\n          \"clickOrDragToImport\": \"Nhấp hoặc kéo tệp vào đây\"\n        },\n        \"tooltips\": {\n          \"createModel\": \"Tạo mô hình\",\n          \"convertModel\": \"Chuyển đổi mô hình\",\n          \"moreModels\": \"Khám phá mô hình khác\"\n        }\n      },\n      \"shortcut\": {\n        \"title\": \"Phím tắt\",\n        \"labels\": {\n          \"toggleCat\": \"Ẩn/Hiện Mèo\",\n          \"togglePreferences\": \"Mở Tùy chỉnh\",\n          \"mirrorMode\": \"Chế độ gương\",\n          \"passThrough\": \"Click xuyên\",\n          \"alwaysOnTop\": \"Luôn trên cùng\"\n        },\n        \"hints\": {\n          \"toggleCat\": \"Bật/Tắt cửa sổ mèo.\",\n          \"togglePreferences\": \"Bật/Tắt cửa sổ tùy chỉnh.\",\n          \"mirrorMode\": \"Bật/Tắt chế độ gương.\",\n          \"passThrough\": \"Bật/Tắt cho phép click xuyên cửa sổ mèo.\",\n          \"alwaysOnTop\": \"Bật/Tắt luôn giữ cửa sổ mèo trên cùng.\"\n        }\n      },\n      \"about\": {\n        \"title\": \"Giới thiệu\",\n        \"labels\": {\n          \"aboutApp\": \"Thông tin ứng dụng\",\n          \"appLog\": \"Nhật ký ứng dụng\",\n          \"appInfo\": \"Thông tin ứng dụng\",\n          \"openSource\": \"Mã nguồn\"\n        },\n        \"hints\": {\n          \"appInfo\": \"Sao chép thông tin để gửi bug.\",\n          \"copySuccess\": \"Đã sao chép\"\n        },\n        \"buttons\": {\n          \"checkUpdate\": \"Kiểm tra cập nhật\",\n          \"copy\": \"Sao chép\",\n          \"feedbackIssues\": \"Báo lỗi\",\n          \"viewLog\": \"Xem nhật ký\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"proShortcut\": {\n      \"hints\": {\n        \"pressRecordShortcut\": \"Nhấn phím/tổ hợp phím để ghi\",\n        \"clickRecordShortcut\": \"Click để ghi phím tắt\"\n      }\n    },\n    \"updateApp\": {\n      \"title\": \"Đã tìm thấy phiên bản mới 🥳\",\n      \"labels\": {\n        \"updateVersion\": \"Phiên bản: \",\n        \"updateTime\": \"Thời gian cập nhật: \",\n        \"changelog\": \"Nhật ký thay đổi: \"\n      },\n      \"hints\": {\n        \"checkingUpdates\": \"Đang kiểm tra cập nhật...\",\n        \"alreadyLatest\": \"Bạn đang dùng phiên bản mới nhất 🎉\"\n      },\n      \"buttons\": {\n        \"updateNow\": \"Cập nhật ngay\",\n        \"updateLater\": \"Để sau\"\n      }\n    }\n  },\n  \"composables\": {\n    \"useSharedMenu\": {\n      \"labels\": {\n        \"preference\": \"Tùy chỉnh...\",\n        \"hideCat\": \"Ẩn Mèo\",\n        \"showCat\": \"Hiện Mèo\",\n        \"passThrough\": \"Click xuyên\",\n        \"windowSize\": \"Kích thước\",\n        \"opacity\": \"Độ mờ\"\n      }\n    },\n    \"useTray\": {\n      \"checkUpdate\": \"Kiểm tra cập nhật\",\n      \"openSource\": \"Mã nguồn\",\n      \"restartApp\": \"Khởi động lại\",\n      \"quitApp\": \"Thoát\"\n    }\n  },\n  \"utils\": {\n    \"live2d\": {\n      \"hints\": {\n        \"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.\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/locales/zh-CN.json",
    "content": "{\n  \"pages\": {\n    \"main\": {\n      \"hints\": {\n        \"redrawing\": \"重绘中...\"\n      }\n    },\n    \"preference\": {\n      \"title\": \"偏好设置\",\n      \"cat\": {\n        \"title\": \"猫咪设置\",\n        \"labels\": {\n          \"modelSettings\": \"模型设置\",\n          \"mirrorMode\": \"镜像模式\",\n          \"singleMode\": \"单键模式\",\n          \"mouseMirror\": \"鼠标镜像\",\n          \"windowSettings\": \"窗口设置\",\n          \"passThrough\": \"窗口穿透\",\n          \"alwaysOnTop\": \"窗口置顶\",\n          \"windowSize\": \"窗口尺寸\",\n          \"windowRadius\": \"窗口圆角\",\n          \"opacity\": \"不透明度\",\n          \"autoReleaseDelay\": \"按键自动释放延迟\",\n          \"hideOnHover\": \"鼠标移入隐藏\",\n          \"position\": \"窗口位置\"\n        },\n        \"hints\": {\n          \"mirrorMode\": \"启用后，模型将水平镜像翻转。\",\n          \"singleMode\": \"启用后，每只手只显示最后按下的一个按键。\",\n          \"mouseMirror\": \"启用后，鼠标将镜像跟随手部移动。\",\n          \"passThrough\": \"启用后，窗口不影响对其他应用程序的操作。\",\n          \"alwaysOnTop\": \"启用后，窗口始终显示在其他应用程序上方。\",\n          \"windowSize\": \"将鼠标移至窗口边缘，或按住 Shift 并右键拖动，也可以调整窗口大小。\",\n          \"autoReleaseDelay\": \"由于 Windows 下部分系统级按键无法捕获释放事件，超时后将自动视为已释放。\",\n          \"hideOnHover\": \"启用后，鼠标悬停在窗口上时，窗口会隐藏。\",\n          \"position\": \"应用启动后，或当此参数、窗口尺寸、模型、电脑分辨率发生变化时生效。\"\n        },\n        \"options\": {\n          \"topLeft\": \"左上角\",\n          \"topRight\": \"右上角\",\n          \"bottomLeft\": \"左下角\",\n          \"bottomRight\": \"右下角\"\n        }\n      },\n      \"general\": {\n        \"title\": \"通用设置\",\n        \"labels\": {\n          \"appSettings\": \"应用设置\",\n          \"launchOnStartup\": \"开机自启动\",\n          \"showTaskbarIcon\": \"显示任务栏图标\",\n          \"appearanceSettings\": \"外观设置\",\n          \"themeMode\": \"主题模式\",\n          \"language\": \"语言\",\n          \"updateSettings\": \"更新设置\",\n          \"autoCheckUpdate\": \"自动检查更新\",\n          \"permissionsSettings\": \"权限设置\",\n          \"inputMonitoringPermission\": \"输入监控权限\"\n        },\n        \"options\": {\n          \"auto\": \"跟随系统\",\n          \"lightMode\": \"亮色模式\",\n          \"darkMode\": \"暗色模式\"\n        },\n        \"hints\": {\n          \"showTaskbarIcon\": \"启用后，即可通过 OBS Studio 捕获窗口。\",\n          \"inputMonitoringPermission\": \"开启输入监控权限，以便接收系统的键盘和鼠标事件来响应你的操作。\",\n          \"inputMonitoringPermissionGuide\": \"如果权限已开启，请先选中并点击“-”按钮将其删除，然后重新手动添加，最后重启应用以确保权限生效。\"\n        },\n        \"status\": {\n          \"authorized\": \"已授权\",\n          \"authorize\": \"去授权\"\n        },\n        \"buttons\": {\n          \"openNow\": \"前往开启\",\n          \"openLater\": \"稍后开启\"\n        }\n      },\n      \"model\": {\n        \"title\": \"模型管理\",\n        \"labels\": {\n          \"deleteModel\": \"删除模型\"\n        },\n        \"hints\": {\n          \"deleteSuccess\": \"删除成功\",\n          \"deleteModel\": \"你确定要删除此模型吗？\",\n          \"importSuccess\": \"导入成功\",\n          \"clickOrDragToImport\": \"点击或拖动至此区域导入\"\n        },\n        \"tooltips\": {\n          \"createModel\": \"制作模型\",\n          \"convertModel\": \"转换模型\",\n          \"moreModels\": \"更多模型\"\n        }\n      },\n      \"shortcut\": {\n        \"title\": \"快捷键\",\n        \"labels\": {\n          \"toggleCat\": \"打开猫咪\",\n          \"togglePreferences\": \"打开偏好设置\",\n          \"mirrorMode\": \"镜像模式\",\n          \"passThrough\": \"窗口穿透\",\n          \"alwaysOnTop\": \"窗口置顶\"\n        },\n        \"hints\": {\n          \"toggleCat\": \"切换猫咪窗口的显示与隐藏。\",\n          \"togglePreferences\": \"切换偏好设置窗口的显示与隐藏。\",\n          \"mirrorMode\": \"切换猫咪的镜像模式。\",\n          \"passThrough\": \"切换猫咪窗口是否可穿透。\",\n          \"alwaysOnTop\": \"切换猫咪窗口是否置顶。\"\n        }\n      },\n      \"about\": {\n        \"title\": \"关于\",\n        \"labels\": {\n          \"aboutApp\": \"关于软件\",\n          \"appLog\": \"软件日志\",\n          \"appInfo\": \"软件信息\",\n          \"openSource\": \"开源地址\"\n        },\n        \"hints\": {\n          \"appInfo\": \"复制软件信息并提供给 Bug Issue。\",\n          \"copySuccess\": \"复制成功\"\n        },\n        \"buttons\": {\n          \"checkUpdate\": \"检查更新\",\n          \"copy\": \"复制\",\n          \"feedbackIssues\": \"反馈问题\",\n          \"viewLog\": \"查看日志\"\n        }\n      }\n    }\n  },\n  \"components\": {\n    \"proShortcut\": {\n      \"hints\": {\n        \"pressRecordShortcut\": \"按下录制快捷键\",\n        \"clickRecordShortcut\": \"点击录制快捷键\"\n      }\n    },\n    \"updateApp\": {\n      \"title\": \"发现新版本🥳\",\n      \"labels\": {\n        \"updateVersion\": \"更新版本：\",\n        \"updateTime\": \"更新时间：\",\n        \"changelog\": \"更新日志：\"\n      },\n      \"hints\": {\n        \"checkingUpdates\": \"正在检查更新...\",\n        \"alreadyLatest\": \"当前已是最新版本🎉\"\n      },\n      \"buttons\": {\n        \"updateNow\": \"立即更新\",\n        \"updateLater\": \"稍后更新\"\n      }\n    }\n  },\n  \"composables\": {\n    \"useSharedMenu\": {\n      \"labels\": {\n        \"preference\": \"偏好设置...\",\n        \"hideCat\": \"隐藏猫咪\",\n        \"showCat\": \"显示猫咪\",\n        \"passThrough\": \"窗口穿透\",\n        \"windowSize\": \"窗口尺寸\",\n        \"opacity\": \"不透明度\"\n      }\n    },\n    \"useTray\": {\n      \"checkUpdate\": \"检查更新\",\n      \"openSource\": \"开源地址\",\n      \"restartApp\": \"重启应用\",\n      \"quitApp\": \"退出应用\"\n    }\n  },\n  \"utils\": {\n    \"live2d\": {\n      \"hints\": {\n        \"notFound\": \"未找到模型主配置文件，请确认模型文件是否完整。\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src/main.ts",
    "content": "import { createPlugin } from '@tauri-store/pinia'\nimport { createPinia } from 'pinia'\nimport { createApp } from 'vue'\n\nimport App from './App.vue'\nimport { i18n } from './locales'\nimport router from './router'\n\nimport 'virtual:uno.css'\nimport 'ant-design-vue/dist/reset.css'\nimport './assets/css/global.scss'\n\nconst pinia = createPinia()\npinia.use(createPlugin({ saveOnChange: true }))\n\ncreateApp(App).use(router).use(pinia).use(i18n).mount('#app')\n"
  },
  {
    "path": "src/pages/main/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { convertFileSrc } from '@tauri-apps/api/core'\nimport { PhysicalSize } from '@tauri-apps/api/dpi'\nimport { Menu } from '@tauri-apps/api/menu'\nimport { sep } from '@tauri-apps/api/path'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { exists, readDir } from '@tauri-apps/plugin-fs'\nimport { useDebounceFn, useEventListener } from '@vueuse/core'\nimport { round } from 'es-toolkit'\nimport { nth } from 'es-toolkit/compat'\nimport { onMounted, onUnmounted, ref, watch } from 'vue'\n\nimport { useDevice } from '@/composables/useDevice'\nimport { useGamepad } from '@/composables/useGamepad'\nimport { useModel } from '@/composables/useModel'\nimport { useSharedMenu } from '@/composables/useSharedMenu'\nimport { useWindowPosition } from '@/composables/useWindowPosition'\nimport { hideWindow, setAlwaysOnTop, setTaskbarVisibility, showWindow } from '@/plugins/window'\nimport { useCatStore } from '@/stores/cat'\nimport { useGeneralStore } from '@/stores/general.ts'\nimport { useModelStore } from '@/stores/model'\nimport { isImage } from '@/utils/is'\nimport { join } from '@/utils/path'\nimport { clearObject } from '@/utils/shared'\n\nconst { startListening } = useDevice()\nconst appWindow = getCurrentWebviewWindow()\nconst { modelSize, handleLoad, handleDestroy, handleResize, handleKeyChange } = useModel()\nconst catStore = useCatStore()\nconst { getSharedMenu } = useSharedMenu()\nconst modelStore = useModelStore()\nconst generalStore = useGeneralStore()\nconst resizing = ref(false)\nconst backgroundImagePath = ref<string>()\nconst { stickActive } = useGamepad()\nconst { isMounted, setWindowPosition } = useWindowPosition()\n\nonMounted(startListening)\n\nonUnmounted(handleDestroy)\n\nconst debouncedResize = useDebounceFn(async () => {\n  await handleResize()\n\n  await setWindowPosition()\n\n  resizing.value = false\n}, 100)\n\nuseEventListener('resize', () => {\n  resizing.value = true\n\n  debouncedResize()\n})\n\nwatch(() => modelStore.currentModel, async (model) => {\n  if (!model) return\n\n  await handleLoad()\n\n  const path = join(model.path, 'resources', 'background.png')\n\n  const existed = await exists(path)\n\n  backgroundImagePath.value = existed ? convertFileSrc(path) : void 0\n\n  clearObject([modelStore.supportKeys, modelStore.pressedKeys])\n\n  const resourcePath = join(model.path, 'resources')\n  const groups = ['left-keys', 'right-keys']\n\n  for await (const groupName of groups) {\n    const groupDir = join(resourcePath, groupName)\n    const files = await readDir(groupDir).catch(() => [])\n    const imageFiles = files.filter(file => isImage(file.name))\n\n    for (const file of imageFiles) {\n      const fileName = file.name.split('.')[0]\n\n      modelStore.supportKeys[fileName] = join(groupDir, file.name)\n    }\n  }\n\n  setWindowPosition()\n}, { deep: true, immediate: true })\n\nwatch([() => catStore.window.scale, modelSize], async ([scale, modelSize]) => {\n  if (!modelSize) return\n\n  const { width, height } = modelSize\n\n  appWindow.setSize(\n    new PhysicalSize({\n      width: Math.round(width * (scale / 100)),\n      height: Math.round(height * (scale / 100)),\n    }),\n  )\n}, { immediate: true })\n\nwatch([modelStore.pressedKeys, stickActive], ([keys, stickActive]) => {\n  const dirs = Object.values(keys).map((path) => {\n    return nth(path.split(sep()), -2)!\n  })\n\n  const hasLeft = dirs.some(dir => dir.startsWith('left'))\n  const hasRight = dirs.some(dir => dir.startsWith('right'))\n\n  handleKeyChange(true, stickActive.left || hasLeft)\n  handleKeyChange(false, stickActive.right || hasRight)\n}, { deep: true })\n\nwatch(() => catStore.window.visible, async (value) => {\n  value ? showWindow() : hideWindow()\n})\n\nwatch(() => catStore.window.passThrough, (value) => {\n  appWindow.setIgnoreCursorEvents(value)\n}, { immediate: true })\n\nwatch(() => catStore.window.alwaysOnTop, setAlwaysOnTop, { immediate: true })\n\nwatch(() => generalStore.app.taskbarVisible, setTaskbarVisibility, { immediate: true })\n\nfunction handleMouseDown() {\n  appWindow.startDragging()\n}\n\nasync function handleContextmenu(event: MouseEvent) {\n  event.preventDefault()\n\n  if (event.shiftKey) return\n\n  const menu = await Menu.new({\n    items: await getSharedMenu(),\n  })\n\n  menu.popup()\n}\n\nfunction handleMouseMove(event: MouseEvent) {\n  const { buttons, shiftKey, movementX, movementY } = event\n\n  if (buttons !== 2 || !shiftKey) return\n\n  const delta = (movementX + movementY) * 0.5\n  const nextScale = Math.max(10, Math.min(catStore.window.scale + delta, 500))\n\n  catStore.window.scale = round(nextScale)\n}\n</script>\n\n<template>\n  <div\n    v-show=\"isMounted\"\n    class=\"relative size-screen overflow-hidden children:(absolute size-full)\"\n    :class=\"{ '-scale-x-100': catStore.model.mirror }\"\n    :style=\"{\n      opacity: catStore.window.opacity / 100,\n      borderRadius: `${catStore.window.radius}%`,\n    }\"\n    @contextmenu=\"handleContextmenu\"\n    @mousedown=\"handleMouseDown\"\n    @mousemove=\"handleMouseMove\"\n  >\n    <img\n      v-if=\"backgroundImagePath\"\n      class=\"object-cover\"\n      :src=\"backgroundImagePath\"\n    >\n\n    <canvas id=\"live2dCanvas\" />\n\n    <img\n      v-for=\"path in modelStore.pressedKeys\"\n      :key=\"path\"\n      class=\"object-cover\"\n      :src=\"convertFileSrc(path)\"\n    >\n\n    <div\n      v-show=\"resizing\"\n      class=\"flex items-center justify-center bg-black\"\n    >\n      <span class=\"text-center text-[10vw] text-white\">\n        {{ $t('pages.main.hints.redrawing') }}\n      </span>\n    </div>\n  </div>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/about/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { getTauriVersion } from '@tauri-apps/api/app'\nimport { emit } from '@tauri-apps/api/event'\nimport { appLogDir } from '@tauri-apps/api/path'\nimport { writeText } from '@tauri-apps/plugin-clipboard-manager'\nimport { openPath, openUrl } from '@tauri-apps/plugin-opener'\nimport { arch, platform, version } from '@tauri-apps/plugin-os'\nimport { Button, message } from 'ant-design-vue'\nimport { onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport ProList from '@/components/pro-list/index.vue'\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { GITHUB_LINK, LISTEN_KEY } from '@/constants'\nimport { useAppStore } from '@/stores/app'\n\nconst appStore = useAppStore()\nconst logDir = ref('')\nconst { t } = useI18n()\n\nonMounted(async () => {\n  logDir.value = await appLogDir()\n})\n\nfunction handleUpdate() {\n  emit(LISTEN_KEY.UPDATE_APP)\n}\n\nasync function copyInfo() {\n  const info = {\n    appName: appStore.name,\n    appVersion: appStore.version,\n    tauriVersion: await getTauriVersion(),\n    platform: platform(),\n    platformArch: arch(),\n    platformVersion: version(),\n  }\n\n  await writeText(JSON.stringify(info, null, 2))\n\n  message.success(t('pages.preference.about.hints.copySuccess'))\n}\n\nfunction feedbackIssue() {\n  openUrl(`${GITHUB_LINK}/issues/new/choose`)\n}\n</script>\n\n<template>\n  <ProList :title=\"$t('pages.preference.about.labels.aboutApp')\">\n    <ProListItem\n      :description=\"`v${appStore.version}`\"\n      :title=\"appStore.name\"\n    >\n      <Button\n        type=\"primary\"\n        @click=\"handleUpdate\"\n      >\n        {{ $t('pages.preference.about.buttons.checkUpdate') }}\n      </Button>\n\n      <template #icon>\n        <div class=\"b b-color-2 rounded-xl b-solid\">\n          <img\n            class=\"size-12\"\n            src=\"/logo.png\"\n          >\n        </div>\n      </template>\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.about.hints.appInfo')\"\n      :title=\"$t('pages.preference.about.labels.appInfo')\"\n    >\n      <Button @click=\"copyInfo\">\n        {{ $t('pages.preference.about.buttons.copy') }}\n      </Button>\n    </ProListItem>\n\n    <ProListItem :title=\"$t('pages.preference.about.labels.openSource')\">\n      <Button\n        danger\n        @click=\"feedbackIssue\"\n      >\n        {{ $t('pages.preference.about.buttons.feedbackIssues') }}\n      </Button>\n\n      <template #description>\n        <a :href=\"GITHUB_LINK\">\n          {{ GITHUB_LINK }}\n        </a>\n      </template>\n    </ProListItem>\n\n    <ProListItem\n      :description=\"logDir\"\n      :title=\"$t('pages.preference.about.labels.appLog')\"\n    >\n      <Button @click=\"openPath(logDir)\">\n        {{ $t('pages.preference.about.buttons.viewLog') }}\n      </Button>\n    </ProListItem>\n  </ProList>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/cat/components/position/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { Select, SelectOption } from 'ant-design-vue'\n\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { useCatStore } from '@/stores/cat'\n\nconst catStore = useCatStore()\n</script>\n\n<template>\n  <ProListItem\n    :description=\"$t('pages.preference.cat.hints.position')\"\n    :title=\"$t('pages.preference.cat.labels.position')\"\n  >\n    <Select v-model:value=\"catStore.window.position\">\n      <SelectOption value=\"bottomRight\">\n        {{ $t('pages.preference.cat.options.bottomRight') }}\n      </SelectOption>\n      <SelectOption value=\"bottomLeft\">\n        {{ $t('pages.preference.cat.options.bottomLeft') }}\n      </SelectOption>\n      <SelectOption value=\"topLeft\">\n        {{ $t('pages.preference.cat.options.topLeft') }}\n      </SelectOption>\n      <SelectOption value=\"topRight\">\n        {{ $t('pages.preference.cat.options.topRight') }}\n      </SelectOption>\n    </Select>\n  </ProListItem>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/cat/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { InputNumber, Slider, Switch } from 'ant-design-vue'\n\nimport Position from './components/position/index.vue'\n\nimport ProList from '@/components/pro-list/index.vue'\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { useCatStore } from '@/stores/cat'\nimport { isWindows } from '@/utils/platform'\n\nconst catStore = useCatStore()\n</script>\n\n<template>\n  <ProList :title=\"$t('pages.preference.cat.labels.modelSettings')\">\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.mirrorMode')\"\n      :title=\"$t('pages.preference.cat.labels.mirrorMode')\"\n    >\n      <Switch v-model:checked=\"catStore.model.mirror\" />\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.singleMode')\"\n      :title=\"$t('pages.preference.cat.labels.singleMode')\"\n    >\n      <Switch v-model:checked=\"catStore.model.single\" />\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.mouseMirror')\"\n      :title=\"$t('pages.preference.cat.labels.mouseMirror')\"\n    >\n      <Switch v-model:checked=\"catStore.model.mouseMirror\" />\n    </ProListItem>\n\n    <ProListItem\n      v-if=\"isWindows\"\n      :description=\"$t('pages.preference.cat.hints.autoReleaseDelay')\"\n      :title=\"$t('pages.preference.cat.labels.autoReleaseDelay')\"\n    >\n      <InputNumber\n        v-model:value=\"catStore.model.autoReleaseDelay\"\n        addon-after=\"s\"\n        class=\"w-28\"\n      />\n    </ProListItem>\n  </ProList>\n\n  <ProList :title=\"$t('pages.preference.cat.labels.windowSettings')\">\n    <Position />\n\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.passThrough')\"\n      :title=\"$t('pages.preference.cat.labels.passThrough')\"\n    >\n      <Switch v-model:checked=\"catStore.window.passThrough\" />\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.alwaysOnTop')\"\n      :title=\"$t('pages.preference.cat.labels.alwaysOnTop')\"\n    >\n      <Switch v-model:checked=\"catStore.window.alwaysOnTop\" />\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.hideOnHover')\"\n      :title=\"$t('pages.preference.cat.labels.hideOnHover')\"\n    >\n      <Switch v-model:checked=\"catStore.window.hideOnHover\" />\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.cat.hints.windowSize')\"\n      :title=\"$t('pages.preference.cat.labels.windowSize')\"\n    >\n      <InputNumber\n        v-model:value=\"catStore.window.scale\"\n        addon-after=\"%\"\n        class=\"w-28\"\n        :max=\"500\"\n        :min=\"1\"\n      />\n    </ProListItem>\n\n    <ProListItem :title=\"$t('pages.preference.cat.labels.windowRadius')\">\n      <InputNumber\n        v-model:value=\"catStore.window.radius\"\n        addon-after=\"%\"\n        class=\"w-28\"\n        :min=\"0\"\n      />\n    </ProListItem>\n\n    <ProListItem\n      :title=\"$t('pages.preference.cat.labels.opacity')\"\n      vertical\n    >\n      <Slider\n        v-model:value=\"catStore.window.opacity\"\n        class=\"m-[0]!\"\n        :max=\"100\"\n        :min=\"10\"\n        :tip-formatter=\"(value) => `${value}%`\"\n      />\n    </ProListItem>\n  </ProList>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/general/components/macos-permissions/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { confirm } from '@tauri-apps/plugin-dialog'\nimport { Space } from 'ant-design-vue'\nimport { checkInputMonitoringPermission, requestInputMonitoringPermission } from 'tauri-plugin-macos-permissions-api'\nimport { onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport ProList from '@/components/pro-list/index.vue'\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { isMac } from '@/utils/platform'\n\nconst authorized = ref(false)\nconst { t } = useI18n()\n\nonMounted(async () => {\n  authorized.value = await checkInputMonitoringPermission()\n\n  if (authorized.value) return\n\n  const appWindow = getCurrentWebviewWindow()\n\n  await appWindow.setAlwaysOnTop(true)\n\n  const confirmed = await confirm(t('pages.preference.general.hints.inputMonitoringPermissionGuide'), {\n    title: t('pages.preference.general.labels.inputMonitoringPermission'),\n    okLabel: t('pages.preference.general.buttons.openNow'),\n    cancelLabel: t('pages.preference.general.buttons.openLater'),\n    kind: 'warning',\n  })\n\n  if (!confirmed) return\n\n  await appWindow.setAlwaysOnTop(false)\n\n  requestInputMonitoringPermission()\n})\n</script>\n\n<template>\n  <ProList\n    v-if=\"isMac\"\n    :title=\"$t('pages.preference.general.labels.permissionsSettings')\"\n  >\n    <ProListItem\n      :description=\"$t('pages.preference.general.hints.inputMonitoringPermission')\"\n      :title=\"$t('pages.preference.general.labels.inputMonitoringPermission')\"\n    >\n      <Space\n        v-if=\"authorized\"\n        class=\"text-success font-bold\"\n        :size=\"4\"\n      >\n        <div class=\"i-solar:verified-check-bold text-4.5\" />\n\n        <span class=\"whitespace-nowrap\">{{ $t('pages.preference.general.status.authorized') }}</span>\n      </Space>\n\n      <Space\n        v-else\n        class=\"cursor-pointer text-danger font-bold\"\n        :size=\"4\"\n        @click=\"requestInputMonitoringPermission\"\n      >\n        <div class=\"i-solar:round-arrow-right-bold text-4.5\" />\n\n        <span class=\"whitespace-nowrap\">{{ $t('pages.preference.general.status.authorize') }}</span>\n      </Space>\n    </ProListItem>\n  </ProList>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/general/components/theme-mode/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { Select, SelectOption } from 'ant-design-vue'\nimport { onMounted, watch } from 'vue'\n\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { useGeneralStore } from '@/stores/general'\n\nconst generalStore = useGeneralStore()\nconst appWindow = getCurrentWebviewWindow()\n\nonMounted(() => {\n  appWindow.onThemeChanged(async ({ payload }) => {\n    if (generalStore.appearance.theme !== 'auto') return\n\n    generalStore.appearance.isDark = payload === 'dark'\n  })\n})\n\nwatch(() => generalStore.appearance.theme, async (value) => {\n  let nextTheme = value === 'auto' ? null : value\n\n  await appWindow.setTheme(nextTheme)\n\n  nextTheme = nextTheme ?? (await appWindow.theme())\n\n  generalStore.appearance.isDark = nextTheme === 'dark'\n}, { immediate: true })\n\nwatch(() => generalStore.appearance.isDark, (value) => {\n  if (value) {\n    document.documentElement.classList.add('dark')\n  } else {\n    document.documentElement.classList.remove('dark')\n  }\n}, { immediate: true })\n</script>\n\n<template>\n  <ProListItem :title=\"$t('pages.preference.general.labels.themeMode')\">\n    <Select v-model:value=\"generalStore.appearance.theme\">\n      <SelectOption value=\"auto\">\n        {{ $t('pages.preference.general.options.auto') }}\n      </SelectOption>\n      <SelectOption value=\"light\">\n        {{ $t('pages.preference.general.options.lightMode') }}\n      </SelectOption>\n      <SelectOption value=\"dark\">\n        {{ $t('pages.preference.general.options.darkMode') }}\n      </SelectOption>\n    </Select>\n  </ProListItem>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/general/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'\nimport { Select, Switch } from 'ant-design-vue'\nimport { watch } from 'vue'\n\nimport MacosPermissions from './components/macos-permissions/index.vue'\nimport ThemeMode from './components/theme-mode/index.vue'\n\nimport ProList from '@/components/pro-list/index.vue'\nimport ProListItem from '@/components/pro-list-item/index.vue'\nimport { useGeneralStore } from '@/stores/general'\n\nconst generalStore = useGeneralStore()\n\nwatch(() => generalStore.app.autostart, async (value) => {\n  const enabled = await isEnabled()\n\n  if (value && !enabled) {\n    return enable()\n  }\n\n  if (!value && enabled) {\n    disable()\n  }\n}, { immediate: true })\n</script>\n\n<template>\n  <MacosPermissions />\n\n  <ProList :title=\"$t('pages.preference.general.labels.appSettings')\">\n    <ProListItem :title=\"$t('pages.preference.general.labels.launchOnStartup')\">\n      <Switch v-model:checked=\"generalStore.app.autostart\" />\n    </ProListItem>\n\n    <ProListItem\n      :description=\"$t('pages.preference.general.hints.showTaskbarIcon')\"\n      :title=\"$t('pages.preference.general.labels.showTaskbarIcon')\"\n    >\n      <Switch v-model:checked=\"generalStore.app.taskbarVisible\" />\n    </ProListItem>\n  </ProList>\n\n  <ProList :title=\"$t('pages.preference.general.labels.appearanceSettings')\">\n    <ThemeMode />\n\n    <ProListItem :title=\"$t('pages.preference.general.labels.language')\">\n      <Select v-model:value=\"generalStore.appearance.language\">\n        <Select.Option value=\"zh-CN\">\n          简体中文\n        </Select.Option>\n        <Select.Option value=\"en-US\">\n          English\n        </Select.Option>\n        <Select.Option value=\"vi-VN\">\n          Tiếng Việt\n        </Select.Option>\n        <Select.Option value=\"pt-BR\">\n          Português\n        </Select.Option>\n      </Select>\n    </ProListItem>\n  </ProList>\n\n  <ProList :title=\"$t('pages.preference.general.labels.updateSettings')\">\n    <ProListItem :title=\"$t('pages.preference.general.labels.autoCheckUpdate')\">\n      <Switch v-model:checked=\"generalStore.update.autoCheck\" />\n    </ProListItem>\n  </ProList>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/model/components/float-menu/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { EditOutlined, MenuOutlined, SyncOutlined, UnorderedListOutlined } from '@ant-design/icons-vue'\nimport { openUrl } from '@tauri-apps/plugin-opener'\nimport { FloatButton, FloatButtonGroup } from 'ant-design-vue'\n</script>\n\n<template>\n  <FloatButtonGroup\n    class=\"bottom-4 right-4\"\n    trigger=\"click\"\n    type=\"primary\"\n  >\n    <template #icon>\n      <MenuOutlined />\n    </template>\n\n    <FloatButton\n      :tooltip=\"$t('pages.preference.model.tooltips.createModel')\"\n      @click=\"openUrl('https://juejin.cn/post/7509872655802269731')\"\n    >\n      <template #icon>\n        <EditOutlined />\n      </template>\n    </FloatButton>\n\n    <FloatButton\n      :tooltip=\"$t('pages.preference.model.tooltips.convertModel')\"\n      @click=\"openUrl('https://bongocat.vteamer.cc')\"\n    >\n      <template #icon>\n        <SyncOutlined />\n      </template>\n    </FloatButton>\n\n    <FloatButton\n      :tooltip=\"$t('pages.preference.model.tooltips.moreModels')\"\n      @click=\"openUrl('https://github.com/ayangweb/Awesome-BongoCat')\"\n    >\n      <template #icon>\n        <UnorderedListOutlined />\n      </template>\n    </FloatButton>\n  </FloatButtonGroup>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/model/components/upload/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { ModelMode } from '@/stores/model'\n\nimport { invoke } from '@tauri-apps/api/core'\nimport { appDataDir } from '@tauri-apps/api/path'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { open } from '@tauri-apps/plugin-dialog'\nimport { readDir } from '@tauri-apps/plugin-fs'\nimport { message } from 'ant-design-vue'\nimport { nanoid } from 'nanoid'\nimport { onMounted, ref, useTemplateRef, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { INVOKE_KEY } from '@/constants'\nimport { useModelStore } from '@/stores/model'\nimport { join } from '@/utils/path'\n\nconst dropRef = useTemplateRef('drop')\nconst dragenter = ref(false)\nconst selectPaths = ref<string[]>([])\nconst modelStore = useModelStore()\nconst { t } = useI18n()\n\nonMounted(() => {\n  const appWindow = getCurrentWebviewWindow()\n\n  appWindow.onDragDropEvent(({ payload }) => {\n    const { type } = payload\n\n    if (type === 'over') {\n      const { x, y } = payload.position\n\n      if (dropRef.value) {\n        const { left, right, top, bottom } = dropRef.value.getBoundingClientRect()\n\n        const inBoundsX = x >= left && x <= right\n        const inBoundsY = y >= top && y <= bottom\n\n        dragenter.value = inBoundsX && inBoundsY\n      }\n    } else if (type === 'drop' && dragenter.value) {\n      dragenter.value = false\n\n      selectPaths.value = payload.paths\n    } else {\n      dragenter.value = false\n    }\n  })\n})\n\nasync function handleUpload() {\n  const selected = await open({ directory: true, multiple: true })\n\n  if (!selected) return\n\n  selectPaths.value = selected\n}\n\nwatch(selectPaths, async (paths) => {\n  for await (const fromPath of paths) {\n    try {\n      const id = nanoid()\n\n      let mode: ModelMode = 'standard'\n\n      const files = await readDir(join(fromPath, 'resources', 'right-keys')).catch(() => [])\n\n      if (files.length > 0) {\n        const fileNames = files.map(file => file.name.split('.')[0])\n\n        if (fileNames.includes('East')) {\n          mode = 'gamepad'\n        } else {\n          mode = 'keyboard'\n        }\n      }\n\n      const toPath = join(await appDataDir(), 'custom-models', id)\n\n      await invoke(INVOKE_KEY.COPY_DIR, {\n        fromPath,\n        toPath,\n      })\n\n      modelStore.models.push({\n        id,\n        path: toPath,\n        mode,\n        isPreset: false,\n      })\n\n      message.success(t('pages.preference.model.hints.importSuccess'))\n    } catch (error) {\n      message.error(String(error))\n    }\n  }\n})\n</script>\n\n<template>\n  <div\n    ref=\"drop\"\n    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\"\n    :class=\"{ 'border-primary': dragenter }\"\n    @click=\"handleUpload\"\n  >\n    <div class=\"i-solar:upload-square-outline text-12 text-primary\" />\n\n    <span>{{ $t('pages.preference.model.hints.clickOrDragToImport') }}</span>\n  </div>\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/model/index.vue",
    "content": "<script setup lang=\"ts\">\nimport type { Model } from '@/stores/model'\nimport type { ComponentPublicInstance } from 'vue'\n\nimport { convertFileSrc } from '@tauri-apps/api/core'\nimport { remove } from '@tauri-apps/plugin-fs'\nimport { revealItemInDir } from '@tauri-apps/plugin-opener'\nimport { useElementSize } from '@vueuse/core'\nimport { Card, message, Popconfirm } from 'ant-design-vue'\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\nimport { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'\n\nimport FloatMenu from './components/float-menu/index.vue'\nimport Upload from './components/upload/index.vue'\n\nimport { useModelStore } from '@/stores/model'\nimport { join } from '@/utils/path'\n\nconst modelStore = useModelStore()\nconst firstItemRef = ref<HTMLElement>()\nconst { height } = useElementSize(firstItemRef)\nconst { t } = useI18n()\n\nfunction setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) {\n  if (!el || index > 0) return\n\n  if ('$el' in el) {\n    return firstItemRef.value = el.$el\n  }\n\n  if (el instanceof HTMLElement) {\n    firstItemRef.value = el\n  }\n}\n\nasync function handleDelete(item: Model) {\n  const { id, path } = item\n\n  try {\n    await remove(path, { recursive: true })\n\n    message.success(t('pages.preference.model.hints.deleteSuccess'))\n  } catch (error) {\n    message.error(String(error))\n  } finally {\n    modelStore.models = modelStore.models.filter(item => item.id !== id)\n\n    if (id === modelStore.currentModel?.id) {\n      modelStore.currentModel = modelStore.models[0]\n    }\n  }\n}\n</script>\n\n<template>\n  <MasonryGrid\n    :columns=\"{ 992: 3, 1200: 4, 1600: 6, default: 8 }\"\n    :gutter=\"16\"\n  >\n    <MasonryGridItem>\n      <Upload :style=\"{ height: `${height}px` }\" />\n    </MasonryGridItem>\n\n    <MasonryGridItem\n      v-for=\"(item, index) in modelStore.models\"\n      :key=\"item.id\"\n    >\n      <Card\n        :ref=\"(el) => setFirstItemRef(el, index)\"\n        hoverable\n        size=\"small\"\n        @click=\"modelStore.currentModel = item\"\n      >\n        <template #cover>\n          <img\n            alt=\"example\"\n            :src=\"convertFileSrc(join(item.path, 'resources', 'cover.png'))\"\n          >\n        </template>\n\n        <template #actions>\n          <i\n            class=\"i-iconamoon:check-circle-1-bold text-4\"\n            :class=\"{ 'text-success': item.id === modelStore.currentModel?.id }\"\n          />\n\n          <i\n            class=\"i-iconamoon:link-external-bold text-4\"\n            @click.stop=\"revealItemInDir(item.path)\"\n          />\n\n          <template v-if=\"!item.isPreset\">\n            <Popconfirm\n              :description=\"$t('pages.preference.model.hints.deleteModel')\"\n              placement=\"topRight\"\n              :title=\"$t('pages.preference.model.labels.deleteModel')\"\n              @confirm=\"handleDelete(item)\"\n            >\n              <i\n                class=\"i-iconamoon:trash-simple-bold text-4\"\n                @click.stop\n              />\n            </Popconfirm>\n          </template>\n        </template>\n      </Card>\n    </MasonryGridItem>\n  </MasonryGrid>\n\n  <FloatMenu />\n</template>\n"
  },
  {
    "path": "src/pages/preference/components/shortcut/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\n\nimport ProList from '@/components/pro-list/index.vue'\nimport ProShortcut from '@/components/pro-shortcut/index.vue'\nimport { useTauriShortcut } from '@/composables/useTauriShortcut'\nimport { toggleWindowVisible } from '@/plugins/window'\nimport { useCatStore } from '@/stores/cat'\nimport { useShortcutStore } from '@/stores/shortcut.ts'\n\nconst shortcutStore = useShortcutStore()\nconst { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore)\nconst catStore = useCatStore()\n\nuseTauriShortcut(visibleCat, () => {\n  catStore.window.visible = !catStore.window.visible\n})\n\nuseTauriShortcut(visiblePreference, () => {\n  toggleWindowVisible('preference')\n})\n\nuseTauriShortcut(mirrorMode, () => {\n  catStore.model.mirror = !catStore.model.mirror\n})\n\nuseTauriShortcut(penetrable, () => {\n  catStore.window.passThrough = !catStore.window.passThrough\n})\n\nuseTauriShortcut(alwaysOnTop, () => {\n  catStore.window.alwaysOnTop = !catStore.window.alwaysOnTop\n})\n</script>\n\n<template>\n  <ProList :title=\"$t('pages.preference.shortcut.title')\">\n    <ProShortcut\n      v-model=\"shortcutStore.visibleCat\"\n      :description=\"$t('pages.preference.shortcut.hints.toggleCat')\"\n      :title=\"$t('pages.preference.shortcut.labels.toggleCat')\"\n    />\n\n    <ProShortcut\n      v-model=\"shortcutStore.visiblePreference\"\n      :description=\"$t('pages.preference.shortcut.hints.togglePreferences')\"\n      :title=\"$t('pages.preference.shortcut.labels.togglePreferences')\"\n    />\n\n    <ProShortcut\n      v-model=\"shortcutStore.mirrorMode\"\n      :description=\"$t('pages.preference.shortcut.hints.mirrorMode')\"\n      :title=\"$t('pages.preference.shortcut.labels.mirrorMode')\"\n    />\n\n    <ProShortcut\n      v-model=\"shortcutStore.penetrable\"\n      :description=\"$t('pages.preference.shortcut.hints.passThrough')\"\n      :title=\"$t('pages.preference.shortcut.labels.passThrough')\"\n    />\n\n    <ProShortcut\n      v-model=\"shortcutStore.alwaysOnTop\"\n      :description=\"$t('pages.preference.shortcut.hints.alwaysOnTop')\"\n      :title=\"$t('pages.preference.shortcut.labels.alwaysOnTop')\"\n    />\n  </ProList>\n</template>\n"
  },
  {
    "path": "src/pages/preference/index.vue",
    "content": "<script setup lang=\"ts\">\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { Flex } from 'ant-design-vue'\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport About from './components/about/index.vue'\nimport Cat from './components/cat/index.vue'\nimport General from './components/general/index.vue'\nimport Model from './components/model/index.vue'\nimport Shortcut from './components/shortcut/index.vue'\n\nimport UpdateApp from '@/components/update-app/index.vue'\nimport { useTray } from '@/composables/useTray'\nimport { useAppStore } from '@/stores/app'\nimport { useGeneralStore } from '@/stores/general'\nimport { isMac } from '@/utils/platform'\n\nconst { createTray } = useTray()\nconst appStore = useAppStore()\nconst current = ref(0)\nconst { t } = useI18n()\nconst generalStore = useGeneralStore()\nconst appWindow = getCurrentWebviewWindow()\n\nonMounted(async () => {\n  createTray()\n})\n\nwatch(() => generalStore.appearance.language, () => {\n  appWindow.setTitle(t('pages.preference.title'))\n}, { immediate: true })\n\nconst menus = computed(() => [\n  {\n    label: t('pages.preference.cat.title'),\n    icon: 'i-solar:cat-bold',\n    component: Cat,\n  },\n  {\n    label: t('pages.preference.general.title'),\n    icon: 'i-solar:settings-minimalistic-bold',\n    component: General,\n  },\n  {\n    label: t('pages.preference.model.title'),\n    icon: 'i-solar:magic-stick-3-bold',\n    component: Model,\n  },\n  {\n    label: t('pages.preference.shortcut.title'),\n    icon: 'i-solar:keyboard-bold',\n    component: Shortcut,\n  },\n  {\n    label: t('pages.preference.about.title'),\n    icon: 'i-solar:info-circle-bold',\n    component: About,\n  },\n])\n</script>\n\n<template>\n  <Flex class=\"h-screen\">\n    <div\n      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\"\n      :class=\"[isMac ? 'pt-8' : 'pt-4']\"\n      data-tauri-drag-region\n    >\n      <div class=\"flex flex-col items-center gap-2\">\n        <div class=\"b b-color-2 rounded-2xl b-solid\">\n          <img\n            class=\"size-15\"\n            data-tauri-drag-region\n            src=\"/logo.png\"\n          >\n        </div>\n\n        <span class=\"font-bold\">{{ appStore.name }}</span>\n      </div>\n\n      <div class=\"flex flex-col gap-2\">\n        <div\n          v-for=\"(item, index) in menus\"\n          :key=\"item.label\"\n          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\"\n          :class=\"{ 'bg-color-2! text-primary-5 dark:text-primary-7 font-bold dark:bg-color-8!': current === index }\"\n          @click=\"current = index\"\n        >\n          <div\n            class=\"size-8\"\n            :class=\"item.icon\"\n          />\n\n          <span>{{ item.label }}</span>\n        </div>\n      </div>\n    </div>\n\n    <div\n      v-for=\"(item, index) in menus\"\n      v-show=\"current === index\"\n      :key=\"item.label\"\n      class=\"flex-1 overflow-auto bg-color-8 dark:bg-color-2 p-4\"\n      data-tauri-drag-region\n    >\n      <component :is=\"item.component\" />\n    </div>\n  </Flex>\n\n  <UpdateApp />\n</template>\n"
  },
  {
    "path": "src/plugins/window.ts",
    "content": "import { invoke } from '@tauri-apps/api/core'\nimport { emit } from '@tauri-apps/api/event'\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\n\nimport { LISTEN_KEY } from '../constants'\n\ntype WindowLabel = 'main' | 'preference'\n\nconst COMMAND = {\n  SHOW_WINDOW: 'plugin:custom-window|show_window',\n  HIDE_WINDOW: 'plugin:custom-window|hide_window',\n  SET_ALWAYS_ON_TOP: 'plugin:custom-window|set_always_on_top',\n  SET_TASKBAR_VISIBILITY: 'plugin:custom-window|set_taskbar_visibility',\n}\n\nexport function showWindow(label?: WindowLabel) {\n  if (label) {\n    emit(LISTEN_KEY.SHOW_WINDOW, label)\n  } else {\n    invoke(COMMAND.SHOW_WINDOW)\n  }\n}\n\nexport function hideWindow(label?: WindowLabel) {\n  if (label) {\n    emit(LISTEN_KEY.HIDE_WINDOW, label)\n  } else {\n    invoke(COMMAND.HIDE_WINDOW)\n  }\n}\n\nexport function setAlwaysOnTop(alwaysOnTop: boolean) {\n  invoke(COMMAND.SET_ALWAYS_ON_TOP, { alwaysOnTop })\n}\n\nexport async function toggleWindowVisible(label?: WindowLabel) {\n  const appWindow = getCurrentWebviewWindow()\n\n  if (appWindow.label !== label) return\n\n  const visible = await appWindow.isVisible()\n\n  if (visible) {\n    return hideWindow(label)\n  }\n\n  return showWindow(label)\n}\n\nexport async function setTaskbarVisibility(visible: boolean) {\n  invoke(COMMAND.SET_TASKBAR_VISIBILITY, { visible })\n}\n"
  },
  {
    "path": "src/router/index.ts",
    "content": "import type { RouteRecordRaw } from 'vue-router'\n\nimport { createRouter, createWebHashHistory } from 'vue-router'\n\nimport Main from '../pages/main/index.vue'\nimport Preference from '../pages/preference/index.vue'\n\nconst routes: Readonly<RouteRecordRaw[]> = [\n  {\n    path: '/',\n    component: Main,\n  },\n  {\n    path: '/preference',\n    component: Preference,\n  },\n]\n\nconst router = createRouter({\n  history: createWebHashHistory(),\n  routes,\n})\n\nexport default router\n"
  },
  {
    "path": "src/stores/app.ts",
    "content": "import type { WindowState } from '@/composables/useWindowState'\n\nimport { getName, getVersion } from '@tauri-apps/api/app'\nimport { defineStore } from 'pinia'\nimport { reactive, ref } from 'vue'\n\nexport const useAppStore = defineStore('app', () => {\n  const name = ref('')\n  const version = ref('')\n  const windowState = reactive<WindowState>({})\n\n  const init = async () => {\n    name.value = await getName()\n    version.value = await getVersion()\n  }\n\n  return {\n    name,\n    version,\n    windowState,\n    init,\n  }\n})\n"
  },
  {
    "path": "src/stores/cat.ts",
    "content": "import { defineStore } from 'pinia'\nimport { reactive, ref } from 'vue'\n\nexport interface CatStore {\n  model: {\n    mirror: boolean\n    single: boolean\n    mouseMirror: boolean\n    autoReleaseDelay: number\n  }\n  window: {\n    visible: boolean\n    passThrough: boolean\n    alwaysOnTop: boolean\n    scale: number\n    opacity: number\n    radius: number\n    hideOnHover: boolean\n    position: 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight'\n  }\n}\n\nexport const useCatStore = defineStore('cat', () => {\n  /* ------------ 废弃字段（后续删除） ------------ */\n\n  /** @deprecated 请使用 `model.mirror` */\n  const mirrorMode = ref(false)\n\n  /** @deprecated 请使用 `model.single` */\n  const singleMode = ref(false)\n\n  /** @deprecated 请使用 `model.mouseMirror` */\n  const mouseMirror = ref(false)\n\n  /** @deprecated 请使用 `window.passThrough` */\n  const penetrable = ref(false)\n\n  /** @deprecated 请使用 `window.alwaysOnTop` */\n  const alwaysOnTop = ref(true)\n\n  /** @deprecated 请使用 `window.scale` */\n  const scale = ref(100)\n\n  /** @deprecated 请使用 `window.opacity` */\n  const opacity = ref(100)\n\n  /** @deprecated 用于标识数据是否已迁移，后续版本将删除 */\n  const migrated = ref(false)\n\n  const model = reactive<CatStore['model']>({\n    mirror: false,\n    single: false,\n    mouseMirror: false,\n    autoReleaseDelay: 3,\n  })\n\n  const window = reactive<CatStore['window']>({\n    visible: true,\n    passThrough: false,\n    alwaysOnTop: false,\n    scale: 100,\n    opacity: 100,\n    radius: 0,\n    hideOnHover: false,\n    position: 'bottomRight',\n  })\n\n  const init = () => {\n    if (migrated.value) return\n\n    model.mirror = mirrorMode.value\n    model.single = singleMode.value\n    model.mouseMirror = mouseMirror.value\n\n    window.visible = true\n    window.passThrough = penetrable.value\n    window.alwaysOnTop = alwaysOnTop.value\n    window.scale = scale.value\n    window.opacity = opacity.value\n\n    migrated.value = true\n  }\n\n  return {\n    migrated,\n    model,\n    window,\n    init,\n  }\n})\n"
  },
  {
    "path": "src/stores/general.ts",
    "content": "import type { Theme } from '@tauri-apps/api/window'\n\nimport { defineStore } from 'pinia'\nimport { getLocale } from 'tauri-plugin-locale-api'\nimport { reactive, ref } from 'vue'\n\nimport { LANGUAGE } from '@/constants'\n\nexport type Language = typeof LANGUAGE[keyof typeof LANGUAGE]\n\nexport interface GeneralStore {\n  app: {\n    autostart: boolean\n    taskbarVisible: boolean\n  }\n  appearance: {\n    theme: 'auto' | Theme\n    isDark: boolean\n    language?: Language\n  }\n  update: {\n    autoCheck: boolean\n  }\n}\n\nexport const useGeneralStore = defineStore('general', () => {\n  /* ------------ 废弃字段（后续删除） ------------ */\n\n  /** @deprecated 请使用 `update.autoCheck` */\n  const autoCheckUpdate = ref(false)\n\n  /** @deprecated 请使用 `app.autostart` */\n  const autostart = ref(false)\n\n  /** @deprecated 请使用 `app.taskbarVisible` */\n  const taskbarVisibility = ref(false)\n\n  /** @deprecated 请使用 `appearance.theme` */\n  const theme = ref<'auto' | Theme>('auto')\n\n  /** @deprecated 请使用 `appearance.isDark` */\n  const isDark = ref(false)\n\n  /** @deprecated 用于标识数据是否已迁移，后续版本将删除 */\n  const migrated = ref(false)\n\n  const app = reactive<GeneralStore['app']>({\n    autostart: false,\n    taskbarVisible: false,\n  })\n\n  const appearance = reactive<GeneralStore['appearance']>({\n    theme: 'auto',\n    isDark: false,\n  })\n\n  const update = reactive<GeneralStore['update']>({\n    autoCheck: false,\n  })\n\n  const getLanguage = async () => {\n    const locale = await getLocale<Language>()\n\n    if (Object.values(LANGUAGE).includes(locale)) {\n      return locale\n    }\n\n    return LANGUAGE.EN_US\n  }\n\n  const init = async () => {\n    appearance.language ??= await getLanguage()\n\n    if (migrated.value) return\n\n    app.autostart = autostart.value\n    app.taskbarVisible = taskbarVisibility.value\n\n    appearance.theme = theme.value\n    appearance.isDark = isDark.value\n\n    update.autoCheck = autoCheckUpdate.value\n\n    migrated.value = true\n  }\n\n  return {\n    migrated,\n    app,\n    appearance,\n    update,\n    init,\n  }\n})\n"
  },
  {
    "path": "src/stores/model.ts",
    "content": "import { resolveResource } from '@tauri-apps/api/path'\nimport { filter, find } from 'es-toolkit/compat'\nimport { nanoid } from 'nanoid'\nimport { defineStore } from 'pinia'\nimport { reactive, ref } from 'vue'\n\nimport { join } from '@/utils/path'\n\nexport type ModelMode = 'standard' | 'keyboard' | 'gamepad'\n\nexport interface Model {\n  id: string\n  path: string\n  mode: ModelMode\n  isPreset: boolean\n}\n\ninterface Motion {\n  Name: string\n  File: string\n  Sound?: string\n  FadeInTime: number\n  FadeOutTime: number\n  Description?: string\n}\n\ntype MotionGroup = Record<string, Motion[]>\n\ninterface Expression {\n  Name: string\n  File: string\n  Description?: string\n}\n\nexport const useModelStore = defineStore('model', () => {\n  const models = ref<Model[]>([])\n  const currentModel = ref<Model>()\n  const motions = ref<MotionGroup>({})\n  const expressions = ref<Expression[]>([])\n  const supportKeys = reactive<Record<string, string>>({})\n  const pressedKeys = reactive<Record<string, string>>({})\n\n  const init = async () => {\n    const modelsPath = await resolveResource('assets/models')\n\n    const nextModels = filter(models.value, { isPreset: false })\n    const presetModels = filter(models.value, { isPreset: true })\n\n    const modes: ModelMode[] = ['gamepad', 'keyboard', 'standard']\n\n    for (const mode of modes) {\n      const matched = find(presetModels, { mode })\n\n      nextModels.unshift({\n        id: matched?.id ?? nanoid(),\n        mode,\n        isPreset: true,\n        path: join(modelsPath, mode),\n      })\n    }\n\n    const matched = find(nextModels, { id: currentModel.value?.id })\n\n    currentModel.value = matched ?? nextModels[0]\n\n    models.value = nextModels\n  }\n\n  return {\n    models,\n    currentModel,\n    motions,\n    expressions,\n    supportKeys,\n    pressedKeys,\n    init,\n  }\n}, {\n  tauri: {\n    filterKeys: ['models', 'currentModel'],\n    filterKeysStrategy: 'pick',\n  },\n})\n"
  },
  {
    "path": "src/stores/shortcut.ts",
    "content": "import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop'\n\nexport const useShortcutStore = defineStore('shortcut', () => {\n  const visibleCat = ref('')\n  const visiblePreference = ref('')\n  const mirrorMode = ref('')\n  const penetrable = ref('')\n  const alwaysOnTop = ref('')\n\n  return {\n    visibleCat,\n    visiblePreference,\n    mirrorMode,\n    penetrable,\n    alwaysOnTop,\n  }\n})\n"
  },
  {
    "path": "src/utils/is.ts",
    "content": "export function isImage(value: string) {\n  const regex = /\\.(?:jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i\n\n  return regex.test(value)\n}\n\nexport function inBetween(value: number, minimum: number, maximum: number) {\n  return value >= minimum && value <= maximum\n}\n"
  },
  {
    "path": "src/utils/keyboard.ts",
    "content": "import { isMac } from './platform'\n\nexport interface Key {\n  eventKey: string\n  tauriKey?: string\n  symbol?: string\n}\n\nexport const modifierKeys: Key[] = [\n  {\n    eventKey: 'Shift',\n    symbol: isMac ? '⇧' : 'Shift',\n  },\n  {\n    eventKey: 'Control',\n    symbol: isMac ? '⌃' : 'Ctrl',\n  },\n  {\n    eventKey: 'Alt',\n    symbol: isMac ? '⌥' : 'Alt',\n  },\n  {\n    eventKey: 'Command',\n    symbol: isMac ? '⌘' : 'Super',\n  },\n].map((item) => {\n  return { ...item, tauriKey: item.eventKey }\n})\n\nexport const standardKeys: Key[] = [\n  // 第一排\n  {\n    eventKey: 'Escape',\n    symbol: isMac ? '⎋' : 'Esc',\n  },\n  {\n    eventKey: 'F1',\n  },\n  {\n    eventKey: 'F2',\n  },\n  {\n    eventKey: 'F3',\n  },\n  {\n    eventKey: 'F4',\n  },\n  {\n    eventKey: 'F5',\n  },\n  {\n    eventKey: 'F6',\n  },\n  {\n    eventKey: 'F7',\n  },\n  {\n    eventKey: 'F8',\n  },\n  {\n    eventKey: 'F9',\n  },\n  {\n    eventKey: 'F10',\n  },\n  {\n    eventKey: 'F11',\n  },\n  {\n    eventKey: 'F12',\n  }, // 第二排\n  {\n    eventKey: 'Backquote',\n    symbol: '`',\n  },\n  {\n    eventKey: 'Digit1',\n  },\n  {\n    eventKey: 'Digit2',\n  },\n  {\n    eventKey: 'Digit3',\n  },\n  {\n    eventKey: 'Digit4',\n  },\n  {\n    eventKey: 'Digit5',\n  },\n  {\n    eventKey: 'Digit6',\n  },\n  {\n    eventKey: 'Digit7',\n  },\n  {\n    eventKey: 'Digit8',\n  },\n  {\n    eventKey: 'Digit9',\n  },\n  {\n    eventKey: 'Digit0',\n  },\n  {\n    eventKey: 'Minus',\n    tauriKey: '-',\n    symbol: '-',\n  },\n  {\n    eventKey: 'Equal',\n    tauriKey: '=',\n    symbol: '=',\n  },\n  {\n    eventKey: 'Backspace',\n    symbol: isMac ? '⌫' : void 0,\n  },\n  // 第三排\n  {\n    eventKey: 'Tab',\n    symbol: isMac ? '⇥' : void 0,\n  },\n  {\n    eventKey: 'KeyQ',\n  },\n  {\n    eventKey: 'KeyW',\n  },\n  {\n    eventKey: 'KeyE',\n  },\n  {\n    eventKey: 'KeyR',\n  },\n  {\n    eventKey: 'KeyT',\n  },\n  {\n    eventKey: 'KeyY',\n  },\n  {\n    eventKey: 'KeyU',\n  },\n  {\n    eventKey: 'KeyI',\n  },\n  {\n    eventKey: 'KeyO',\n  },\n  {\n    eventKey: 'KeyP',\n  },\n  {\n    eventKey: 'BracketLeft',\n    symbol: '[',\n  },\n  {\n    eventKey: 'BracketRight',\n    symbol: ']',\n  },\n  {\n    eventKey: 'Backslash',\n    symbol: '\\\\',\n  },\n  // 第四排\n  {\n    eventKey: 'KeyA',\n  },\n  {\n    eventKey: 'KeyS',\n  },\n  {\n    eventKey: 'KeyD',\n  },\n  {\n    eventKey: 'KeyF',\n  },\n  {\n    eventKey: 'KeyG',\n  },\n  {\n    eventKey: 'KeyH',\n  },\n  {\n    eventKey: 'KeyJ',\n  },\n  {\n    eventKey: 'KeyK',\n  },\n  {\n    eventKey: 'KeyL',\n  },\n  {\n    eventKey: 'Semicolon',\n    symbol: ';',\n  },\n  {\n    eventKey: 'Quote',\n    symbol: '\\'',\n  },\n  {\n    eventKey: 'Enter',\n    symbol: isMac ? '↩︎' : void 0,\n  },\n  // 第五排\n  {\n    eventKey: 'KeyZ',\n  },\n  {\n    eventKey: 'KeyX',\n  },\n  {\n    eventKey: 'KeyC',\n  },\n  {\n    eventKey: 'KeyV',\n  },\n  {\n    eventKey: 'KeyB',\n  },\n  {\n    eventKey: 'KeyN',\n  },\n  {\n    eventKey: 'KeyM',\n  },\n  {\n    eventKey: 'Comma',\n    symbol: ',',\n  },\n  {\n    eventKey: 'Period',\n    symbol: '.',\n  },\n  {\n    eventKey: 'Slash',\n    symbol: '/',\n  },\n  // 第六排\n  {\n    eventKey: 'Space',\n    symbol: isMac ? '␣' : void 0,\n  },\n  // 方向键\n  {\n    eventKey: 'ArrowUp',\n    symbol: '↑',\n  },\n  {\n    eventKey: 'ArrowDown',\n    symbol: '↓',\n  },\n  {\n    eventKey: 'ArrowLeft',\n    symbol: '←',\n  },\n  {\n    eventKey: 'ArrowRight',\n    symbol: '→',\n  },\n].map((item) => {\n  const { eventKey } = item\n\n  item.symbol ??= eventKey\n  item.tauriKey ??= eventKey\n\n  if (eventKey.startsWith('Digit') || eventKey.startsWith('Key')) {\n    item.tauriKey = item.symbol = eventKey.slice(-1)\n  }\n\n  return item\n})\n\nexport const keys = modifierKeys.concat(standardKeys)\n"
  },
  {
    "path": "src/utils/live2d.ts",
    "content": "import type { ModelSize } from '@/composables/useModel'\nimport type { Cubism4InternalModel } from 'pixi-live2d-display'\n\nimport { convertFileSrc } from '@tauri-apps/api/core'\nimport { readDir, readTextFile } from '@tauri-apps/plugin-fs'\nimport JSON5 from 'json5'\nimport { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'\nimport { Application, Ticker } from 'pixi.js'\n\nimport { join } from './path'\n\nimport { i18n } from '@/locales'\n\nLive2DModel.registerTicker(Ticker)\n\nclass Live2d {\n  private app: Application | null = null\n  public model: Live2DModel | null = null\n\n  constructor() { }\n\n  private initApp() {\n    if (this.app) return\n\n    const view = document.getElementById('live2dCanvas') as HTMLCanvasElement\n\n    this.app = new Application({\n      view,\n      resizeTo: window,\n      backgroundAlpha: 0,\n      resolution: devicePixelRatio,\n    })\n  }\n\n  public async load(path: string) {\n    this.initApp()\n\n    this.destroy()\n\n    const files = await readDir(path)\n\n    const modelFile = files.find(file => file.name.endsWith('.model3.json'))\n\n    if (!modelFile) {\n      throw new Error(i18n.global.t('utils.live2d.hints.notFound'))\n    }\n\n    const modelPath = join(path, modelFile.name)\n\n    const modelJSON = JSON5.parse(await readTextFile(modelPath))\n\n    const modelSettings = new Cubism4ModelSettings({\n      ...modelJSON,\n      url: convertFileSrc(modelPath),\n    })\n\n    modelSettings.replaceFiles((file) => {\n      return convertFileSrc(join(path, file))\n    })\n\n    this.model = await Live2DModel.from(modelSettings)\n\n    this.app?.stage.addChild(this.model)\n\n    const { width, height } = this.model\n    const { motions, expressions } = modelSettings\n\n    return {\n      width,\n      height,\n      motions,\n      expressions,\n    }\n  }\n\n  public destroy() {\n    if (!this.model) return\n\n    this.model?.destroy()\n\n    this.model = null\n  }\n\n  public resizeModel(modelSize: ModelSize) {\n    if (!this.model) return\n\n    const { width, height } = modelSize\n\n    const scaleX = innerWidth / width\n    const scaleY = innerHeight / height\n    const scale = Math.min(scaleX, scaleY)\n\n    this.model.scale.set(scale)\n    this.model.x = innerWidth / 2\n    this.model.y = innerHeight / 2\n    this.model.anchor.set(0.5)\n  }\n\n  public playMotion(group: string, index: number) {\n    return this.model?.motion(group, index)\n  }\n\n  public playExpressions(index: number) {\n    return this.model?.expression(index)\n  }\n\n  public getCoreModel() {\n    const internalModel = this.model?.internalModel as Cubism4InternalModel\n\n    return internalModel?.coreModel\n  }\n\n  public getParameterRange(id: string) {\n    const coreModel = this.getCoreModel()\n\n    const index = coreModel?.getParameterIndex(id)\n    const min = coreModel?.getParameterMinimumValue(index)\n    const max = coreModel?.getParameterMaximumValue(index)\n\n    return {\n      min,\n      max,\n    }\n  }\n\n  public setParameterValue(id: string, value: number | boolean) {\n    const coreModel = this.getCoreModel()\n\n    return coreModel?.setParameterValueById?.(id, Number(value))\n  }\n}\n\nconst live2d = new Live2d()\n\nexport default live2d\n"
  },
  {
    "path": "src/utils/monitor.ts",
    "content": "import type { PhysicalPosition } from '@tauri-apps/api/window'\n\nimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'\nimport { cursorPosition, monitorFromPoint } from '@tauri-apps/api/window'\n\nexport async function getCursorMonitor(cursorPoint?: PhysicalPosition) {\n  cursorPoint ??= await cursorPosition()\n\n  const appWindow = getCurrentWebviewWindow()\n\n  const scaleFactor = await appWindow.scaleFactor()\n\n  const { x, y } = cursorPoint.toLogical(scaleFactor)\n\n  const monitor = await monitorFromPoint(x, y)\n\n  if (!monitor) return\n\n  return monitor\n}\n"
  },
  {
    "path": "src/utils/path.ts",
    "content": "import { sep } from '@tauri-apps/api/path'\n\nexport function join(...paths: string[]) {\n  const joinPaths = paths.map((path, index) => {\n    if (index === 0) {\n      return path.replace(new RegExp(`${sep()}+$`), '')\n    } else {\n      return path.replace(new RegExp(`^${sep()}+|${sep()}+$`, 'g'), '')\n    }\n  })\n\n  return joinPaths.join(sep())\n}\n"
  },
  {
    "path": "src/utils/platform.ts",
    "content": "import { platform } from '@tauri-apps/plugin-os'\n\nexport const isMac = platform() === 'macos'\n\nexport const isWindows = platform() === 'windows'\n\nexport const isLinux = platform() === 'linux'\n"
  },
  {
    "path": "src/utils/shared.ts",
    "content": "import { castArray } from 'es-toolkit/compat'\n\nexport function clearObject<T extends Record<string, unknown>>(targets: T | T[]) {\n  for (const target of castArray<T>(targets)) {\n    for (const key of Object.keys(target)) {\n      delete target[key]\n    }\n  }\n}\n"
  },
  {
    "path": "src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n\ndeclare module '*.vue' {\n  import type { DefineComponent } from 'vue'\n\n  const component: DefineComponent<object, object, any>\n  export default component\n}\n"
  },
  {
    "path": "src-tauri/.gitignore",
    "content": "# Generated by Cargo\n# will have compiled files and executables\n/target/\n\n# Generated by Tauri\n# will have schema files for capabilities auto-completion\n/gen/schemas\n\nicons\nautogenerated\nschemas"
  },
  {
    "path": "src-tauri/BongoCat.desktop",
    "content": "[Desktop Entry]\nType=Application\nName={{{name}}}\nExec={{{exec}}}\nIcon={{{icon}}}\nCategories={{{categories}}}\nComment={{{comment}}}\nTerminal=false\n"
  },
  {
    "path": "src-tauri/Cargo.toml",
    "content": "[package]\nname = \"bongo-cat\"\nversion = \"0.9.0\"\ndescription = \"A Tauri App\"\nauthors = [ \"ayangweb\" ]\nedition = \"2024\"\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n[lib]\n# The `_lib` suffix may seem redundant but it is necessary\n# to make the lib name unique and wouldn't conflict with the bin name.\n# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519\nname = \"bongo_cat_lib\"\ncrate-type = [\"staticlib\", \"cdylib\", \"rlib\"]\n\n[build-dependencies]\ntauri-build = { version = \"2\", features = [] }\n\n[dependencies]\ntauri = { workspace = true, features = [\"tray-icon\", \"protocol-asset\", \"macos-private-api\", \"image-png\"] }\nserde = { workspace = true, features = [\"derive\"] }\nserde_json.workspace = true\ntauri-plugin-custom-window.workspace = true\ntauri-plugin-os = \"2\"\ntauri-plugin-process = \"2\"\ntauri-plugin-opener = \"2\"\ntauri-plugin-pinia = \"3\"\ntauri-plugin-log = \"2\"\ntauri-plugin-updater = \"2\"\ntauri-plugin-prevent-default = \"1\"\ntauri-plugin-single-instance = \"2\"\ntauri-plugin-autostart = \"2\"\ntauri-plugin-macos-permissions = \"2\"\ntauri-plugin-dialog = \"2\"\ntauri-plugin-fs = \"2\"\nfs_extra = \"1\"\ntauri-plugin-clipboard-manager = \"2\"\ntauri-plugin-global-shortcut = \"2\"\ntauri-plugin-locale = \"2\"\nrdev = { git = \"https://github.com/kunkunsh/rdev\" }\ngilrs = { git = \"https://github.com/ayangweb/gilrs\", default-features = false, features = [\"xinput\"] }\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies]\ntauri-nspanel.workspace = true\n\n[features]\ncargo-clippy = []\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/cat.model3.json",
    "content": "{\n  \"Version\": 3,\n  \"FileReferences\": {\n    \"Moc\": \"demomodel3.moc3\",\n    \"Textures\": [\n      \"demomodel3.1024/texture_00.png\",\n      \"demomodel3.1024/texture_01.png\",\n      \"demomodel3.1024/texture_02.png\"\n    ],\n    \"DisplayInfo\": \"demomodel3.cdi3.json\",\n    \"Expressions\": [\n      {\n        \"Name\": \"live2d_expression0.exp3.json\",\n        \"File\": \"live2d_expression0.exp3.json\"\n      },\n      {\n        \"Name\": \"live2d_expression1.exp3.json\",\n        \"File\": \"live2d_expression1.exp3.json\"\n      },\n      {\n        \"Name\": \"live2d_expression2.exp3.json\",\n        \"File\": \"live2d_expression2.exp3.json\"\n      }\n    ],\n    \"Motions\": {\n      \"CAT_motion\": [\n        {\n          \"File\": \"live2d_motion1.motion3.json\",\n          \"Sound\": \"live2d_motion1.flac\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        },\n        {\n          \"File\": \"live2d_motion2.motion3.json\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        }\n      ],\n      \"CAT_motion_lock\": [\n        {\n          \"File\": \"live2d_motion1.motion3.json\",\n          \"Sound\": \"live2d_motion1.flac\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        },\n        {\n          \"File\": \"live2d_motion2.motion3.json\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        }\n      ]\n    }\n  },\n  \"Groups\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Name\": \"LipSync\",\n      \"Ids\": []\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Name\": \"EyeBlink\",\n      \"Ids\": [\n        \"ParamEyeLOpen\",\n        \"ParamEyeROpen\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/demomodel3.cdi3.json",
    "content": "{\n  \"Version\": 3,\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 X\"\n    },\n    {\n      \"Id\": \"ParamAngleY\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 Y\"\n    },\n    {\n      \"Id\": \"CatParamLeftHandDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"左手按下\"\n    },\n    {\n      \"Id\": \"CatParamRightHandDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"右手按下\"\n    },\n    {\n      \"Id\": \"CatParamStickLeftDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"左摇杆点亮\"\n    },\n    {\n      \"Id\": \"CatParamStickRightDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"右摇杆点亮\"\n    },\n    {\n      \"Id\": \"CatParamStickShowLeftHand\",\n      \"GroupId\": \"\",\n      \"Name\": \"显示摇杆左手\"\n    },\n    {\n      \"Id\": \"CatParamStickShowRightHand\",\n      \"GroupId\": \"\",\n      \"Name\": \"显示摇杆右手\"\n    },\n    {\n      \"Id\": \"CatParamStickLX\",\n      \"GroupId\": \"\",\n      \"Name\": \"左摇杆X\"\n    },\n    {\n      \"Id\": \"CatParamStickLY\",\n      \"GroupId\": \"\",\n      \"Name\": \"左摇杆Y\"\n    },\n    {\n      \"Id\": \"CatParamStickRX\",\n      \"GroupId\": \"\",\n      \"Name\": \"右摇杆X\"\n    },\n    {\n      \"Id\": \"CatParamStickRY\",\n      \"GroupId\": \"\",\n      \"Name\": \"右摇杆Y\"\n    },\n    {\n      \"Id\": \"ParamAngleZ\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 Z\"\n    },\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眼 开闭\"\n    },\n    {\n      \"Id\": \"ParamEyeLSmile\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眼 微笑\"\n    },\n    {\n      \"Id\": \"ParamEyeROpen\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眼\"\n    },\n    {\n      \"Id\": \"ParamEyeRSmile\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眼 微笑\"\n    },\n    {\n      \"Id\": \"Param3\",\n      \"GroupId\": \"\",\n      \"Name\": \"挥手\"\n    },\n    {\n      \"Id\": \"Param\",\n      \"GroupId\": \"ParamGroup\",\n      \"Name\": \"开启闪电\"\n    },\n    {\n      \"Id\": \"Param2\",\n      \"GroupId\": \"ParamGroup\",\n      \"Name\": \"闪电划过\"\n    },\n    {\n      \"Id\": \"Param4\",\n      \"GroupId\": \"ParamGroup2\",\n      \"Name\": \"表情:thuglife\"\n    },\n    {\n      \"Id\": \"Param5\",\n      \"GroupId\": \"ParamGroup2\",\n      \"Name\": \"表情:升天\"\n    },\n    {\n      \"Id\": \"ParamEyeBallX\",\n      \"GroupId\": \"\",\n      \"Name\": \"眼球 X\"\n    },\n    {\n      \"Id\": \"ParamEyeBallY\",\n      \"GroupId\": \"\",\n      \"Name\": \"眼球 Y\"\n    },\n    {\n      \"Id\": \"ParamBrowLY\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉上下\"\n    },\n    {\n      \"Id\": \"ParamBrowRY\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 上下\"\n    },\n    {\n      \"Id\": \"ParamBrowLX\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 左右\"\n    },\n    {\n      \"Id\": \"ParamBrowRX\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 左右\"\n    },\n    {\n      \"Id\": \"ParamBrowLAngle\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 角度\"\n    },\n    {\n      \"Id\": \"ParamBrowRAngle\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 角度\"\n    },\n    {\n      \"Id\": \"ParamBrowLForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 変形\"\n    },\n    {\n      \"Id\": \"ParamBrowRForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 変形\"\n    },\n    {\n      \"Id\": \"ParamMouthForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"嘴部 变形\"\n    },\n    {\n      \"Id\": \"ParamMouthOpenY\",\n      \"GroupId\": \"\",\n      \"Name\": \"嘴巴 张开和闭合\"\n    },\n    {\n      \"Id\": \"ParamCheek\",\n      \"GroupId\": \"\",\n      \"Name\": \"脸颊\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 X\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleY\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 Y\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleZ\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 Z\"\n    },\n    {\n      \"Id\": \"ParamBreath\",\n      \"GroupId\": \"\",\n      \"Name\": \"呼吸\"\n    },\n    {\n      \"Id\": \"ParamHairFront\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 前发\"\n    },\n    {\n      \"Id\": \"ParamHairSide\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 侧发\"\n    },\n    {\n      \"Id\": \"ParamHairBack\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 后发\"\n    }\n  ],\n  \"ParameterGroups\": [\n    {\n      \"Id\": \"ParamGroup\",\n      \"GroupId\": \"\",\n      \"Name\": \"闪电\"\n    },\n    {\n      \"Id\": \"ParamGroup2\",\n      \"GroupId\": \"\",\n      \"Name\": \"表情\"\n    }\n  ],\n  \"Parts\": [\n    {\n      \"Id\": \"Part12\",\n      \"Name\": \"右手\"\n    },\n    {\n      \"Id\": \"Part9\",\n      \"Name\": \"左手\"\n    },\n    {\n      \"Id\": \"Part11\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part7\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part3\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part2\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part10\",\n      \"Name\": \"天使环\"\n    },\n    {\n      \"Id\": \"Part5\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"PartSketch0\",\n      \"Name\": \"[ 参考图 ]\"\n    },\n    {\n      \"Id\": \"rightstick\",\n      \"Name\": \"rightstick\"\n    },\n    {\n      \"Id\": \"leftstick\",\n      \"Name\": \"leftstick\"\n    },\n    {\n      \"Id\": \"Part8\",\n      \"Name\": \"thug life\"\n    },\n    {\n      \"Id\": \"Part6\",\n      \"Name\": \"闪电\"\n    },\n    {\n      \"Id\": \"Part4\",\n      \"Name\": \"闪电\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/exp_1.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": 0.321,\n      \"Blend\": \"Multiply\"\n    },\n    {\n      \"Id\": \"ParamEyeROpen\",\n      \"Value\": 0.313,\n      \"Blend\": \"Multiply\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/exp_2.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": -1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_expression0.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_expression1.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.8,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param4\",\n      \"Value\": 1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_expression2.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.5,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param5\",\n      \"Value\": 1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_motion1.motion3.json",
    "content": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 1.633,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": false,\n    \"CurveCount\": 2,\n    \"TotalSegmentCount\": 8,\n    \"TotalPointCount\": 20,\n    \"UserDataCount\": 0,\n    \"TotalUserDataSize\": 0\n  },\n  \"Curves\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param\",\n      \"Segments\": [\n        0,\n        0,\n        1,\n        0.033,\n        0,\n        0.067,\n        1,\n        0.1,\n        1,\n        1,\n        0.411,\n        1,\n        0.722,\n        1,\n        1.033,\n        1,\n        1,\n        1.189,\n        1,\n        1.344,\n        0,\n        1.5,\n        0,\n        0,\n        1.633,\n        0\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param2\",\n      \"Segments\": [\n        0,\n        0,\n        0,\n        0.067,\n        0,\n        1,\n        0.1,\n        0,\n        0.133,\n        0.142,\n        0.167,\n        0.2,\n        1,\n        0.489,\n        0.764,\n        0.811,\n        1,\n        1.133,\n        1,\n        0,\n        1.633,\n        1\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/gamepad/live2d_motion2.motion3.json",
    "content": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 2.333,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": true,\n    \"CurveCount\": 2,\n    \"TotalSegmentCount\": 7,\n    \"TotalPointCount\": 21,\n    \"UserDataCount\": 0,\n    \"TotalUserDataSize\": 0\n  },\n  \"Curves\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"CatParamLeftHandDown\",\n      \"Segments\": [\n        0,\n        0,\n        0,\n        2.333,\n        0\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param3\",\n      \"Segments\": [\n        0,\n        0,\n        1,\n        0.133,\n        0,\n        0.267,\n        30,\n        0.4,\n        30,\n        1,\n        0.522,\n        30,\n        0.644,\n        0,\n        0.767,\n        0,\n        1,\n        0.9,\n        0,\n        1.033,\n        30,\n        1.167,\n        30,\n        1,\n        1.3,\n        30,\n        1.433,\n        0,\n        1.567,\n        0,\n        1,\n        1.7,\n        0,\n        1.833,\n        30,\n        1.967,\n        30,\n        1,\n        2.089,\n        30,\n        2.211,\n        0,\n        2.333,\n        0\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/cat.model3.json",
    "content": "{\n  \"Version\": 3,\n  \"FileReferences\": {\n    \"Moc\": \"demomodel2.moc3\",\n    \"Textures\": [\n      \"demomodel2.1024/texture_00.png\",\n      \"demomodel2.1024/texture_01.png\",\n      \"demomodel2.1024/texture_02.png\"\n    ],\n    \"DisplayInfo\": \"demomodel2.cdi3.json\",\n    \"Expressions\": [\n      {\n        \"Name\": \"live2d_expression0.exp3.json\",\n        \"File\": \"live2d_expression0.exp3.json\"\n      },\n      {\n        \"Name\": \"live2d_expression1.exp3.json\",\n        \"File\": \"live2d_expression1.exp3.json\"\n      },\n      {\n        \"Name\": \"live2d_expression2.exp3.json\",\n        \"File\": \"live2d_expression2.exp3.json\"\n      }\n    ],\n    \"Motions\": {\n      \"CAT_motion\": [\n        {\n          \"File\": \"live2d_motion1.motion3.json\",\n          \"Sound\": \"live2d_motion1.flac\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        },\n        {\n          \"File\": \"live2d_motion2.motion3.json\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        }\n      ],\n      \"CAT_motion_lock\": [\n        {\n          \"File\": \"live2d_motion1.motion3.json\",\n          \"Sound\": \"live2d_motion1.flac\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        },\n        {\n          \"File\": \"live2d_motion2.motion3.json\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        }\n      ]\n    }\n  },\n  \"Groups\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Name\": \"EyeBlink\",\n      \"Ids\": [\n        \"ParamEyeLOpen\",\n        \"ParamEyeROpen\"\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Name\": \"LipSync\",\n      \"Ids\": []\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/demomodel2.cdi3.json",
    "content": "{\n  \"Version\": 3,\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 X\"\n    },\n    {\n      \"Id\": \"ParamAngleY\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 Y\"\n    },\n    {\n      \"Id\": \"CatParamRightHandDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"右手按下\"\n    },\n    {\n      \"Id\": \"CatParamLeftHandDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"左手按下\"\n    },\n    {\n      \"Id\": \"ParamAngleZ\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 Z\"\n    },\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眼 开闭\"\n    },\n    {\n      \"Id\": \"ParamEyeLSmile\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眼 微笑\"\n    },\n    {\n      \"Id\": \"ParamEyeROpen\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眼\"\n    },\n    {\n      \"Id\": \"ParamEyeRSmile\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眼 微笑\"\n    },\n    {\n      \"Id\": \"Param3\",\n      \"GroupId\": \"\",\n      \"Name\": \"挥手\"\n    },\n    {\n      \"Id\": \"Param\",\n      \"GroupId\": \"ParamGroup\",\n      \"Name\": \"开启闪电\"\n    },\n    {\n      \"Id\": \"Param2\",\n      \"GroupId\": \"ParamGroup\",\n      \"Name\": \"闪电划过\"\n    },\n    {\n      \"Id\": \"Param4\",\n      \"GroupId\": \"ParamGroup2\",\n      \"Name\": \"表情:thuglife\"\n    },\n    {\n      \"Id\": \"Param5\",\n      \"GroupId\": \"ParamGroup2\",\n      \"Name\": \"表情:升天\"\n    },\n    {\n      \"Id\": \"ParamEyeBallX\",\n      \"GroupId\": \"\",\n      \"Name\": \"眼球 X\"\n    },\n    {\n      \"Id\": \"ParamEyeBallY\",\n      \"GroupId\": \"\",\n      \"Name\": \"眼球 Y\"\n    },\n    {\n      \"Id\": \"ParamBrowLY\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉上下\"\n    },\n    {\n      \"Id\": \"ParamBrowRY\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 上下\"\n    },\n    {\n      \"Id\": \"ParamBrowLX\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 左右\"\n    },\n    {\n      \"Id\": \"ParamBrowRX\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 左右\"\n    },\n    {\n      \"Id\": \"ParamBrowLAngle\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 角度\"\n    },\n    {\n      \"Id\": \"ParamBrowRAngle\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 角度\"\n    },\n    {\n      \"Id\": \"ParamBrowLForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 変形\"\n    },\n    {\n      \"Id\": \"ParamBrowRForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 変形\"\n    },\n    {\n      \"Id\": \"ParamMouthForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"嘴部 变形\"\n    },\n    {\n      \"Id\": \"ParamMouthOpenY\",\n      \"GroupId\": \"\",\n      \"Name\": \"嘴巴 张开和闭合\"\n    },\n    {\n      \"Id\": \"ParamCheek\",\n      \"GroupId\": \"\",\n      \"Name\": \"脸颊\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 X\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleY\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 Y\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleZ\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 Z\"\n    },\n    {\n      \"Id\": \"ParamBreath\",\n      \"GroupId\": \"\",\n      \"Name\": \"呼吸\"\n    },\n    {\n      \"Id\": \"ParamHairFront\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 前发\"\n    },\n    {\n      \"Id\": \"ParamHairSide\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 侧发\"\n    },\n    {\n      \"Id\": \"ParamHairBack\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 后发\"\n    }\n  ],\n  \"ParameterGroups\": [\n    {\n      \"Id\": \"ParamGroup\",\n      \"GroupId\": \"\",\n      \"Name\": \"闪电\"\n    },\n    {\n      \"Id\": \"ParamGroup2\",\n      \"GroupId\": \"\",\n      \"Name\": \"表情\"\n    }\n  ],\n  \"Parts\": [\n    {\n      \"Id\": \"Part11\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part7\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part3\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part2\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part10\",\n      \"Name\": \"天使环\"\n    },\n    {\n      \"Id\": \"Part5\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"PartSketch0\",\n      \"Name\": \"[ 参考图 ]\"\n    },\n    {\n      \"Id\": \"Part8\",\n      \"Name\": \"thug life\"\n    },\n    {\n      \"Id\": \"Part6\",\n      \"Name\": \"闪电\"\n    },\n    {\n      \"Id\": \"Part4\",\n      \"Name\": \"闪电\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/exp_1.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": 0.321,\n      \"Blend\": \"Multiply\"\n    },\n    {\n      \"Id\": \"ParamEyeROpen\",\n      \"Value\": 0.313,\n      \"Blend\": \"Multiply\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/exp_2.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": -1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_expression0.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_expression1.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.8,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param4\",\n      \"Value\": 1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_expression2.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.5,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param5\",\n      \"Value\": 1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_motion1.motion3.json",
    "content": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 1.633,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": false,\n    \"CurveCount\": 2,\n    \"TotalSegmentCount\": 8,\n    \"TotalPointCount\": 20,\n    \"UserDataCount\": 0,\n    \"TotalUserDataSize\": 0\n  },\n  \"Curves\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param\",\n      \"Segments\": [\n        0,\n        0,\n        1,\n        0.033,\n        0,\n        0.067,\n        1,\n        0.1,\n        1,\n        1,\n        0.411,\n        1,\n        0.722,\n        1,\n        1.033,\n        1,\n        1,\n        1.189,\n        1,\n        1.344,\n        0,\n        1.5,\n        0,\n        0,\n        1.633,\n        0\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param2\",\n      \"Segments\": [\n        0,\n        0,\n        0,\n        0.067,\n        0,\n        1,\n        0.1,\n        0,\n        0.133,\n        0.142,\n        0.167,\n        0.2,\n        1,\n        0.489,\n        0.764,\n        0.811,\n        1,\n        1.133,\n        1,\n        0,\n        1.633,\n        1\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/keyboard/live2d_motion2.motion3.json",
    "content": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 2.333,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": true,\n    \"CurveCount\": 2,\n    \"TotalSegmentCount\": 7,\n    \"TotalPointCount\": 21,\n    \"UserDataCount\": 0,\n    \"TotalUserDataSize\": 0\n  },\n  \"Curves\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"CatParamLeftHandDown\",\n      \"Segments\": [\n        0,\n        0,\n        0,\n        2.333,\n        0\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param3\",\n      \"Segments\": [\n        0,\n        0,\n        1,\n        0.133,\n        0,\n        0.267,\n        30,\n        0.4,\n        30,\n        1,\n        0.522,\n        30,\n        0.644,\n        0,\n        0.767,\n        0,\n        1,\n        0.9,\n        0,\n        1.033,\n        30,\n        1.167,\n        30,\n        1,\n        1.3,\n        30,\n        1.433,\n        0,\n        1.567,\n        0,\n        1,\n        1.7,\n        0,\n        1.833,\n        30,\n        1.967,\n        30,\n        1,\n        2.089,\n        30,\n        2.211,\n        0,\n        2.333,\n        0\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/cat.model3.json",
    "content": "{\n  \"Version\": 3,\n  \"FileReferences\": {\n    \"Moc\": \"demomodel.moc3\",\n    \"Textures\": [\n      \"demomodel.1024/texture_00.png\",\n      \"demomodel.1024/texture_01.png\",\n      \"demomodel.1024/texture_02.png\"\n    ],\n    \"DisplayInfo\": \"demomodel.cdi3.json\",\n    \"Expressions\": [\n      {\n        \"Name\": \"live2d_expression0.exp3.json\",\n        \"File\": \"live2d_expression0.exp3.json\"\n      },\n      {\n        \"Name\": \"live2d_expression1.exp3.json\",\n        \"File\": \"live2d_expression1.exp3.json\"\n      },\n      {\n        \"Name\": \"live2d_expression2.exp3.json\",\n        \"File\": \"live2d_expression2.exp3.json\"\n      }\n    ],\n    \"Motions\": {\n      \"CAT_motion\": [\n        {\n          \"File\": \"live2d_motion1.motion3.json\",\n          \"Sound\": \"live2d_motion1.flac\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        },\n        {\n          \"File\": \"live2d_motion2.motion3.json\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        }\n      ],\n      \"CAT_motion_lock\": [\n        {\n          \"File\": \"live2d_motion1.motion3.json\",\n          \"Sound\": \"live2d_motion1.flac\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        },\n        {\n          \"File\": \"live2d_motion2.motion3.json\",\n          \"FadeInTime\": 0,\n          \"FadeOutTime\": 0\n        }\n      ]\n    }\n  },\n  \"Groups\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Name\": \"EyeBlink\",\n      \"Ids\": [\n        \"ParamEyeLOpen\",\n        \"ParamEyeROpen\"\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Name\": \"LipSync\",\n      \"Ids\": []\n    }\n  ],\n  \"HitAreas\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/demomodel.cdi3.json",
    "content": "{\n  \"Version\": 3,\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 X\"\n    },\n    {\n      \"Id\": \"ParamAngleY\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 Y\"\n    },\n    {\n      \"Id\": \"ParamMouseX\",\n      \"GroupId\": \"\",\n      \"Name\": \"鼠标X\"\n    },\n    {\n      \"Id\": \"ParamMouseY\",\n      \"GroupId\": \"\",\n      \"Name\": \"鼠标Y\"\n    },\n    {\n      \"Id\": \"ParamMouseLeftDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"鼠标左键按下\"\n    },\n    {\n      \"Id\": \"ParamMouseRightDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"鼠标右键按下\"\n    },\n    {\n      \"Id\": \"CatParamLeftHandDown\",\n      \"GroupId\": \"\",\n      \"Name\": \"键盘按下\"\n    },\n    {\n      \"Id\": \"ParamAngleZ\",\n      \"GroupId\": \"\",\n      \"Name\": \"角度 Z\"\n    },\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眼 开闭\"\n    },\n    {\n      \"Id\": \"ParamEyeLSmile\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眼 微笑\"\n    },\n    {\n      \"Id\": \"ParamEyeROpen\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眼\"\n    },\n    {\n      \"Id\": \"ParamEyeRSmile\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眼 微笑\"\n    },\n    {\n      \"Id\": \"Param3\",\n      \"GroupId\": \"\",\n      \"Name\": \"挥手\"\n    },\n    {\n      \"Id\": \"Param\",\n      \"GroupId\": \"ParamGroup\",\n      \"Name\": \"开启闪电\"\n    },\n    {\n      \"Id\": \"Param2\",\n      \"GroupId\": \"ParamGroup\",\n      \"Name\": \"闪电划过\"\n    },\n    {\n      \"Id\": \"Param4\",\n      \"GroupId\": \"ParamGroup2\",\n      \"Name\": \"表情:thuglife\"\n    },\n    {\n      \"Id\": \"Param5\",\n      \"GroupId\": \"ParamGroup2\",\n      \"Name\": \"表情:升天\"\n    },\n    {\n      \"Id\": \"ParamEyeBallX\",\n      \"GroupId\": \"\",\n      \"Name\": \"眼球 X\"\n    },\n    {\n      \"Id\": \"ParamEyeBallY\",\n      \"GroupId\": \"\",\n      \"Name\": \"眼球 Y\"\n    },\n    {\n      \"Id\": \"ParamBrowLY\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉上下\"\n    },\n    {\n      \"Id\": \"ParamBrowRY\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 上下\"\n    },\n    {\n      \"Id\": \"ParamBrowLX\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 左右\"\n    },\n    {\n      \"Id\": \"ParamBrowRX\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 左右\"\n    },\n    {\n      \"Id\": \"ParamBrowLAngle\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 角度\"\n    },\n    {\n      \"Id\": \"ParamBrowRAngle\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 角度\"\n    },\n    {\n      \"Id\": \"ParamBrowLForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"左眉 変形\"\n    },\n    {\n      \"Id\": \"ParamBrowRForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"右眉 変形\"\n    },\n    {\n      \"Id\": \"ParamMouthForm\",\n      \"GroupId\": \"\",\n      \"Name\": \"嘴部 变形\"\n    },\n    {\n      \"Id\": \"ParamMouthOpenY\",\n      \"GroupId\": \"\",\n      \"Name\": \"嘴巴 张开和闭合\"\n    },\n    {\n      \"Id\": \"ParamCheek\",\n      \"GroupId\": \"\",\n      \"Name\": \"脸颊\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleX\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 X\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleY\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 Y\"\n    },\n    {\n      \"Id\": \"ParamBodyAngleZ\",\n      \"GroupId\": \"\",\n      \"Name\": \"身体旋转 Z\"\n    },\n    {\n      \"Id\": \"ParamBreath\",\n      \"GroupId\": \"\",\n      \"Name\": \"呼吸\"\n    },\n    {\n      \"Id\": \"ParamHairFront\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 前发\"\n    },\n    {\n      \"Id\": \"ParamHairSide\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 侧发\"\n    },\n    {\n      \"Id\": \"ParamHairBack\",\n      \"GroupId\": \"\",\n      \"Name\": \"摇动 后发\"\n    }\n  ],\n  \"ParameterGroups\": [\n    {\n      \"Id\": \"ParamGroup\",\n      \"GroupId\": \"\",\n      \"Name\": \"闪电\"\n    },\n    {\n      \"Id\": \"ParamGroup2\",\n      \"GroupId\": \"\",\n      \"Name\": \"表情\"\n    }\n  ],\n  \"Parts\": [\n    {\n      \"Id\": \"Part11\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part7\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part3\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part2\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part10\",\n      \"Name\": \"天使环\"\n    },\n    {\n      \"Id\": \"Part5\",\n      \"Name\": \"demomodel.psd(未找到对应图层)\"\n    },\n    {\n      \"Id\": \"Part8\",\n      \"Name\": \"thug life\"\n    },\n    {\n      \"Id\": \"Part6\",\n      \"Name\": \"闪电\"\n    },\n    {\n      \"Id\": \"Part4\",\n      \"Name\": \"闪电\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/exp_1.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": 0.321,\n      \"Blend\": \"Multiply\"\n    },\n    {\n      \"Id\": \"ParamEyeROpen\",\n      \"Value\": 0.313,\n      \"Blend\": \"Multiply\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/exp_2.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": [\n    {\n      \"Id\": \"ParamEyeLOpen\",\n      \"Value\": -1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_expression0.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"Parameters\": []\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_expression1.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.8,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param4\",\n      \"Value\": 1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_expression2.exp3.json",
    "content": "{\n  \"Type\": \"Live2D Expression\",\n  \"FadeInTime\": 0.5,\n  \"Parameters\": [\n    {\n      \"Id\": \"Param5\",\n      \"Value\": 1,\n      \"Blend\": \"Add\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_motion1.motion3.json",
    "content": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 1.633,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": false,\n    \"CurveCount\": 2,\n    \"TotalSegmentCount\": 8,\n    \"TotalPointCount\": 20,\n    \"UserDataCount\": 0,\n    \"TotalUserDataSize\": 0\n  },\n  \"Curves\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param\",\n      \"Segments\": [\n        0,\n        0,\n        1,\n        0.033,\n        0,\n        0.067,\n        1,\n        0.1,\n        1,\n        1,\n        0.411,\n        1,\n        0.722,\n        1,\n        1.033,\n        1,\n        1,\n        1.189,\n        1,\n        1.344,\n        0,\n        1.5,\n        0,\n        0,\n        1.633,\n        0\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param2\",\n      \"Segments\": [\n        0,\n        0,\n        0,\n        0.067,\n        0,\n        1,\n        0.1,\n        0,\n        0.133,\n        0.142,\n        0.167,\n        0.2,\n        1,\n        0.489,\n        0.764,\n        0.811,\n        1,\n        1.133,\n        1,\n        0,\n        1.633,\n        1\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/assets/models/standard/live2d_motion2.motion3.json",
    "content": "{\n  \"Version\": 3,\n  \"Meta\": {\n    \"Duration\": 2.333,\n    \"Fps\": 30.0,\n    \"Loop\": true,\n    \"AreBeziersRestricted\": true,\n    \"CurveCount\": 2,\n    \"TotalSegmentCount\": 7,\n    \"TotalPointCount\": 21,\n    \"UserDataCount\": 0,\n    \"TotalUserDataSize\": 0\n  },\n  \"Curves\": [\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"CatParamLeftHandDown\",\n      \"Segments\": [\n        0,\n        0,\n        0,\n        2.333,\n        0\n      ]\n    },\n    {\n      \"Target\": \"Parameter\",\n      \"Id\": \"Param3\",\n      \"Segments\": [\n        0,\n        0,\n        1,\n        0.133,\n        0,\n        0.267,\n        30,\n        0.4,\n        30,\n        1,\n        0.522,\n        30,\n        0.644,\n        0,\n        0.767,\n        0,\n        1,\n        0.9,\n        0,\n        1.033,\n        30,\n        1.167,\n        30,\n        1,\n        1.3,\n        30,\n        1.433,\n        0,\n        1.567,\n        0,\n        1,\n        1.7,\n        0,\n        1.833,\n        30,\n        1.967,\n        30,\n        1,\n        2.089,\n        30,\n        2.211,\n        0,\n        2.333,\n        0\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "src-tauri/build.rs",
    "content": "fn main() {\n    tauri_build::build()\n}\n"
  },
  {
    "path": "src-tauri/capabilities/default.json",
    "content": "{\n  \"$schema\": \"../gen/schemas/desktop-schema.json\",\n  \"identifier\": \"default\",\n  \"description\": \"Capability for the main window\",\n  \"windows\": [\n    \"*\"\n  ],\n  \"permissions\": [\n    \"core:default\",\n    \"core:window:allow-start-dragging\",\n    \"core:window:allow-set-size\",\n    \"core:window:deny-internal-toggle-maximize\",\n    \"core:window:allow-set-always-on-top\",\n    \"core:window:allow-set-ignore-cursor-events\",\n    \"core:window:allow-set-decorations\",\n    \"core:window:allow-set-position\",\n    \"core:window:allow-set-theme\",\n    \"core:window:allow-set-title\",\n    \"custom-window:default\",\n    \"os:default\",\n    \"process:default\",\n    \"opener:default\",\n    {\n      \"identifier\": \"opener:allow-open-path\",\n      \"allow\": [\n        {\n          \"path\": \"**/*\"\n        }\n      ]\n    },\n    \"pinia:default\",\n    \"log:default\",\n    \"updater:default\",\n    \"prevent-default:default\",\n    \"autostart:default\",\n    \"macos-permissions:default\",\n    \"dialog:default\",\n    \"fs:default\",\n    \"fs:read-all\",\n    \"fs:write-all\",\n    {\n      \"identifier\": \"fs:scope\",\n      \"allow\": [\n        \"**/*\"\n      ]\n    },\n    \"clipboard-manager:allow-write-text\",\n    \"global-shortcut:allow-is-registered\",\n    \"global-shortcut:allow-register\",\n    \"global-shortcut:allow-unregister\",\n    \"locale:default\"\n  ]\n}\n"
  },
  {
    "path": "src-tauri/src/core/device.rs",
    "content": "use rdev::{Event, EventType, listen};\nuse serde::Serialize;\nuse serde_json::{Value, json};\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse tauri::{AppHandle, Emitter, Runtime, command};\n\n#[derive(Debug, Clone, Serialize)]\npub enum DeviceEventKind {\n    MousePress,\n    MouseRelease,\n    MouseMove,\n    KeyboardPress,\n    KeyboardRelease,\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct DeviceEvent {\n    kind: DeviceEventKind,\n    value: Value,\n}\n\nstatic IS_LISTENING: AtomicBool = AtomicBool::new(false);\n\n#[command]\npub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {\n    if IS_LISTENING.load(Ordering::SeqCst) {\n        return Ok(());\n    }\n\n    IS_LISTENING.store(true, Ordering::SeqCst);\n\n    let callback = move |event: Event| {\n        let device_event = match event.event_type {\n            EventType::ButtonPress(button) => DeviceEvent {\n                kind: DeviceEventKind::MousePress,\n                value: json!(format!(\"{:?}\", button)),\n            },\n            EventType::ButtonRelease(button) => DeviceEvent {\n                kind: DeviceEventKind::MouseRelease,\n                value: json!(format!(\"{:?}\", button)),\n            },\n            EventType::MouseMove { x, y } => DeviceEvent {\n                kind: DeviceEventKind::MouseMove,\n                value: json!({ \"x\": x, \"y\": y }),\n            },\n            EventType::KeyPress(key) => DeviceEvent {\n                kind: DeviceEventKind::KeyboardPress,\n                value: json!(format!(\"{:?}\", key)),\n            },\n            EventType::KeyRelease(key) => DeviceEvent {\n                kind: DeviceEventKind::KeyboardRelease,\n                value: json!(format!(\"{:?}\", key)),\n            },\n            _ => return,\n        };\n\n        let _ = app_handle.emit(\"device-changed\", device_event);\n    };\n\n    listen(callback).map_err(|err| format!(\"Failed to listen device: {:?}\", err))?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src-tauri/src/core/gamepad.rs",
    "content": "use gilrs::{EventType, Gilrs};\nuse serde::Serialize;\nuse std::sync::atomic::{AtomicBool, Ordering};\nuse tauri::{AppHandle, Emitter, Runtime, command};\n\nstatic IS_LISTENING: AtomicBool = AtomicBool::new(false);\n\n#[derive(Debug, Clone, Serialize)]\npub enum GamepadEventKind {\n    ButtonChanged,\n    AxisChanged,\n}\n\n#[derive(Debug, Clone, Serialize)]\npub struct GamepadEvent {\n    kind: GamepadEventKind,\n    name: String,\n    value: f32,\n}\n\n#[command]\npub async fn start_gamepad_listing<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {\n    if IS_LISTENING.load(Ordering::SeqCst) {\n        return Ok(());\n    }\n\n    IS_LISTENING.store(true, Ordering::SeqCst);\n\n    let mut gilrs = Gilrs::new().map_err(|err| err.to_string())?;\n\n    while IS_LISTENING.load(Ordering::SeqCst) {\n        while let Some(event) = gilrs.next_event() {\n            let gamepad_event = match event.event {\n                EventType::ButtonChanged(button, value, ..) => GamepadEvent {\n                    kind: GamepadEventKind::ButtonChanged,\n                    name: format!(\"{:?}\", button),\n                    value,\n                },\n                EventType::AxisChanged(axis, value, ..) => GamepadEvent {\n                    kind: GamepadEventKind::AxisChanged,\n                    name: format!(\"{:?}\", axis),\n                    value,\n                },\n                _ => continue,\n            };\n\n            let _ = app_handle.emit(\"gamepad-changed\", gamepad_event);\n        }\n    }\n\n    Ok(())\n}\n\n#[command]\npub async fn stop_gamepad_listing() {\n    if !IS_LISTENING.load(Ordering::SeqCst) {\n        return;\n    }\n\n    IS_LISTENING.store(false, Ordering::SeqCst);\n}\n"
  },
  {
    "path": "src-tauri/src/core/mod.rs",
    "content": "pub mod device;\npub mod gamepad;\npub mod prevent_default;\npub mod setup;\n"
  },
  {
    "path": "src-tauri/src/core/prevent_default.rs",
    "content": "pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> {\n    #[cfg(debug_assertions)]\n    {\n        use tauri_plugin_prevent_default::Flags;\n\n        tauri_plugin_prevent_default::Builder::new()\n            .with_flags(Flags::all().difference(Flags::CONTEXT_MENU))\n            .build()\n    }\n\n    #[cfg(not(debug_assertions))]\n    tauri_plugin_prevent_default::init()\n}\n"
  },
  {
    "path": "src-tauri/src/core/setup/common.rs",
    "content": "use tauri::{AppHandle, WebviewWindow};\n\npub fn platform(\n    _app_handle: &AppHandle,\n    _main_window: WebviewWindow,\n    _preference_window: WebviewWindow,\n) {\n}\n"
  },
  {
    "path": "src-tauri/src/core/setup/macos.rs",
    "content": "#![allow(deprecated)]\nuse tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};\nuse tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};\nuse tauri_plugin_custom_window::MAIN_WINDOW_LABEL;\n\n#[allow(non_upper_case_globals)]\nconst NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;\n#[allow(non_upper_case_globals)]\nconst NSResizableWindowMask: i32 = 1 << 3;\nconst WINDOW_FOCUS_EVENT: &str = \"tauri://focus\";\nconst WINDOW_BLUR_EVENT: &str = \"tauri://blur\";\nconst WINDOW_MOVED_EVENT: &str = \"tauri://move\";\nconst WINDOW_RESIZED_EVENT: &str = \"tauri://resize\";\n\npub fn platform(\n    app_handle: &AppHandle,\n    main_window: WebviewWindow,\n    _preference_window: WebviewWindow,\n) {\n    let _ = app_handle.plugin(tauri_nspanel::init());\n\n    let _ = app_handle.set_dock_visibility(false);\n\n    let panel = main_window.to_panel().unwrap();\n\n    panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel | NSResizableWindowMask);\n\n    panel.set_collection_behaviour(\n        NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces\n            | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary\n            | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,\n    );\n\n    let delegate = panel_delegate!(EcoPanelDelegate {\n        window_did_become_key,\n        window_did_resign_key,\n        window_did_resize,\n        window_did_move\n    });\n\n    delegate.set_listener(Box::new(move |delegate_name: String| {\n        let target = EventTarget::labeled(MAIN_WINDOW_LABEL);\n\n        let window_move_event = || {\n            if let Ok(position) = main_window.outer_position() {\n                let _ = main_window.emit_to(target.clone(), WINDOW_MOVED_EVENT, position);\n            }\n        };\n\n        match delegate_name.as_str() {\n            \"window_did_become_key\" => {\n                let _ = main_window.emit_to(target, WINDOW_FOCUS_EVENT, true);\n            }\n            \"window_did_resign_key\" => {\n                let _ = main_window.emit_to(target, WINDOW_BLUR_EVENT, true);\n            }\n            \"window_did_resize\" => {\n                window_move_event();\n\n                if let Ok(size) = main_window.outer_size() {\n                    let _ = main_window.emit_to(target, WINDOW_RESIZED_EVENT, size);\n                }\n            }\n            \"window_did_move\" => window_move_event(),\n            _ => (),\n        }\n    }));\n\n    panel.set_delegate(delegate);\n}\n"
  },
  {
    "path": "src-tauri/src/core/setup/mod.rs",
    "content": "use tauri::{AppHandle, WebviewWindow};\n\n#[cfg(target_os = \"macos\")]\nmod macos;\n\n#[cfg(not(target_os = \"macos\"))]\npub mod common;\n\n#[cfg(target_os = \"macos\")]\npub use macos::*;\n\n#[cfg(not(target_os = \"macos\"))]\npub use common::*;\n\npub fn default(\n    app_handle: &AppHandle,\n    main_window: WebviewWindow,\n    preference_window: WebviewWindow,\n) {\n    #[cfg(debug_assertions)]\n    main_window.open_devtools();\n\n    platform(app_handle, main_window.clone(), preference_window.clone());\n}\n"
  },
  {
    "path": "src-tauri/src/lib.rs",
    "content": "mod core;\nmod utils;\n\nuse core::{\n    device::start_device_listening,\n    gamepad::{start_gamepad_listing, stop_gamepad_listing},\n    prevent_default, setup,\n};\nuse tauri::{Manager, WindowEvent, generate_handler};\nuse tauri_plugin_autostart::MacosLauncher;\nuse tauri_plugin_custom_window::{\n    MAIN_WINDOW_LABEL, PREFERENCE_WINDOW_LABEL, show_preference_window,\n};\nuse utils::fs_extra::copy_dir;\n\n#[cfg_attr(mobile, tauri::mobile_entry_point)]\npub fn run() {\n    let app = tauri::Builder::default()\n        .setup(|app| {\n            let app_handle = app.handle();\n\n            let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap();\n\n            let preference_window = app.get_webview_window(PREFERENCE_WINDOW_LABEL).unwrap();\n\n            setup::default(&app_handle, main_window.clone(), preference_window.clone());\n\n            Ok(())\n        })\n        .invoke_handler(generate_handler![\n            copy_dir,\n            start_device_listening,\n            start_gamepad_listing,\n            stop_gamepad_listing\n        ])\n        .plugin(tauri_plugin_custom_window::init())\n        .plugin(tauri_plugin_os::init())\n        .plugin(tauri_plugin_process::init())\n        .plugin(tauri_plugin_opener::init())\n        .plugin(tauri_plugin_pinia::init())\n        .plugin(tauri_plugin_updater::Builder::new().build())\n        .plugin(prevent_default::init())\n        .plugin(tauri_plugin_single_instance::init(\n            |app_handle, _argv, _cwd| {\n                show_preference_window(app_handle);\n            },\n        ))\n        .plugin(\n            tauri_plugin_log::Builder::new()\n                .timezone_strategy(tauri_plugin_log::TimezoneStrategy::UseLocal)\n                .filter(|metadata| !metadata.target().contains(\"gilrs\"))\n                .build(),\n        )\n        .plugin(tauri_plugin_autostart::init(\n            MacosLauncher::LaunchAgent,\n            None,\n        ))\n        .plugin(tauri_plugin_macos_permissions::init())\n        .plugin(tauri_plugin_dialog::init())\n        .plugin(tauri_plugin_fs::init())\n        .plugin(tauri_plugin_clipboard_manager::init())\n        .plugin(tauri_plugin_global_shortcut::Builder::new().build())\n        .plugin(tauri_plugin_locale::init())\n        .on_window_event(|window, event| match event {\n            WindowEvent::CloseRequested { api, .. } => {\n                let _ = window.hide();\n\n                api.prevent_close();\n            }\n            _ => {}\n        })\n        .build(tauri::generate_context!())\n        .expect(\"error while running tauri application\");\n\n    app.run(|app_handle, event| match event {\n        #[cfg(target_os = \"macos\")]\n        tauri::RunEvent::Reopen { .. } => {\n            show_preference_window(app_handle);\n        }\n        _ => {\n            let _ = app_handle;\n        }\n    });\n}\n"
  },
  {
    "path": "src-tauri/src/main.rs",
    "content": "#![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",
    "content": "[package]\nname = \"tauri-plugin-custom-window\"\nversion = \"0.1.0\"\nauthors = []\ndescription = \"\"\nedition = \"2024\"\nrust-version = \"1.85.0\"\nlinks = \"tauri-plugin-custom-window\"\n\n[dependencies]\ntauri.workspace = true\nserde.workspace = true\n\n[build-dependencies]\ntauri-plugin.workspace = true\n\n[target.\"cfg(target_os = \\\"macos\\\")\".dependencies]\ntauri-nspanel.workspace = true\n"
  },
  {
    "path": "src-tauri/src/plugins/window/build.rs",
    "content": "const COMMANDS: &[&str] = &[\n    \"show_window\",\n    \"hide_window\",\n    \"set_always_on_top\",\n    \"set_taskbar_visibility\",\n];\n\nfn main() {\n    tauri_plugin::Builder::new(COMMANDS).build();\n}\n"
  },
  {
    "path": "src-tauri/src/plugins/window/permissions/default.toml",
    "content": "\"$schema\" = \"schemas/schema.json\"\n\n[default]\ndescription = \"Default permissions for the plugin\"\npermissions = [\"allow-show-window\", \"allow-hide-window\", \"allow-set-always-on-top\", \"allow-set-taskbar-visibility\"]\n"
  },
  {
    "path": "src-tauri/src/plugins/window/src/commands/common.rs",
    "content": "use super::{shared_hide_window, shared_set_always_on_top, shared_show_window};\nuse tauri::{AppHandle, Runtime, WebviewWindow, command};\n\n#[command]\npub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {\n    shared_show_window(&app_handle, &window);\n}\n\n#[command]\npub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {\n    shared_hide_window(&app_handle, &window);\n}\n\n#[command]\npub async fn set_always_on_top<R: Runtime>(\n    app_handle: AppHandle<R>,\n    window: WebviewWindow<R>,\n    always_on_top: bool,\n) {\n    shared_set_always_on_top(&app_handle, &window, always_on_top);\n}\n\n#[command]\npub async fn set_taskbar_visibility<R: Runtime>(window: WebviewWindow<R>, visible: bool) {\n    let _ = window.set_skip_taskbar(!visible);\n}\n"
  },
  {
    "path": "src-tauri/src/plugins/window/src/commands/macos.rs",
    "content": "#![allow(deprecated)]\nuse super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window};\nuse crate::MAIN_WINDOW_LABEL;\nuse tauri::{AppHandle, Runtime, WebviewWindow, command};\nuse tauri_nspanel::{ManagerExt, cocoa::appkit::NSMainMenuWindowLevel};\n\npub enum MacOSPanelStatus {\n    Show,\n    Hide,\n    SetAlwaysOnTop(bool),\n}\n\n#[command]\npub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {\n    if is_main_window(&window) {\n        set_macos_panel(&app_handle, &window, MacOSPanelStatus::Show);\n    } else {\n        shared_show_window(&app_handle, &window);\n    }\n}\n\n#[command]\npub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) {\n    if is_main_window(&window) {\n        set_macos_panel(&app_handle, &window, MacOSPanelStatus::Hide);\n    } else {\n        shared_hide_window(&app_handle, &window);\n    }\n}\n\n#[command]\npub async fn set_always_on_top<R: Runtime>(\n    app_handle: AppHandle<R>,\n    window: WebviewWindow<R>,\n    always_on_top: bool,\n) {\n    if is_main_window(&window) {\n        set_macos_panel(\n            &app_handle,\n            &window,\n            MacOSPanelStatus::SetAlwaysOnTop(always_on_top),\n        );\n    } else {\n        shared_set_always_on_top(&app_handle, &window, always_on_top);\n    }\n}\n\npub fn set_macos_panel<R: Runtime>(\n    app_handle: &AppHandle<R>,\n    window: &WebviewWindow<R>,\n    status: MacOSPanelStatus,\n) {\n    if is_main_window(window) {\n        let app_handle_clone = app_handle.clone();\n\n        let _ = app_handle.run_on_main_thread(move || {\n            if let Ok(panel) = app_handle_clone.get_webview_panel(MAIN_WINDOW_LABEL) {\n                match status {\n                    MacOSPanelStatus::Show => {\n                        panel.show();\n                    }\n                    MacOSPanelStatus::Hide => {\n                        panel.order_out(None);\n                    }\n                    MacOSPanelStatus::SetAlwaysOnTop(always_on_top) => {\n                        if always_on_top {\n                            panel.set_level(NSMainMenuWindowLevel);\n                        } else {\n                            panel.set_level(-1);\n                        };\n                    }\n                }\n            }\n        });\n    }\n}\n\n#[command]\npub async fn set_taskbar_visibility<R: Runtime>(app_handle: AppHandle<R>, visible: bool) {\n    let _ = app_handle.set_dock_visibility(visible);\n}\n"
  },
  {
    "path": "src-tauri/src/plugins/window/src/commands/mod.rs",
    "content": "use tauri::{AppHandle, Manager, Runtime, WebviewWindow, async_runtime::spawn};\n\npub static MAIN_WINDOW_LABEL: &str = \"main\";\npub static PREFERENCE_WINDOW_LABEL: &str = \"preference\";\n\n#[cfg(target_os = \"macos\")]\nmod macos;\n\n#[cfg(not(target_os = \"macos\"))]\nmod common;\n\n#[cfg(target_os = \"macos\")]\npub use macos::*;\n\n#[cfg(not(target_os = \"macos\"))]\npub use common::*;\n\npub fn is_main_window<R: Runtime>(window: &WebviewWindow<R>) -> bool {\n    window.label() == MAIN_WINDOW_LABEL\n}\n\nfn shared_show_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) {\n    let _ = window.show();\n    let _ = window.unminimize();\n    let _ = window.set_focus();\n}\n\nfn shared_hide_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) {\n    let _ = window.hide();\n}\n\nfn shared_set_always_on_top<R: Runtime>(\n    _app_handle: &AppHandle<R>,\n    window: &WebviewWindow<R>,\n    always_on_top: bool,\n) {\n    if always_on_top {\n        let _ = window.set_always_on_bottom(false);\n        let _ = window.set_always_on_top(true);\n    } else {\n        let _ = window.set_always_on_top(false);\n        let _ = window.set_always_on_bottom(true);\n    }\n}\n\npub fn show_main_window(app_handle: &AppHandle) {\n    show_window_by_label(app_handle, MAIN_WINDOW_LABEL);\n}\n\npub fn show_preference_window(app_handle: &AppHandle) {\n    show_window_by_label(app_handle, PREFERENCE_WINDOW_LABEL);\n}\n\nfn show_window_by_label(app_handle: &AppHandle, label: &str) {\n    if let Some(window) = app_handle.get_webview_window(label) {\n        let app_handle_clone = app_handle.clone();\n\n        spawn(async move {\n            show_window(app_handle_clone, window).await;\n        });\n    }\n}\n"
  },
  {
    "path": "src-tauri/src/plugins/window/src/lib.rs",
    "content": "use tauri::{\n    Runtime, generate_handler,\n    plugin::{Builder, TauriPlugin},\n};\n\nmod commands;\n\npub use commands::*;\n\npub fn init<R: Runtime>() -> TauriPlugin<R> {\n    Builder::new(\"custom-window\")\n        .invoke_handler(generate_handler![\n            commands::show_window,\n            commands::hide_window,\n            commands::set_always_on_top,\n            commands::set_taskbar_visibility,\n        ])\n        .build()\n}\n"
  },
  {
    "path": "src-tauri/src/utils/fs_extra.rs",
    "content": "use fs_extra::dir::{CopyOptions, copy};\nuse std::fs::create_dir_all;\nuse tauri::command;\n\n#[command]\npub async fn copy_dir(from_path: String, to_path: String) -> Result<(), String> {\n    let mut options = CopyOptions::new();\n    options.content_only = true;\n\n    create_dir_all(&to_path).map_err(|err| err.to_string())?;\n\n    copy(from_path, to_path, &options).map_err(|err| err.to_string())?;\n\n    Ok(())\n}\n"
  },
  {
    "path": "src-tauri/src/utils/mod.rs",
    "content": "pub mod fs_extra;\n"
  },
  {
    "path": "src-tauri/tauri.conf.json",
    "content": "{\n  \"$schema\": \"https://schema.tauri.app/config/2\",\n  \"productName\": \"BongoCat\",\n  \"version\": \"../package.json\",\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"build\": {\n    \"beforeDevCommand\": \"pnpm dev\",\n    \"devUrl\": \"http://localhost:1420\",\n    \"beforeBuildCommand\": \"pnpm build\",\n    \"frontendDist\": \"../dist\"\n  },\n  \"app\": {\n    \"macOSPrivateApi\": true,\n    \"windows\": [\n      {\n        \"label\": \"main\",\n        \"title\": \"BongoCat\",\n        \"url\": \"index.html/#/\",\n        \"shadow\": false,\n        \"alwaysOnTop\": true,\n        \"transparent\": true,\n        \"decorations\": false,\n        \"acceptFirstMouse\": true,\n        \"skipTaskbar\": true,\n        \"maximizable\": false\n      },\n      {\n        \"label\": \"preference\",\n        \"url\": \"index.html/#/preference\",\n        \"visible\": false,\n        \"titleBarStyle\": \"Overlay\",\n        \"hiddenTitle\": true,\n        \"minWidth\": 800,\n        \"minHeight\": 600\n      }\n    ],\n    \"security\": {\n      \"csp\": null,\n      \"dangerousDisableAssetCspModification\": true,\n      \"assetProtocol\": {\n        \"enable\": true,\n        \"scope\": {\n          \"allow\": [\"**/*\"],\n          \"requireLiteralLeadingDot\": false\n        }\n      }\n    }\n  },\n  \"bundle\": {\n    \"active\": true,\n    \"category\": \"Entertainment\",\n    \"createUpdaterArtifacts\": true,\n    \"targets\": [\"nsis\", \"dmg\", \"app\", \"appimage\", \"deb\", \"rpm\"],\n    \"shortDescription\": \"BongoCat\",\n    \"icon\": [\n      \"icons/32x32.png\",\n      \"icons/128x128.png\",\n      \"icons/128x128@2x.png\",\n      \"icons/icon.icns\",\n      \"icons/icon.ico\"\n    ],\n    \"resources\": [\"assets/tray.png\", \"assets/models\"]\n  },\n  \"plugins\": {\n    \"updater\": {\n      \"dangerousInsecureTransportProtocol\": true,\n      \"pubkey\": \"dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRjJFMzE3MjEwMUZEMTAKUldRUS9RRWhGK1B5NmdkemhKcUFrVjZBQXlzdExpakdWVEJDeU9XckVsbzV2cFIycVJOempWa2UK\",\n      \"endpoints\": [\n        \"http://api.upgrade.toolsetlink.com/v1/tauri/upgrade?tauriKey=KtGlsZUVXmWfjkRKCuqpfw&versionName={{current_version}}&target={{target}}&arch={{arch}}&appointVersionName=&devModelKey=&devKey=\",\n        \"https://gh-proxy.com/github.com/ayangweb/BongoCat/releases/latest/download/latest.json\"\n      ]\n    },\n    \"fs\": {\n      \"requireLiteralLeadingDot\": false\n    }\n  }\n}\n"
  },
  {
    "path": "src-tauri/tauri.linux.conf.json",
    "content": "{\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"bundle\": {\n    \"linux\": {\n      \"deb\": {\n        \"depends\": [\"gstreamer1.0-plugins-good\"],\n        \"desktopTemplate\": \"./BongoCat.desktop\"\n      },\n      \"rpm\": {\n        \"depends\": [\"gstreamer1-plugins-good\"],\n        \"desktopTemplate\": \"./BongoCat.desktop\"\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "src-tauri/tauri.macos.conf.json",
    "content": "{\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"bundle\": {\n    \"resources\": [\"assets/tray-mac.png\", \"assets/models\"]\n  }\n}\n"
  },
  {
    "path": "src-tauri/tauri.windows.conf.json",
    "content": "{\n  \"identifier\": \"com.ayangweb.BongoCat\",\n  \"bundle\": {\n    \"windows\": {\n      \"digestAlgorithm\": \"sha256\",\n      \"nsis\": {\n        \"languages\": [\n          \"English\",\n          \"Vietnamese\",\n          \"SimpChinese\",\n          \"PortugueseBR\"\n        ],\n        \"installMode\": \"both\",\n        \"displayLanguageSelector\": true\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"jsx\": \"preserve\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"useDefineForClassFields\": true,\n\n    \"baseUrl\": \".\",\n    \"module\": \"ESNext\",\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"paths\": {\n      \"@/*\": [\"src/*\"]\n    },\n    \"resolveJsonModule\": true,\n    \"allowImportingTsExtensions\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noFallthroughCasesInSwitch\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noEmit\": true,\n    \"isolatedModules\": true,\n    \"skipLibCheck\": true\n  },\n  \"references\": [{ \"path\": \"./tsconfig.node.json\" }],\n  \"include\": [\"src/**/*.ts\", \"src/**/*.d.ts\", \"src/**/*.tsx\", \"src/**/*.vue\"]\n}\n"
  },
  {
    "path": "tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"composite\": true,\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"bundler\",\n    \"allowSyntheticDefaultImports\": true,\n    \"skipLibCheck\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "uno.config.ts",
    "content": "import {\n  defineConfig,\n  presetIcons,\n  presetWind3,\n  transformerDirectives,\n  transformerVariantGroup,\n} from 'unocss'\n\nexport default defineConfig({\n  presets: [\n    presetWind3(),\n    presetIcons(),\n  ],\n  transformers: [\n    transformerVariantGroup(),\n    transformerDirectives({\n      applyVariable: ['--uno'],\n    }),\n  ],\n  shortcuts: [\n    [/^bg-color-(\\d+)$/, ([, d]) => `bg-bg-${d}`],\n    [/^text-color-(\\d+)$/, ([, d]) => `text-text-${d}`],\n    [/^b-color-(\\d+)$/, ([, d]) => `b-border-${d}`],\n    [/^(.*)-primary-(\\d+)$/, ([, s, d]) => `${s}-[var(--ant-blue-${d})]`],\n  ],\n  theme: {\n    colors: {\n      'bg-1': 'var(--ant-color-bg-layout)',\n      'bg-2': 'var(--ant-color-bg-container)',\n      'bg-3': 'var(--ant-color-bg-elevated)',\n      'bg-4': 'var(--ant-color-bg-spotlight)',\n      'bg-5': 'var(--ant-color-fill)',\n      'bg-6': 'var(--ant-color-fill-secondary)',\n      'bg-7': 'var(--ant-color-fill-tertiary)',\n      'bg-8': 'var(--ant-color-fill-quaternary)',\n      'text-1': 'var(--ant-color-text)',\n      'text-2': 'var(--ant-color-text-secondary)',\n      'text-3': 'var(--ant-color-text-tertiary)',\n      'text-4': 'var(--ant-color-text-quaternary)',\n      'border-1': 'var(--ant-color-border)',\n      'border-2': 'var(--ant-color-border-secondary)',\n      'primary': 'var(--ant-blue)',\n      'success': 'var(--ant-green)',\n      'danger': 'var(--ant-red)',\n    },\n  },\n})\n"
  },
  {
    "path": "vite.config.ts",
    "content": "import { resolve } from 'node:path'\nimport { env } from 'node:process'\n\nimport vue from '@vitejs/plugin-vue'\nimport UnoCSS from 'unocss/vite'\nimport { defineConfig } from 'vite'\n\nconst host = env.TAURI_DEV_HOST\n\n// https://vitejs.dev/config/\nexport default defineConfig(async () => ({\n  plugins: [vue(), UnoCSS()],\n  resolve: {\n    alias: {\n      '@': resolve(__dirname, 'src'),\n    },\n  },\n  // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build`\n  //\n  // 1. prevent vite from obscuring rust errors\n  clearScreen: false,\n  // 2. tauri expects a fixed port, fail if that port is not available\n  server: {\n    port: 1420,\n    strictPort: true,\n    host: host || false,\n    hmr: host\n      ? {\n          protocol: 'ws',\n          host,\n          port: 1421,\n        }\n      : undefined,\n    watch: {\n      // 3. tell vite to ignore watching `src-tauri`\n      ignored: ['**/src-tauri/**'],\n    },\n  },\n}))\n"
  }
]