├── .commitlintrc ├── .github ├── CONTRIBUTING.md ├── DOWNLOAD_GUIDE.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── release.yml │ ├── sync-to-gitee.yml │ └── upgradelink.yml ├── .gitignore ├── .release-it.ts ├── .vscode ├── extensions.json └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── eslint.config.ts ├── index.html ├── package.json ├── pnpm-lock.yaml ├── public ├── js │ ├── live2d.min.js │ └── live2dcubismcore.min.js └── logo.png ├── scripts ├── buildIcon.ts └── release.ts ├── src-tauri ├── .gitignore ├── BongoCat.desktop ├── Cargo.toml ├── assets │ ├── logo-mac.png │ ├── logo.png │ ├── models │ │ ├── keyboard │ │ │ ├── cat.model3.json │ │ │ ├── demomodel2.1024 │ │ │ │ ├── texture_00.png │ │ │ │ ├── texture_01.png │ │ │ │ └── texture_02.png │ │ │ ├── demomodel2.cdi3.json │ │ │ ├── demomodel2.moc3 │ │ │ ├── exp_1.exp3.json │ │ │ ├── exp_2.exp3.json │ │ │ ├── live2d_expression0.exp3.json │ │ │ ├── live2d_expression1.exp3.json │ │ │ ├── live2d_expression2.exp3.json │ │ │ ├── live2d_motion1.flac │ │ │ ├── live2d_motion1.motion3.json │ │ │ ├── live2d_motion2.motion3.json │ │ │ └── resources │ │ │ │ ├── background.png │ │ │ │ ├── cover.png │ │ │ │ ├── left-keys │ │ │ │ ├── Alt.png │ │ │ │ ├── AltGr.png │ │ │ │ ├── BackQuote.png │ │ │ │ ├── Backspace.png │ │ │ │ ├── CapsLock.png │ │ │ │ ├── Control.png │ │ │ │ ├── ControlLeft.png │ │ │ │ ├── ControlRight.png │ │ │ │ ├── Delete.png │ │ │ │ ├── Escape.png │ │ │ │ ├── Fn.png │ │ │ │ ├── KeyA.png │ │ │ │ ├── KeyB.png │ │ │ │ ├── KeyC.png │ │ │ │ ├── KeyD.png │ │ │ │ ├── KeyE.png │ │ │ │ ├── KeyF.png │ │ │ │ ├── KeyG.png │ │ │ │ ├── KeyH.png │ │ │ │ ├── KeyI.png │ │ │ │ ├── KeyJ.png │ │ │ │ ├── KeyK.png │ │ │ │ ├── KeyL.png │ │ │ │ ├── KeyM.png │ │ │ │ ├── KeyN.png │ │ │ │ ├── KeyO.png │ │ │ │ ├── KeyP.png │ │ │ │ ├── KeyQ.png │ │ │ │ ├── KeyR.png │ │ │ │ ├── KeyS.png │ │ │ │ ├── KeyT.png │ │ │ │ ├── KeyU.png │ │ │ │ ├── KeyV.png │ │ │ │ ├── KeyW.png │ │ │ │ ├── KeyX.png │ │ │ │ ├── KeyY.png │ │ │ │ ├── KeyZ.png │ │ │ │ ├── Meta.png │ │ │ │ ├── Num0.png │ │ │ │ ├── Num1.png │ │ │ │ ├── Num2.png │ │ │ │ ├── Num3.png │ │ │ │ ├── Num4.png │ │ │ │ ├── Num5.png │ │ │ │ ├── Num6.png │ │ │ │ ├── Num7.png │ │ │ │ ├── Num8.png │ │ │ │ ├── Num9.png │ │ │ │ ├── Return.png │ │ │ │ ├── Shift.png │ │ │ │ ├── ShiftLeft.png │ │ │ │ ├── ShiftRight.png │ │ │ │ ├── Slash.png │ │ │ │ ├── Space.png │ │ │ │ └── Tab.png │ │ │ │ └── right-keys │ │ │ │ ├── DownArrow.png │ │ │ │ ├── LeftArrow.png │ │ │ │ ├── RightArrow.png │ │ │ │ └── UpArrow.png │ │ └── standard │ │ │ ├── cat.model3.json │ │ │ ├── demomodel.1024 │ │ │ ├── texture_00.png │ │ │ ├── texture_01.png │ │ │ └── texture_02.png │ │ │ ├── demomodel.cdi3.json │ │ │ ├── demomodel.moc3 │ │ │ ├── exp_1.exp3.json │ │ │ ├── exp_2.exp3.json │ │ │ ├── live2d_expression0.exp3.json │ │ │ ├── live2d_expression1.exp3.json │ │ │ ├── live2d_expression2.exp3.json │ │ │ ├── live2d_motion1.flac │ │ │ ├── live2d_motion1.motion3.json │ │ │ ├── live2d_motion2.motion3.json │ │ │ └── resources │ │ │ ├── background.png │ │ │ ├── cover.png │ │ │ └── left-keys │ │ │ ├── Alt.png │ │ │ ├── AltGr.png │ │ │ ├── BackQuote.png │ │ │ ├── Backspace.png │ │ │ ├── CapsLock.png │ │ │ ├── Control.png │ │ │ ├── ControlLeft.png │ │ │ ├── ControlRight.png │ │ │ ├── Delete.png │ │ │ ├── Escape.png │ │ │ ├── Fn.png │ │ │ ├── KeyA.png │ │ │ ├── KeyB.png │ │ │ ├── KeyC.png │ │ │ ├── KeyD.png │ │ │ ├── KeyE.png │ │ │ ├── KeyF.png │ │ │ ├── KeyG.png │ │ │ ├── KeyH.png │ │ │ ├── KeyI.png │ │ │ ├── KeyJ.png │ │ │ ├── KeyK.png │ │ │ ├── KeyL.png │ │ │ ├── KeyM.png │ │ │ ├── KeyN.png │ │ │ ├── KeyO.png │ │ │ ├── KeyP.png │ │ │ ├── KeyQ.png │ │ │ ├── KeyR.png │ │ │ ├── KeyS.png │ │ │ ├── KeyT.png │ │ │ ├── KeyU.png │ │ │ ├── KeyV.png │ │ │ ├── KeyW.png │ │ │ ├── KeyX.png │ │ │ ├── KeyY.png │ │ │ ├── KeyZ.png │ │ │ ├── Meta.png │ │ │ ├── Num0.png │ │ │ ├── Num1.png │ │ │ ├── Num2.png │ │ │ ├── Num3.png │ │ │ ├── Num4.png │ │ │ ├── Num5.png │ │ │ ├── Num6.png │ │ │ ├── Num7.png │ │ │ ├── Num8.png │ │ │ ├── Num9.png │ │ │ ├── Return.png │ │ │ ├── Shift.png │ │ │ ├── ShiftLeft.png │ │ │ ├── ShiftRight.png │ │ │ ├── Slash.png │ │ │ ├── Space.png │ │ │ └── Tab.png │ └── tray.png ├── build.rs ├── capabilities │ └── default.json ├── src │ ├── core │ │ ├── device │ │ │ ├── common.rs │ │ │ ├── linux.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── prevent_default.rs │ │ └── setup │ │ │ ├── common.rs │ │ │ ├── macos.rs │ │ │ └── mod.rs │ ├── lib.rs │ ├── main.rs │ ├── plugins │ │ └── window │ │ │ ├── Cargo.toml │ │ │ ├── build.rs │ │ │ ├── permissions │ │ │ └── default.toml │ │ │ └── src │ │ │ ├── commands │ │ │ ├── common.rs │ │ │ ├── macos.rs │ │ │ └── mod.rs │ │ │ └── lib.rs │ └── utils │ │ ├── fs_extra.rs │ │ └── mod.rs ├── tauri.conf.json ├── tauri.linux.conf.json ├── tauri.macos.conf.json └── tauri.windows.conf.json ├── src ├── App.vue ├── assets │ └── css │ │ └── global.scss ├── components │ ├── pro-list-item │ │ └── index.vue │ ├── pro-list │ │ └── index.vue │ ├── pro-shortcut │ │ └── index.vue │ └── update-app │ │ └── index.vue ├── composables │ ├── useDevice.ts │ ├── useModel.ts │ ├── useSharedMenu.ts │ ├── useTauriKeyPress.ts │ ├── useTauriListen.ts │ ├── useThemeVars.ts │ ├── useTray.ts │ └── useWindowState.ts ├── constants │ └── index.ts ├── main.ts ├── pages │ ├── main │ │ └── index.vue │ └── preference │ │ ├── components │ │ ├── about │ │ │ └── index.vue │ │ ├── cat │ │ │ └── index.vue │ │ ├── general │ │ │ ├── components │ │ │ │ └── macos-permissions │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ ├── model │ │ │ ├── components │ │ │ │ ├── float-menu │ │ │ │ │ └── index.vue │ │ │ │ └── upload │ │ │ │ │ └── index.vue │ │ │ └── index.vue │ │ └── shortcut │ │ │ └── index.vue │ │ └── index.vue ├── plugins │ └── window.ts ├── router │ └── index.ts ├── stores │ ├── app.ts │ ├── cat.ts │ ├── general.ts │ ├── model.ts │ └── shortcut.ts ├── utils │ ├── is.ts │ ├── keyboard.ts │ ├── live2d.ts │ ├── monitor.ts │ ├── path.ts │ └── platform.ts └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json ├── uno.config.ts └── vite.config.ts /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@commitlint/config-conventional" 3 | } 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 贡献指南 2 | 3 | 非常感谢您对 BongoCat 的关注和贡献!在您提交贡献之前,请先花一些时间阅读以下指南,以确保您的贡献能够顺利进行。 4 | 5 | ## 透明的开发 6 | 7 | 所有工作都在 GitHub 上公开进行。无论是核心团队成员还是外部贡献者的 Pull Request,都需要经过相同的 review 流程。 8 | 9 | ## 提交 Issue 10 | 11 | 我们使用 [Github Issues](https://github.com/ayangweb/BongoCat/issues) 进行 Bug 报告和新 Feature 建议。在提交 Issue 之前,请确保已经搜索过类似的问题,因为它们可能已经得到解答或正在被修复。对于 Bug 报告,请包含可用于重现问题的完整步骤。对于新 Feature 建议,请指出你想要的更改以及期望的行为。 12 | 13 | ## 提交 Pull Request 14 | 15 | ### 共建流程 16 | 17 | - 认领 issue:在 Github 建立 Issue 并认领(或直接认领已有 Issue),告知大家自己正在修复,避免重复工作。 18 | - 项目开发:在完成准备工作后,进行 Bug 修复或功能开发。 19 | - 提交 PR。 20 | 21 | ### 准备工作 22 | 23 | - [Rust](https://v2.tauri.app/start/prerequisites/): 请自行根据官网步骤安装 rust 环境。 24 | - [Node.js](https://nodejs.org/en/): 用于运行项目。 25 | - [Pnpm](https://pnpm.io/):本项目使用 Pnpm 进行包管理。 26 | 27 | ### 下载依赖 28 | 29 | ```shell 30 | pnpm install 31 | ``` 32 | 33 | ### 启动应用 34 | 35 | ```shell 36 | pnpm tauri dev 37 | ``` 38 | 39 | ### 打包应用 40 | 41 | > 如果需要打包后进行调试,请在以下命令后面加上 `--debug` 42 | 43 | ```shell 44 | pnpm tauri build 45 | ``` 46 | 47 | ## Commit 指南 48 | 49 | Commit messages 请遵循[conventional-changelog 标准](https://www.conventionalcommits.org/en/v1.0.0/)。 50 | 51 | ### Commit 类型 52 | 53 | 以下是 commit 类型列表: 54 | 55 | - feat: 新特性或功能 56 | - fix: 缺陷修复 57 | - docs: 文档更新 58 | - style: 代码风格更新 59 | - refactor: 代码重构,不引入新功能和缺陷修复 60 | - perf: 性能优化 61 | - chore: 其他提交 62 | 63 | 期待您的参与,让我们一起使 BongoCat 变得更好! 64 | -------------------------------------------------------------------------------- /.github/DOWNLOAD_GUIDE.md: -------------------------------------------------------------------------------- 1 | # 下载指南 2 | 3 | ## 系统要求 4 | 5 | - macOS 12 或更高版本。 6 | - Windows 10 或更高版本。 7 | - Linux 带有 X11 环境。 8 | 9 | ## macOS 10 | 11 | ### 手动下载 12 | 13 | - Apple Silicon:下载 `BongoCat_aarch64.dmg` 14 | - Intel Chip:下载 `BongoCat_x64.dmg` 15 | 16 | ### Homebrew 下载 17 | 18 | 1. 添加 BongoCat 的 tap 源: 19 | 20 | ```bash 21 | brew tap ayangweb/BongoCat 22 | ``` 23 | 24 | 2. 安装: 25 | 26 | ```bash 27 | brew install --no-quarantine bongo-cat 28 | ``` 29 | 30 | 3. 更新: 31 | 32 | ```bash 33 | brew upgrade bongo-cat 34 | ``` 35 | 36 | 4. 卸载: 37 | 38 | ```bash 39 | brew uninstall --cask bongo-cat 40 | 41 | brew untap ayangweb/BongoCat 42 | ``` 43 | 44 | ## Windows 45 | 46 | - 64 位系统:下载 `BongoCat_x64.exe` 47 | - 32 位系统:下载 `BongoCat_x86.exe` 48 | - ARM64 架构:下载 `BongoCat_arm64.exe` 49 | 50 | ## Linux(X11) 51 | 52 | ### 手动下载 53 | 54 | - 64 位系统: 55 | - Debian / Ubuntu:下载 `BongoCat_amd64.deb` 56 | - Fedora / RHEL:下载 `BongoCat_x86_64.rpm` 57 | - 通用版本:下载 `BongoCat_amd64.AppImage` 58 | - ARM64 架构: 59 | - Debian / Ubuntu:下载 `BongoCat_arm64.deb` 60 | - Fedora / RHEL:下载 `BongoCat_aarch64.rpm` 61 | - 通用版本:下载 `BongoCat_aarch64.AppImage` 62 | 63 | ### AUR 下载 64 | 65 | - Manjaro / ArchLinux: `yay -S bongo-cat` 66 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug 报告 2 | title: '[bug] ' 3 | description: 报告一个 Bug 4 | labels: bug 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | ## 温馨提示 10 | 1. 请先查阅现有的 [issues](https://github.com/ayangweb/BongoCat/issues)。 11 | 2. 请确保你使用的是[最新版本](https://github.com/ayangweb/BongoCat/releases/latest)。 12 | 3. 请确保该问题不是由其他软件引起的。 13 | 4. 请始终保持友好与尊重,感谢你的理解与配合。 14 | 15 | - type: textarea 16 | id: description 17 | attributes: 18 | label: 描述 Bug 19 | description: 请详细描述 Bug 并提供截图或视频以帮助我们更好地理解问题。 20 | validations: 21 | required: true 22 | 23 | - type: textarea 24 | id: reproduction 25 | attributes: 26 | label: 重现步骤 27 | description: 请详细列出重现问题的步骤,并附带截图或视频。 28 | 29 | - type: textarea 30 | id: expected-behavior 31 | attributes: 32 | label: 预期行为 33 | description: 请描述你期望发生的行为。 34 | 35 | - type: textarea 36 | id: info 37 | attributes: 38 | render: text 39 | label: 软件信息 40 | description: 请前往偏好设置窗口的「关于 > 关于软件 > 软件信息」复制软件信息。 41 | validations: 42 | required: true 43 | 44 | - type: textarea 45 | id: context 46 | attributes: 47 | label: 附加信息 48 | description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 💡 功能请求 2 | title: '[feat] ' 3 | description: 提出一个想法 4 | labels: feature request 5 | body: 6 | - type: textarea 7 | id: problem 8 | attributes: 9 | label: 描述问题 10 | description: 请清晰地描述此功能将解决的具体问题。 11 | validations: 12 | required: true 13 | 14 | - type: textarea 15 | id: solution 16 | attributes: 17 | label: 描述您希望的解决方案 18 | description: 请清晰地描述您期望的变更或改进。 19 | validations: 20 | required: true 21 | 22 | - type: textarea 23 | id: alternatives 24 | attributes: 25 | label: 考虑的替代方案 26 | description: 提供您考虑过的其他替代解决方案。 27 | 28 | - type: textarea 29 | id: context 30 | attributes: 31 | label: 附加信息 32 | description: 请在此提供有关该问题的其他相关信息,帮助我们更全面地理解问题。 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: BongoCat Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | create-release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 16 | - name: Set output 17 | id: vars 18 | run: echo "tag=${GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT 19 | 20 | - name: Setup node 21 | uses: actions/setup-node@v4 22 | with: 23 | node-version: 20 24 | 25 | - name: Generate changelog 26 | id: create_release 27 | run: npx changelogithub --draft --name ${{ steps.vars.outputs.tag }} 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 30 | 31 | build-app: 32 | needs: create-release 33 | permissions: 34 | contents: write 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | include: 39 | - platform: macos-latest 40 | target: aarch64-apple-darwin 41 | - platform: macos-latest 42 | target: x86_64-apple-darwin 43 | 44 | - platform: windows-latest 45 | target: x86_64-pc-windows-msvc 46 | - platform: windows-latest 47 | target: i686-pc-windows-msvc 48 | - platform: windows-latest 49 | target: aarch64-pc-windows-msvc 50 | 51 | - platform: ubuntu-22.04 52 | target: x86_64-unknown-linux-gnu 53 | - platform: ubuntu-22.04-arm 54 | target: aarch64-unknown-linux-gnu 55 | 56 | runs-on: ${{ matrix.platform }} 57 | steps: 58 | - name: Checkout repository 59 | uses: actions/checkout@v4 60 | - name: Setup node 61 | uses: actions/setup-node@v4 62 | with: 63 | node-version: 20 64 | - uses: pnpm/action-setup@v3 65 | with: 66 | version: latest 67 | 68 | - name: Install rust target 69 | run: rustup target add ${{ matrix.target }} 70 | 71 | - name: Install dependencies (ubuntu only) 72 | if: startsWith(matrix.platform, 'ubuntu') 73 | run: | 74 | sudo apt-get update 75 | sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf xdg-utils 76 | 77 | - name: Install Rust stable 78 | uses: dtolnay/rust-toolchain@stable 79 | 80 | - name: Rust cache 81 | uses: swatinem/rust-cache@v2 82 | with: 83 | workspaces: target 84 | 85 | - name: Sync node version and setup cache 86 | uses: actions/setup-node@v4 87 | with: 88 | node-version: 20 89 | cache: pnpm 90 | 91 | - name: Install front-end dependencies 92 | run: pnpm install --frozen-lockfile 93 | 94 | - name: Build the app 95 | uses: tauri-apps/tauri-action@v0 96 | env: 97 | CI: false 98 | PLATFORM: ${{ matrix.platform }} 99 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 100 | TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} 101 | TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} 102 | with: 103 | tagName: ${{ github.ref_name }} 104 | releaseName: BongoCat ${{ needs.create-release.outputs.APP_VERSION }} 105 | releaseBody: '' 106 | releaseDraft: true 107 | prerelease: false 108 | args: --target ${{ matrix.target }} 109 | -------------------------------------------------------------------------------- /.github/workflows/sync-to-gitee.yml: -------------------------------------------------------------------------------- 1 | name: Sync Github Repos To Gitee 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | repo-sync: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Sync Github Repos To Gitee 12 | uses: Yikun/hub-mirror-action@master 13 | with: 14 | src: github/ayangweb 15 | dst: gitee/ayangweb 16 | dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} 17 | dst_token: ${{ secrets.GITEE_TOKEN }} 18 | static_list: BongoCat 19 | force_update: true 20 | -------------------------------------------------------------------------------- /.github/workflows/upgradelink.yml: -------------------------------------------------------------------------------- 1 | name: Upload Release to UpgradeLink 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | upgradeLink-upload: 10 | permissions: 11 | contents: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Send a request to UpgradeLink 15 | uses: toolsetlink/upgradelink-action@v5 16 | with: 17 | source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json' 18 | access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }} 19 | tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }} 20 | github-token: ${{ secrets.RELEASE_TOKEN }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | target 15 | 16 | # Editor directories and files 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /.release-it.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-template-curly-in-string */ 2 | import type { Config } from 'release-it' 3 | 4 | export default { 5 | git: { 6 | commitMessage: 'v${version}', 7 | tagName: 'v${version}', 8 | }, 9 | npm: { 10 | publish: false, 11 | }, 12 | hooks: { 13 | 'after:bump': 'tsx scripts/release.ts', 14 | }, 15 | } satisfies Config 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "tauri-apps.tauri-vscode", 4 | "rust-lang.rust-analyzer", 5 | "antfu.unocss", 6 | "dbaeumer.vscode-eslint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | 5 | "eslint.format.enable": true, 6 | 7 | // Auto fix 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit", 10 | "source.organizeImports": "never" 11 | }, 12 | 13 | // Silent the stylistic rules in you IDE, but still auto fix them 14 | "eslint.rules.customizations": [ 15 | { "rule": "style/*", "severity": "off", "fixable": true }, 16 | { "rule": "format/*", "severity": "off", "fixable": true }, 17 | { "rule": "*-indent", "severity": "off", "fixable": true }, 18 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 19 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 20 | { "rule": "*-order", "severity": "off", "fixable": true }, 21 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 22 | { "rule": "*-newline", "severity": "off", "fixable": true }, 23 | { "rule": "*quotes", "severity": "off", "fixable": true }, 24 | { "rule": "*semi", "severity": "off", "fixable": true } 25 | ], 26 | 27 | // Enable eslint for all supported languages 28 | "eslint.validate": [ 29 | "javascript", 30 | "javascriptreact", 31 | "typescript", 32 | "typescriptreact", 33 | "vue", 34 | "html", 35 | "markdown", 36 | "json", 37 | "json5", 38 | "jsonc", 39 | "yaml", 40 | "toml", 41 | "xml", 42 | "gql", 43 | "graphql", 44 | "astro", 45 | "svelte", 46 | "css", 47 | "less", 48 | "scss", 49 | "pcss", 50 | "postcss" 51 | ], 52 | 53 | "typescript.enablePromptUseWorkspaceTsdk": true, 54 | "typescript.tsdk": "./node_modules/typescript/lib" 55 | } 56 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = [ "src-tauri" ] 4 | 5 | [profile.release] 6 | strip = true 7 | opt-level = 3 8 | codegen-units = 1 9 | panic = "abort" 10 | debug-assertions = false 11 | overflow-checks = false 12 | lto = true 13 | 14 | [workspace.dependencies] 15 | tauri = "2" 16 | serde = "1" 17 | serde_json = "1" 18 | fs_extra = "1" 19 | tauri-plugin = { version = "2", features = [ "build" ] } 20 | tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" } 21 | tauri-plugin-custom-window = { path = "./src-tauri/src/plugins/window" } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ayangweb 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | <div align="center"> 4 | <div> 5 | <a href="https://github.com/ayangweb/BongoCat/releases"> 6 | <img 7 | alt="Windows" 8 | src="https://img.shields.io/badge/-Windows-blue?style=flat-square&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNzI2MzA1OTcxMDA2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE1NDgiIHdpZHRoPSIxMjgiIGhlaWdodD0iMTI4Ij48cGF0aCBkPSJNNTI3LjI3NTU1MTYxIDk2Ljk3MTAzMDEzdjM3My45OTIxMDY2N2g0OTQuNTEzNjE5NzVWMTUuMDI2NzU3NTN6TTUyNy4yNzU1NTE2MSA5MjguMzIzNTA4MTVsNDk0LjUxMzYxOTc1IDgwLjUyMDI4MDQ5di00NTUuNjc3NDcxNjFoLTQ5NC41MTM2MTk3NXpNNC42NzA0NTEzNiA0NzAuODMzNjgyOTdINDIyLjY3Njg1OTI1VjExMC41NjM2ODE5N2wtNDE4LjAwNjQwNzg5IDY5LjI1Nzc5NzUzek00LjY3MDQ1MTM2IDg0Ni43Njc1OTcwM0w0MjIuNjc2ODU5MjUgOTE0Ljg2MDMxMDEzVjU1My4xNjYzMTcwM0g0LjY3MDQ1MTM2eiIgcC1pZD0iMTU0OSIgZmlsbD0iI2ZmZmZmZiI+PC9wYXRoPjwvc3ZnPg==" 9 | /> 10 | </a> 11 | <a href="https://github.com/ayangweb/BongoCat/releases"> 12 | <img 13 | alt="MacOS" 14 | src="https://img.shields.io/badge/-MacOS-black?style=flat-square&logo=apple&logoColor=white" 15 | /> 16 | </a> 17 | <a href="https://github.com/ayangweb/BongoCat/releases"> 18 | <img 19 | alt="Linux" 20 | src="https://img.shields.io/badge/-Linux-yellow?style=flat-square&logo=linux&logoColor=white" 21 | /> 22 | </a> 23 | </div> 24 | 25 | <p> 26 | <a href="./LICENSE"> 27 | <img 28 | src="https://img.shields.io/github/license/ayangweb/BongoCat?style=flat-square" 29 | /> 30 | </a> 31 | <a href="https://github.com/ayangweb/BongoCat/releases/latest"> 32 | <img 33 | src="https://img.shields.io/github/package-json/v/ayangweb/BongoCat?style=flat-square" 34 | /> 35 | </a> 36 | <a href="https://github.com/ayangweb/BongoCat/releases"> 37 | <img 38 | src="https://img.shields.io/github/downloads/ayangweb/BongoCat/total?style=flat-square" 39 | /> 40 | </a> 41 | </p> 42 | 43 | <p> 44 | <a href="https://hellogithub.com/repository/7d23863fd4be47b39e816193ded385c9" target="_blank"> 45 | <picture> 46 | <source media="(prefers-color-scheme: dark)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=dark" /> 47 | <source media="(prefers-color-scheme: light)" srcset="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" /> 48 | <img alt="Star History Chart" src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=7d23863fd4be47b39e816193ded385c9&claim_uid=5ihRVIuTYBmSGtQ&theme=neutral" /> 49 | </picture> 50 | </a> 51 | </p> 52 | </div> 53 | 54 | | macOS | Windows | Linux(x11) | 55 | | -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | 56 | |  |  |  | 57 | 58 | ## 开发背景 59 | 60 | 本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱,但仅支持 Windows 平台。作为一名深度 macOS 用户,我特别希望在自己的设备上也能使用这款可爱的猫咪,于是我决定开发一个适配 macOS 的版本。 61 | 62 | 同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux,让更多的用户都能与这只可爱的猫咪互动! 63 | 64 | ## 下载 65 | 66 | - [夸克网盘](https://pan.quark.cn/s/70f2f2663ce1) 67 | - [GitHub Releases](https://github.com/ayangweb/BongoCat/releases) 68 | 69 | > 不确定下载哪一个?请查阅[下载指南](.github/DOWNLOAD_GUIDE.md)。 70 | 71 | ## 功能介绍 72 | 73 | - 适配 macOS、Windows 和 Linux。 74 | - 根据据键盘或鼠标操作,同步移动鼠标或敲击键盘。 75 | - 支持导入自定义模型,自由打造专属猫咪形象。 76 | - 完全开源,代码公开透明,绝不收集任何用户数据。 77 | - 支持离线运行,无需联网,保护用户隐私。 78 | 79 | ## 使用提示 80 | 81 | - Linux 下需要用户系统安装 libinput 并加入 `input` 用户组,方可在 X11 和 Wayland 下正常使用。 82 | 83 | ## 更多模型 84 | 85 | 你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享: 86 | 87 | 📦 [Awesome-BongoCat](https://github.com/ayangweb/Awesome-BongoCat) 88 | 89 | ## 社区交流 90 | 91 | <a href="https://qm.qq.com/q/AS3gNv2Vzy"> 92 | <picture> 93 | <source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/5ad8e4278c525cca6d3b4426c30b6d299d8a9654.png" /> 94 | <source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" /> 95 | <img alt="QQ Group" src="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" height="250" /> 96 | </picture> 97 | </a> 98 | 99 | ## 贡献指南 100 | 101 | 感谢大家为 BongoCat 做出的宝贵贡献!如果你也希望为 BongoCat 做出贡献,请查阅[贡献指南](.github/CONTRIBUTING.md)。 102 | 103 | <a href="https://github.com/ayangweb/BongoCat/graphs/contributors"> 104 | <img src="https://contrib.rocks/image?repo=ayangweb/BongoCat" /> 105 | </a> 106 | 107 | ## 历史星标 108 | 109 | <a href="https://www.star-history.com/#ayangweb/BongoCat&Date"> 110 | <picture> 111 | <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date&theme=dark" /> 112 | <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" /> 113 | <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" /> 114 | </picture> 115 | </a> 116 | 117 | ## 致谢 118 | 119 | - 特别感谢 [UpgradeLink](https://www.toolsetlink.com/) 提供高效稳定的自动更新服务,让本项目得以持续为用户带来最新版本的优质体验。 120 | -------------------------------------------------------------------------------- /eslint.config.ts: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default antfu({ 4 | formatters: true, 5 | unocss: true, 6 | rules: { 7 | 'antfu/if-newline': 'off', 8 | 'style/brace-style': ['error', '1tbs'], 9 | 'ts/no-use-before-define': 'off', 10 | 'unused-imports/no-unused-imports': 'error', 11 | 'perfectionist/sort-imports': 'off', 12 | 'import/order': [ 13 | 'error', 14 | { 15 | 'newlines-between': 'always', 16 | 'groups': ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object'], 17 | 'alphabetize': { 18 | order: 'asc', 19 | caseInsensitive: true, 20 | }, 21 | }, 22 | ], 23 | 'vue/attributes-order': ['error', { alphabetical: true }], 24 | 'vue/max-attributes-per-line': 'error', 25 | }, 26 | ignores: ['**/*.toml'], 27 | }) 28 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | <!doctype html> 2 | <html lang="zh"> 3 | <head> 4 | <meta charset="UTF-8" /> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 | <title>BongoCat</title> 7 | <script src="/js/live2dcubismcore.min.js"></script> 8 | <script src="/js/live2d.min.js"></script> 9 | </head> 10 | 11 | <body> 12 | <div id="app"></div> 13 | <script type="module" src="/src/main.ts"></script> 14 | </body> 15 | </html> 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bongo-cat", 3 | "type": "module", 4 | "version": "0.6.2", 5 | "private": true, 6 | "author": { 7 | "name": "ayangweb", 8 | "email": "ayangweb@foxmail.com" 9 | }, 10 | "scripts": { 11 | "dev": "run-s build:icon dev:vite", 12 | "build": "run-s build:*", 13 | "dev:vite": "vite", 14 | "build:vite": "vite build", 15 | "build:icon": "tsx scripts/buildIcon.ts", 16 | "preview": "vite preview", 17 | "tauri": "tauri", 18 | "lint": "eslint --fix src", 19 | "preinstall": "npx only-allow pnpm", 20 | "prepare": "simple-git-hooks", 21 | "release": "release-it" 22 | }, 23 | "dependencies": { 24 | "@ant-design/icons-vue": "^7.0.1", 25 | "@tauri-apps/api": "^2.5.0", 26 | "@tauri-apps/plugin-autostart": "~2.3.0", 27 | "@tauri-apps/plugin-clipboard-manager": "~2.2.2", 28 | "@tauri-apps/plugin-dialog": "~2.2.2", 29 | "@tauri-apps/plugin-fs": "~2.3.0", 30 | "@tauri-apps/plugin-global-shortcut": "~2.2.1", 31 | "@tauri-apps/plugin-log": "~2.3.1", 32 | "@tauri-apps/plugin-opener": "~2.2.7", 33 | "@tauri-apps/plugin-os": "^2.2.1", 34 | "@tauri-apps/plugin-process": "^2.2.1", 35 | "@tauri-apps/plugin-updater": "~2.7.1", 36 | "@tauri-store/pinia": "^3.7.0", 37 | "@vueuse/core": "^13.3.0", 38 | "ant-design-vue": "^4.2.6", 39 | "dayjs": "^1.11.13", 40 | "es-toolkit": "^1.38.0", 41 | "is-url": "^1.2.4", 42 | "nanoid": "^5.1.5", 43 | "pinia": "^3.0.3", 44 | "pixi-live2d-display": "^0.4.0", 45 | "pixi.js": "^6.5.10", 46 | "tauri-plugin-macos-permissions-api": "^2.3.0", 47 | "vue": "^3.5.16", 48 | "vue-markdown-render": "^2.2.1", 49 | "vue-router": "^4.5.1", 50 | "vue3-masonry-css": "^1.0.7" 51 | }, 52 | "devDependencies": { 53 | "@antfu/eslint-config": "^4.13.3", 54 | "@commitlint/cli": "^19.8.1", 55 | "@commitlint/config-conventional": "^19.8.1", 56 | "@iconify-json/iconamoon": "^1.2.2", 57 | "@iconify-json/solar": "^1.2.2", 58 | "@tauri-apps/cli": "^2.5.0", 59 | "@types/is-url": "^1.2.32", 60 | "@types/node": "^22.15.29", 61 | "@unocss/eslint-plugin": "^66.1.3", 62 | "@vitejs/plugin-vue": "^5.2.4", 63 | "eslint": "^9.28.0", 64 | "eslint-plugin-format": "^1.0.1", 65 | "lint-staged": "^15.5.2", 66 | "npm-run-all": "^4.1.5", 67 | "release-it": "^18.1.2", 68 | "sass": "^1.89.1", 69 | "simple-git-hooks": "^2.13.0", 70 | "tsx": "^4.19.4", 71 | "typescript": "~5.6.3", 72 | "unocss": "66.1.0-beta.7", 73 | "vite": "^6.3.5" 74 | }, 75 | "simple-git-hooks": { 76 | "commit-msg": "npx --no-install commitlint -e", 77 | "pre-commit": "npx lint-staged" 78 | }, 79 | "lint-staged": { 80 | "*": "eslint --fix" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/public/logo.png -------------------------------------------------------------------------------- /scripts/buildIcon.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process' 2 | import { env, platform } from 'node:process' 3 | 4 | (() => { 5 | const isMac = env.PLATFORM?.startsWith('macos') ?? platform === 'darwin' 6 | 7 | const logoName = isMac ? 'logo-mac' : 'logo' 8 | 9 | const command = `tauri icon src-tauri/assets/${logoName}.png` 10 | 11 | execSync(command, { stdio: 'inherit' }) 12 | })() 13 | -------------------------------------------------------------------------------- /scripts/release.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'node:fs' 2 | import { dirname, resolve } from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | import { name, version } from '../package.json' 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | 9 | (() => { 10 | const tomlPath = resolve(__dirname, '..', 'src-tauri', 'Cargo.toml') 11 | const lockPath = resolve(__dirname, '..', 'Cargo.lock') 12 | 13 | for (const path of [tomlPath, lockPath]) { 14 | let content = readFileSync(path, 'utf-8') 15 | 16 | const regexp = new RegExp( 17 | `(name\\s*=\\s*"${name}"\\s*version\\s*=\\s*)"(\\d+\\.\\d+\\.\\d+(-\\w+\\.\\d+)?)"`, 18 | ) 19 | 20 | content = content.replace(regexp, `$1"${version}"`) 21 | 22 | writeFileSync(path, content) 23 | } 24 | })() 25 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Generated by Tauri 6 | # will have schema files for capabilities auto-completion 7 | /gen/schemas 8 | 9 | icons 10 | autogenerated 11 | schemas -------------------------------------------------------------------------------- /src-tauri/BongoCat.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | Name={{{name}}} 4 | Exec={{{exec}}} 5 | Icon={{{icon}}} 6 | Categories={{{categories}}} 7 | Comment={{{comment}}} 8 | Terminal=false 9 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bongo-cat" 3 | version = "0.6.2" 4 | description = "A Tauri App" 5 | authors = [ "ayangweb" ] 6 | edition = "2024" 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [lib] 11 | # The `_lib` suffix may seem redundant but it is necessary 12 | # to make the lib name unique and wouldn't conflict with the bin name. 13 | # This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519 14 | name = "bongo_cat_lib" 15 | crate-type = ["staticlib", "cdylib", "rlib"] 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2", features = [] } 19 | 20 | [dependencies] 21 | tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-private-api", "image-png"] } 22 | serde = { workspace = true, features = ["derive"] } 23 | serde_json.workspace = true 24 | tauri-plugin-custom-window.workspace = true 25 | tauri-plugin-os = "2" 26 | tauri-plugin-process = "2" 27 | tauri-plugin-opener = "2" 28 | tauri-plugin-pinia = "3" 29 | tauri-plugin-log = "2" 30 | tauri-plugin-updater = "2" 31 | tauri-plugin-prevent-default = "1" 32 | tauri-plugin-single-instance = "2" 33 | tauri-plugin-autostart = "2" 34 | tauri-plugin-macos-permissions = "2" 35 | tauri-plugin-dialog = "2" 36 | tauri-plugin-fs = "2" 37 | fs_extra = "1" 38 | tauri-plugin-clipboard-manager = "2" 39 | tauri-plugin-global-shortcut = "2" 40 | 41 | [target."cfg(target_os = \"macos\")".dependencies] 42 | tauri-nspanel.workspace = true 43 | 44 | [target."cfg(not(target_os = \"linux\"))".dependencies] 45 | rdev = { git = "https://github.com/ayangweb/rdev" } 46 | 47 | [target."cfg(target_os = \"linux\")".dependencies] 48 | nix = { version = "0.30", features = ["poll"] } 49 | input = "0.9" 50 | 51 | [features] 52 | cargo-clippy = [] 53 | -------------------------------------------------------------------------------- /src-tauri/assets/logo-mac.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/logo-mac.png -------------------------------------------------------------------------------- /src-tauri/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/logo.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/cat.model3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "FileReferences": { 4 | "Moc": "demomodel2.moc3", 5 | "Textures": [ 6 | "demomodel2.1024/texture_00.png", 7 | "demomodel2.1024/texture_01.png", 8 | "demomodel2.1024/texture_02.png" 9 | ], 10 | "DisplayInfo": "demomodel2.cdi3.json", 11 | "Expressions": [ 12 | { 13 | "Name": "默认喵", 14 | "File": "live2d_expression0.exp3.json" 15 | }, 16 | { 17 | "Name": "社会喵", 18 | "File": "live2d_expression1.exp3.json", 19 | "Description": "喵喵我叼根小烟,耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!" 20 | }, 21 | { 22 | "Name": "天使喵", 23 | "File": "live2d_expression2.exp3.json" 24 | } 25 | ], 26 | "Motions": { 27 | "CAT_motion": [ 28 | { 29 | "Name": "雷霆喵", 30 | "File": "live2d_motion1.motion3.json", 31 | "Sound": "live2d_motion1.flac", 32 | "FadeInTime": 0, 33 | "FadeOutTime": 0 34 | }, 35 | { 36 | "Name": "摇摆喵", 37 | "File": "live2d_motion2.motion3.json", 38 | "FadeInTime": 0, 39 | "FadeOutTime": 0 40 | } 41 | ] 42 | } 43 | }, 44 | "Groups": [ 45 | { 46 | "Target": "Parameter", 47 | "Name": "EyeBlink", 48 | "Ids": [ 49 | "ParamEyeLOpen", 50 | "ParamEyeROpen" 51 | ] 52 | }, 53 | { 54 | "Target": "Parameter", 55 | "Name": "LipSync", 56 | "Ids": [] 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/demomodel2.1024/texture_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.1024/texture_00.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/demomodel2.1024/texture_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.1024/texture_01.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/demomodel2.1024/texture_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.1024/texture_02.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/demomodel2.cdi3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Parameters": [ 4 | { 5 | "Id": "ParamAngleX", 6 | "GroupId": "", 7 | "Name": "角度 X" 8 | }, 9 | { 10 | "Id": "ParamAngleY", 11 | "GroupId": "", 12 | "Name": "角度 Y" 13 | }, 14 | { 15 | "Id": "CatParamRightHandDown", 16 | "GroupId": "", 17 | "Name": "右手按下" 18 | }, 19 | { 20 | "Id": "CatParamLeftHandDown", 21 | "GroupId": "", 22 | "Name": "左手按下" 23 | }, 24 | { 25 | "Id": "ParamAngleZ", 26 | "GroupId": "", 27 | "Name": "角度 Z" 28 | }, 29 | { 30 | "Id": "ParamEyeLOpen", 31 | "GroupId": "", 32 | "Name": "左眼 开闭" 33 | }, 34 | { 35 | "Id": "ParamEyeLSmile", 36 | "GroupId": "", 37 | "Name": "左眼 微笑" 38 | }, 39 | { 40 | "Id": "ParamEyeROpen", 41 | "GroupId": "", 42 | "Name": "右眼" 43 | }, 44 | { 45 | "Id": "ParamEyeRSmile", 46 | "GroupId": "", 47 | "Name": "右眼 微笑" 48 | }, 49 | { 50 | "Id": "Param3", 51 | "GroupId": "", 52 | "Name": "挥手" 53 | }, 54 | { 55 | "Id": "Param", 56 | "GroupId": "ParamGroup", 57 | "Name": "开启闪电" 58 | }, 59 | { 60 | "Id": "Param2", 61 | "GroupId": "ParamGroup", 62 | "Name": "闪电划过" 63 | }, 64 | { 65 | "Id": "Param4", 66 | "GroupId": "ParamGroup2", 67 | "Name": "表情:thuglife" 68 | }, 69 | { 70 | "Id": "Param5", 71 | "GroupId": "ParamGroup2", 72 | "Name": "表情:升天" 73 | }, 74 | { 75 | "Id": "ParamEyeBallX", 76 | "GroupId": "", 77 | "Name": "眼球 X" 78 | }, 79 | { 80 | "Id": "ParamEyeBallY", 81 | "GroupId": "", 82 | "Name": "眼球 Y" 83 | }, 84 | { 85 | "Id": "ParamBrowLY", 86 | "GroupId": "", 87 | "Name": "左眉上下" 88 | }, 89 | { 90 | "Id": "ParamBrowRY", 91 | "GroupId": "", 92 | "Name": "右眉 上下" 93 | }, 94 | { 95 | "Id": "ParamBrowLX", 96 | "GroupId": "", 97 | "Name": "左眉 左右" 98 | }, 99 | { 100 | "Id": "ParamBrowRX", 101 | "GroupId": "", 102 | "Name": "右眉 左右" 103 | }, 104 | { 105 | "Id": "ParamBrowLAngle", 106 | "GroupId": "", 107 | "Name": "左眉 角度" 108 | }, 109 | { 110 | "Id": "ParamBrowRAngle", 111 | "GroupId": "", 112 | "Name": "右眉 角度" 113 | }, 114 | { 115 | "Id": "ParamBrowLForm", 116 | "GroupId": "", 117 | "Name": "左眉 変形" 118 | }, 119 | { 120 | "Id": "ParamBrowRForm", 121 | "GroupId": "", 122 | "Name": "右眉 変形" 123 | }, 124 | { 125 | "Id": "ParamMouthForm", 126 | "GroupId": "", 127 | "Name": "嘴部 变形" 128 | }, 129 | { 130 | "Id": "ParamMouthOpenY", 131 | "GroupId": "", 132 | "Name": "嘴巴 张开和闭合" 133 | }, 134 | { 135 | "Id": "ParamCheek", 136 | "GroupId": "", 137 | "Name": "脸颊" 138 | }, 139 | { 140 | "Id": "ParamBodyAngleX", 141 | "GroupId": "", 142 | "Name": "身体旋转 X" 143 | }, 144 | { 145 | "Id": "ParamBodyAngleY", 146 | "GroupId": "", 147 | "Name": "身体旋转 Y" 148 | }, 149 | { 150 | "Id": "ParamBodyAngleZ", 151 | "GroupId": "", 152 | "Name": "身体旋转 Z" 153 | }, 154 | { 155 | "Id": "ParamBreath", 156 | "GroupId": "", 157 | "Name": "呼吸" 158 | }, 159 | { 160 | "Id": "ParamHairFront", 161 | "GroupId": "", 162 | "Name": "摇动 前发" 163 | }, 164 | { 165 | "Id": "ParamHairSide", 166 | "GroupId": "", 167 | "Name": "摇动 侧发" 168 | }, 169 | { 170 | "Id": "ParamHairBack", 171 | "GroupId": "", 172 | "Name": "摇动 后发" 173 | } 174 | ], 175 | "ParameterGroups": [ 176 | { 177 | "Id": "ParamGroup", 178 | "GroupId": "", 179 | "Name": "闪电" 180 | }, 181 | { 182 | "Id": "ParamGroup2", 183 | "GroupId": "", 184 | "Name": "表情" 185 | } 186 | ], 187 | "Parts": [ 188 | { 189 | "Id": "Part11", 190 | "Name": "demomodel.psd(未找到对应图层)" 191 | }, 192 | { 193 | "Id": "Part7", 194 | "Name": "demomodel.psd(未找到对应图层)" 195 | }, 196 | { 197 | "Id": "Part3", 198 | "Name": "demomodel.psd(未找到对应图层)" 199 | }, 200 | { 201 | "Id": "Part2", 202 | "Name": "demomodel.psd(未找到对应图层)" 203 | }, 204 | { 205 | "Id": "Part", 206 | "Name": "demomodel.psd(未找到对应图层)" 207 | }, 208 | { 209 | "Id": "Part10", 210 | "Name": "天使环" 211 | }, 212 | { 213 | "Id": "Part5", 214 | "Name": "demomodel.psd(未找到对应图层)" 215 | }, 216 | { 217 | "Id": "PartSketch0", 218 | "Name": "[ 参考图 ]" 219 | }, 220 | { 221 | "Id": "Part8", 222 | "Name": "thug life" 223 | }, 224 | { 225 | "Id": "Part6", 226 | "Name": "闪电" 227 | }, 228 | { 229 | "Id": "Part4", 230 | "Name": "闪电" 231 | } 232 | ] 233 | } 234 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/demomodel2.moc3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/demomodel2.moc3 -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/exp_1.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "ParamEyeLOpen", 6 | "Value": 0.321, 7 | "Blend": "Multiply" 8 | }, 9 | { 10 | "Id": "ParamEyeROpen", 11 | "Value": 0.313, 12 | "Blend": "Multiply" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/exp_2.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "ParamEyeLOpen", 6 | "Value": -1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/live2d_expression0.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [] 4 | } 5 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/live2d_expression1.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "FadeInTime": 0.8, 4 | "Parameters": [ 5 | { 6 | "Id": "Param4", 7 | "Value": 1, 8 | "Blend": "Add" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/live2d_expression2.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "FadeInTime": 0.5, 4 | "Parameters": [ 5 | { 6 | "Id": "Param5", 7 | "Value": 1, 8 | "Blend": "Add" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/live2d_motion1.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/live2d_motion1.flac -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/live2d_motion1.motion3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Meta": { 4 | "Duration": 1.633, 5 | "Fps": 30.0, 6 | "Loop": true, 7 | "AreBeziersRestricted": false, 8 | "CurveCount": 2, 9 | "TotalSegmentCount": 8, 10 | "TotalPointCount": 20, 11 | "UserDataCount": 0, 12 | "TotalUserDataSize": 0 13 | }, 14 | "Curves": [ 15 | { 16 | "Target": "Parameter", 17 | "Id": "Param", 18 | "Segments": [ 19 | 0, 20 | 0, 21 | 1, 22 | 0.033, 23 | 0, 24 | 0.067, 25 | 1, 26 | 0.1, 27 | 1, 28 | 1, 29 | 0.411, 30 | 1, 31 | 0.722, 32 | 1, 33 | 1.033, 34 | 1, 35 | 1, 36 | 1.189, 37 | 1, 38 | 1.344, 39 | 0, 40 | 1.5, 41 | 0, 42 | 0, 43 | 1.633, 44 | 0 45 | ] 46 | }, 47 | { 48 | "Target": "Parameter", 49 | "Id": "Param2", 50 | "Segments": [ 51 | 0, 52 | 0, 53 | 0, 54 | 0.067, 55 | 0, 56 | 1, 57 | 0.1, 58 | 0, 59 | 0.133, 60 | 0.142, 61 | 0.167, 62 | 0.2, 63 | 1, 64 | 0.489, 65 | 0.764, 66 | 0.811, 67 | 1, 68 | 1.133, 69 | 1, 70 | 0, 71 | 1.633, 72 | 1 73 | ] 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/live2d_motion2.motion3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Meta": { 4 | "Duration": 2.333, 5 | "Fps": 30.0, 6 | "Loop": true, 7 | "AreBeziersRestricted": true, 8 | "CurveCount": 2, 9 | "TotalSegmentCount": 7, 10 | "TotalPointCount": 21, 11 | "UserDataCount": 0, 12 | "TotalUserDataSize": 0 13 | }, 14 | "Curves": [ 15 | { 16 | "Target": "Parameter", 17 | "Id": "CatParamLeftHandDown", 18 | "Segments": [ 19 | 0, 20 | 0, 21 | 0, 22 | 2.333, 23 | 0 24 | ] 25 | }, 26 | { 27 | "Target": "Parameter", 28 | "Id": "Param3", 29 | "Segments": [ 30 | 0, 31 | 0, 32 | 1, 33 | 0.133, 34 | 0, 35 | 0.267, 36 | 30, 37 | 0.4, 38 | 30, 39 | 1, 40 | 0.522, 41 | 30, 42 | 0.644, 43 | 0, 44 | 0.767, 45 | 0, 46 | 1, 47 | 0.9, 48 | 0, 49 | 1.033, 50 | 30, 51 | 1.167, 52 | 30, 53 | 1, 54 | 1.3, 55 | 30, 56 | 1.433, 57 | 0, 58 | 1.567, 59 | 0, 60 | 1, 61 | 1.7, 62 | 0, 63 | 1.833, 64 | 30, 65 | 1.967, 66 | 30, 67 | 1, 68 | 2.089, 69 | 30, 70 | 2.211, 71 | 0, 72 | 2.333, 73 | 0 74 | ] 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/background.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/cover.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Alt.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/AltGr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/AltGr.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/BackQuote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/BackQuote.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Backspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Backspace.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/CapsLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/CapsLock.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Control.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/ControlLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ControlLeft.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/ControlRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ControlRight.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Delete.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Escape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Escape.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Fn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Fn.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyA.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyB.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyC.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyD.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyE.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyF.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyG.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyH.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyI.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyJ.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyK.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyL.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyM.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyN.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyO.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyP.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyQ.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyR.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyS.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyT.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyU.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyV.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyW.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyX.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyY.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/KeyZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/KeyZ.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Meta.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num0.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num1.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num2.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num3.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num4.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num5.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num6.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num7.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num8.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Num9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Num9.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Return.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Shift.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/ShiftLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ShiftLeft.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/ShiftRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/ShiftRight.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Slash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Slash.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Space.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/left-keys/Tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/left-keys/Tab.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/right-keys/DownArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/DownArrow.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/right-keys/LeftArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/LeftArrow.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/right-keys/RightArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/RightArrow.png -------------------------------------------------------------------------------- /src-tauri/assets/models/keyboard/resources/right-keys/UpArrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/keyboard/resources/right-keys/UpArrow.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/cat.model3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "FileReferences": { 4 | "Moc": "demomodel.moc3", 5 | "Textures": [ 6 | "demomodel.1024/texture_00.png", 7 | "demomodel.1024/texture_01.png", 8 | "demomodel.1024/texture_02.png" 9 | ], 10 | "DisplayInfo": "demomodel.cdi3.json", 11 | "Expressions": [ 12 | { 13 | "Name": "默认喵", 14 | "File": "live2d_expression0.exp3.json" 15 | }, 16 | { 17 | "Name": "社会喵", 18 | "File": "live2d_expression1.exp3.json", 19 | "Description": "喵喵我叼根小烟,耍个帅气俏皮的wink~超有范儿!但直播里用这招,怕是会让铲屎官和平台瞪大眼,本喵还是低调点,偷偷耍酷好啦!" 20 | 21 | }, 22 | { 23 | "Name": "天使喵", 24 | "File": "live2d_expression2.exp3.json" 25 | } 26 | ], 27 | "Motions": { 28 | "CAT_motion": [ 29 | { 30 | "Name": "雷霆喵", 31 | "File": "live2d_motion1.motion3.json", 32 | "Sound": "live2d_motion1.flac", 33 | "FadeInTime": 0, 34 | "FadeOutTime": 0 35 | }, 36 | { 37 | "Name": "摇摆喵", 38 | "File": "live2d_motion2.motion3.json", 39 | "FadeInTime": 0, 40 | "FadeOutTime": 0 41 | } 42 | ] 43 | } 44 | }, 45 | "Groups": [ 46 | { 47 | "Target": "Parameter", 48 | "Name": "EyeBlink", 49 | "Ids": [ 50 | "ParamEyeLOpen", 51 | "ParamEyeROpen" 52 | ] 53 | }, 54 | { 55 | "Target": "Parameter", 56 | "Name": "LipSync", 57 | "Ids": [] 58 | } 59 | ], 60 | "HitAreas": [] 61 | } 62 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/demomodel.1024/texture_00.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.1024/texture_00.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/demomodel.1024/texture_01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.1024/texture_01.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/demomodel.1024/texture_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.1024/texture_02.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/demomodel.cdi3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Parameters": [ 4 | { 5 | "Id": "ParamAngleX", 6 | "GroupId": "", 7 | "Name": "角度 X" 8 | }, 9 | { 10 | "Id": "ParamAngleY", 11 | "GroupId": "", 12 | "Name": "角度 Y" 13 | }, 14 | { 15 | "Id": "ParamMouseX", 16 | "GroupId": "", 17 | "Name": "鼠标X" 18 | }, 19 | { 20 | "Id": "ParamMouseY", 21 | "GroupId": "", 22 | "Name": "鼠标Y" 23 | }, 24 | { 25 | "Id": "ParamMouseLeftDown", 26 | "GroupId": "", 27 | "Name": "鼠标左键按下" 28 | }, 29 | { 30 | "Id": "ParamMouseRightDown", 31 | "GroupId": "", 32 | "Name": "鼠标右键按下" 33 | }, 34 | { 35 | "Id": "CatParamLeftHandDown", 36 | "GroupId": "", 37 | "Name": "键盘按下" 38 | }, 39 | { 40 | "Id": "ParamAngleZ", 41 | "GroupId": "", 42 | "Name": "角度 Z" 43 | }, 44 | { 45 | "Id": "ParamEyeLOpen", 46 | "GroupId": "", 47 | "Name": "左眼 开闭" 48 | }, 49 | { 50 | "Id": "ParamEyeLSmile", 51 | "GroupId": "", 52 | "Name": "左眼 微笑" 53 | }, 54 | { 55 | "Id": "ParamEyeROpen", 56 | "GroupId": "", 57 | "Name": "右眼" 58 | }, 59 | { 60 | "Id": "ParamEyeRSmile", 61 | "GroupId": "", 62 | "Name": "右眼 微笑" 63 | }, 64 | { 65 | "Id": "Param3", 66 | "GroupId": "", 67 | "Name": "挥手" 68 | }, 69 | { 70 | "Id": "Param", 71 | "GroupId": "ParamGroup", 72 | "Name": "开启闪电" 73 | }, 74 | { 75 | "Id": "Param2", 76 | "GroupId": "ParamGroup", 77 | "Name": "闪电划过" 78 | }, 79 | { 80 | "Id": "Param4", 81 | "GroupId": "ParamGroup2", 82 | "Name": "表情:thuglife" 83 | }, 84 | { 85 | "Id": "Param5", 86 | "GroupId": "ParamGroup2", 87 | "Name": "表情:升天" 88 | }, 89 | { 90 | "Id": "ParamEyeBallX", 91 | "GroupId": "", 92 | "Name": "眼球 X" 93 | }, 94 | { 95 | "Id": "ParamEyeBallY", 96 | "GroupId": "", 97 | "Name": "眼球 Y" 98 | }, 99 | { 100 | "Id": "ParamBrowLY", 101 | "GroupId": "", 102 | "Name": "左眉上下" 103 | }, 104 | { 105 | "Id": "ParamBrowRY", 106 | "GroupId": "", 107 | "Name": "右眉 上下" 108 | }, 109 | { 110 | "Id": "ParamBrowLX", 111 | "GroupId": "", 112 | "Name": "左眉 左右" 113 | }, 114 | { 115 | "Id": "ParamBrowRX", 116 | "GroupId": "", 117 | "Name": "右眉 左右" 118 | }, 119 | { 120 | "Id": "ParamBrowLAngle", 121 | "GroupId": "", 122 | "Name": "左眉 角度" 123 | }, 124 | { 125 | "Id": "ParamBrowRAngle", 126 | "GroupId": "", 127 | "Name": "右眉 角度" 128 | }, 129 | { 130 | "Id": "ParamBrowLForm", 131 | "GroupId": "", 132 | "Name": "左眉 変形" 133 | }, 134 | { 135 | "Id": "ParamBrowRForm", 136 | "GroupId": "", 137 | "Name": "右眉 変形" 138 | }, 139 | { 140 | "Id": "ParamMouthForm", 141 | "GroupId": "", 142 | "Name": "嘴部 变形" 143 | }, 144 | { 145 | "Id": "ParamMouthOpenY", 146 | "GroupId": "", 147 | "Name": "嘴巴 张开和闭合" 148 | }, 149 | { 150 | "Id": "ParamCheek", 151 | "GroupId": "", 152 | "Name": "脸颊" 153 | }, 154 | { 155 | "Id": "ParamBodyAngleX", 156 | "GroupId": "", 157 | "Name": "身体旋转 X" 158 | }, 159 | { 160 | "Id": "ParamBodyAngleY", 161 | "GroupId": "", 162 | "Name": "身体旋转 Y" 163 | }, 164 | { 165 | "Id": "ParamBodyAngleZ", 166 | "GroupId": "", 167 | "Name": "身体旋转 Z" 168 | }, 169 | { 170 | "Id": "ParamBreath", 171 | "GroupId": "", 172 | "Name": "呼吸" 173 | }, 174 | { 175 | "Id": "ParamHairFront", 176 | "GroupId": "", 177 | "Name": "摇动 前发" 178 | }, 179 | { 180 | "Id": "ParamHairSide", 181 | "GroupId": "", 182 | "Name": "摇动 侧发" 183 | }, 184 | { 185 | "Id": "ParamHairBack", 186 | "GroupId": "", 187 | "Name": "摇动 后发" 188 | } 189 | ], 190 | "ParameterGroups": [ 191 | { 192 | "Id": "ParamGroup", 193 | "GroupId": "", 194 | "Name": "闪电" 195 | }, 196 | { 197 | "Id": "ParamGroup2", 198 | "GroupId": "", 199 | "Name": "表情" 200 | } 201 | ], 202 | "Parts": [ 203 | { 204 | "Id": "Part11", 205 | "Name": "demomodel.psd(未找到对应图层)" 206 | }, 207 | { 208 | "Id": "Part7", 209 | "Name": "demomodel.psd(未找到对应图层)" 210 | }, 211 | { 212 | "Id": "Part3", 213 | "Name": "demomodel.psd(未找到对应图层)" 214 | }, 215 | { 216 | "Id": "Part2", 217 | "Name": "demomodel.psd(未找到对应图层)" 218 | }, 219 | { 220 | "Id": "Part", 221 | "Name": "demomodel.psd(未找到对应图层)" 222 | }, 223 | { 224 | "Id": "Part10", 225 | "Name": "天使环" 226 | }, 227 | { 228 | "Id": "Part5", 229 | "Name": "demomodel.psd(未找到对应图层)" 230 | }, 231 | { 232 | "Id": "Part8", 233 | "Name": "thug life" 234 | }, 235 | { 236 | "Id": "Part6", 237 | "Name": "闪电" 238 | }, 239 | { 240 | "Id": "Part4", 241 | "Name": "闪电" 242 | } 243 | ] 244 | } 245 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/demomodel.moc3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/demomodel.moc3 -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/exp_1.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "ParamEyeLOpen", 6 | "Value": 0.321, 7 | "Blend": "Multiply" 8 | }, 9 | { 10 | "Id": "ParamEyeROpen", 11 | "Value": 0.313, 12 | "Blend": "Multiply" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/exp_2.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [ 4 | { 5 | "Id": "ParamEyeLOpen", 6 | "Value": -1, 7 | "Blend": "Add" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/live2d_expression0.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "Parameters": [] 4 | } 5 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/live2d_expression1.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "FadeInTime": 0.8, 4 | "Parameters": [ 5 | { 6 | "Id": "Param4", 7 | "Value": 1, 8 | "Blend": "Add" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/live2d_expression2.exp3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Type": "Live2D Expression", 3 | "FadeInTime": 0.5, 4 | "Parameters": [ 5 | { 6 | "Id": "Param5", 7 | "Value": 1, 8 | "Blend": "Add" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/live2d_motion1.flac: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/live2d_motion1.flac -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/live2d_motion1.motion3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Meta": { 4 | "Duration": 1.633, 5 | "Fps": 30.0, 6 | "Loop": true, 7 | "AreBeziersRestricted": false, 8 | "CurveCount": 2, 9 | "TotalSegmentCount": 8, 10 | "TotalPointCount": 20, 11 | "UserDataCount": 0, 12 | "TotalUserDataSize": 0 13 | }, 14 | "Curves": [ 15 | { 16 | "Target": "Parameter", 17 | "Id": "Param", 18 | "Segments": [ 19 | 0, 20 | 0, 21 | 1, 22 | 0.033, 23 | 0, 24 | 0.067, 25 | 1, 26 | 0.1, 27 | 1, 28 | 1, 29 | 0.411, 30 | 1, 31 | 0.722, 32 | 1, 33 | 1.033, 34 | 1, 35 | 1, 36 | 1.189, 37 | 1, 38 | 1.344, 39 | 0, 40 | 1.5, 41 | 0, 42 | 0, 43 | 1.633, 44 | 0 45 | ] 46 | }, 47 | { 48 | "Target": "Parameter", 49 | "Id": "Param2", 50 | "Segments": [ 51 | 0, 52 | 0, 53 | 0, 54 | 0.067, 55 | 0, 56 | 1, 57 | 0.1, 58 | 0, 59 | 0.133, 60 | 0.142, 61 | 0.167, 62 | 0.2, 63 | 1, 64 | 0.489, 65 | 0.764, 66 | 0.811, 67 | 1, 68 | 1.133, 69 | 1, 70 | 0, 71 | 1.633, 72 | 1 73 | ] 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/live2d_motion2.motion3.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": 3, 3 | "Meta": { 4 | "Duration": 2.333, 5 | "Fps": 30.0, 6 | "Loop": true, 7 | "AreBeziersRestricted": true, 8 | "CurveCount": 2, 9 | "TotalSegmentCount": 7, 10 | "TotalPointCount": 21, 11 | "UserDataCount": 0, 12 | "TotalUserDataSize": 0 13 | }, 14 | "Curves": [ 15 | { 16 | "Target": "Parameter", 17 | "Id": "CatParamLeftHandDown", 18 | "Segments": [ 19 | 0, 20 | 0, 21 | 0, 22 | 2.333, 23 | 0 24 | ] 25 | }, 26 | { 27 | "Target": "Parameter", 28 | "Id": "Param3", 29 | "Segments": [ 30 | 0, 31 | 0, 32 | 1, 33 | 0.133, 34 | 0, 35 | 0.267, 36 | 30, 37 | 0.4, 38 | 30, 39 | 1, 40 | 0.522, 41 | 30, 42 | 0.644, 43 | 0, 44 | 0.767, 45 | 0, 46 | 1, 47 | 0.9, 48 | 0, 49 | 1.033, 50 | 30, 51 | 1.167, 52 | 30, 53 | 1, 54 | 1.3, 55 | 30, 56 | 1.433, 57 | 0, 58 | 1.567, 59 | 0, 60 | 1, 61 | 1.7, 62 | 0, 63 | 1.833, 64 | 30, 65 | 1.967, 66 | 30, 67 | 1, 68 | 2.089, 69 | 30, 70 | 2.211, 71 | 0, 72 | 2.333, 73 | 0 74 | ] 75 | } 76 | ] 77 | } 78 | -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/background.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/cover.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Alt.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/AltGr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/AltGr.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/BackQuote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/BackQuote.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Backspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Backspace.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/CapsLock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/CapsLock.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Control.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Control.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/ControlLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ControlLeft.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/ControlRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ControlRight.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Delete.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Escape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Escape.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Fn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Fn.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyA.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyA.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyB.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyB.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyC.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyD.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyD.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyE.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyE.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyF.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyG.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyG.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyH.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyH.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyI.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyJ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyJ.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyK.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyK.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyL.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyL.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyM.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyM.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyN.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyO.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyP.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyP.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyQ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyQ.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyR.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyR.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyS.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyT.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyU.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyU.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyV.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyV.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyW.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyX.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyY.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyY.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/KeyZ.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/KeyZ.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Meta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Meta.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num0.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num1.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num2.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num3.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num4.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num5.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num6.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num7.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num8.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Num9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Num9.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Return.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Return.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Shift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Shift.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/ShiftLeft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ShiftLeft.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/ShiftRight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/ShiftRight.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Slash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Slash.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Space.png -------------------------------------------------------------------------------- /src-tauri/assets/models/standard/resources/left-keys/Tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/models/standard/resources/left-keys/Tab.png -------------------------------------------------------------------------------- /src-tauri/assets/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ayangweb/BongoCat/c50b197fb37ccfad3db5cd130317afa6cb951e00/src-tauri/assets/tray.png -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "default", 4 | "description": "Capability for the main window", 5 | "windows": [ 6 | "*" 7 | ], 8 | "permissions": [ 9 | "core:default", 10 | "core:window:allow-start-dragging", 11 | "core:window:allow-set-size", 12 | "core:window:deny-internal-toggle-maximize", 13 | "core:window:allow-set-always-on-top", 14 | "core:window:allow-set-ignore-cursor-events", 15 | "core:window:allow-set-decorations", 16 | "core:window:allow-set-position", 17 | "custom-window:default", 18 | "os:default", 19 | "process:default", 20 | "opener:default", 21 | { 22 | "identifier": "opener:allow-open-path", 23 | "allow": [ 24 | { 25 | "path": "**/*" 26 | } 27 | ] 28 | }, 29 | "pinia:default", 30 | "log:default", 31 | "updater:default", 32 | "prevent-default:default", 33 | "autostart:default", 34 | "macos-permissions:default", 35 | "dialog:default", 36 | "fs:default", 37 | "fs:read-all", 38 | "fs:write-all", 39 | { 40 | "identifier": "fs:scope", 41 | "allow": [ 42 | "**/*" 43 | ] 44 | }, 45 | "clipboard-manager:allow-write-text", 46 | "global-shortcut:allow-is-registered", 47 | "global-shortcut:allow-register", 48 | "global-shortcut:allow-unregister" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src-tauri/src/core/device/common.rs: -------------------------------------------------------------------------------- 1 | use crate::core::device::{DeviceEvent, DeviceEventKind}; 2 | use rdev::{Event, EventType, listen}; 3 | use serde_json::json; 4 | use std::sync::atomic::{AtomicBool, Ordering}; 5 | use tauri::{AppHandle, Emitter, Runtime, command}; 6 | 7 | static IS_RUNNING: AtomicBool = AtomicBool::new(false); 8 | 9 | #[command] 10 | pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> { 11 | if IS_RUNNING.load(Ordering::SeqCst) { 12 | return Err("Device is already listening".to_string()); 13 | } 14 | 15 | IS_RUNNING.store(true, Ordering::SeqCst); 16 | 17 | let callback = move |event: Event| { 18 | let device_event = match event.event_type { 19 | EventType::ButtonPress(button) => DeviceEvent { 20 | kind: DeviceEventKind::MousePress, 21 | value: json!(format!("{:?}", button)), 22 | }, 23 | EventType::ButtonRelease(button) => DeviceEvent { 24 | kind: DeviceEventKind::MouseRelease, 25 | value: json!(format!("{:?}", button)), 26 | }, 27 | EventType::MouseMove { x, y } => DeviceEvent { 28 | kind: DeviceEventKind::MouseMove, 29 | value: json!({ "x": x, "y": y }), 30 | }, 31 | EventType::KeyPress(key) => DeviceEvent { 32 | kind: DeviceEventKind::KeyboardPress, 33 | value: json!(format!("{:?}", key)), 34 | }, 35 | EventType::KeyRelease(key) => DeviceEvent { 36 | kind: DeviceEventKind::KeyboardRelease, 37 | value: json!(format!("{:?}", key)), 38 | }, 39 | _ => return, 40 | }; 41 | 42 | let _ = app_handle.emit("device-changed", device_event); 43 | }; 44 | 45 | listen(callback).map_err(|err| format!("Failed to listen device: {:?}", err))?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src-tauri/src/core/device/linux.rs: -------------------------------------------------------------------------------- 1 | use crate::core::device::{DeviceEvent, DeviceEventKind}; 2 | use input::{ 3 | Event, Libinput, LibinputInterface, 4 | event::{ 5 | PointerEvent, 6 | keyboard::{KeyState, KeyboardEventTrait}, 7 | pointer::ButtonState, 8 | }, 9 | }; 10 | use nix::{ 11 | libc::{O_RDONLY, O_RDWR, O_WRONLY}, 12 | poll::{PollFd, PollFlags, PollTimeout, poll}, 13 | }; 14 | use serde_json::json; 15 | use std::{ 16 | fs::{File, OpenOptions}, 17 | os::{ 18 | fd::{AsFd, OwnedFd}, 19 | unix::prelude::OpenOptionsExt, 20 | }, 21 | path::Path, 22 | sync::atomic::{AtomicBool, Ordering}, 23 | }; 24 | use tauri::{AppHandle, Emitter, Runtime, command}; 25 | 26 | static IS_RUNNING: AtomicBool = AtomicBool::new(false); 27 | 28 | pub struct Interface; 29 | 30 | impl LibinputInterface for Interface { 31 | fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> { 32 | OpenOptions::new() 33 | .custom_flags(flags) 34 | .read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0)) 35 | .write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0)) 36 | .open(path) 37 | .map(|file| file.into()) 38 | .map_err(|err| err.raw_os_error().unwrap()) 39 | } 40 | 41 | #[allow(unused_must_use)] 42 | fn close_restricted(&mut self, fd: OwnedFd) { 43 | File::from(fd); 44 | } 45 | } 46 | 47 | pub fn keyname_from_code(code: u32) -> Option<&'static str> { 48 | match code { 49 | // Function key 50 | 1 => Some("Escape"), 51 | 28 => Some("Return"), 52 | 14 => Some("Backspace"), 53 | 15 => Some("Tab"), 54 | 57 => Some("Space"), 55 | 58 => Some("CapsLock"), 56 | 99 => Some("PrintScreen"), 57 | 70 => Some("ScrollLock"), 58 | 119 => Some("Pause"), 59 | 69 => Some("NumLock"), 60 | 110 => Some("Insert"), 61 | 102 => Some("Home"), 62 | 107 => Some("End"), 63 | 104 => Some("PageUp"), 64 | 109 => Some("PageDown"), 65 | 111 => Some("Delete"), 66 | 67 | // Arrow key 68 | 103 => Some("UpArrow"), 69 | 108 => Some("DownArrow"), 70 | 105 => Some("LeftArrow"), 71 | 106 => Some("RightArrow"), 72 | 73 | // F key 74 | 59 => Some("F1"), 75 | 60 => Some("F2"), 76 | 61 => Some("F3"), 77 | 62 => Some("F4"), 78 | 63 => Some("F5"), 79 | 64 => Some("F6"), 80 | 65 => Some("F7"), 81 | 66 => Some("F8"), 82 | 67 => Some("F9"), 83 | 68 => Some("F10"), 84 | 87 => Some("F11"), 85 | 88 => Some("F12"), 86 | 87 | // Numeric 88 | 2 => Some("Num1"), 89 | 3 => Some("Num2"), 90 | 4 => Some("Num3"), 91 | 5 => Some("Num4"), 92 | 6 => Some("Num5"), 93 | 7 => Some("Num6"), 94 | 8 => Some("Num7"), 95 | 9 => Some("Num8"), 96 | 10 => Some("Num9"), 97 | 11 => Some("Num0"), 98 | 99 | // Alphabetic 100 | 16 => Some("KeyQ"), 101 | 17 => Some("KeyW"), 102 | 18 => Some("KeyE"), 103 | 19 => Some("KeyR"), 104 | 20 => Some("KeyT"), 105 | 21 => Some("KeyY"), 106 | 22 => Some("KeyU"), 107 | 23 => Some("KeyI"), 108 | 24 => Some("KeyO"), 109 | 25 => Some("KeyP"), 110 | 30 => Some("KeyA"), 111 | 31 => Some("KeyS"), 112 | 32 => Some("KeyD"), 113 | 33 => Some("KeyF"), 114 | 34 => Some("KeyG"), 115 | 35 => Some("KeyH"), 116 | 36 => Some("KeyJ"), 117 | 37 => Some("KeyK"), 118 | 38 => Some("KeyL"), 119 | 44 => Some("KeyZ"), 120 | 45 => Some("KeyX"), 121 | 46 => Some("KeyC"), 122 | 47 => Some("KeyV"), 123 | 48 => Some("KeyB"), 124 | 49 => Some("KeyN"), 125 | 50 => Some("KeyM"), 126 | 127 | // Symbolic 128 | 41 => Some("BackQuote"), 129 | 12 => Some("Minus"), 130 | 13 => Some("Equal"), 131 | 26 => Some("LeftBracket"), 132 | 27 => Some("RightBracket"), 133 | 39 => Some("SemiColon"), 134 | 40 => Some("Quote"), 135 | 43 => Some("BackSlash"), 136 | 86 => Some("IntlBackslash"), 137 | 89 => Some("IntlRo"), 138 | 124 => Some("IntlYen"), 139 | 101 => Some("KanaMode"), 140 | 51 => Some("Comma"), 141 | 52 => Some("Dot"), 142 | 53 => Some("Slash"), 143 | 144 | // Control key 145 | 29 => Some("ControlLeft"), 146 | 97 => Some("ControlRight"), 147 | 42 => Some("ShiftLeft"), 148 | 54 => Some("ShiftRight"), 149 | 56 => Some("Alt"), 150 | 100 => Some("AltGr"), 151 | 125 => Some("MetaLeft"), 152 | 126 => Some("MetaRight"), 153 | 127 => Some("Apps"), 154 | 155 | // NumPad 156 | 55 => Some("KpMultiply"), 157 | 78 => Some("KpMinus"), 158 | 74 => Some("KpPlus"), 159 | 98 => Some("KpDivide"), 160 | 117 => Some("KpEqual"), 161 | 121 => Some("KpComma"), 162 | 96 => Some("KpReturn"), 163 | 83 => Some("KpDecimal"), 164 | 79 => Some("Kp1"), 165 | 80 => Some("Kp2"), 166 | 81 => Some("Kp3"), 167 | 75 => Some("Kp4"), 168 | 76 => Some("Kp5"), 169 | 77 => Some("Kp6"), 170 | 71 => Some("Kp7"), 171 | 72 => Some("Kp8"), 172 | 73 => Some("Kp9"), 173 | 82 => Some("Kp0"), 174 | 175 | // Media key 176 | 115 => Some("VolumeUp"), 177 | 114 => Some("VolumeDown"), 178 | 113 => Some("VolumeMute"), 179 | 180 | // Language key 181 | 90 => Some("Lang1"), 182 | 91 => Some("Lang2"), 183 | 92 => Some("Lang3"), 184 | 93 => Some("Lang4"), 185 | 94 => Some("Lang5"), 186 | 187 | // Other 188 | _ => None, 189 | } 190 | } 191 | 192 | fn build_device_event(event: &Event) -> Option<DeviceEvent> { 193 | match event { 194 | Event::Keyboard(ev) => { 195 | let key_code = ev.key(); 196 | let key_name = match keyname_from_code(key_code) { 197 | Some(name) => name.to_string(), 198 | None => format!("Unknown({})", key_code), 199 | }; 200 | match ev.key_state() { 201 | KeyState::Pressed => Some(DeviceEvent { 202 | kind: DeviceEventKind::KeyboardPress, 203 | value: json!(key_name), 204 | }), 205 | KeyState::Released => Some(DeviceEvent { 206 | kind: DeviceEventKind::KeyboardRelease, 207 | value: json!(key_name), 208 | }), 209 | } 210 | } 211 | Event::Pointer(ev) => match ev { 212 | PointerEvent::Button(e) => { 213 | let btn_code = e.button(); 214 | let btn_name = match btn_code { 215 | 0x110 => String::from("Left"), 216 | 0x111 => String::from("Right"), 217 | 0x112 => String::from("Middle"), 218 | _ => format!("Unknown({})", btn_code as u8), 219 | }; 220 | match e.button_state() { 221 | ButtonState::Pressed => Some(DeviceEvent { 222 | kind: DeviceEventKind::MousePress, 223 | value: json!(btn_name), 224 | }), 225 | ButtonState::Released => Some(DeviceEvent { 226 | kind: DeviceEventKind::MouseRelease, 227 | value: json!(btn_name), 228 | }), 229 | } 230 | } 231 | PointerEvent::Motion(e) => Some(DeviceEvent { 232 | kind: DeviceEventKind::MouseMove, 233 | value: json!({ 234 | "x": e.dx_unaccelerated(), 235 | "y": e.dy_unaccelerated() 236 | }), 237 | }), 238 | _ => None, 239 | }, 240 | _ => None, 241 | } 242 | } 243 | 244 | #[command] 245 | pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> { 246 | if IS_RUNNING.load(Ordering::SeqCst) { 247 | return Err("Device is already listening".to_string()); 248 | } 249 | 250 | IS_RUNNING.store(true, Ordering::SeqCst); 251 | 252 | let mut input = Libinput::new_with_udev(Interface); 253 | match input.udev_assign_seat("seat0") { 254 | Ok(_) => { 255 | let input_clone = &input.clone(); 256 | let mut pollfds = [PollFd::new(input_clone.as_fd(), PollFlags::POLLIN)]; 257 | while poll(&mut pollfds, PollTimeout::NONE).is_ok() { 258 | input.dispatch().unwrap(); 259 | for event in &mut input { 260 | let device_event = build_device_event(&event); 261 | if let Some(e) = device_event { 262 | app_handle.emit("device-changed", e).unwrap(); 263 | } 264 | } 265 | } 266 | } 267 | Err(err) => return Err(format!("Failed to assign seat: {:?}", err)), 268 | } 269 | 270 | Ok(()) 271 | } 272 | -------------------------------------------------------------------------------- /src-tauri/src/core/device/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::Serialize; 2 | use serde_json::Value; 3 | 4 | #[cfg(not(target_os = "linux"))] 5 | pub mod common; 6 | 7 | #[cfg(not(target_os = "linux"))] 8 | pub use common::*; 9 | 10 | #[cfg(target_os = "linux")] 11 | pub mod linux; 12 | 13 | #[cfg(target_os = "linux")] 14 | pub use linux::*; 15 | 16 | #[derive(Debug, Clone, Serialize)] 17 | pub enum DeviceEventKind { 18 | MousePress, 19 | MouseRelease, 20 | MouseMove, 21 | KeyboardPress, 22 | KeyboardRelease, 23 | } 24 | 25 | #[derive(Debug, Clone, Serialize)] 26 | pub struct DeviceEvent { 27 | kind: DeviceEventKind, 28 | value: Value, 29 | } 30 | -------------------------------------------------------------------------------- /src-tauri/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod device; 2 | pub mod prevent_default; 3 | pub mod setup; 4 | -------------------------------------------------------------------------------- /src-tauri/src/core/prevent_default.rs: -------------------------------------------------------------------------------- 1 | pub fn init() -> tauri::plugin::TauriPlugin<tauri::Wry> { 2 | #[cfg(debug_assertions)] 3 | { 4 | use tauri_plugin_prevent_default::Flags; 5 | 6 | tauri_plugin_prevent_default::Builder::new() 7 | .with_flags(Flags::all().difference(Flags::CONTEXT_MENU)) 8 | .build() 9 | } 10 | 11 | #[cfg(not(debug_assertions))] 12 | tauri_plugin_prevent_default::init() 13 | } 14 | -------------------------------------------------------------------------------- /src-tauri/src/core/setup/common.rs: -------------------------------------------------------------------------------- 1 | use tauri::{AppHandle, WebviewWindow}; 2 | 3 | pub fn platform( 4 | _app_handle: &AppHandle, 5 | _main_window: WebviewWindow, 6 | _preference_window: WebviewWindow, 7 | ) { 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/core/setup/macos.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use tauri::{AppHandle, Emitter, EventTarget, WebviewWindow}; 3 | use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate}; 4 | use tauri_plugin_custom_window::MAIN_WINDOW_LABEL; 5 | 6 | #[allow(non_upper_case_globals)] 7 | const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7; 8 | #[allow(non_upper_case_globals)] 9 | const NSResizableWindowMask: i32 = 1 << 3; 10 | const WINDOW_FOCUS_EVENT: &str = "tauri://focus"; 11 | const WINDOW_BLUR_EVENT: &str = "tauri://blur"; 12 | const WINDOW_MOVED_EVENT: &str = "tauri://move"; 13 | const WINDOW_RESIZED_EVENT: &str = "tauri://resize"; 14 | 15 | pub fn platform( 16 | app_handle: &AppHandle, 17 | main_window: WebviewWindow, 18 | _preference_window: WebviewWindow, 19 | ) { 20 | let _ = app_handle.plugin(tauri_nspanel::init()); 21 | 22 | let _ = app_handle.set_dock_visibility(false); 23 | 24 | let panel = main_window.to_panel().unwrap(); 25 | 26 | panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel | NSResizableWindowMask); 27 | 28 | panel.set_collection_behaviour( 29 | NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces 30 | | NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary 31 | | NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary, 32 | ); 33 | 34 | let delegate = panel_delegate!(EcoPanelDelegate { 35 | window_did_become_key, 36 | window_did_resign_key, 37 | window_did_resize, 38 | window_did_move 39 | }); 40 | 41 | delegate.set_listener(Box::new(move |delegate_name: String| { 42 | let target = EventTarget::labeled(MAIN_WINDOW_LABEL); 43 | 44 | let window_move_event = || { 45 | if let Ok(position) = main_window.outer_position() { 46 | let _ = main_window.emit_to(target.clone(), WINDOW_MOVED_EVENT, position); 47 | } 48 | }; 49 | 50 | match delegate_name.as_str() { 51 | "window_did_become_key" => { 52 | let _ = main_window.emit_to(target, WINDOW_FOCUS_EVENT, true); 53 | } 54 | "window_did_resign_key" => { 55 | let _ = main_window.emit_to(target, WINDOW_BLUR_EVENT, true); 56 | } 57 | "window_did_resize" => { 58 | window_move_event(); 59 | 60 | if let Ok(size) = main_window.inner_size() { 61 | let _ = main_window.emit_to(target, WINDOW_RESIZED_EVENT, size); 62 | } 63 | } 64 | "window_did_move" => window_move_event(), 65 | _ => (), 66 | } 67 | })); 68 | 69 | panel.set_delegate(delegate); 70 | } 71 | -------------------------------------------------------------------------------- /src-tauri/src/core/setup/mod.rs: -------------------------------------------------------------------------------- 1 | use tauri::{AppHandle, WebviewWindow}; 2 | 3 | #[cfg(target_os = "macos")] 4 | mod macos; 5 | 6 | #[cfg(not(target_os = "macos"))] 7 | pub mod common; 8 | 9 | #[cfg(target_os = "macos")] 10 | pub use macos::*; 11 | 12 | #[cfg(not(target_os = "macos"))] 13 | pub use common::*; 14 | 15 | pub fn default( 16 | app_handle: &AppHandle, 17 | main_window: WebviewWindow, 18 | preference_window: WebviewWindow, 19 | ) { 20 | #[cfg(debug_assertions)] 21 | main_window.open_devtools(); 22 | 23 | platform(app_handle, main_window.clone(), preference_window.clone()); 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod core; 2 | mod utils; 3 | 4 | use core::{device::start_device_listening, prevent_default, setup}; 5 | use tauri::{Manager, WindowEvent, generate_handler}; 6 | use tauri_plugin_autostart::MacosLauncher; 7 | use tauri_plugin_custom_window::{ 8 | MAIN_WINDOW_LABEL, PREFERENCE_WINDOW_LABEL, show_preference_window, 9 | }; 10 | use utils::fs_extra::copy_dir; 11 | 12 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 13 | pub fn run() { 14 | let app = tauri::Builder::default() 15 | .setup(|app| { 16 | let app_handle = app.handle(); 17 | 18 | let main_window = app.get_webview_window(MAIN_WINDOW_LABEL).unwrap(); 19 | 20 | let preference_window = app.get_webview_window(PREFERENCE_WINDOW_LABEL).unwrap(); 21 | 22 | setup::default(&app_handle, main_window.clone(), preference_window.clone()); 23 | 24 | Ok(()) 25 | }) 26 | .invoke_handler(generate_handler![copy_dir, start_device_listening]) 27 | .plugin(tauri_plugin_custom_window::init()) 28 | .plugin(tauri_plugin_os::init()) 29 | .plugin(tauri_plugin_process::init()) 30 | .plugin(tauri_plugin_opener::init()) 31 | .plugin(tauri_plugin_pinia::init()) 32 | .plugin(tauri_plugin_updater::Builder::new().build()) 33 | .plugin(prevent_default::init()) 34 | .plugin(tauri_plugin_single_instance::init( 35 | |app_handle, _argv, _cwd| { 36 | show_preference_window(app_handle); 37 | }, 38 | )) 39 | .plugin(tauri_plugin_log::Builder::new().build()) 40 | .plugin(tauri_plugin_autostart::init( 41 | MacosLauncher::LaunchAgent, 42 | None, 43 | )) 44 | .plugin(tauri_plugin_macos_permissions::init()) 45 | .plugin(tauri_plugin_dialog::init()) 46 | .plugin(tauri_plugin_fs::init()) 47 | .plugin(tauri_plugin_clipboard_manager::init()) 48 | .plugin(tauri_plugin_global_shortcut::Builder::new().build()) 49 | .on_window_event(|window, event| match event { 50 | WindowEvent::CloseRequested { api, .. } => { 51 | let _ = window.hide(); 52 | 53 | api.prevent_close(); 54 | } 55 | _ => {} 56 | }) 57 | .build(tauri::generate_context!()) 58 | .expect("error while running tauri application"); 59 | 60 | app.run(|app_handle, event| match event { 61 | #[cfg(target_os = "macos")] 62 | tauri::RunEvent::Reopen { .. } => { 63 | show_preference_window(app_handle); 64 | } 65 | _ => { 66 | let _ = app_handle; 67 | } 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] 2 | 3 | fn main() { 4 | bongo_cat_lib::run() 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri-plugin-custom-window" 3 | version = "0.1.0" 4 | authors = [] 5 | description = "" 6 | edition = "2024" 7 | rust-version = "1.85.0" 8 | links = "tauri-plugin-custom-window" 9 | 10 | [dependencies] 11 | tauri.workspace = true 12 | serde.workspace = true 13 | 14 | [build-dependencies] 15 | tauri-plugin.workspace = true 16 | 17 | [target."cfg(target_os = \"macos\")".dependencies] 18 | tauri-nspanel.workspace = true 19 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/build.rs: -------------------------------------------------------------------------------- 1 | const COMMANDS: &[&str] = &["show_window", "hide_window", "set_always_on_top"]; 2 | 3 | fn main() { 4 | tauri_plugin::Builder::new(COMMANDS).build(); 5 | } 6 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/permissions/default.toml: -------------------------------------------------------------------------------- 1 | "$schema" = "schemas/schema.json" 2 | 3 | [default] 4 | description = "Default permissions for the plugin" 5 | permissions = ["allow-show-window", "allow-hide-window", "allow-set-always-on-top"] 6 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/src/commands/common.rs: -------------------------------------------------------------------------------- 1 | use super::{shared_hide_window, shared_set_always_on_top, shared_show_window}; 2 | use tauri::{AppHandle, Runtime, WebviewWindow, command}; 3 | 4 | #[command] 5 | pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) { 6 | shared_show_window(&app_handle, &window); 7 | } 8 | 9 | #[command] 10 | pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) { 11 | shared_hide_window(&app_handle, &window); 12 | } 13 | 14 | #[command] 15 | pub async fn set_always_on_top<R: Runtime>( 16 | app_handle: AppHandle<R>, 17 | window: WebviewWindow<R>, 18 | always_on_top: bool, 19 | ) { 20 | shared_set_always_on_top(&app_handle, &window, always_on_top); 21 | } 22 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/src/commands/macos.rs: -------------------------------------------------------------------------------- 1 | #![allow(deprecated)] 2 | use super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window}; 3 | use crate::MAIN_WINDOW_LABEL; 4 | use tauri::{AppHandle, Runtime, WebviewWindow, command}; 5 | use tauri_nspanel::{ManagerExt, cocoa::appkit::NSMainMenuWindowLevel}; 6 | 7 | pub enum MacOSPanelStatus { 8 | Show, 9 | Hide, 10 | SetAlwaysOnTop(bool), 11 | } 12 | 13 | #[command] 14 | pub async fn show_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) { 15 | if is_main_window(&window) { 16 | set_macos_panel(&app_handle, &window, MacOSPanelStatus::Show); 17 | } else { 18 | shared_show_window(&app_handle, &window); 19 | } 20 | } 21 | 22 | #[command] 23 | pub async fn hide_window<R: Runtime>(app_handle: AppHandle<R>, window: WebviewWindow<R>) { 24 | if is_main_window(&window) { 25 | set_macos_panel(&app_handle, &window, MacOSPanelStatus::Hide); 26 | } else { 27 | shared_hide_window(&app_handle, &window); 28 | } 29 | } 30 | 31 | #[command] 32 | pub async fn set_always_on_top<R: Runtime>( 33 | app_handle: AppHandle<R>, 34 | window: WebviewWindow<R>, 35 | always_on_top: bool, 36 | ) { 37 | if is_main_window(&window) { 38 | set_macos_panel( 39 | &app_handle, 40 | &window, 41 | MacOSPanelStatus::SetAlwaysOnTop(always_on_top), 42 | ); 43 | } else { 44 | shared_set_always_on_top(&app_handle, &window, always_on_top); 45 | } 46 | } 47 | 48 | pub fn set_macos_panel<R: Runtime>( 49 | app_handle: &AppHandle<R>, 50 | window: &WebviewWindow<R>, 51 | status: MacOSPanelStatus, 52 | ) { 53 | if is_main_window(window) { 54 | let app_handle_clone = app_handle.clone(); 55 | 56 | let _ = app_handle.run_on_main_thread(move || { 57 | if let Ok(panel) = app_handle_clone.get_webview_panel(MAIN_WINDOW_LABEL) { 58 | match status { 59 | MacOSPanelStatus::Show => { 60 | panel.show(); 61 | } 62 | MacOSPanelStatus::Hide => { 63 | panel.order_out(None); 64 | } 65 | MacOSPanelStatus::SetAlwaysOnTop(always_on_top) => { 66 | if always_on_top { 67 | panel.set_level(NSMainMenuWindowLevel); 68 | } else { 69 | panel.set_level(-1); 70 | }; 71 | } 72 | } 73 | } 74 | }); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | use tauri::{AppHandle, Manager, Runtime, WebviewWindow, async_runtime::spawn}; 2 | 3 | pub static MAIN_WINDOW_LABEL: &str = "main"; 4 | pub static PREFERENCE_WINDOW_LABEL: &str = "preference"; 5 | 6 | #[cfg(target_os = "macos")] 7 | mod macos; 8 | 9 | #[cfg(not(target_os = "macos"))] 10 | mod common; 11 | 12 | #[cfg(target_os = "macos")] 13 | pub use macos::*; 14 | 15 | #[cfg(not(target_os = "macos"))] 16 | pub use common::*; 17 | 18 | pub fn is_main_window<R: Runtime>(window: &WebviewWindow<R>) -> bool { 19 | window.label() == MAIN_WINDOW_LABEL 20 | } 21 | 22 | fn shared_show_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) { 23 | let _ = window.show(); 24 | let _ = window.unminimize(); 25 | let _ = window.set_focus(); 26 | } 27 | 28 | fn shared_hide_window<R: Runtime>(_app_handle: &AppHandle<R>, window: &WebviewWindow<R>) { 29 | let _ = window.hide(); 30 | } 31 | 32 | fn shared_set_always_on_top<R: Runtime>( 33 | _app_handle: &AppHandle<R>, 34 | window: &WebviewWindow<R>, 35 | always_on_top: bool, 36 | ) { 37 | if always_on_top { 38 | let _ = window.set_always_on_bottom(false); 39 | let _ = window.set_always_on_top(true); 40 | } else { 41 | let _ = window.set_always_on_top(false); 42 | let _ = window.set_always_on_bottom(true); 43 | } 44 | } 45 | 46 | pub fn show_main_window(app_handle: &AppHandle) { 47 | show_window_by_label(app_handle, MAIN_WINDOW_LABEL); 48 | } 49 | 50 | pub fn show_preference_window(app_handle: &AppHandle) { 51 | show_window_by_label(app_handle, PREFERENCE_WINDOW_LABEL); 52 | } 53 | 54 | fn show_window_by_label(app_handle: &AppHandle, label: &str) { 55 | if let Some(window) = app_handle.get_webview_window(label) { 56 | let app_handle_clone = app_handle.clone(); 57 | 58 | spawn(async move { 59 | show_window(app_handle_clone, window).await; 60 | }); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src-tauri/src/plugins/window/src/lib.rs: -------------------------------------------------------------------------------- 1 | use tauri::{ 2 | Runtime, generate_handler, 3 | plugin::{Builder, TauriPlugin}, 4 | }; 5 | 6 | mod commands; 7 | 8 | pub use commands::*; 9 | 10 | pub fn init<R: Runtime>() -> TauriPlugin<R> { 11 | Builder::new("custom-window") 12 | .invoke_handler(generate_handler![ 13 | commands::show_window, 14 | commands::hide_window, 15 | commands::set_always_on_top 16 | ]) 17 | .build() 18 | } 19 | -------------------------------------------------------------------------------- /src-tauri/src/utils/fs_extra.rs: -------------------------------------------------------------------------------- 1 | use fs_extra::dir::{CopyOptions, copy}; 2 | use std::fs::create_dir_all; 3 | use tauri::command; 4 | 5 | #[command] 6 | pub async fn copy_dir(from_path: String, to_path: String) -> Result<(), String> { 7 | let mut options = CopyOptions::new(); 8 | options.content_only = true; 9 | 10 | create_dir_all(&to_path).map_err(|err| err.to_string())?; 11 | 12 | copy(from_path, to_path, &options).map_err(|err| err.to_string())?; 13 | 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /src-tauri/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fs_extra; 2 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://schema.tauri.app/config/2", 3 | "productName": "BongoCat", 4 | "version": "../package.json", 5 | "identifier": "com.ayangweb.BongoCat", 6 | "build": { 7 | "beforeDevCommand": "pnpm dev", 8 | "devUrl": "http://localhost:1420", 9 | "beforeBuildCommand": "pnpm build", 10 | "frontendDist": "../dist" 11 | }, 12 | "app": { 13 | "macOSPrivateApi": true, 14 | "windows": [ 15 | { 16 | "label": "main", 17 | "title": "BongoCat", 18 | "url": "index.html/#/", 19 | "shadow": false, 20 | "alwaysOnTop": true, 21 | "transparent": true, 22 | "decorations": false, 23 | "acceptFirstMouse": true, 24 | "skipTaskbar": true 25 | }, 26 | { 27 | "label": "preference", 28 | "title": "偏好设置", 29 | "url": "index.html/#/preference", 30 | "visible": false, 31 | "titleBarStyle": "Overlay", 32 | "hiddenTitle": true, 33 | "minWidth": 800, 34 | "minHeight": 600 35 | } 36 | ], 37 | "security": { 38 | "csp": null, 39 | "dangerousDisableAssetCspModification": true, 40 | "assetProtocol": { 41 | "enable": true, 42 | "scope": { 43 | "allow": ["**/*"], 44 | "requireLiteralLeadingDot": false 45 | } 46 | } 47 | } 48 | }, 49 | "bundle": { 50 | "active": true, 51 | "category": "Game", 52 | "createUpdaterArtifacts": true, 53 | "targets": ["nsis", "dmg", "app", "appimage", "deb", "rpm"], 54 | "shortDescription": "BongoCat", 55 | "icon": [ 56 | "icons/32x32.png", 57 | "icons/128x128.png", 58 | "icons/128x128@2x.png", 59 | "icons/icon.icns", 60 | "icons/icon.ico" 61 | ], 62 | "resources": ["assets/tray.png", "assets/models"] 63 | }, 64 | "plugins": { 65 | "updater": { 66 | "dangerousInsecureTransportProtocol": true, 67 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRjJFMzE3MjEwMUZEMTAKUldRUS9RRWhGK1B5NmdkemhKcUFrVjZBQXlzdExpakdWVEJDeU9XckVsbzV2cFIycVJOempWa2UK", 68 | "endpoints": [ 69 | "http://api.upgrade.toolsetlink.com/v1/tauri/upgrade?tauriKey=KtGlsZUVXmWfjkRKCuqpfw&versionName={{current_version}}&target={{target}}&arch={{arch}}&appointVersionName=&devModelKey=&devKey=", 70 | "https://gh-proxy.com/github.com/ayangweb/BongoCat/releases/latest/download/latest.json" 71 | ] 72 | }, 73 | "fs": { 74 | "requireLiteralLeadingDot": false 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src-tauri/tauri.linux.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.ayangweb.BongoCat", 3 | "bundle": { 4 | "linux": { 5 | "deb": { 6 | "depends": ["gstreamer1.0-plugins-good"], 7 | "desktopTemplate": "./BongoCat.desktop" 8 | }, 9 | "rpm": { 10 | "depends": ["gstreamer1-plugins-good"], 11 | "desktopTemplate": "./BongoCat.desktop" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src-tauri/tauri.macos.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.ayangweb.BongoCat", 3 | "bundle": { 4 | "resources": ["assets/tray.png", "assets/models"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src-tauri/tauri.windows.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "identifier": "com.ayangweb.BongoCat", 3 | "bundle": { 4 | "windows": { 5 | "digestAlgorithm": "sha256", 6 | "nsis": { 7 | "languages": ["SimpChinese"], 8 | "installMode": "both" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 3 | import { error } from '@tauri-apps/plugin-log' 4 | import { openUrl } from '@tauri-apps/plugin-opener' 5 | import { useEventListener } from '@vueuse/core' 6 | import { ConfigProvider } from 'ant-design-vue' 7 | import zhCN from 'ant-design-vue/es/locale/zh_CN' 8 | import { isString } from 'es-toolkit' 9 | import isURL from 'is-url' 10 | import { onMounted } from 'vue' 11 | import { RouterView } from 'vue-router' 12 | 13 | import { useTauriListen } from './composables/useTauriListen' 14 | import { useThemeVars } from './composables/useThemeVars' 15 | import { useWindowState } from './composables/useWindowState' 16 | import { LISTEN_KEY } from './constants' 17 | import { hideWindow, showWindow } from './plugins/window' 18 | import { useAppStore } from './stores/app' 19 | import { useCatStore } from './stores/cat' 20 | import { useGeneralStore } from './stores/general' 21 | import { useModelStore } from './stores/model' 22 | import { useShortcutStore } from './stores/shortcut.ts' 23 | 24 | const { generateColorVars } = useThemeVars() 25 | const appStore = useAppStore() 26 | const modelStore = useModelStore() 27 | const catStore = useCatStore() 28 | const generalStore = useGeneralStore() 29 | const shortcutStore = useShortcutStore() 30 | const appWindow = getCurrentWebviewWindow() 31 | const { isRestored, restoreState } = useWindowState() 32 | 33 | onMounted(async () => { 34 | generateColorVars() 35 | 36 | await appStore.$tauri.start() 37 | await modelStore.$tauri.start() 38 | await modelStore.init() 39 | await catStore.$tauri.start() 40 | await generalStore.$tauri.start() 41 | await shortcutStore.$tauri.start() 42 | await restoreState() 43 | catStore.init() 44 | }) 45 | 46 | useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => { 47 | if (appWindow.label !== payload) return 48 | 49 | showWindow() 50 | }) 51 | 52 | useTauriListen(LISTEN_KEY.HIDE_WINDOW, ({ payload }) => { 53 | if (appWindow.label !== payload) return 54 | 55 | hideWindow() 56 | }) 57 | 58 | useEventListener('unhandledrejection', ({ reason }) => { 59 | const message = isString(reason) ? reason : JSON.stringify(reason) 60 | 61 | error(message) 62 | }) 63 | 64 | useEventListener('click', (event) => { 65 | const link = (event.target as HTMLElement).closest('a') 66 | 67 | if (!link) return 68 | 69 | const { href, target } = link 70 | 71 | if (target === '_blank') return 72 | 73 | event.preventDefault() 74 | 75 | if (!isURL(href)) return 76 | 77 | openUrl(href) 78 | }) 79 | </script> 80 | 81 | <template> 82 | <ConfigProvider :locale="zhCN"> 83 | <RouterView v-if="isRestored" /> 84 | </ConfigProvider> 85 | </template> 86 | -------------------------------------------------------------------------------- /src/assets/css/global.scss: -------------------------------------------------------------------------------- 1 | html { 2 | --uno: select-none overscroll-none antialiased; 3 | 4 | color-scheme: light; 5 | 6 | &.dark { 7 | color-scheme: dark; 8 | } 9 | 10 | img { 11 | -webkit-user-drag: none; 12 | } 13 | 14 | button { 15 | outline: none !important; 16 | } 17 | 18 | .ant-card { 19 | .ant-card-actions { 20 | > li { 21 | --uno: flex items-center justify-center; 22 | > span { 23 | --uno: inline-flex items-center justify-center min-w-unset; 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/pro-list-item/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { Flex } from 'ant-design-vue' 3 | import { computed, useSlots } from 'vue' 4 | 5 | const { title, description, vertical } = defineProps<{ 6 | title: string 7 | description?: string 8 | vertical?: boolean 9 | }>() 10 | 11 | const slots = useSlots() 12 | 13 | const hasDescription = computed(() => { 14 | return description || slots.description 15 | }) 16 | </script> 17 | 18 | <template> 19 | <Flex 20 | :align="vertical ? void 0 : 'center'" 21 | class="b b-color-2 rounded-lg b-solid bg-white p-4" 22 | gap="middle" 23 | justify="space-between" 24 | :vertical="vertical" 25 | > 26 | <Flex align="center"> 27 | <Flex vertical> 28 | <div class="text-sm font-medium"> 29 | {{ title }} 30 | </div> 31 | 32 | <div 33 | class="break-all text-xs [&_a]:(active:text-color-primary-7 hover:text-color-primary-5 text-color-3) text-color-3" 34 | :class="{ 'mt-2': hasDescription }" 35 | > 36 | <slot name="description"> 37 | {{ description }} 38 | </slot> 39 | </div> 40 | </Flex> 41 | </Flex> 42 | 43 | <slot /> 44 | </Flex> 45 | </template> 46 | -------------------------------------------------------------------------------- /src/components/pro-list/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { Flex } from 'ant-design-vue' 3 | 4 | const { title } = defineProps<{ 5 | title: string 6 | }>() 7 | </script> 8 | 9 | <template> 10 | <Flex 11 | class="not-last:mb-4" 12 | gap="small" 13 | vertical 14 | > 15 | <div 16 | class="text-4 font-medium" 17 | data-tauri-drag-region 18 | > 19 | {{ title }} 20 | </div> 21 | 22 | <Flex 23 | gap="middle" 24 | vertical 25 | > 26 | <slot /> 27 | </Flex> 28 | </FLex> 29 | </template> 30 | -------------------------------------------------------------------------------- /src/components/pro-shortcut/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import type { Key } from '@/utils/keyboard' 3 | 4 | import { find, map, remove, some, split } from 'es-toolkit/compat' 5 | import { ref, useTemplateRef, watch } from 'vue' 6 | 7 | import ProListItem from '@/components/pro-list-item/index.vue' 8 | import { keys, modifierKeys, standardKeys } from '@/utils/keyboard' 9 | 10 | const props = defineProps<{ 11 | title: string 12 | description?: string 13 | }>() 14 | 15 | const modelValue = defineModel<string>() 16 | const shortcutInputRef = useTemplateRef('shortcutInput') 17 | const isFocusing = ref(false) 18 | const isHovering = ref(false) 19 | const pressedKeys = ref<Key[]>([]) 20 | 21 | watch(modelValue, () => { 22 | parseModelValue() 23 | }, { immediate: true }) 24 | 25 | function parseModelValue() { 26 | if (!modelValue.value) { 27 | return pressedKeys.value = [] 28 | } 29 | 30 | pressedKeys.value = split(modelValue.value, '+').map((tauriKey) => { 31 | return find(keys, { tauriKey })! 32 | }) 33 | } 34 | 35 | function getEventKey(event: KeyboardEvent) { 36 | const { key, code } = event 37 | 38 | const eventKey = key.replace('Meta', 'Command') 39 | 40 | const isModifierKey = some(modifierKeys, { eventKey }) 41 | 42 | return isModifierKey ? eventKey : code 43 | } 44 | 45 | function isValidShortcut() { 46 | if (pressedKeys.value?.[0]?.eventKey?.startsWith('F')) { 47 | return true 48 | } 49 | 50 | const hasModifierKey = some(pressedKeys.value, ({ eventKey }) => { 51 | return some(modifierKeys, { eventKey }) 52 | }) 53 | const hasStandardKey = some(pressedKeys.value, ({ eventKey }) => { 54 | return some(standardKeys, { eventKey }) 55 | }) 56 | 57 | return hasModifierKey && hasStandardKey 58 | } 59 | 60 | function handleFocus() { 61 | isFocusing.value = true 62 | 63 | pressedKeys.value = [] 64 | } 65 | 66 | function handleBlur() { 67 | isFocusing.value = false 68 | 69 | if (!isValidShortcut()) { 70 | return parseModelValue() 71 | } 72 | 73 | modelValue.value = map(pressedKeys.value, 'tauriKey').join('+') 74 | } 75 | 76 | function handleKeyDown(event: KeyboardEvent) { 77 | const eventKey = getEventKey(event) 78 | 79 | const matched = find(keys, { eventKey }) 80 | const isInvalid = !matched 81 | const isDuplicate = some(pressedKeys.value, { eventKey }) 82 | 83 | if (isInvalid || isDuplicate) return 84 | 85 | pressedKeys.value.push(matched) 86 | 87 | if (isValidShortcut()) { 88 | shortcutInputRef.value?.blur() 89 | } 90 | } 91 | 92 | function handleKeyUp(event: KeyboardEvent) { 93 | remove(pressedKeys.value, { eventKey: getEventKey(event) }) 94 | } 95 | </script> 96 | 97 | <template> 98 | <ProListItem v-bind="props"> 99 | <div 100 | ref="shortcutInput" 101 | align="center" 102 | class="relative h-8 min-w-32 flex cursor-text items-center justify-center b b-color-1 hover:b-primary-5 rounded-md b-solid px-2.5 text-color-3 outline-none transition focus:(b-primary shadow-[0_0_0_2px_rgba(5,145,255,0.1)])" 103 | justify="center" 104 | :tabindex="0" 105 | @blur="handleBlur" 106 | @focus="handleFocus" 107 | @keydown="handleKeyDown" 108 | @keyup="handleKeyUp" 109 | @mouseout="isHovering = false" 110 | @mouseover="isHovering = true" 111 | > 112 | <span v-if="pressedKeys.length === 0"> 113 | {{ isFocusing ? '按下录制快捷键' : '点击录制快捷键' }} 114 | </span> 115 | 116 | <span class="text-primary font-bold"> 117 | {{ map(pressedKeys, 'symbol').join(' ') }} 118 | </span> 119 | 120 | <div 121 | class="i-iconamoon:close-circle-1 absolute right-2 cursor-pointer text-4 transition hover:text-primary" 122 | :hidden="isFocusing || !isHovering || pressedKeys.length === 0" 123 | @mousedown.prevent="modelValue = ''" 124 | /> 125 | </div> 126 | </ProListItem> 127 | </template> 128 | -------------------------------------------------------------------------------- /src/components/update-app/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import type { Update } from '@tauri-apps/plugin-updater' 3 | 4 | import { relaunch } from '@tauri-apps/plugin-process' 5 | import { check } from '@tauri-apps/plugin-updater' 6 | import { useIntervalFn } from '@vueuse/core' 7 | import { Flex, message, Modal } from 'ant-design-vue' 8 | import dayjs from 'dayjs' 9 | import utc from 'dayjs/plugin/utc' 10 | import { computed, reactive, watch } from 'vue' 11 | import VueMarkdown from 'vue-markdown-render' 12 | 13 | import { useTauriListen } from '@/composables/useTauriListen' 14 | import { GITHUB_LINK, LISTEN_KEY, UPGRADE_LINK_ACCESS_KEY } from '@/constants' 15 | import { showWindow } from '@/plugins/window' 16 | import { useGeneralStore } from '@/stores/general' 17 | 18 | dayjs.extend(utc) 19 | 20 | interface State { 21 | open: boolean 22 | update?: Update 23 | downloading: boolean 24 | totalProgress?: number 25 | downloadProgress: number 26 | } 27 | 28 | const generalStore = useGeneralStore() 29 | const state = reactive<State>({ 30 | open: false, 31 | downloading: false, 32 | downloadProgress: 0, 33 | }) 34 | const MESSAGE_KEY = 'updatable' 35 | 36 | const { pause, resume } = useIntervalFn(checkUpdate, 1000 * 60 * 60 * 24) 37 | 38 | watch(() => generalStore.autoCheckUpdate, (value) => { 39 | pause() 40 | 41 | if (!value) return 42 | 43 | checkUpdate() 44 | 45 | resume() 46 | }, { immediate: true }) 47 | 48 | useTauriListen<boolean>(LISTEN_KEY.UPDATE_APP, () => { 49 | checkUpdate(true) 50 | 51 | message.loading({ 52 | key: MESSAGE_KEY, 53 | duration: 0, 54 | content: '正在检查更新...', 55 | }) 56 | }) 57 | 58 | const downloadProgress = computed(() => { 59 | const { downloadProgress, totalProgress } = state 60 | 61 | if (!totalProgress) return '0%' 62 | 63 | const progress = ((downloadProgress / totalProgress) * 100).toFixed(2) 64 | 65 | return `${progress}%` 66 | }) 67 | 68 | async function checkUpdate(visibleMessage = false) { 69 | try { 70 | const update = await check({ 71 | timeout: 5000, 72 | headers: { 73 | 'X-AccessKey': UPGRADE_LINK_ACCESS_KEY, 74 | }, 75 | }) 76 | 77 | if (update) { 78 | const { version, currentVersion, body = '', date, downloadAndInstall } = update 79 | 80 | state.update = Object.assign(update, { 81 | version: `v${version}`, 82 | currentVersion: `v${currentVersion}`, 83 | body: replaceBody(body), 84 | date: dayjs.utc(date?.split('.')[0]).local().format('YYYY-MM-DD HH:mm:ss'), 85 | downloadAndInstall: downloadAndInstall.bind(update), 86 | }) 87 | 88 | showWindow() 89 | 90 | state.open = true 91 | 92 | message.destroy(MESSAGE_KEY) 93 | } else if (visibleMessage) { 94 | message.success({ key: MESSAGE_KEY, content: '当前已是最新版本🎉' }) 95 | } 96 | } catch (error) { 97 | if (!visibleMessage) return 98 | 99 | message.error({ key: MESSAGE_KEY, content: String(error) }) 100 | } 101 | } 102 | 103 | function replaceBody(body: string) { 104 | return body 105 | .replace(/ /g, '') 106 | .split('\n') 107 | .map(line => line.replace(/\s*-\s+by\s+@.*/, '')) 108 | .join('\n') 109 | } 110 | 111 | async function handleOk() { 112 | try { 113 | state.downloading = true 114 | 115 | await state.update?.downloadAndInstall((progress) => { 116 | switch (progress.event) { 117 | case 'Started': 118 | state.totalProgress = progress.data.contentLength ?? 0 119 | break 120 | case 'Progress': 121 | state.downloadProgress += progress.data.chunkLength 122 | break 123 | } 124 | }) 125 | 126 | relaunch() 127 | } catch (error) { 128 | message.error(String(error)) 129 | } finally { 130 | state.downloading = false 131 | } 132 | } 133 | </script> 134 | 135 | <template> 136 | <Modal 137 | v-model:open="state.open" 138 | cancel-text="稍后更新" 139 | centered 140 | :closable="false" 141 | :mask-closable="false" 142 | title="发现新版本🥳" 143 | @ok="handleOk" 144 | > 145 | <template #okText> 146 | {{ state.downloading ? downloadProgress : "立即更新" }} 147 | </template> 148 | 149 | <Flex 150 | class="pt-1" 151 | gap="small" 152 | vertical 153 | > 154 | <Flex align="center"> 155 | <span>更新版本:</span> 156 | <span> 157 | <span>{{ state.update?.currentVersion }} 👉 </span> 158 | <a 159 | :href="`${GITHUB_LINK}/releases/tag/${state.update?.version}`" 160 | > 161 | {{ state.update?.version }} 162 | </a> 163 | </span> 164 | </Flex> 165 | 166 | <Flex align="center"> 167 | <span>更新时间:</span> 168 | <span>{{ state.update?.date }}</span> 169 | </Flex> 170 | 171 | <Flex vertical> 172 | <span>更新日志:</span> 173 | 174 | <VueMarkdown 175 | class="update-note max-h-40 overflow-auto" 176 | :source="state.update?.body ?? ''" 177 | /> 178 | </Flex> 179 | </Flex> 180 | </Modal> 181 | </template> 182 | 183 | <style lang="scss" scoped> 184 | .update-note { 185 | :not(a) { 186 | all: revert; 187 | } 188 | } 189 | </style> 190 | -------------------------------------------------------------------------------- /src/composables/useDevice.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | 3 | import { readDir } from '@tauri-apps/plugin-fs' 4 | import { uniq } from 'es-toolkit' 5 | import { reactive, ref, watch } from 'vue' 6 | 7 | import { LISTEN_KEY } from '../constants' 8 | 9 | import { useTauriListen } from './useTauriListen' 10 | 11 | import { useCatStore } from '@/stores/cat' 12 | import { useModelStore } from '@/stores/model' 13 | import { isImage } from '@/utils/is' 14 | import { join } from '@/utils/path' 15 | import { isWindows } from '@/utils/platform' 16 | 17 | interface MouseButtonEvent { 18 | kind: 'MousePress' | 'MouseRelease' 19 | value: string 20 | } 21 | 22 | export interface MouseMoveValue { 23 | x: number 24 | y: number 25 | } 26 | 27 | interface MouseMoveEvent { 28 | kind: 'MouseMove' 29 | value: MouseMoveValue 30 | } 31 | 32 | interface KeyboardEvent { 33 | kind: 'KeyboardPress' | 'KeyboardRelease' 34 | value: string 35 | } 36 | 37 | type DeviceEvent = MouseButtonEvent | MouseMoveEvent | KeyboardEvent 38 | 39 | export function useDevice() { 40 | const supportLeftKeys = ref<string[]>([]) 41 | const supportRightKeys = ref<string[]>([]) 42 | const pressedMouses = ref<string[]>([]) 43 | const mousePosition = reactive<MouseMoveValue>({ x: 0, y: 0 }) 44 | const pressedLeftKeys = ref<string[]>([]) 45 | const pressedRightKeys = ref<string[]>([]) 46 | const catStore = useCatStore() 47 | const modelStore = useModelStore() 48 | const releaseTimers = new Map<string, NodeJS.Timeout>() 49 | 50 | watch(() => modelStore.currentModel, async (model) => { 51 | if (!model) return 52 | 53 | const keySides = [ 54 | { 55 | side: 'left', 56 | supportKeys: supportLeftKeys, 57 | pressedKeys: pressedLeftKeys, 58 | }, 59 | { 60 | side: 'right', 61 | supportKeys: supportRightKeys, 62 | pressedKeys: pressedRightKeys, 63 | }, 64 | ] 65 | 66 | for await (const item of keySides) { 67 | const { side, supportKeys, pressedKeys } = item 68 | 69 | try { 70 | const files = await readDir(join(model.path, 'resources', `${side}-keys`)) 71 | 72 | const imageFiles = files.filter(file => isImage(file.name)) 73 | 74 | supportKeys.value = imageFiles.map((item) => { 75 | return item.name.split('.')[0] 76 | }) 77 | 78 | pressedKeys.value = pressedKeys.value.filter((key) => { 79 | return supportKeys.value.includes(key) 80 | }) 81 | } catch { 82 | supportKeys.value = [] 83 | pressedKeys.value = [] 84 | } 85 | } 86 | }, { deep: true, immediate: true }) 87 | 88 | const handlePress = (array: Ref<string[]>, value?: string) => { 89 | if (!value) return 90 | 91 | if (catStore.singleMode) { 92 | array.value = [value] 93 | } else { 94 | array.value = uniq(array.value.concat(value)) 95 | } 96 | } 97 | 98 | const handleRelease = (array: Ref<string[]>, value?: string) => { 99 | if (!value) return 100 | 101 | array.value = array.value.filter(item => item !== value) 102 | } 103 | 104 | const getSupportedKey = (key: string) => { 105 | for (const side of ['left', 'right']) { 106 | let nextKey = key 107 | 108 | const supportKeys = side === 'left' ? supportLeftKeys.value : supportRightKeys.value 109 | 110 | const unsupportedKeys = !supportKeys.includes(key) 111 | 112 | if (key.startsWith('F') && unsupportedKeys) { 113 | nextKey = key.replace(/F(\d+)/, 'Fn') 114 | } 115 | 116 | for (const item of ['Meta', 'Shift', 'Alt', 'Control']) { 117 | if (key.startsWith(item) && unsupportedKeys) { 118 | const regex = new RegExp(`^(${item}).*`) 119 | nextKey = key.replace(regex, '$1') 120 | } 121 | } 122 | 123 | if (!supportKeys.includes(nextKey)) continue 124 | 125 | return nextKey 126 | } 127 | } 128 | 129 | const handleScheduleRelease = (keys: Ref<string[]>, key: string, delay = 500) => { 130 | if (releaseTimers.has(key)) { 131 | clearTimeout(releaseTimers.get(key)) 132 | } 133 | 134 | const timer = setTimeout(() => { 135 | handleRelease(keys, key) 136 | 137 | releaseTimers.delete(key) 138 | }, delay) 139 | 140 | releaseTimers.set(key, timer) 141 | } 142 | 143 | useTauriListen<DeviceEvent>(LISTEN_KEY.DEVICE_CHANGED, ({ payload }) => { 144 | const { kind, value } = payload 145 | 146 | if (kind === 'KeyboardPress' || kind === 'KeyboardRelease') { 147 | const nextValue = getSupportedKey(value) 148 | 149 | if (!nextValue) return 150 | 151 | const isLeftSide = supportLeftKeys.value.includes(nextValue) 152 | 153 | const pressedKeys = isLeftSide ? pressedLeftKeys : pressedRightKeys 154 | 155 | if (nextValue === 'CapsLock') { 156 | handlePress(pressedKeys, nextValue) 157 | 158 | return handleScheduleRelease(pressedKeys, nextValue, 100) 159 | } 160 | 161 | if (kind === 'KeyboardPress') { 162 | if (isWindows) { 163 | handleScheduleRelease(pressedKeys, nextValue) 164 | } 165 | 166 | return handlePress(pressedKeys, nextValue) 167 | } 168 | 169 | return handleRelease(pressedKeys, nextValue) 170 | } 171 | 172 | switch (kind) { 173 | case 'MousePress': 174 | return handlePress(pressedMouses, value) 175 | case 'MouseRelease': 176 | return handleRelease(pressedMouses, value) 177 | case 'MouseMove': 178 | return Object.assign(mousePosition, value) 179 | } 180 | }) 181 | 182 | return { 183 | pressedMouses, 184 | mousePosition, 185 | pressedLeftKeys, 186 | pressedRightKeys, 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/composables/useModel.ts: -------------------------------------------------------------------------------- 1 | import type { MouseMoveValue } from './useDevice.ts' 2 | import type { Monitor } from '@tauri-apps/api/window' 3 | 4 | import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi' 5 | import { resolveResource } from '@tauri-apps/api/path' 6 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 7 | import { availableMonitors as getAvailableMonitors } from '@tauri-apps/api/window' 8 | import { message } from 'ant-design-vue' 9 | import { isNil, round } from 'es-toolkit' 10 | import { computed, onBeforeMount, ref, watch } from 'vue' 11 | 12 | import live2d from '../utils/live2d' 13 | import { getCursorMonitor } from '../utils/monitor' 14 | 15 | import { useCatStore } from '@/stores/cat' 16 | import { useModelStore } from '@/stores/model' 17 | 18 | const appWindow = getCurrentWebviewWindow() 19 | 20 | interface ModelSize { 21 | width: number 22 | height: number 23 | } 24 | 25 | export function useModel() { 26 | const modelStore = useModelStore() 27 | const catStore = useCatStore() 28 | const modelSize = ref<ModelSize>() 29 | const availableMonitors = ref<Monitor[]>([]) 30 | 31 | const isOnlySingleMonitor = computed(() => availableMonitors.value.length === 1) 32 | 33 | onBeforeMount(async () => { 34 | availableMonitors.value = await getAvailableMonitors() 35 | }) 36 | 37 | watch(() => modelStore.currentModel, handleLoad, { deep: true, immediate: true }) 38 | 39 | watch([() => catStore.scale, modelSize], async () => { 40 | if (!modelSize.value) return 41 | 42 | const { width, height } = modelSize.value 43 | 44 | appWindow.setSize( 45 | new PhysicalSize({ 46 | width: round(width * (catStore.scale / 100)), 47 | height: round(height * (catStore.scale / 100)), 48 | }), 49 | ) 50 | }, { immediate: true }) 51 | 52 | async function handleLoad() { 53 | try { 54 | if (!modelStore.currentModel) return 55 | 56 | const { path } = modelStore.currentModel 57 | 58 | await resolveResource(path) 59 | 60 | const { width, height, ...rest } = await live2d.load(path) 61 | 62 | modelSize.value = { width, height } 63 | 64 | handleResize() 65 | 66 | Object.assign(modelStore, rest) 67 | } catch (error) { 68 | message.error(String(error)) 69 | } 70 | } 71 | 72 | function handleDestroy() { 73 | live2d.destroy() 74 | } 75 | 76 | async function handleResize() { 77 | if (!modelSize.value) return 78 | 79 | live2d.fitModel() 80 | 81 | const { width, height } = modelSize.value 82 | 83 | if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) { 84 | await appWindow.setSize( 85 | new LogicalSize({ 86 | width: innerWidth, 87 | height: Math.ceil(innerWidth * (height / width)), 88 | }), 89 | ) 90 | } 91 | 92 | const size = await appWindow.size() 93 | 94 | catStore.scale = round((size.width / width) * 100) 95 | } 96 | 97 | function handleKeyDown(side: 'left' | 'right', pressed: boolean) { 98 | const id = side === 'left' ? 'CatParamLeftHandDown' : 'CatParamRightHandDown' 99 | 100 | const { min, max } = live2d.getParameterRange(id) 101 | 102 | live2d.setParameterValue(id, pressed ? max : min) 103 | } 104 | 105 | async function _getCursorMonitor(mousePosition: MouseMoveValue) { 106 | return isOnlySingleMonitor.value 107 | ? { ...availableMonitors.value[0], cursorPosition: mousePosition } 108 | : await getCursorMonitor() 109 | } 110 | 111 | async function handleMouseMove(mousePosition: MouseMoveValue) { 112 | const monitor = await _getCursorMonitor(mousePosition) 113 | 114 | if (!monitor) return 115 | 116 | const { size, position, cursorPosition } = monitor 117 | 118 | const xRatio = (cursorPosition.x - position.x) / size.width 119 | const yRatio = (cursorPosition.y - position.y) / size.height 120 | 121 | for (const id of ['ParamMouseX', 'ParamMouseY', 'ParamAngleX', 'ParamAngleY']) { 122 | const { min, max } = live2d.getParameterRange(id) 123 | 124 | if (isNil(min) || isNil(max)) continue 125 | 126 | const isXAxis = id.endsWith('X') 127 | 128 | const ratio = isXAxis ? xRatio : yRatio 129 | let value = max - (ratio * (max - min)) 130 | 131 | if (isXAxis && catStore.mouseMirror) { 132 | value *= -1 133 | } 134 | 135 | live2d.setParameterValue(id, value) 136 | } 137 | } 138 | 139 | function handleMouseDown(value: string[]) { 140 | const params = { 141 | ParamMouseLeftDown: value.includes('Left'), 142 | ParamMouseRightDown: value.includes('Right'), 143 | } 144 | 145 | for (const [id, pressed] of Object.entries(params)) { 146 | const { min, max } = live2d.getParameterRange(id) 147 | 148 | live2d.setParameterValue(id, pressed ? max : min) 149 | } 150 | } 151 | 152 | return { 153 | handleLoad, 154 | handleDestroy, 155 | handleResize, 156 | handleKeyDown, 157 | handleMouseMove, 158 | handleMouseDown, 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/composables/useSharedMenu.ts: -------------------------------------------------------------------------------- 1 | import { CheckMenuItem, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu' 2 | import { range } from 'es-toolkit' 3 | 4 | import { showWindow } from '@/plugins/window' 5 | import { useCatStore } from '@/stores/cat' 6 | import { isMac } from '@/utils/platform' 7 | 8 | export function useSharedMenu() { 9 | const catStore = useCatStore() 10 | 11 | const getScaleMenuItems = async () => { 12 | const options = range(50, 151, 25) 13 | 14 | const items = options.map((item) => { 15 | return CheckMenuItem.new({ 16 | text: item === 100 ? '默认' : `${item}%`, 17 | checked: catStore.scale === item, 18 | action: () => { 19 | catStore.scale = item 20 | }, 21 | }) 22 | }) 23 | 24 | if (!options.includes(catStore.scale)) { 25 | items.unshift(CheckMenuItem.new({ 26 | text: `${catStore.scale}%`, 27 | checked: true, 28 | enabled: false, 29 | })) 30 | } 31 | 32 | return Promise.all(items) 33 | } 34 | 35 | const getOpacityMenuItems = async () => { 36 | const options = range(25, 101, 25) 37 | 38 | const items = options.map((item) => { 39 | return CheckMenuItem.new({ 40 | text: `${item}%`, 41 | checked: catStore.opacity === item, 42 | action: () => { 43 | catStore.opacity = item 44 | }, 45 | }) 46 | }) 47 | 48 | if (!options.includes(catStore.opacity)) { 49 | items.unshift(CheckMenuItem.new({ 50 | text: `${catStore.opacity}%`, 51 | checked: true, 52 | enabled: false, 53 | })) 54 | } 55 | 56 | return Promise.all(items) 57 | } 58 | 59 | const getSharedMenu = async () => { 60 | return await Promise.all([ 61 | MenuItem.new({ 62 | text: '偏好设置...', 63 | accelerator: isMac ? 'Cmd+,' : '', 64 | action: () => showWindow('preference'), 65 | }), 66 | MenuItem.new({ 67 | text: catStore.visible ? '隐藏猫咪' : '显示猫咪', 68 | action: () => { 69 | catStore.visible = !catStore.visible 70 | }, 71 | }), 72 | PredefinedMenuItem.new({ item: 'Separator' }), 73 | CheckMenuItem.new({ 74 | text: '窗口穿透', 75 | checked: catStore.penetrable, 76 | action: () => { 77 | catStore.penetrable = !catStore.penetrable 78 | }, 79 | }), 80 | Submenu.new({ 81 | text: '窗口尺寸', 82 | items: await getScaleMenuItems(), 83 | }), 84 | Submenu.new({ 85 | text: '不透明度', 86 | items: await getOpacityMenuItems(), 87 | }), 88 | ]) 89 | } 90 | 91 | return { 92 | getSharedMenu, 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/composables/useTauriKeyPress.ts: -------------------------------------------------------------------------------- 1 | import type { ShortcutHandler } from '@tauri-apps/plugin-global-shortcut' 2 | import type { Ref } from 'vue' 3 | 4 | import { 5 | isRegistered, 6 | register, 7 | unregister, 8 | } from '@tauri-apps/plugin-global-shortcut' 9 | import { ref, watch } from 'vue' 10 | 11 | export function useTauriKeyPress(shortcut: Ref<string, string>, callback: ShortcutHandler) { 12 | const oldShortcut = ref(shortcut.value) 13 | 14 | watch(shortcut, async (value) => { 15 | if (oldShortcut.value) { 16 | const registered = await isRegistered(oldShortcut.value) 17 | 18 | if (registered) { 19 | await unregister(oldShortcut.value) 20 | } 21 | } 22 | 23 | if (!value) return 24 | 25 | await register(value, (event) => { 26 | if (event.state === 'Released') return 27 | 28 | callback(event) 29 | }) 30 | 31 | oldShortcut.value = value 32 | }, { immediate: true }) 33 | } 34 | -------------------------------------------------------------------------------- /src/composables/useTauriListen.ts: -------------------------------------------------------------------------------- 1 | import { listen } from '@tauri-apps/api/event' 2 | import { noop } from '@vueuse/core' 3 | import { onMounted, onUnmounted, ref } from 'vue' 4 | 5 | export function useTauriListen<T>(...args: Parameters<typeof listen<T>>) { 6 | const unlisten = ref(noop) 7 | 8 | onMounted(async () => { 9 | unlisten.value = await listen<T>(...args) 10 | }) 11 | 12 | onUnmounted(() => { 13 | unlisten.value() 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /src/composables/useThemeVars.ts: -------------------------------------------------------------------------------- 1 | import { theme } from 'ant-design-vue' 2 | import { kebabCase } from 'es-toolkit' 3 | 4 | export function useThemeVars() { 5 | const { defaultAlgorithm, darkAlgorithm, defaultConfig } = theme 6 | 7 | const generateColorVars = () => { 8 | const { token } = defaultConfig 9 | 10 | const colors = [ 11 | defaultAlgorithm(token), 12 | darkAlgorithm(token), 13 | ] 14 | 15 | for (const [index, item] of colors.entries()) { 16 | const isDark = index !== 0 17 | const vars: Record<string, any> = {} 18 | 19 | for (const [key, value] of Object.entries(item)) { 20 | vars[`--ant-${kebabCase(key)}`] = value 21 | } 22 | 23 | const style = document.createElement('style') 24 | style.dataset.theme = isDark ? 'dark' : 'light' 25 | const selector = isDark ? 'html.dark' : ':root' 26 | const values = Object.entries(vars).map(([key, value]) => `${key}: ${value};`) 27 | 28 | style.innerHTML = `${selector}{\n${values.join('\n')}\n}` 29 | document.head.appendChild(style) 30 | } 31 | } 32 | 33 | return { 34 | generateColorVars, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/composables/useTray.ts: -------------------------------------------------------------------------------- 1 | import type { TrayIconOptions } from '@tauri-apps/api/tray' 2 | 3 | import { getName, getVersion } from '@tauri-apps/api/app' 4 | import { emit } from '@tauri-apps/api/event' 5 | import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu' 6 | import { resolveResource } from '@tauri-apps/api/path' 7 | import { TrayIcon } from '@tauri-apps/api/tray' 8 | import { openUrl } from '@tauri-apps/plugin-opener' 9 | import { exit, relaunch } from '@tauri-apps/plugin-process' 10 | import { watchDebounced } from '@vueuse/core' 11 | import { watch } from 'vue' 12 | 13 | import { GITHUB_LINK, LISTEN_KEY } from '../constants' 14 | import { showWindow } from '../plugins/window' 15 | import { isMac } from '../utils/platform' 16 | 17 | import { useSharedMenu } from './useSharedMenu' 18 | 19 | import { useCatStore } from '@/stores/cat' 20 | 21 | const TRAY_ID = 'BONGO_CAT_TRAY' 22 | 23 | export function useTray() { 24 | const catStore = useCatStore() 25 | const { getSharedMenu } = useSharedMenu() 26 | 27 | watch([() => catStore.visible, () => catStore.penetrable], () => { 28 | updateTrayMenu() 29 | }) 30 | 31 | watchDebounced([() => catStore.scale, () => catStore.opacity], () => { 32 | updateTrayMenu() 33 | }, { debounce: 200 }) 34 | 35 | const createTray = async () => { 36 | const tray = await getTrayById() 37 | 38 | if (tray) return 39 | 40 | const appName = await getName() 41 | const appVersion = await getVersion() 42 | 43 | const menu = await getTrayMenu() 44 | 45 | const icon = await resolveResource('assets/tray.png') 46 | 47 | const options: TrayIconOptions = { 48 | menu, 49 | icon, 50 | id: TRAY_ID, 51 | tooltip: `${appName} v${appVersion}`, 52 | iconAsTemplate: false, 53 | menuOnLeftClick: true, 54 | } 55 | 56 | return TrayIcon.new(options) 57 | } 58 | 59 | const getTrayById = () => { 60 | return TrayIcon.getById(TRAY_ID) 61 | } 62 | 63 | const getTrayMenu = async () => { 64 | const appVersion = await getVersion() 65 | 66 | const items = await Promise.all([ 67 | ...await getSharedMenu(), 68 | PredefinedMenuItem.new({ item: 'Separator' }), 69 | MenuItem.new({ 70 | text: '检查更新', 71 | action: () => { 72 | showWindow() 73 | 74 | emit(LISTEN_KEY.UPDATE_APP) 75 | }, 76 | }), 77 | MenuItem.new({ 78 | text: '开源地址', 79 | action: () => openUrl(GITHUB_LINK), 80 | }), 81 | PredefinedMenuItem.new({ item: 'Separator' }), 82 | MenuItem.new({ 83 | text: `版本 ${appVersion}`, 84 | enabled: false, 85 | }), 86 | MenuItem.new({ 87 | text: '重启应用', 88 | action: relaunch, 89 | }), 90 | MenuItem.new({ 91 | text: '退出应用', 92 | accelerator: isMac ? 'Cmd+Q' : '', 93 | action: () => exit(0), 94 | }), 95 | ]) 96 | 97 | return Menu.new({ items }) 98 | } 99 | 100 | const updateTrayMenu = async () => { 101 | const tray = await getTrayById() 102 | 103 | if (!tray) return 104 | 105 | const menu = await getTrayMenu() 106 | 107 | tray.setMenu(menu) 108 | } 109 | 110 | return { 111 | createTray, 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/composables/useWindowState.ts: -------------------------------------------------------------------------------- 1 | import type { Event } from '@tauri-apps/api/event' 2 | 3 | import { PhysicalPosition, PhysicalSize } from '@tauri-apps/api/dpi' 4 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 5 | import { availableMonitors } from '@tauri-apps/api/window' 6 | import { isNumber } from 'es-toolkit/compat' 7 | import { onMounted, ref } from 'vue' 8 | 9 | import { useAppStore } from '@/stores/app' 10 | 11 | export type WindowState = Record<string, Partial<PhysicalPosition & PhysicalSize> | undefined> 12 | 13 | const appWindow = getCurrentWebviewWindow() 14 | const { label } = appWindow 15 | 16 | export function useWindowState() { 17 | const appStore = useAppStore() 18 | const isRestored = ref(false) 19 | 20 | onMounted(() => { 21 | appWindow.onMoved(onChange) 22 | 23 | appWindow.onResized(onChange) 24 | }) 25 | 26 | const onChange = async (event: Event<PhysicalPosition | PhysicalSize>) => { 27 | const minimized = await appWindow.isMinimized() 28 | 29 | if (minimized) return 30 | 31 | appStore.windowState[label] ??= {} 32 | 33 | Object.assign(appStore.windowState[label], event.payload) 34 | } 35 | 36 | const restoreState = async () => { 37 | const { x, y, width, height } = appStore.windowState[label] ?? {} 38 | 39 | if (isNumber(x) && isNumber(y)) { 40 | const monitors = await availableMonitors() 41 | 42 | const monitor = monitors.find((monitor) => { 43 | const { position, size } = monitor 44 | 45 | const inBoundsX = x >= position.x && x <= position.x + size.width 46 | const inBoundsY = y >= position.y && y <= position.y + size.height 47 | 48 | return inBoundsX && inBoundsY 49 | }) 50 | 51 | if (monitor) { 52 | await appWindow.setPosition(new PhysicalPosition(x, y)) 53 | } 54 | } 55 | 56 | if (width && height) { 57 | await appWindow.setSize(new PhysicalSize(width, height)) 58 | } 59 | 60 | isRestored.value = true 61 | } 62 | 63 | return { 64 | isRestored, 65 | restoreState, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat' 2 | 3 | export const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA' 4 | 5 | export const LISTEN_KEY = { 6 | SHOW_WINDOW: 'show-window', 7 | HIDE_WINDOW: 'hide-window', 8 | DEVICE_CHANGED: 'device-changed', 9 | UPDATE_APP: 'update-app', 10 | } 11 | 12 | export const INVOKE_KEY = { 13 | COPY_DIR: 'copy_dir', 14 | START_DEVICE_LISTENING: 'start_device_listening', 15 | } 16 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createPlugin } from '@tauri-store/pinia' 2 | import { createPinia } from 'pinia' 3 | import { createApp } from 'vue' 4 | 5 | import App from './App.vue' 6 | import router from './router' 7 | import 'virtual:uno.css' 8 | import 'ant-design-vue/dist/reset.css' 9 | import './assets/css/global.scss' 10 | 11 | const pinia = createPinia() 12 | pinia.use(createPlugin({ saveOnChange: true })) 13 | 14 | createApp(App).use(router).use(pinia).mount('#app') 15 | -------------------------------------------------------------------------------- /src/pages/main/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import type { MouseMoveValue } from '@/composables/useDevice' 3 | 4 | import { convertFileSrc, invoke } from '@tauri-apps/api/core' 5 | import { Menu } from '@tauri-apps/api/menu' 6 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 7 | import { exists } from '@tauri-apps/plugin-fs' 8 | import { useDebounceFn, useEventListener, useRafFn } from '@vueuse/core' 9 | import { isEqual } from 'es-toolkit' 10 | import { onMounted, onUnmounted, ref, watch } from 'vue' 11 | 12 | import { useDevice } from '@/composables/useDevice' 13 | import { useModel } from '@/composables/useModel' 14 | import { useSharedMenu } from '@/composables/useSharedMenu' 15 | import { INVOKE_KEY } from '@/constants' 16 | import { hideWindow, setAlwaysOnTop, showWindow } from '@/plugins/window' 17 | import { useCatStore } from '@/stores/cat' 18 | import { useModelStore } from '@/stores/model' 19 | import { join } from '@/utils/path' 20 | 21 | const appWindow = getCurrentWebviewWindow() 22 | const { pressedMouses, mousePosition, pressedLeftKeys, pressedRightKeys } = useDevice() 23 | const { handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel() 24 | const catStore = useCatStore() 25 | const { getSharedMenu } = useSharedMenu() 26 | const modelStore = useModelStore() 27 | const resizing = ref(false) 28 | const backgroundImagePath = ref<string>() 29 | 30 | onMounted(() => { 31 | invoke(INVOKE_KEY.START_DEVICE_LISTENING) 32 | }) 33 | 34 | onUnmounted(handleDestroy) 35 | 36 | const debouncedResize = useDebounceFn(async () => { 37 | await handleResize() 38 | 39 | resizing.value = false 40 | }, 100) 41 | 42 | useEventListener('resize', () => { 43 | resizing.value = true 44 | 45 | debouncedResize() 46 | }) 47 | 48 | watch(pressedMouses, handleMouseDown) 49 | 50 | useRafFn((() => { 51 | const cached: MouseMoveValue = { x: 0, y: 0 } 52 | return () => { 53 | if (isEqual(cached, mousePosition)) return 54 | Object.assign(cached, mousePosition) 55 | handleMouseMove(mousePosition) 56 | } 57 | })()) 58 | 59 | watch(pressedLeftKeys, (keys) => { 60 | handleKeyDown('left', keys.length > 0) 61 | }) 62 | 63 | watch(pressedRightKeys, (keys) => { 64 | handleKeyDown('right', keys.length > 0) 65 | }) 66 | 67 | watch(() => catStore.visible, async (value) => { 68 | value ? showWindow() : hideWindow() 69 | }) 70 | 71 | watch(() => catStore.penetrable, (value) => { 72 | appWindow.setIgnoreCursorEvents(value) 73 | }, { immediate: true }) 74 | 75 | watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true }) 76 | 77 | watch(() => modelStore.currentModel, async (model) => { 78 | if (!model) return 79 | 80 | const path = join(model.path, 'resources', 'background.png') 81 | 82 | const existed = await exists(path) 83 | 84 | backgroundImagePath.value = existed ? convertFileSrc(path) : void 0 85 | }, { deep: true, immediate: true }) 86 | 87 | function handleWindowDrag() { 88 | appWindow.startDragging() 89 | } 90 | 91 | async function handleContextmenu(event: MouseEvent) { 92 | event.preventDefault() 93 | 94 | const menu = await Menu.new({ 95 | items: await getSharedMenu(), 96 | }) 97 | 98 | menu.popup() 99 | } 100 | 101 | function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') { 102 | return convertFileSrc(join(modelStore.currentModel!.path, 'resources', `${side}-keys`, `${key}.png`)) 103 | } 104 | </script> 105 | 106 | <template> 107 | <div 108 | class="relative size-screen overflow-hidden children:(absolute size-full)" 109 | :class="{ '-scale-x-100': catStore.mirrorMode }" 110 | :style="{ opacity: catStore.opacity / 100 }" 111 | @contextmenu="handleContextmenu" 112 | @mousedown="handleWindowDrag" 113 | > 114 | <img 115 | v-if="backgroundImagePath" 116 | :src="backgroundImagePath" 117 | > 118 | 119 | <canvas id="live2dCanvas" /> 120 | 121 | <img 122 | v-for="key in pressedLeftKeys" 123 | :key="key" 124 | :src="resolveKeyImagePath(key)" 125 | > 126 | 127 | <img 128 | v-for="key in pressedRightKeys" 129 | :key="key" 130 | :src="resolveKeyImagePath(key, 'right')" 131 | > 132 | 133 | <div 134 | v-show="resizing" 135 | class="flex items-center justify-center bg-black" 136 | > 137 | <span class="text-center text-5xl text-white"> 138 | 重绘中... 139 | </span> 140 | </div> 141 | </div> 142 | </template> 143 | -------------------------------------------------------------------------------- /src/pages/preference/components/about/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { getTauriVersion } from '@tauri-apps/api/app' 3 | import { emit } from '@tauri-apps/api/event' 4 | import { appLogDir } from '@tauri-apps/api/path' 5 | import { writeText } from '@tauri-apps/plugin-clipboard-manager' 6 | import { openPath, openUrl } from '@tauri-apps/plugin-opener' 7 | import { arch, platform, version } from '@tauri-apps/plugin-os' 8 | import { Button, message } from 'ant-design-vue' 9 | import { onMounted, ref } from 'vue' 10 | 11 | import ProList from '@/components/pro-list/index.vue' 12 | import ProListItem from '@/components/pro-list-item/index.vue' 13 | import { GITHUB_LINK, LISTEN_KEY } from '@/constants' 14 | import { useAppStore } from '@/stores/app' 15 | 16 | const appStore = useAppStore() 17 | const logDir = ref('') 18 | 19 | onMounted(async () => { 20 | logDir.value = await appLogDir() 21 | }) 22 | 23 | function handleUpdate() { 24 | emit(LISTEN_KEY.UPDATE_APP) 25 | } 26 | 27 | async function copyInfo() { 28 | const info = { 29 | appName: appStore.name, 30 | appVersion: appStore.version, 31 | tauriVersion: await getTauriVersion(), 32 | platform: platform(), 33 | platformArch: arch(), 34 | platformVersion: version(), 35 | } 36 | 37 | await writeText(JSON.stringify(info, null, 2)) 38 | 39 | message.success('复制成功') 40 | } 41 | 42 | function feedbackIssue() { 43 | openUrl(`${GITHUB_LINK}/issues/new/choose`) 44 | } 45 | </script> 46 | 47 | <template> 48 | <ProList title="关于软件"> 49 | <ProListItem 50 | :description="`版本:v${appStore.version}`" 51 | :title="appStore.name" 52 | > 53 | <Button 54 | type="primary" 55 | @click="handleUpdate" 56 | > 57 | 检查更新 58 | </Button> 59 | 60 | <template #icon> 61 | <div class="b b-color-2 rounded-xl b-solid"> 62 | <img 63 | class="size-12" 64 | src="/logo.png" 65 | > 66 | </div> 67 | </template> 68 | </ProListItem> 69 | 70 | <ProListItem 71 | description="复制软件信息并提供给 Bug Issue" 72 | title="软件信息" 73 | > 74 | <Button @click="copyInfo"> 75 | 复制 76 | </Button> 77 | </ProListItem> 78 | 79 | <ProListItem title="开源地址"> 80 | <Button 81 | danger 82 | @click="feedbackIssue" 83 | > 84 | 反馈问题 85 | </Button> 86 | 87 | <template #description> 88 | <a :href="GITHUB_LINK"> 89 | {{ GITHUB_LINK }} 90 | </a> 91 | </template> 92 | </ProListItem> 93 | 94 | <ProListItem 95 | :description="logDir" 96 | title="软件日志" 97 | > 98 | <Button @click="openPath(logDir)"> 99 | 查看日志 100 | </Button> 101 | </ProListItem> 102 | </ProList> 103 | </template> 104 | -------------------------------------------------------------------------------- /src/pages/preference/components/cat/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { InputNumber, Slider, Switch } from 'ant-design-vue' 3 | 4 | import ProList from '@/components/pro-list/index.vue' 5 | import ProListItem from '@/components/pro-list-item/index.vue' 6 | import { useCatStore } from '@/stores/cat' 7 | 8 | const catStore = useCatStore() 9 | 10 | function opacityFormatter(value?: number) { 11 | return `${value}%` 12 | } 13 | </script> 14 | 15 | <template> 16 | <ProList title="模型设置"> 17 | <ProListItem 18 | description="启用后,模型将水平镜像翻转" 19 | title="镜像模式" 20 | > 21 | <Switch v-model:checked="catStore.mirrorMode" /> 22 | </ProListItem> 23 | 24 | <ProListItem 25 | description="启用后,每只手只显示最后按下的一个按键" 26 | title="单键模式" 27 | > 28 | <Switch v-model:checked="catStore.singleMode" /> 29 | </ProListItem> 30 | 31 | <ProListItem 32 | description="启用后,鼠标将镜像跟随手部移动" 33 | title="鼠标镜像" 34 | > 35 | <Switch v-model:checked="catStore.mouseMirror" /> 36 | </ProListItem> 37 | </ProList> 38 | 39 | <ProList title="窗口设置"> 40 | <ProListItem 41 | description="启用后,窗口不影响对其他应用程序的操作" 42 | title="窗口穿透" 43 | > 44 | <Switch v-model:checked="catStore.penetrable" /> 45 | </ProListItem> 46 | 47 | <ProListItem 48 | description="启用后,窗口始终显示在其他应用程序上方" 49 | title="窗口置顶" 50 | > 51 | <Switch v-model:checked="catStore.alwaysOnTop" /> 52 | </ProListItem> 53 | 54 | <ProListItem 55 | description="将鼠标移动到窗口边缘后,也可以拖动调整窗口尺寸" 56 | title="窗口尺寸" 57 | > 58 | <InputNumber 59 | v-model:value="catStore.scale" 60 | class="w-28" 61 | :min="1" 62 | > 63 | <template #addonAfter> 64 | % 65 | </template> 66 | </InputNumber> 67 | </ProListItem> 68 | 69 | <ProListItem 70 | title="不透明度" 71 | vertical 72 | > 73 | <Slider 74 | v-model:value="catStore.opacity" 75 | class="m-0!" 76 | :max="100" 77 | :min="10" 78 | :tip-formatter="opacityFormatter" 79 | /> 80 | </ProListItem> 81 | </ProList> 82 | </template> 83 | -------------------------------------------------------------------------------- /src/pages/preference/components/general/components/macos-permissions/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 3 | import { message } from '@tauri-apps/plugin-dialog' 4 | import { Space } from 'ant-design-vue' 5 | import { checkInputMonitoringPermission, requestInputMonitoringPermission } from 'tauri-plugin-macos-permissions-api' 6 | import { onMounted, ref } from 'vue' 7 | 8 | import ProList from '@/components/pro-list/index.vue' 9 | import ProListItem from '@/components/pro-list-item/index.vue' 10 | import { isMac } from '@/utils/platform' 11 | 12 | const authorized = ref(false) 13 | 14 | onMounted(async () => { 15 | authorized.value = await checkInputMonitoringPermission() 16 | 17 | if (authorized.value) return 18 | 19 | const appWindow = getCurrentWebviewWindow() 20 | 21 | await appWindow.setAlwaysOnTop(true) 22 | 23 | await message('如果权限已开启,先选中后点击“-”按钮将其删除,再重新手动添加,并重启应用以确保权限生效。', { 24 | title: '输入监控权限', 25 | okLabel: '前往开启', 26 | kind: 'warning', 27 | }) 28 | 29 | await appWindow.setAlwaysOnTop(false) 30 | 31 | requestInputMonitoringPermission() 32 | }) 33 | </script> 34 | 35 | <template> 36 | <ProList 37 | v-if="isMac" 38 | title="权限设置" 39 | > 40 | <ProListItem 41 | description="开启输入监控权限,以便接收系统的键盘和鼠标事件来响应你的操作。" 42 | title="输入监控权限" 43 | > 44 | <Space 45 | v-if="authorized" 46 | class="text-success font-bold" 47 | :size="4" 48 | > 49 | <div class="i-solar:verified-check-bold text-4.5" /> 50 | 51 | <span>已授权</span> 52 | </Space> 53 | 54 | <Space 55 | v-else 56 | class="cursor-pointer text-danger font-bold" 57 | :size="4" 58 | @click="requestInputMonitoringPermission" 59 | > 60 | <div class="i-solar:round-arrow-right-bold text-4.5" /> 61 | 62 | <span>去授权</span> 63 | </Space> 64 | </ProListItem> 65 | </ProList> 66 | </template> 67 | -------------------------------------------------------------------------------- /src/pages/preference/components/general/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart' 3 | import { Switch } from 'ant-design-vue' 4 | import { watch } from 'vue' 5 | 6 | import MacosPermissions from './components/macos-permissions/index.vue' 7 | 8 | import ProList from '@/components/pro-list/index.vue' 9 | import ProListItem from '@/components/pro-list-item/index.vue' 10 | import { useGeneralStore } from '@/stores/general' 11 | 12 | const generalStore = useGeneralStore() 13 | 14 | watch(() => generalStore.autostart, async (value) => { 15 | const enabled = await isEnabled() 16 | 17 | if (value && !enabled) { 18 | return enable() 19 | } 20 | 21 | if (!value && enabled) { 22 | disable() 23 | } 24 | }, { immediate: true }) 25 | </script> 26 | 27 | <template> 28 | <MacosPermissions /> 29 | 30 | <ProList title="应用设置"> 31 | <ProListItem title="开机自启动"> 32 | <Switch v-model:checked="generalStore.autostart" /> 33 | </ProListItem> 34 | </ProList> 35 | 36 | <ProList title="更新设置"> 37 | <ProListItem title="自动检查更新"> 38 | <Switch v-model:checked="generalStore.autoCheckUpdate" /> 39 | </ProListItem> 40 | </ProList> 41 | </template> 42 | -------------------------------------------------------------------------------- /src/pages/preference/components/model/components/float-menu/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { EditOutlined, MenuOutlined, UnorderedListOutlined } from '@ant-design/icons-vue' 3 | import { openUrl } from '@tauri-apps/plugin-opener' 4 | import { FloatButton, FloatButtonGroup } from 'ant-design-vue' 5 | </script> 6 | 7 | <template> 8 | <FloatButtonGroup 9 | class="bottom-4 right-4" 10 | trigger="click" 11 | type="primary" 12 | > 13 | <template #icon> 14 | <MenuOutlined /> 15 | </template> 16 | 17 | <FloatButton 18 | tooltip="如何制作模型?" 19 | @click="openUrl('https://juejin.cn/post/7509872655802269731')" 20 | > 21 | <template #icon> 22 | <EditOutlined /> 23 | </template> 24 | </FloatButton> 25 | 26 | <FloatButton 27 | tooltip="更多模型" 28 | @click="openUrl('https://github.com/ayangweb/Awesome-BongoCat')" 29 | > 30 | <template #icon> 31 | <UnorderedListOutlined /> 32 | </template> 33 | </FloatButton> 34 | </FloatButtonGroup> 35 | </template> 36 | -------------------------------------------------------------------------------- /src/pages/preference/components/model/components/upload/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { invoke } from '@tauri-apps/api/core' 3 | import { appDataDir } from '@tauri-apps/api/path' 4 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 5 | import { open } from '@tauri-apps/plugin-dialog' 6 | import { readDir } from '@tauri-apps/plugin-fs' 7 | import { message } from 'ant-design-vue' 8 | import { nanoid } from 'nanoid' 9 | import { onMounted, ref, useTemplateRef, watch } from 'vue' 10 | 11 | import { INVOKE_KEY } from '@/constants' 12 | import { useModelStore } from '@/stores/model' 13 | import { join } from '@/utils/path' 14 | 15 | const dropRef = useTemplateRef('drop') 16 | const dragenter = ref(false) 17 | const selectPaths = ref<string[]>([]) 18 | const modelStore = useModelStore() 19 | 20 | onMounted(() => { 21 | const appWindow = getCurrentWebviewWindow() 22 | 23 | appWindow.onDragDropEvent(({ payload }) => { 24 | const { type } = payload 25 | 26 | if (type === 'over') { 27 | const { x, y } = payload.position 28 | 29 | if (dropRef.value) { 30 | const { left, right, top, bottom } = dropRef.value.getBoundingClientRect() 31 | 32 | const inBoundsX = x >= left && x <= right 33 | const inBoundsY = y >= top && y <= bottom 34 | 35 | dragenter.value = inBoundsX && inBoundsY 36 | } 37 | } else if (type === 'drop' && dragenter.value) { 38 | dragenter.value = false 39 | 40 | selectPaths.value = payload.paths 41 | } else { 42 | dragenter.value = false 43 | } 44 | }) 45 | }) 46 | 47 | async function handleUpload() { 48 | const selected = await open({ directory: true, multiple: true }) 49 | 50 | if (!selected) return 51 | 52 | selectPaths.value = selected 53 | } 54 | 55 | watch(selectPaths, async (paths) => { 56 | for await (const path of paths) { 57 | try { 58 | const id = nanoid() 59 | 60 | const files = await readDir(join(path, 'resources')) 61 | 62 | const isKeyboardMode = files.some(file => file.name === 'right-keys') 63 | 64 | const toPath = join(await appDataDir(), 'custom-models', id) 65 | 66 | await invoke(INVOKE_KEY.COPY_DIR, { 67 | fromPath: path, 68 | toPath, 69 | }) 70 | 71 | modelStore.models.push({ 72 | id, 73 | path: toPath, 74 | mode: isKeyboardMode ? 'keyboard' : 'standard', 75 | isPreset: false, 76 | }) 77 | 78 | message.success('导入成功') 79 | } catch (error) { 80 | message.error(String(error)) 81 | } 82 | } 83 | }) 84 | </script> 85 | 86 | <template> 87 | <div 88 | ref="drop" 89 | class="w-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary" 90 | :class="{ 'border-primary': dragenter }" 91 | @click="handleUpload" 92 | > 93 | <div class="i-solar:upload-square-outline text-12 text-primary" /> 94 | 95 | <span>点击或拖动至此区域导入</span> 96 | </div> 97 | </template> 98 | -------------------------------------------------------------------------------- /src/pages/preference/components/model/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import type { Model } from '@/stores/model' 3 | import type { ComponentPublicInstance } from 'vue' 4 | 5 | import { convertFileSrc } from '@tauri-apps/api/core' 6 | import { remove } from '@tauri-apps/plugin-fs' 7 | import { revealItemInDir } from '@tauri-apps/plugin-opener' 8 | import { useElementSize } from '@vueuse/core' 9 | import { Card, message, Popconfirm } from 'ant-design-vue' 10 | import { ref } from 'vue' 11 | import { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css' 12 | 13 | import FloatMenu from './components/float-menu/index.vue' 14 | import Upload from './components/upload/index.vue' 15 | 16 | import { useModelStore } from '@/stores/model' 17 | import { join } from '@/utils/path' 18 | 19 | const modelStore = useModelStore() 20 | const firstItemRef = ref<HTMLElement>() 21 | 22 | const { height } = useElementSize(firstItemRef) 23 | 24 | function setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) { 25 | if (!el || index > 0) return 26 | 27 | if ('$el' in el) { 28 | return firstItemRef.value = el.$el 29 | } 30 | 31 | if (el instanceof HTMLElement) { 32 | firstItemRef.value = el 33 | } 34 | } 35 | 36 | async function handleDelete(item: Model) { 37 | const { id, path } = item 38 | 39 | try { 40 | await remove(path, { recursive: true }) 41 | 42 | message.success('删除成功') 43 | } catch (error) { 44 | message.error(String(error)) 45 | } finally { 46 | modelStore.models = modelStore.models.filter(item => item.id !== id) 47 | 48 | if (id === modelStore.currentModel?.id) { 49 | modelStore.currentModel = modelStore.models[0] 50 | } 51 | } 52 | } 53 | </script> 54 | 55 | <template> 56 | <MasonryGrid 57 | :columns="{ 992: 3, 1200: 4, 1600: 6, default: 8 }" 58 | :gutter="16" 59 | > 60 | <MasonryGridItem> 61 | <Upload :style="{ height: `${height}px` }" /> 62 | </MasonryGridItem> 63 | 64 | <MasonryGridItem 65 | v-for="(item, index) in modelStore.models" 66 | :key="item.id" 67 | > 68 | <Card 69 | :ref="(el) => setFirstItemRef(el, index)" 70 | hoverable 71 | size="small" 72 | @click="modelStore.currentModel = item" 73 | > 74 | <template #cover> 75 | <img 76 | alt="example" 77 | :src="convertFileSrc(join(item.path, 'resources', 'cover.png'))" 78 | > 79 | </template> 80 | 81 | <template #actions> 82 | <i 83 | class="i-iconamoon:check-circle-1-bold text-4" 84 | :class="{ 'text-success': item.id === modelStore.currentModel?.id }" 85 | /> 86 | 87 | <i 88 | class="i-iconamoon:link-external-bold text-4" 89 | @click.stop="revealItemInDir(item.path)" 90 | /> 91 | 92 | <template v-if="!item.isPreset"> 93 | <Popconfirm 94 | description="你确定要删除此模型吗?" 95 | placement="topRight" 96 | title="删除模型" 97 | @confirm="handleDelete(item)" 98 | > 99 | <i 100 | class="i-iconamoon:trash-simple-bold text-4" 101 | @click.stop 102 | /> 103 | </Popconfirm> 104 | </template> 105 | </template> 106 | </Card> 107 | </MasonryGridItem> 108 | </MasonryGrid> 109 | 110 | <FloatMenu /> 111 | </template> 112 | -------------------------------------------------------------------------------- /src/pages/preference/components/shortcut/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { storeToRefs } from 'pinia' 3 | 4 | import ProList from '@/components/pro-list/index.vue' 5 | import ProShortcut from '@/components/pro-shortcut/index.vue' 6 | import { useTauriKeyPress } from '@/composables/useTauriKeyPress' 7 | import { toggleWindowVisible } from '@/plugins/window' 8 | import { useCatStore } from '@/stores/cat' 9 | import { useShortcutStore } from '@/stores/shortcut.ts' 10 | 11 | const shortcutStore = useShortcutStore() 12 | const { visibleCat, visiblePreference, mirrorMode, penetrable, alwaysOnTop } = storeToRefs(shortcutStore) 13 | const catStore = useCatStore() 14 | 15 | useTauriKeyPress(visibleCat, () => { 16 | catStore.visible = !catStore.visible 17 | }) 18 | 19 | useTauriKeyPress(visiblePreference, () => { 20 | toggleWindowVisible('preference') 21 | }) 22 | 23 | useTauriKeyPress(mirrorMode, () => { 24 | catStore.mirrorMode = !catStore.mirrorMode 25 | }) 26 | 27 | useTauriKeyPress(penetrable, () => { 28 | catStore.penetrable = !catStore.penetrable 29 | }) 30 | 31 | useTauriKeyPress(alwaysOnTop, () => { 32 | catStore.alwaysOnTop = !catStore.alwaysOnTop 33 | }) 34 | </script> 35 | 36 | <template> 37 | <ProList title="快捷键"> 38 | <ProShortcut 39 | v-model="shortcutStore.visibleCat" 40 | description="切换猫咪窗口的显示与隐藏" 41 | title="打开猫咪" 42 | /> 43 | 44 | <ProShortcut 45 | v-model="shortcutStore.visiblePreference" 46 | description="切换偏好设置窗口的显示与隐藏" 47 | title="打开偏好设置" 48 | /> 49 | 50 | <ProShortcut 51 | v-model="shortcutStore.mirrorMode" 52 | description="切换猫咪的镜像模式" 53 | title="镜像模式" 54 | /> 55 | 56 | <ProShortcut 57 | v-model="shortcutStore.penetrable" 58 | description="切换猫咪窗口是否可穿透" 59 | title="窗口穿透" 60 | /> 61 | 62 | <ProShortcut 63 | v-model="shortcutStore.alwaysOnTop" 64 | description="切换猫咪窗口是否置顶" 65 | title="窗口置顶" 66 | /> 67 | </ProList> 68 | </template> 69 | -------------------------------------------------------------------------------- /src/pages/preference/index.vue: -------------------------------------------------------------------------------- 1 | <script setup lang="ts"> 2 | import { Flex } from 'ant-design-vue' 3 | import { onMounted, ref } from 'vue' 4 | 5 | import About from './components/about/index.vue' 6 | import Cat from './components/cat/index.vue' 7 | import General from './components/general/index.vue' 8 | import Model from './components/model/index.vue' 9 | import Shortcut from './components/shortcut/index.vue' 10 | 11 | import UpdateApp from '@/components/update-app/index.vue' 12 | import { useTray } from '@/composables/useTray' 13 | import { useAppStore } from '@/stores/app' 14 | import { isMac } from '@/utils/platform' 15 | 16 | const { createTray } = useTray() 17 | const appStore = useAppStore() 18 | const current = ref(0) 19 | 20 | onMounted(async () => { 21 | createTray() 22 | }) 23 | 24 | const menus = [ 25 | { 26 | label: '猫咪设置', 27 | icon: 'i-solar:cat-bold', 28 | component: Cat, 29 | }, 30 | { 31 | label: '通用设置', 32 | icon: 'i-solar:settings-minimalistic-bold', 33 | component: General, 34 | }, 35 | { 36 | label: '模型管理', 37 | icon: 'i-solar:magic-stick-3-bold', 38 | component: Model, 39 | }, 40 | { 41 | label: '快捷键', 42 | icon: 'i-solar:keyboard-bold', 43 | component: Shortcut, 44 | }, 45 | { 46 | label: '关于', 47 | icon: 'i-solar:info-circle-bold', 48 | component: About, 49 | }, 50 | ] 51 | </script> 52 | 53 | <template> 54 | <Flex class="h-screen"> 55 | <div 56 | class="h-full w-30 flex flex-col items-center gap-4 overflow-auto bg-gradient-from-primary-1 bg-gradient-to-black/1 bg-gradient-linear" 57 | :class="[isMac ? 'pt-8' : 'pt-4']" 58 | data-tauri-drag-region 59 | > 60 | <div class="flex flex-col items-center gap-2"> 61 | <div class="b b-color-2 rounded-2xl b-solid"> 62 | <img 63 | class="size-15" 64 | data-tauri-drag-region 65 | src="/logo.png" 66 | > 67 | </div> 68 | 69 | <span class="font-bold">{{ appStore.name }}</span> 70 | </div> 71 | 72 | <div class="flex flex-col gap-2"> 73 | <div 74 | v-for="(item, index) in menus" 75 | :key="item.label" 76 | class="size-20 flex flex-col cursor-pointer items-center justify-center gap-2 rounded-lg hover:bg-color-7 text-color-3 transition" 77 | :class="{ 'bg-white! text-primary-5 font-bold': current === index }" 78 | @click="current = index" 79 | > 80 | <div 81 | class="size-8" 82 | :class="item.icon" 83 | /> 84 | 85 | <span>{{ item.label }}</span> 86 | </div> 87 | </div> 88 | </div> 89 | 90 | <div 91 | v-for="(item, index) in menus" 92 | v-show="current === index" 93 | :key="item.label" 94 | class="flex-1 overflow-auto bg-color-8 p-4" 95 | data-tauri-drag-region 96 | > 97 | <component :is="item.component" /> 98 | </div> 99 | </Flex> 100 | 101 | <UpdateApp /> 102 | </template> 103 | -------------------------------------------------------------------------------- /src/plugins/window.ts: -------------------------------------------------------------------------------- 1 | import { invoke } from '@tauri-apps/api/core' 2 | import { emit } from '@tauri-apps/api/event' 3 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 4 | 5 | import { LISTEN_KEY } from '../constants' 6 | 7 | type WindowLabel = 'main' | 'preference' 8 | 9 | const COMMAND = { 10 | SHOW_WINDOW: 'plugin:custom-window|show_window', 11 | HIDE_WINDOW: 'plugin:custom-window|hide_window', 12 | SET_ALWAYS_ON_TOP: 'plugin:custom-window|set_always_on_top', 13 | } 14 | 15 | export function showWindow(label?: WindowLabel) { 16 | if (label) { 17 | emit(LISTEN_KEY.SHOW_WINDOW, label) 18 | } else { 19 | invoke(COMMAND.SHOW_WINDOW) 20 | } 21 | } 22 | 23 | export function hideWindow(label?: WindowLabel) { 24 | if (label) { 25 | emit(LISTEN_KEY.HIDE_WINDOW, label) 26 | } else { 27 | invoke(COMMAND.HIDE_WINDOW) 28 | } 29 | } 30 | 31 | export function setAlwaysOnTop(alwaysOnTop: boolean) { 32 | invoke(COMMAND.SET_ALWAYS_ON_TOP, { alwaysOnTop }) 33 | } 34 | 35 | export async function toggleWindowVisible(label?: WindowLabel) { 36 | const appWindow = getCurrentWebviewWindow() 37 | 38 | if (appWindow.label !== label) return 39 | 40 | const visible = await appWindow.isVisible() 41 | 42 | if (visible) { 43 | return hideWindow(label) 44 | } 45 | 46 | return showWindow(label) 47 | } 48 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | import { createRouter, createWebHashHistory } from 'vue-router' 4 | 5 | import Main from '../pages/main/index.vue' 6 | import Preference from '../pages/preference/index.vue' 7 | 8 | const routes: Readonly<RouteRecordRaw[]> = [ 9 | { 10 | path: '/', 11 | component: Main, 12 | }, 13 | { 14 | path: '/preference', 15 | component: Preference, 16 | }, 17 | ] 18 | 19 | const router = createRouter({ 20 | history: createWebHashHistory(), 21 | routes, 22 | }) 23 | 24 | export default router 25 | -------------------------------------------------------------------------------- /src/stores/app.ts: -------------------------------------------------------------------------------- 1 | import type { WindowState } from '@/composables/useWindowState' 2 | 3 | import { getName, getVersion } from '@tauri-apps/api/app' 4 | import { defineStore } from 'pinia' 5 | import { onMounted, reactive, ref } from 'vue' 6 | 7 | export const useAppStore = defineStore('app', () => { 8 | const name = ref('') 9 | const version = ref('') 10 | const windowState = reactive<WindowState>({}) 11 | 12 | onMounted(async () => { 13 | name.value = await getName() 14 | version.value = await getVersion() 15 | }) 16 | 17 | return { 18 | name, 19 | version, 20 | windowState, 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /src/stores/cat.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useCatStore = defineStore('cat', () => { 5 | const visible = ref(false) 6 | const mirrorMode = ref(false) 7 | const singleMode = ref(false) 8 | const mouseMirror = ref(false) 9 | const penetrable = ref(false) 10 | const alwaysOnTop = ref(true) 11 | const scale = ref(100) 12 | const opacity = ref(100) 13 | 14 | const init = () => { 15 | visible.value = true 16 | } 17 | 18 | return { 19 | visible, 20 | mirrorMode, 21 | singleMode, 22 | mouseMirror, 23 | penetrable, 24 | alwaysOnTop, 25 | scale, 26 | opacity, 27 | init, 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /src/stores/general.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export const useGeneralStore = defineStore('general', () => { 5 | const autoCheckUpdate = ref(false) 6 | const autostart = ref(false) 7 | 8 | return { 9 | autoCheckUpdate, 10 | autostart, 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/stores/model.ts: -------------------------------------------------------------------------------- 1 | import { resolveResource } from '@tauri-apps/api/path' 2 | import { nanoid } from 'nanoid' 3 | import { defineStore } from 'pinia' 4 | import { ref } from 'vue' 5 | 6 | import { join } from '@/utils/path' 7 | 8 | export type ModelMode = 'standard' | 'keyboard' | 'handle' 9 | 10 | export interface Model { 11 | id: string 12 | path: string 13 | mode: ModelMode 14 | isPreset: boolean 15 | } 16 | 17 | interface Motion { 18 | Name: string 19 | File: string 20 | Sound?: string 21 | FadeInTime: number 22 | FadeOutTime: number 23 | Description?: string 24 | } 25 | 26 | type MotionGroup = Record<string, Motion[]> 27 | 28 | interface Expression { 29 | Name: string 30 | File: string 31 | Description?: string 32 | } 33 | 34 | export const useModelStore = defineStore('model', () => { 35 | const models = ref<Model[]>([]) 36 | const currentModel = ref<Model>() 37 | const motions = ref<MotionGroup>({}) 38 | const expressions = ref<Expression[]>([]) 39 | 40 | const init = async () => { 41 | const presetModelsPath = await resolveResource('assets/models') 42 | 43 | if (models.value.length === 0) { 44 | const modes: ModelMode[] = ['standard', 'keyboard'] 45 | 46 | for (const mode of modes) { 47 | models.value.push({ 48 | mode, 49 | id: nanoid(), 50 | isPreset: true, 51 | path: join(presetModelsPath, mode), 52 | }) 53 | } 54 | } else { 55 | models.value = models.value.map((item) => { 56 | const { isPreset, mode } = item 57 | 58 | if (!isPreset) return item 59 | 60 | return { 61 | ...item, 62 | path: join(presetModelsPath, mode), 63 | } 64 | }) 65 | } 66 | 67 | const matched = models.value.find(item => item.id === currentModel.value?.id) 68 | 69 | if (matched) { 70 | return currentModel.value = matched 71 | } 72 | 73 | currentModel.value = models.value[0] 74 | } 75 | 76 | return { 77 | models, 78 | currentModel, 79 | motions, 80 | expressions, 81 | init, 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /src/stores/shortcut.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { ref } from 'vue' 3 | 4 | export type HotKey = 'visibleCat' | 'mirrorMode' | 'penetrable' | 'alwaysOnTop' 5 | 6 | export const useShortcutStore = defineStore('shortcut', () => { 7 | const visibleCat = ref('') 8 | const visiblePreference = ref('') 9 | const mirrorMode = ref('') 10 | const penetrable = ref('') 11 | const alwaysOnTop = ref('') 12 | 13 | return { 14 | visibleCat, 15 | visiblePreference, 16 | mirrorMode, 17 | penetrable, 18 | alwaysOnTop, 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | export function isImage(value: string) { 2 | const regex = /\.(?:jpe?g|png|webp|avif|gif|svg|bmp|ico|tiff?|heic|apng)$/i 3 | 4 | return regex.test(value) 5 | } 6 | -------------------------------------------------------------------------------- /src/utils/keyboard.ts: -------------------------------------------------------------------------------- 1 | import { isMac } from './platform' 2 | 3 | export interface Key { 4 | eventKey: string 5 | tauriKey?: string 6 | symbol?: string 7 | } 8 | 9 | export const modifierKeys: Key[] = [ 10 | { 11 | eventKey: 'Shift', 12 | symbol: isMac ? '⇧' : 'Shift', 13 | }, 14 | { 15 | eventKey: 'Control', 16 | symbol: isMac ? '⌃' : 'Ctrl', 17 | }, 18 | { 19 | eventKey: 'Alt', 20 | symbol: isMac ? '⌥' : 'Alt', 21 | }, 22 | { 23 | eventKey: 'Command', 24 | symbol: isMac ? '⌘' : 'Super', 25 | }, 26 | ].map((item) => { 27 | return { ...item, tauriKey: item.eventKey } 28 | }) 29 | 30 | export const standardKeys: Key[] = [ 31 | // 第一排 32 | { 33 | eventKey: 'Escape', 34 | symbol: isMac ? '⎋' : 'Esc', 35 | }, 36 | { 37 | eventKey: 'F1', 38 | }, 39 | { 40 | eventKey: 'F2', 41 | }, 42 | { 43 | eventKey: 'F3', 44 | }, 45 | { 46 | eventKey: 'F4', 47 | }, 48 | { 49 | eventKey: 'F5', 50 | }, 51 | { 52 | eventKey: 'F6', 53 | }, 54 | { 55 | eventKey: 'F7', 56 | }, 57 | { 58 | eventKey: 'F8', 59 | }, 60 | { 61 | eventKey: 'F9', 62 | }, 63 | { 64 | eventKey: 'F10', 65 | }, 66 | { 67 | eventKey: 'F11', 68 | }, 69 | { 70 | eventKey: 'F12', 71 | }, // 第二排 72 | { 73 | eventKey: 'Backquote', 74 | symbol: '`', 75 | }, 76 | { 77 | eventKey: 'Digit1', 78 | }, 79 | { 80 | eventKey: 'Digit2', 81 | }, 82 | { 83 | eventKey: 'Digit3', 84 | }, 85 | { 86 | eventKey: 'Digit4', 87 | }, 88 | { 89 | eventKey: 'Digit5', 90 | }, 91 | { 92 | eventKey: 'Digit6', 93 | }, 94 | { 95 | eventKey: 'Digit7', 96 | }, 97 | { 98 | eventKey: 'Digit8', 99 | }, 100 | { 101 | eventKey: 'Digit9', 102 | }, 103 | { 104 | eventKey: 'Digit0', 105 | }, 106 | { 107 | eventKey: 'Minus', 108 | tauriKey: '-', 109 | symbol: '-', 110 | }, 111 | { 112 | eventKey: 'Equal', 113 | tauriKey: '=', 114 | symbol: '=', 115 | }, 116 | { 117 | eventKey: 'Backspace', 118 | symbol: isMac ? '⌫' : void 0, 119 | }, 120 | // 第三排 121 | { 122 | eventKey: 'Tab', 123 | symbol: isMac ? '⇥' : void 0, 124 | }, 125 | { 126 | eventKey: 'KeyQ', 127 | }, 128 | { 129 | eventKey: 'KeyW', 130 | }, 131 | { 132 | eventKey: 'KeyE', 133 | }, 134 | { 135 | eventKey: 'KeyR', 136 | }, 137 | { 138 | eventKey: 'KeyT', 139 | }, 140 | { 141 | eventKey: 'KeyY', 142 | }, 143 | { 144 | eventKey: 'KeyU', 145 | }, 146 | { 147 | eventKey: 'KeyI', 148 | }, 149 | { 150 | eventKey: 'KeyO', 151 | }, 152 | { 153 | eventKey: 'KeyP', 154 | }, 155 | { 156 | eventKey: 'BracketLeft', 157 | symbol: '[', 158 | }, 159 | { 160 | eventKey: 'BracketRight', 161 | symbol: ']', 162 | }, 163 | { 164 | eventKey: 'Backslash', 165 | symbol: '\\', 166 | }, 167 | // 第四排 168 | { 169 | eventKey: 'KeyA', 170 | }, 171 | { 172 | eventKey: 'KeyS', 173 | }, 174 | { 175 | eventKey: 'KeyD', 176 | }, 177 | { 178 | eventKey: 'KeyF', 179 | }, 180 | { 181 | eventKey: 'KeyG', 182 | }, 183 | { 184 | eventKey: 'KeyH', 185 | }, 186 | { 187 | eventKey: 'KeyJ', 188 | }, 189 | { 190 | eventKey: 'KeyK', 191 | }, 192 | { 193 | eventKey: 'KeyL', 194 | }, 195 | { 196 | eventKey: 'Semicolon', 197 | symbol: ';', 198 | }, 199 | { 200 | eventKey: 'Quote', 201 | symbol: '\'', 202 | }, 203 | { 204 | eventKey: 'Enter', 205 | symbol: isMac ? '↩︎' : void 0, 206 | }, 207 | // 第五排 208 | { 209 | eventKey: 'KeyZ', 210 | }, 211 | { 212 | eventKey: 'KeyX', 213 | }, 214 | { 215 | eventKey: 'KeyC', 216 | }, 217 | { 218 | eventKey: 'KeyV', 219 | }, 220 | { 221 | eventKey: 'KeyB', 222 | }, 223 | { 224 | eventKey: 'KeyN', 225 | }, 226 | { 227 | eventKey: 'KeyM', 228 | }, 229 | { 230 | eventKey: 'Comma', 231 | symbol: ',', 232 | }, 233 | { 234 | eventKey: 'Period', 235 | symbol: '.', 236 | }, 237 | { 238 | eventKey: 'Slash', 239 | symbol: '/', 240 | }, 241 | // 第六排 242 | { 243 | eventKey: 'Space', 244 | symbol: isMac ? '␣' : void 0, 245 | }, 246 | // 方向键 247 | { 248 | eventKey: 'ArrowUp', 249 | symbol: '↑', 250 | }, 251 | { 252 | eventKey: 'ArrowDown', 253 | symbol: '↓', 254 | }, 255 | { 256 | eventKey: 'ArrowLeft', 257 | symbol: '←', 258 | }, 259 | { 260 | eventKey: 'ArrowRight', 261 | symbol: '→', 262 | }, 263 | ].map((item) => { 264 | const { eventKey } = item 265 | 266 | item.symbol ??= eventKey 267 | item.tauriKey ??= eventKey 268 | 269 | if (eventKey.startsWith('Digit') || eventKey.startsWith('Key')) { 270 | item.tauriKey = item.symbol = eventKey.slice(-1) 271 | } 272 | 273 | return item 274 | }) 275 | 276 | export const keys = modifierKeys.concat(standardKeys) 277 | -------------------------------------------------------------------------------- /src/utils/live2d.ts: -------------------------------------------------------------------------------- 1 | import type { Cubism4InternalModel } from 'pixi-live2d-display' 2 | 3 | import { convertFileSrc } from '@tauri-apps/api/core' 4 | import { readDir, readTextFile } from '@tauri-apps/plugin-fs' 5 | import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display' 6 | import { Application, Ticker } from 'pixi.js' 7 | 8 | import { join } from './path' 9 | 10 | Live2DModel.registerTicker(Ticker) 11 | 12 | class Live2d { 13 | private app: Application | null = null 14 | public model: Live2DModel | null = null 15 | private modelWidth = 0 16 | private modelHeight = 0 17 | 18 | constructor() { } 19 | 20 | private initApp() { 21 | if (this.app) return 22 | 23 | const view = document.getElementById('live2dCanvas') as HTMLCanvasElement 24 | 25 | this.app = new Application({ 26 | view, 27 | resizeTo: window, 28 | backgroundAlpha: 0, 29 | resolution: devicePixelRatio, 30 | }) 31 | } 32 | 33 | public async load(path: string) { 34 | this.initApp() 35 | 36 | this.destroy() 37 | 38 | const files = await readDir(path) 39 | 40 | const modelFile = files.find(file => file.name.endsWith('.model3.json')) 41 | 42 | if (!modelFile) { 43 | throw new Error('未找到模型主配置文件,请确认模型文件是否完整。') 44 | } 45 | 46 | const modelPath = join(path, modelFile.name) 47 | 48 | const modelJSON = JSON.parse(await readTextFile(modelPath)) 49 | 50 | const modelSettings = new Cubism4ModelSettings({ 51 | ...modelJSON, 52 | url: convertFileSrc(modelPath), 53 | }) 54 | 55 | modelSettings.replaceFiles((file) => { 56 | return convertFileSrc(join(path, file)) 57 | }) 58 | 59 | this.model = await Live2DModel.from(modelSettings) 60 | 61 | const { width, height } = this.model 62 | this.modelWidth = width 63 | this.modelHeight = height 64 | 65 | this.app?.stage.addChild(this.model) 66 | 67 | const { motions, expressions } = modelSettings 68 | 69 | return { 70 | width, 71 | height, 72 | motions, 73 | expressions, 74 | } 75 | } 76 | 77 | public destroy() { 78 | this.model?.destroy() 79 | } 80 | 81 | public fitModel() { 82 | if (!this.model) return 83 | 84 | const scaleX = innerWidth / this.modelWidth 85 | const scaleY = innerHeight / this.modelHeight 86 | const scale = Math.min(scaleX, scaleY) 87 | 88 | this.model.scale.set(scale) 89 | this.model.x = innerWidth / 2 90 | this.model.y = innerHeight / 2 91 | this.model.anchor.set(0.5) 92 | } 93 | 94 | public playMotion(group: string, index: number) { 95 | return this.model?.motion(group, index) 96 | } 97 | 98 | public playExpressions(index: number) { 99 | return this.model?.expression(index) 100 | } 101 | 102 | public getCoreModel() { 103 | const internalModel = this.model?.internalModel as Cubism4InternalModel 104 | 105 | return internalModel?.coreModel 106 | } 107 | 108 | public getParameterRange(id: string) { 109 | const coreModel = this.getCoreModel() 110 | 111 | const index = coreModel?.getParameterIndex(id) 112 | const min = coreModel?.getParameterMinimumValue(index) 113 | const max = coreModel?.getParameterMaximumValue(index) 114 | 115 | return { 116 | min, 117 | max, 118 | } 119 | } 120 | 121 | public setParameterValue(id: string, value: number) { 122 | const coreModel = this.getCoreModel() 123 | 124 | return coreModel?.setParameterValueById?.(id, Number(value)) 125 | } 126 | } 127 | 128 | const live2d = new Live2d() 129 | 130 | export default live2d 131 | -------------------------------------------------------------------------------- /src/utils/monitor.ts: -------------------------------------------------------------------------------- 1 | import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow' 2 | import { 3 | cursorPosition, 4 | monitorFromPoint, 5 | } from '@tauri-apps/api/window' 6 | 7 | export async function getCursorMonitor() { 8 | const appWindow = getCurrentWebviewWindow() 9 | 10 | const scaleFactor = await appWindow.scaleFactor() 11 | 12 | const point = await cursorPosition() 13 | 14 | const { x, y } = point.toLogical(scaleFactor) 15 | 16 | const monitor = await monitorFromPoint(x, y) 17 | 18 | if (!monitor) return 19 | 20 | return { 21 | ...monitor, 22 | cursorPosition: point, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import type { LiteralUnion } from 'ant-design-vue/es/_util/type' 2 | 3 | import { sep } from '@tauri-apps/api/path' 4 | 5 | export function join(...paths: LiteralUnion<'resources' | 'left-keys' | 'right-keys' | 'background.png' | 'cover.png'>[]) { 6 | const joinPaths = paths.map((path) => { 7 | if (path.endsWith(sep())) { 8 | return path.slice(0, -1) 9 | } 10 | 11 | return path 12 | }) 13 | 14 | return joinPaths.join(sep()) 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/platform.ts: -------------------------------------------------------------------------------- 1 | import { platform } from '@tauri-apps/plugin-os' 2 | 3 | export const isMac = platform() === 'macos' 4 | 5 | export const isWindows = platform() === 'windows' 6 | 7 | export const isLinux = platform() === 'linux' 8 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// <reference types="vite/client" /> 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | 6 | const component: DefineComponent<object, object, any> 7 | export default component 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "jsx": "preserve", 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "useDefineForClassFields": true, 7 | 8 | "baseUrl": ".", 9 | "module": "ESNext", 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "paths": { 13 | "@/*": ["src/*"] 14 | }, 15 | "resolveJsonModule": true, 16 | "allowImportingTsExtensions": true, 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUnusedLocals": true, 22 | "noUnusedParameters": true, 23 | "noEmit": true, 24 | "isolatedModules": true, 25 | "skipLibCheck": true 26 | }, 27 | "references": [{ "path": "./tsconfig.node.json" }], 28 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"] 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "bundler", 6 | "allowSyntheticDefaultImports": true, 7 | "skipLibCheck": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetIcons, 4 | presetWind3, 5 | transformerDirectives, 6 | transformerVariantGroup, 7 | } from 'unocss' 8 | 9 | export default defineConfig({ 10 | presets: [ 11 | presetWind3(), 12 | presetIcons(), 13 | ], 14 | transformers: [ 15 | transformerVariantGroup(), 16 | transformerDirectives({ 17 | applyVariable: ['--uno'], 18 | }), 19 | ], 20 | shortcuts: [ 21 | [/^bg-color-(\d+)$/, ([, d]) => `bg-bg-${d}`], 22 | [/^text-color-(\d+)$/, ([, d]) => `text-text-${d}`], 23 | [/^b-color-(\d+)$/, ([, d]) => `b-border-${d}`], 24 | [/^(.*)-primary-(\d+)$/, ([, s, d]) => `${s}-[var(--ant-blue-${d})]`], 25 | ], 26 | theme: { 27 | colors: { 28 | 'bg-1': 'var(--ant-color-bg-layout)', 29 | 'bg-2': 'var(--ant-color-bg-container)', 30 | 'bg-3': 'var(--ant-color-bg-elevated)', 31 | 'bg-4': 'var(--ant-color-bg-spotlight)', 32 | 'bg-5': 'var(--ant-color-fill)', 33 | 'bg-6': 'var(--ant-color-fill-secondary)', 34 | 'bg-7': 'var(--ant-color-fill-tertiary)', 35 | 'bg-8': 'var(--ant-color-fill-quaternary)', 36 | 'text-1': 'var(--ant-color-text)', 37 | 'text-2': 'var(--ant-color-text-secondary)', 38 | 'text-3': 'var(--ant-color-text-tertiary)', 39 | 'text-4': 'var(--ant-color-text-quaternary)', 40 | 'border-1': 'var(--ant-color-border)', 41 | 'border-2': 'var(--ant-color-border-secondary)', 42 | 'primary': 'var(--ant-blue)', 43 | 'success': 'var(--ant-green)', 44 | 'danger': 'var(--ant-red)', 45 | }, 46 | }, 47 | }) 48 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import { env } from 'node:process' 3 | 4 | import vue from '@vitejs/plugin-vue' 5 | import UnoCSS from 'unocss/vite' 6 | import { defineConfig } from 'vite' 7 | 8 | const host = env.TAURI_DEV_HOST 9 | 10 | // https://vitejs.dev/config/ 11 | export default defineConfig(async () => ({ 12 | plugins: [vue(), UnoCSS()], 13 | resolve: { 14 | alias: { 15 | '@': resolve(__dirname, 'src'), 16 | }, 17 | }, 18 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 19 | // 20 | // 1. prevent vite from obscuring rust errors 21 | clearScreen: false, 22 | // 2. tauri expects a fixed port, fail if that port is not available 23 | server: { 24 | port: 1420, 25 | strictPort: true, 26 | host: host || false, 27 | hmr: host 28 | ? { 29 | protocol: 'ws', 30 | host, 31 | port: 1421, 32 | } 33 | : undefined, 34 | watch: { 35 | // 3. tell vite to ignore watching `src-tauri` 36 | ignored: ['**/src-tauri/**'], 37 | }, 38 | }, 39 | })) 40 | --------------------------------------------------------------------------------