mirror of
https://github.com/ayangweb/BongoCat.git
synced 2026-03-17 04:11:37 +08:00
Compare commits
1 Commits
linux-wayl
...
easy-live2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97d28ed9ba |
2
.github/DOWNLOAD_GUIDE.md
vendored
2
.github/DOWNLOAD_GUIDE.md
vendored
@@ -24,7 +24,7 @@ brew tap ayangweb/BongoCat
|
||||
2. 安装:
|
||||
|
||||
```bash
|
||||
brew install --no-quarantine bongo-cat
|
||||
brew install bongo-cat
|
||||
```
|
||||
|
||||
3. 更新:
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: BongoCat Release
|
||||
name: Release CI
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
node-version: 20
|
||||
cache: pnpm
|
||||
|
||||
- name: Install front-end dependencies
|
||||
- name: Install app dependencies and build web
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build the app
|
||||
|
||||
20
.github/workflows/upgradelink.yml
vendored
20
.github/workflows/upgradelink.yml
vendored
@@ -1,20 +0,0 @@
|
||||
name: Upload Release to UpgradeLink
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
upgradeLink-upload:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send a request to UpgradeLink
|
||||
uses: toolsetlink/upgradelink-action@v5
|
||||
with:
|
||||
source-url: 'https://github.com/ayangweb/BongoCat/releases/latest/download/latest.json'
|
||||
access-key: ${{ secrets.UPGRADE_LINK_ACCESS_KEY }}
|
||||
tauri-key: ${{ secrets.UPGRADE_LINK_TAURI_KEY }}
|
||||
github-token: ${{ secrets.RELEASE_TOKEN }}
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -48,8 +48,5 @@
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
],
|
||||
|
||||
"typescript.enablePromptUseWorkspaceTsdk": true,
|
||||
"typescript.tsdk": "./node_modules/typescript/lib"
|
||||
]
|
||||
}
|
||||
|
||||
64
Cargo.lock
generated
64
Cargo.lock
generated
@@ -450,11 +450,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bongo-cat"
|
||||
version = "0.6.2"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"fs_extra",
|
||||
"input",
|
||||
"nix",
|
||||
"rdev",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1981,12 +1979,6 @@ version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.5.1"
|
||||
@@ -2330,36 +2322,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"input-sys",
|
||||
"libc",
|
||||
"log",
|
||||
"udev",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "input-sys"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0"
|
||||
|
||||
[[package]]
|
||||
name = "io-lifetimes"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.9",
|
||||
"libc",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ipnet"
|
||||
version = "2.11.0"
|
||||
@@ -2580,16 +2542,6 @@ dependencies = [
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libudev-sys"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -3481,7 +3433,7 @@ checksum = "b53a684391ad002dd6a596ceb6c74fd004fdce75f4be2e3f615068abbea5fd50"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"concurrent-queue",
|
||||
"hermit-abi 0.5.1",
|
||||
"hermit-abi",
|
||||
"pin-project-lite",
|
||||
"rustix 1.0.7",
|
||||
"tracing",
|
||||
@@ -5654,18 +5606,6 @@ version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
|
||||
|
||||
[[package]]
|
||||
name = "udev"
|
||||
version = "0.9.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"libc",
|
||||
"libudev-sys",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uds_windows"
|
||||
version = "1.1.0"
|
||||
|
||||
24
README.md
24
README.md
@@ -51,15 +51,15 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
| macOS | Windows | Linux(x11) |
|
||||
| -------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
||||
|  |  |  |
|
||||
| macOS | Window | Linux(x11) |
|
||||
| ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
|  |  |  |
|
||||
|
||||
## 开发背景
|
||||
|
||||
本项目的灵感来源于 [MMmmmoko](https://github.com/MMmmmoko) 大佬开发的 [Bongo-Cat-Mver](https://github.com/MMmmmoko/Bongo-Cat-Mver)。它以独特的猫咪互动功能深受用户喜爱,但仅支持 Windows 平台。作为一名深度 macOS 用户,我特别希望在自己的设备上也能使用这款可爱的猫咪,于是我决定开发一个适配 macOS 的版本。
|
||||
|
||||
同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux,让更多的用户都能与这只可爱的猫咪互动!
|
||||
同时,得益于 [Tauri](https://github.com/tauri-apps/tauri) 强大的跨平台能力,本项目不仅支持 macOS,还兼容 Windows 和 Linux(x11),让更多的用户都能与这只可爱的猫咪互动!
|
||||
|
||||
## 下载
|
||||
|
||||
@@ -70,16 +70,12 @@
|
||||
|
||||
## 功能介绍
|
||||
|
||||
- 适配 macOS、Windows 和 Linux。
|
||||
- 适配 macOS、Windows 和 Linux(x11)。
|
||||
- 根据据键盘或鼠标操作,同步移动鼠标或敲击键盘。
|
||||
- 支持导入自定义模型,自由打造专属猫咪形象。
|
||||
- 完全开源,代码公开透明,绝不收集任何用户数据。
|
||||
- 支持离线运行,无需联网,保护用户隐私。
|
||||
|
||||
## 使用提示
|
||||
|
||||
- Linux 下需要用户系统安装 libinput 并加入 `input` 用户组,方可在 X11 和 Wayland 下正常使用。
|
||||
|
||||
## 更多模型
|
||||
|
||||
你可以在这个仓库中探索、下载更多猫咪模型,或提交你的创作,与大家一起分享:
|
||||
@@ -90,9 +86,9 @@
|
||||
|
||||
<a href="https://qm.qq.com/q/AS3gNv2Vzy">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://i0.hdslb.com/bfs/openplatform/5ad8e4278c525cca6d3b4426c30b6d299d8a9654.png" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" />
|
||||
<img alt="QQ Group" src="https://i0.hdslb.com/bfs/openplatform/599680ad67bc9f9f876f76069c2239e9a85bb54d.png" height="250" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/6428b6ff-0b39-4e16-a750-bad6f6b376e9" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/e50883e2-0be7-4eea-9879-e725fa1dffd2" />
|
||||
<img alt="QQ Group" src="https://github.com/user-attachments/assets/e50883e2-0be7-4eea-9879-e725fa1dffd2" height="250" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
@@ -113,7 +109,3 @@
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=ayangweb/BongoCat&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 致谢
|
||||
|
||||
- 特别感谢 [UpgradeLink](https://www.toolsetlink.com/) 提供高效稳定的自动更新服务,让本项目得以持续为用户带来最新版本的优质体验。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "bongo-cat",
|
||||
"type": "module",
|
||||
"version": "0.6.2",
|
||||
"version": "0.5.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "ayangweb",
|
||||
@@ -37,17 +37,16 @@
|
||||
"@vueuse/core": "^13.3.0",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"dayjs": "^1.11.13",
|
||||
"easy-live2d": "^0.3.2",
|
||||
"es-toolkit": "^1.38.0",
|
||||
"is-url": "^1.2.4",
|
||||
"nanoid": "^5.1.5",
|
||||
"pinia": "^3.0.3",
|
||||
"pixi-live2d-display": "^0.4.0",
|
||||
"pixi.js": "^6.5.10",
|
||||
"pixi.js": "^8.10.1",
|
||||
"tauri-plugin-macos-permissions-api": "^2.3.0",
|
||||
"vue": "^3.5.16",
|
||||
"vue-markdown-render": "^2.2.1",
|
||||
"vue-router": "^4.5.1",
|
||||
"vue3-masonry-css": "^1.0.7"
|
||||
"vue-router": "^4.5.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^4.13.3",
|
||||
|
||||
901
pnpm-lock.yaml
generated
901
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "bongo-cat"
|
||||
version = "0.6.2"
|
||||
version = "0.5.0"
|
||||
description = "A Tauri App"
|
||||
authors = [ "ayangweb" ]
|
||||
edition = "2024"
|
||||
@@ -22,6 +22,7 @@ tauri = { workspace = true, features = ["tray-icon", "protocol-asset", "macos-pr
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
tauri-plugin-custom-window.workspace = true
|
||||
rdev = { git = "https://github.com/ayangweb/rdev" }
|
||||
tauri-plugin-os = "2"
|
||||
tauri-plugin-process = "2"
|
||||
tauri-plugin-opener = "2"
|
||||
@@ -41,12 +42,5 @@ tauri-plugin-global-shortcut = "2"
|
||||
[target."cfg(target_os = \"macos\")".dependencies]
|
||||
tauri-nspanel.workspace = true
|
||||
|
||||
[target."cfg(not(target_os = \"linux\"))".dependencies]
|
||||
rdev = { git = "https://github.com/ayangweb/rdev" }
|
||||
|
||||
[target."cfg(target_os = \"linux\")".dependencies]
|
||||
nix = { version = "0.30", features = ["poll"] }
|
||||
input = "0.9"
|
||||
|
||||
[features]
|
||||
cargo-clippy = []
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
use rdev::{Event, EventType, listen};
|
||||
use serde_json::json;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
use crate::core::{device::{DeviceEvent, DeviceKind}};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum DeviceKind {
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
MouseMove,
|
||||
KeyboardPress,
|
||||
KeyboardRelease,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceEvent {
|
||||
kind: DeviceKind,
|
||||
value: Value,
|
||||
}
|
||||
|
||||
pub fn start_listening(app_handle: AppHandle) {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
let callback = move |event: Event| {
|
||||
let device_event = match event.event_type {
|
||||
let device = match event.event_type {
|
||||
EventType::ButtonPress(button) => DeviceEvent {
|
||||
kind: DeviceKind::MousePress,
|
||||
value: json!(format!("{:?}", button)),
|
||||
@@ -40,10 +53,20 @@ pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Res
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let _ = app_handle.emit("device-changed", device_event);
|
||||
if let Err(e) = app_handle.emit("device-changed", device) {
|
||||
eprintln!("Failed to emit event: {:?}", e);
|
||||
}
|
||||
};
|
||||
|
||||
listen(callback).map_err(|err| format!("Failed to listen device: {:?}", err))?;
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Err(e) = listen(callback) {
|
||||
eprintln!("Device listening error: {:?}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
std::thread::spawn(move || {
|
||||
if let Err(e) = listen(callback) {
|
||||
eprintln!("Device listening error: {:?}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
use input::{
|
||||
event::{
|
||||
keyboard::{KeyState, KeyboardEventTrait},
|
||||
pointer::ButtonState,
|
||||
PointerEvent,
|
||||
},
|
||||
Event, Libinput, LibinputInterface,
|
||||
};
|
||||
use nix::{
|
||||
libc::{O_RDONLY, O_RDWR, O_WRONLY},
|
||||
poll::{poll, PollFd, PollFlags, PollTimeout},
|
||||
};
|
||||
use std::{
|
||||
fs::{File, OpenOptions}, os::{fd::{AsFd, OwnedFd}, unix::prelude::OpenOptionsExt}, path::Path
|
||||
};
|
||||
|
||||
use serde_json::json;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tauri::{AppHandle, Emitter, Runtime, command};
|
||||
|
||||
use crate::core::{device::{DeviceEvent, DeviceKind}, setup::key_from_code};
|
||||
|
||||
static IS_RUNNING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub struct Interface;
|
||||
|
||||
impl LibinputInterface for Interface {
|
||||
fn open_restricted(&mut self, path: &Path, flags: i32) -> Result<OwnedFd, i32> {
|
||||
OpenOptions::new()
|
||||
.custom_flags(flags)
|
||||
.read((flags & O_RDONLY != 0) | (flags & O_RDWR != 0))
|
||||
.write((flags & O_WRONLY != 0) | (flags & O_RDWR != 0))
|
||||
.open(path)
|
||||
.map(|file| file.into())
|
||||
.map_err(|err| err.raw_os_error().unwrap())
|
||||
}
|
||||
|
||||
#[allow(unused_must_use)]
|
||||
fn close_restricted(&mut self, fd: OwnedFd) {
|
||||
File::from(fd);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_device_event(event: &Event) -> Option<DeviceEvent> {
|
||||
match event {
|
||||
Event::Keyboard(ev) => {
|
||||
let key_code = ev.key();
|
||||
let key_name = match key_from_code(key_code) {
|
||||
Some(name) => name.to_string(),
|
||||
None => format!("Unknown({})", key_code),
|
||||
};
|
||||
match ev.key_state() {
|
||||
KeyState::Pressed => Some(DeviceEvent {
|
||||
kind: DeviceKind::KeyboardPress,
|
||||
value: json!(key_name),
|
||||
}),
|
||||
KeyState::Released => Some(DeviceEvent{
|
||||
kind: DeviceKind::KeyboardRelease,
|
||||
value: json!(key_name),
|
||||
})
|
||||
}
|
||||
},
|
||||
Event::Pointer(ev) => {
|
||||
match ev {
|
||||
PointerEvent::Button(e) => {
|
||||
let btn_code = e.button();
|
||||
let btn_name = match btn_code {
|
||||
0x110 => String::from("Left"),
|
||||
0x111 => String::from("Right"),
|
||||
0x112 => String::from("Middle"),
|
||||
_ => format!("Unknown({})", btn_code as u8),
|
||||
};
|
||||
match e.button_state() {
|
||||
ButtonState::Pressed => Some(DeviceEvent {
|
||||
kind: DeviceKind::MousePress,
|
||||
value: json!(btn_name),
|
||||
}),
|
||||
ButtonState::Released => Some(DeviceEvent {
|
||||
kind: DeviceKind::MouseRelease,
|
||||
value: json!(btn_name),
|
||||
})
|
||||
}
|
||||
},
|
||||
PointerEvent::Motion(e) => {
|
||||
Some(DeviceEvent {
|
||||
kind: DeviceKind::MouseMove,
|
||||
value: json!({
|
||||
"x": e.dx_unaccelerated(),
|
||||
"y": e.dy_unaccelerated()
|
||||
}),
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[command]
|
||||
pub async fn start_device_listening<R: Runtime>(app_handle: AppHandle<R>) -> Result<(), String> {
|
||||
if IS_RUNNING.load(Ordering::SeqCst) {
|
||||
return Err("Device is already listening".to_string());
|
||||
}
|
||||
|
||||
IS_RUNNING.store(true, Ordering::SeqCst);
|
||||
|
||||
let mut input = Libinput::new_with_udev(Interface);
|
||||
match input.udev_assign_seat("seat0") {
|
||||
Ok(_) => {
|
||||
let input_clone = &input.clone();
|
||||
let mut pollfds = [PollFd::new(input_clone.as_fd(), PollFlags::POLLIN)];
|
||||
while poll(&mut pollfds, PollTimeout::NONE).is_ok() {
|
||||
input.dispatch().unwrap();
|
||||
for event in &mut input {
|
||||
let device_event = build_device_event(&event);
|
||||
if let Some(e) = device_event {
|
||||
app_handle.emit("device-changed", e).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(_) => return Err("Failed to assign seat".to_string()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub mod common;
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub use common::*;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod linux;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub enum DeviceKind {
|
||||
MousePress,
|
||||
MouseRelease,
|
||||
MouseMove,
|
||||
KeyboardPress,
|
||||
KeyboardRelease,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct DeviceEvent {
|
||||
kind: DeviceKind,
|
||||
value: Value,
|
||||
}
|
||||
@@ -6,149 +6,3 @@ pub fn platform(
|
||||
_preference_window: WebviewWindow,
|
||||
) {
|
||||
}
|
||||
|
||||
pub fn key_from_code(code: u32) -> Option<&'static str> {
|
||||
match code {
|
||||
// Function key
|
||||
1 => Some("Escape"),
|
||||
28 => Some("Return"),
|
||||
14 => Some("Backspace"),
|
||||
15 => Some("Tab"),
|
||||
57 => Some("Space"),
|
||||
58 => Some("CapsLock"),
|
||||
99 => Some("PrintScreen"),
|
||||
70 => Some("ScrollLock"),
|
||||
119 => Some("Pause"),
|
||||
69 => Some("NumLock"),
|
||||
110 => Some("Insert"),
|
||||
102 => Some("Home"),
|
||||
107 => Some("End"),
|
||||
104 => Some("PageUp"),
|
||||
109 => Some("PageDown"),
|
||||
111 => Some("Delete"),
|
||||
|
||||
// Arrow key
|
||||
103 => Some("UpArrow"),
|
||||
108 => Some("DownArrow"),
|
||||
105 => Some("LeftArrow"),
|
||||
106 => Some("RightArrow"),
|
||||
|
||||
// F key
|
||||
59 => Some("F1"),
|
||||
60 => Some("F2"),
|
||||
61 => Some("F3"),
|
||||
62 => Some("F4"),
|
||||
63 => Some("F5"),
|
||||
64 => Some("F6"),
|
||||
65 => Some("F7"),
|
||||
66 => Some("F8"),
|
||||
67 => Some("F9"),
|
||||
68 => Some("F10"),
|
||||
87 => Some("F11"),
|
||||
88 => Some("F12"),
|
||||
|
||||
// Numeric
|
||||
2 => Some("Num1"),
|
||||
3 => Some("Num2"),
|
||||
4 => Some("Num3"),
|
||||
5 => Some("Num4"),
|
||||
6 => Some("Num5"),
|
||||
7 => Some("Num6"),
|
||||
8 => Some("Num7"),
|
||||
9 => Some("Num8"),
|
||||
10 => Some("Num9"),
|
||||
11 => Some("Num0"),
|
||||
|
||||
// Alphabetic
|
||||
16 => Some("KeyQ"),
|
||||
17 => Some("KeyW"),
|
||||
18 => Some("KeyE"),
|
||||
19 => Some("KeyR"),
|
||||
20 => Some("KeyT"),
|
||||
21 => Some("KeyY"),
|
||||
22 => Some("KeyU"),
|
||||
23 => Some("KeyI"),
|
||||
24 => Some("KeyO"),
|
||||
25 => Some("KeyP"),
|
||||
30 => Some("KeyA"),
|
||||
31 => Some("KeyS"),
|
||||
32 => Some("KeyD"),
|
||||
33 => Some("KeyF"),
|
||||
34 => Some("KeyG"),
|
||||
35 => Some("KeyH"),
|
||||
36 => Some("KeyJ"),
|
||||
37 => Some("KeyK"),
|
||||
38 => Some("KeyL"),
|
||||
44 => Some("KeyZ"),
|
||||
45 => Some("KeyX"),
|
||||
46 => Some("KeyC"),
|
||||
47 => Some("KeyV"),
|
||||
48 => Some("KeyB"),
|
||||
49 => Some("KeyN"),
|
||||
50 => Some("KeyM"),
|
||||
|
||||
// Symbolic
|
||||
41 => Some("BackQuote"),
|
||||
12 => Some("Minus"),
|
||||
13 => Some("Equal"),
|
||||
26 => Some("LeftBracket"),
|
||||
27 => Some("RightBracket"),
|
||||
39 => Some("SemiColon"),
|
||||
40 => Some("Quote"),
|
||||
43 => Some("BackSlash"),
|
||||
86 => Some("IntlBackslash"),
|
||||
89 => Some("IntlRo"),
|
||||
124 => Some("IntlYen"),
|
||||
101 => Some("KanaMode"),
|
||||
51 => Some("Comma"),
|
||||
52 => Some("Dot"),
|
||||
53 => Some("Slash"),
|
||||
|
||||
// Control key
|
||||
29 => Some("ControlLeft"),
|
||||
97 => Some("ControlRight"),
|
||||
42 => Some("ShiftLeft"),
|
||||
54 => Some("ShiftRight"),
|
||||
56 => Some("Alt"),
|
||||
100 => Some("AltGr"),
|
||||
125 => Some("MetaLeft"),
|
||||
126 => Some("MetaRight"),
|
||||
127 => Some("Apps"),
|
||||
|
||||
// NumPad
|
||||
55 => Some("KpMultiply"),
|
||||
78 => Some("KpMinus"),
|
||||
74 => Some("KpPlus"),
|
||||
98 => Some("KpDivide"),
|
||||
117 => Some("KpEqual"),
|
||||
121 => Some("KpComma"),
|
||||
96 => Some("KpReturn"),
|
||||
83 => Some("KpDecimal"),
|
||||
79 => Some("Kp1"),
|
||||
80 => Some("Kp2"),
|
||||
81 => Some("Kp3"),
|
||||
75 => Some("Kp4"),
|
||||
76 => Some("Kp5"),
|
||||
77 => Some("Kp6"),
|
||||
71 => Some("Kp7"),
|
||||
72 => Some("Kp8"),
|
||||
73 => Some("Kp9"),
|
||||
82 => Some("Kp0"),
|
||||
|
||||
// Media key
|
||||
115 => Some("VolumeUp"),
|
||||
114 => Some("VolumeDown"),
|
||||
113 => Some("VolumeMute"),
|
||||
|
||||
// Language key
|
||||
90 => Some("Lang1"),
|
||||
91 => Some("Lang2"),
|
||||
92 => Some("Lang3"),
|
||||
93 => Some("Lang4"),
|
||||
94 => Some("Lang5"),
|
||||
|
||||
// Other
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
use tauri::{AppHandle, Emitter, EventTarget, WebviewWindow};
|
||||
use tauri_nspanel::{WebviewWindowExt, cocoa::appkit::NSWindowCollectionBehavior, panel_delegate};
|
||||
use tauri_plugin_custom_window::MAIN_WINDOW_LABEL;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod core;
|
||||
mod utils;
|
||||
|
||||
use core::{device::start_device_listening, prevent_default, setup};
|
||||
use core::{device, prevent_default, setup};
|
||||
use tauri::{Manager, WindowEvent, generate_handler};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_custom_window::{
|
||||
@@ -21,9 +21,11 @@ pub fn run() {
|
||||
|
||||
setup::default(&app_handle, main_window.clone(), preference_window.clone());
|
||||
|
||||
device::start_listening(app_handle.clone());
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(generate_handler![copy_dir, start_device_listening])
|
||||
.invoke_handler(generate_handler![copy_dir])
|
||||
.plugin(tauri_plugin_custom_window::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(deprecated)]
|
||||
use super::{is_main_window, shared_hide_window, shared_set_always_on_top, shared_show_window};
|
||||
use crate::MAIN_WINDOW_LABEL;
|
||||
use tauri::{AppHandle, Runtime, WebviewWindow, command};
|
||||
|
||||
@@ -63,10 +63,8 @@
|
||||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"dangerousInsecureTransportProtocol": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEVBRjJFMzE3MjEwMUZEMTAKUldRUS9RRWhGK1B5NmdkemhKcUFrVjZBQXlzdExpakdWVEJDeU9XckVsbzV2cFIycVJOempWa2UK",
|
||||
"endpoints": [
|
||||
"http://api.upgrade.toolsetlink.com/v1/tauri/upgrade?tauriKey=KtGlsZUVXmWfjkRKCuqpfw&versionName={{current_version}}&target={{target}}&arch={{arch}}&appointVersionName=&devModelKey=&devKey=",
|
||||
"https://gh-proxy.com/github.com/ayangweb/BongoCat/releases/latest/download/latest.json"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -35,12 +35,12 @@ onMounted(async () => {
|
||||
|
||||
await appStore.$tauri.start()
|
||||
await modelStore.$tauri.start()
|
||||
await modelStore.init()
|
||||
await catStore.$tauri.start()
|
||||
await generalStore.$tauri.start()
|
||||
await shortcutStore.$tauri.start()
|
||||
await restoreState()
|
||||
catStore.init()
|
||||
catStore.visible = true
|
||||
|
||||
restoreState()
|
||||
})
|
||||
|
||||
useTauriListen(LISTEN_KEY.SHOW_WINDOW, ({ payload }) => {
|
||||
|
||||
@@ -11,7 +11,7 @@ import { computed, reactive, watch } from 'vue'
|
||||
import VueMarkdown from 'vue-markdown-render'
|
||||
|
||||
import { useTauriListen } from '@/composables/useTauriListen'
|
||||
import { GITHUB_LINK, LISTEN_KEY, UPGRADE_LINK_ACCESS_KEY } from '@/constants'
|
||||
import { GITHUB_LINK, LISTEN_KEY } from '@/constants'
|
||||
import { showWindow } from '@/plugins/window'
|
||||
import { useGeneralStore } from '@/stores/general'
|
||||
|
||||
@@ -67,12 +67,7 @@ const downloadProgress = computed(() => {
|
||||
|
||||
async function checkUpdate(visibleMessage = false) {
|
||||
try {
|
||||
const update = await check({
|
||||
timeout: 5000,
|
||||
headers: {
|
||||
'X-AccessKey': UPGRADE_LINK_ACCESS_KEY,
|
||||
},
|
||||
})
|
||||
const update = await check()
|
||||
|
||||
if (update) {
|
||||
const { version, currentVersion, body = '', date, downloadAndInstall } = update
|
||||
|
||||
@@ -1,34 +1,33 @@
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { LogicalSize, PhysicalSize } from '@tauri-apps/api/dpi'
|
||||
import { resolveResource } from '@tauri-apps/api/path'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { isNil, round } from 'es-toolkit'
|
||||
import { ref, watch } from 'vue'
|
||||
import { computed, watch } from 'vue'
|
||||
|
||||
import live2d from '../utils/live2d'
|
||||
import { getCursorMonitor } from '../utils/monitor'
|
||||
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
import { getImageSize } from '@/utils/dom'
|
||||
import { join } from '@/utils/path'
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
|
||||
interface ModelSize {
|
||||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export function useModel() {
|
||||
const modelStore = useModelStore()
|
||||
const catStore = useCatStore()
|
||||
const modelSize = ref<ModelSize>()
|
||||
|
||||
const backgroundImage = computed(() => {
|
||||
return convertFileSrc(join(modelStore.currentModel!.path, 'resources', 'background.png'))
|
||||
})
|
||||
|
||||
watch(() => modelStore.currentModel, handleLoad, { deep: true, immediate: true })
|
||||
|
||||
watch([() => catStore.scale, modelSize], async () => {
|
||||
if (!modelSize.value) return
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
watch(() => catStore.scale, async () => {
|
||||
const { width, height } = await getImageSize(backgroundImage.value)
|
||||
|
||||
appWindow.setSize(
|
||||
new PhysicalSize({
|
||||
@@ -40,19 +39,15 @@ export function useModel() {
|
||||
|
||||
async function handleLoad() {
|
||||
try {
|
||||
if (!modelStore.currentModel) return
|
||||
|
||||
const { path } = modelStore.currentModel
|
||||
const { path } = modelStore.currentModel!
|
||||
|
||||
await resolveResource(path)
|
||||
|
||||
const { width, height, ...rest } = await live2d.load(path)
|
||||
await live2d.load(path)
|
||||
|
||||
modelSize.value = { width, height }
|
||||
// handleResize()
|
||||
|
||||
handleResize()
|
||||
|
||||
Object.assign(modelStore, rest)
|
||||
// Object.assign(modelStore, data)
|
||||
} catch (error) {
|
||||
message.error(String(error))
|
||||
}
|
||||
@@ -63,11 +58,13 @@ export function useModel() {
|
||||
}
|
||||
|
||||
async function handleResize() {
|
||||
if (!modelSize.value) return
|
||||
if (!live2d.model) return
|
||||
|
||||
live2d.fitModel()
|
||||
const { innerWidth, innerHeight } = window
|
||||
|
||||
const { width, height } = modelSize.value
|
||||
const { width, height } = await getImageSize(backgroundImage.value)
|
||||
|
||||
live2d.model?.scale.set(innerWidth / width)
|
||||
|
||||
if (round(innerWidth / innerHeight, 1) !== round(width / height, 1)) {
|
||||
await appWindow.setSize(
|
||||
@@ -133,6 +130,7 @@ export function useModel() {
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage,
|
||||
handleLoad,
|
||||
handleDestroy,
|
||||
handleResize,
|
||||
|
||||
@@ -8,7 +8,6 @@ import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
||||
import { watchDebounced } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
|
||||
import { GITHUB_LINK, LISTEN_KEY } from '../constants'
|
||||
import { showWindow } from '../plugins/window'
|
||||
@@ -24,13 +23,9 @@ export function useTray() {
|
||||
const catStore = useCatStore()
|
||||
const { getSharedMenu } = useSharedMenu()
|
||||
|
||||
watch([() => catStore.visible, () => catStore.penetrable], () => {
|
||||
watchDebounced(() => catStore, () => {
|
||||
updateTrayMenu()
|
||||
})
|
||||
|
||||
watchDebounced([() => catStore.scale, () => catStore.opacity], () => {
|
||||
updateTrayMenu()
|
||||
}, { debounce: 200 })
|
||||
}, { deep: true, debounce: 500 })
|
||||
|
||||
const createTray = async () => {
|
||||
const tray = await getTrayById()
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
export const GITHUB_LINK = 'https://github.com/ayangweb/BongoCat'
|
||||
|
||||
export const UPGRADE_LINK_ACCESS_KEY = 'xDbrq2rOoRThDqKOHL2ZRA'
|
||||
|
||||
export const LISTEN_KEY = {
|
||||
SHOW_WINDOW: 'show-window',
|
||||
HIDE_WINDOW: 'hide-window',
|
||||
@@ -11,5 +9,4 @@ export const LISTEN_KEY = {
|
||||
|
||||
export const INVOKE_KEY = {
|
||||
COPY_DIR: 'copy_dir',
|
||||
START_DEVICE_LISTENING: 'start_device_listening',
|
||||
}
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { convertFileSrc, invoke } from '@tauri-apps/api/core'
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { Menu } from '@tauri-apps/api/menu'
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||
import { exists } from '@tauri-apps/plugin-fs'
|
||||
import { useDebounceFn, useEventListener } from '@vueuse/core'
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useDevice } from '@/composables/useDevice'
|
||||
import { useModel } from '@/composables/useModel'
|
||||
import { useSharedMenu } from '@/composables/useSharedMenu'
|
||||
import { INVOKE_KEY } from '@/constants'
|
||||
import { hideWindow, setAlwaysOnTop, showWindow } from '@/plugins/window'
|
||||
import { useCatStore } from '@/stores/cat'
|
||||
import { useModelStore } from '@/stores/model'
|
||||
@@ -17,20 +15,15 @@ import { join } from '@/utils/path'
|
||||
|
||||
const appWindow = getCurrentWebviewWindow()
|
||||
const { pressedMouses, mousePosition, pressedLeftKeys, pressedRightKeys } = useDevice()
|
||||
const { handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel()
|
||||
const { backgroundImage, handleDestroy, handleResize, handleMouseDown, handleMouseMove, handleKeyDown } = useModel()
|
||||
const catStore = useCatStore()
|
||||
const { getSharedMenu } = useSharedMenu()
|
||||
const modelStore = useModelStore()
|
||||
const resizing = ref(false)
|
||||
const backgroundImagePath = ref<string>()
|
||||
|
||||
onMounted(() => {
|
||||
invoke(INVOKE_KEY.START_DEVICE_LISTENING)
|
||||
})
|
||||
|
||||
onUnmounted(handleDestroy)
|
||||
|
||||
const debouncedResize = useDebounceFn(async () => {
|
||||
const handleDebounceResize = useDebounceFn(async () => {
|
||||
await handleResize()
|
||||
|
||||
resizing.value = false
|
||||
@@ -39,7 +32,7 @@ const debouncedResize = useDebounceFn(async () => {
|
||||
useEventListener('resize', () => {
|
||||
resizing.value = true
|
||||
|
||||
debouncedResize()
|
||||
handleDebounceResize()
|
||||
})
|
||||
|
||||
watch(pressedMouses, handleMouseDown)
|
||||
@@ -64,16 +57,6 @@ watch(() => catStore.penetrable, (value) => {
|
||||
|
||||
watch(() => catStore.alwaysOnTop, setAlwaysOnTop, { immediate: true })
|
||||
|
||||
watch(() => modelStore.currentModel, async (model) => {
|
||||
if (!model) return
|
||||
|
||||
const path = join(model.path, 'resources', 'background.png')
|
||||
|
||||
const existed = await exists(path)
|
||||
|
||||
backgroundImagePath.value = existed ? convertFileSrc(path) : void 0
|
||||
}, { deep: true, immediate: true })
|
||||
|
||||
function handleWindowDrag() {
|
||||
appWindow.startDragging()
|
||||
}
|
||||
@@ -88,7 +71,7 @@ async function handleContextmenu(event: MouseEvent) {
|
||||
menu.popup()
|
||||
}
|
||||
|
||||
function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') {
|
||||
function resolveImagePath(key: string, side: 'left' | 'right' = 'left') {
|
||||
return convertFileSrc(join(modelStore.currentModel!.path, 'resources', `${side}-keys`, `${key}.png`))
|
||||
}
|
||||
</script>
|
||||
@@ -96,28 +79,25 @@ function resolveKeyImagePath(key: string, side: 'left' | 'right' = 'left') {
|
||||
<template>
|
||||
<div
|
||||
class="relative size-screen overflow-hidden children:(absolute size-full)"
|
||||
:class="{ '-scale-x-100': catStore.mirrorMode }"
|
||||
:class="[catStore.mirrorMode ? '-scale-x-100' : 'scale-x-100']"
|
||||
:style="{ opacity: catStore.opacity / 100 }"
|
||||
@contextmenu="handleContextmenu"
|
||||
@mousedown="handleWindowDrag"
|
||||
>
|
||||
<img
|
||||
v-if="backgroundImagePath"
|
||||
:src="backgroundImagePath"
|
||||
>
|
||||
<img :src="backgroundImage">
|
||||
|
||||
<canvas id="live2dCanvas" />
|
||||
|
||||
<img
|
||||
v-for="key in pressedLeftKeys"
|
||||
:key="key"
|
||||
:src="resolveKeyImagePath(key)"
|
||||
:src="resolveImagePath(key)"
|
||||
>
|
||||
|
||||
<img
|
||||
v-for="key in pressedRightKeys"
|
||||
:key="key"
|
||||
:src="resolveKeyImagePath(key, 'right')"
|
||||
:src="resolveImagePath(key, 'right')"
|
||||
>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { InputNumber, Slider, Switch } from 'ant-design-vue'
|
||||
import { Slider, Switch } from 'ant-design-vue'
|
||||
|
||||
import ProList from '@/components/pro-list/index.vue'
|
||||
import ProListItem from '@/components/pro-list-item/index.vue'
|
||||
@@ -7,6 +7,10 @@ import { useCatStore } from '@/stores/cat'
|
||||
|
||||
const catStore = useCatStore()
|
||||
|
||||
function scaleFormatter(value?: number) {
|
||||
return value === 100 ? '默认' : `${value}%`
|
||||
}
|
||||
|
||||
function opacityFormatter(value?: number) {
|
||||
return `${value}%`
|
||||
}
|
||||
@@ -54,16 +58,15 @@ function opacityFormatter(value?: number) {
|
||||
<ProListItem
|
||||
description="将鼠标移动到窗口边缘后,也可以拖动调整窗口尺寸"
|
||||
title="窗口尺寸"
|
||||
vertical
|
||||
>
|
||||
<InputNumber
|
||||
<Slider
|
||||
v-model:value="catStore.scale"
|
||||
class="w-28"
|
||||
:min="1"
|
||||
>
|
||||
<template #addonAfter>
|
||||
%
|
||||
</template>
|
||||
</InputNumber>
|
||||
class="m-0!"
|
||||
:max="150"
|
||||
:min="50"
|
||||
:tip-formatter="scaleFormatter"
|
||||
/>
|
||||
</ProListItem>
|
||||
|
||||
<ProListItem
|
||||
|
||||
@@ -86,7 +86,7 @@ watch(selectPaths, async (paths) => {
|
||||
<template>
|
||||
<div
|
||||
ref="drop"
|
||||
class="w-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary"
|
||||
class="size-full flex flex-col cursor-pointer items-center justify-center gap-4 b b-color-1 rounded-lg b-dashed bg-color-8 transition hover:border-primary"
|
||||
:class="{ 'border-primary': dragenter }"
|
||||
@click="handleUpload"
|
||||
>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import type { Model } from '@/stores/model'
|
||||
import type { ComponentPublicInstance } from 'vue'
|
||||
import type { ColProps } from 'ant-design-vue'
|
||||
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { remove } from '@tauri-apps/plugin-fs'
|
||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
import { Card, message, Popconfirm } from 'ant-design-vue'
|
||||
import { ref } from 'vue'
|
||||
import { MasonryGrid, MasonryGridItem } from 'vue3-masonry-css'
|
||||
import { Card, Col, message, Popconfirm, Row } from 'ant-design-vue'
|
||||
|
||||
import FloatMenu from './components/float-menu/index.vue'
|
||||
import Upload from './components/upload/index.vue'
|
||||
@@ -17,20 +14,12 @@ import { useModelStore } from '@/stores/model'
|
||||
import { join } from '@/utils/path'
|
||||
|
||||
const modelStore = useModelStore()
|
||||
const firstItemRef = ref<HTMLElement>()
|
||||
|
||||
const { height } = useElementSize(firstItemRef)
|
||||
|
||||
function setFirstItemRef(el: Element | ComponentPublicInstance | null, index: number) {
|
||||
if (!el || index > 0) return
|
||||
|
||||
if ('$el' in el) {
|
||||
return firstItemRef.value = el.$el
|
||||
}
|
||||
|
||||
if (el instanceof HTMLElement) {
|
||||
firstItemRef.value = el
|
||||
}
|
||||
const colProps: ColProps = {
|
||||
xs: 12,
|
||||
md: 8,
|
||||
lg: 6,
|
||||
xl: 4,
|
||||
}
|
||||
|
||||
async function handleDelete(item: Model) {
|
||||
@@ -53,23 +42,19 @@ async function handleDelete(item: Model) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MasonryGrid
|
||||
:columns="{ 992: 3, 1200: 4, 1600: 6, default: 8 }"
|
||||
:gutter="16"
|
||||
>
|
||||
<MasonryGridItem>
|
||||
<Upload :style="{ height: `${height}px` }" />
|
||||
</MasonryGridItem>
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col v-bind="colProps">
|
||||
<Upload />
|
||||
</Col>
|
||||
|
||||
<MasonryGridItem
|
||||
v-for="(item, index) in modelStore.models"
|
||||
<Col
|
||||
v-for="item in modelStore.models"
|
||||
:key="item.id"
|
||||
v-bind="colProps"
|
||||
>
|
||||
<Card
|
||||
:ref="(el) => setFirstItemRef(el, index)"
|
||||
hoverable
|
||||
size="small"
|
||||
@click="modelStore.currentModel = item"
|
||||
>
|
||||
<template #cover>
|
||||
<img
|
||||
@@ -82,11 +67,12 @@ async function handleDelete(item: Model) {
|
||||
<i
|
||||
class="i-iconamoon:check-circle-1-bold text-4"
|
||||
:class="{ 'text-success': item.id === modelStore.currentModel?.id }"
|
||||
@click="modelStore.currentModel = item"
|
||||
/>
|
||||
|
||||
<i
|
||||
class="i-iconamoon:link-external-bold text-4"
|
||||
@click.stop="revealItemInDir(item.path)"
|
||||
@click="revealItemInDir(item.path)"
|
||||
/>
|
||||
|
||||
<template v-if="!item.isPreset">
|
||||
@@ -96,16 +82,13 @@ async function handleDelete(item: Model) {
|
||||
title="删除模型"
|
||||
@confirm="handleDelete(item)"
|
||||
>
|
||||
<i
|
||||
class="i-iconamoon:trash-simple-bold text-4"
|
||||
@click.stop
|
||||
/>
|
||||
<i class="i-iconamoon:trash-simple-bold text-4" />
|
||||
</Popconfirm>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</MasonryGridItem>
|
||||
</MasonryGrid>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<FloatMenu />
|
||||
</template>
|
||||
|
||||
@@ -61,7 +61,6 @@ const menus = [
|
||||
<div class="b b-color-2 rounded-2xl b-solid">
|
||||
<img
|
||||
class="size-15"
|
||||
data-tauri-drag-region
|
||||
src="/logo.png"
|
||||
>
|
||||
</div>
|
||||
|
||||
@@ -11,10 +11,6 @@ export const useCatStore = defineStore('cat', () => {
|
||||
const scale = ref(100)
|
||||
const opacity = ref(100)
|
||||
|
||||
const init = () => {
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
return {
|
||||
visible,
|
||||
mirrorMode,
|
||||
@@ -24,6 +20,5 @@ export const useCatStore = defineStore('cat', () => {
|
||||
alwaysOnTop,
|
||||
scale,
|
||||
opacity,
|
||||
init,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveResource } from '@tauri-apps/api/path'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
import { join } from '@/utils/path'
|
||||
|
||||
@@ -37,47 +37,33 @@ export const useModelStore = defineStore('model', () => {
|
||||
const motions = ref<MotionGroup>({})
|
||||
const expressions = ref<Expression[]>([])
|
||||
|
||||
const init = async () => {
|
||||
const presetModelsPath = await resolveResource('assets/models')
|
||||
onMounted(async () => {
|
||||
const modelsPath = await resolveResource('assets/models')
|
||||
|
||||
if (models.value.length === 0) {
|
||||
const modes: ModelMode[] = ['standard', 'keyboard']
|
||||
|
||||
for (const mode of modes) {
|
||||
for await (const mode of modes) {
|
||||
const path = join(modelsPath, mode)
|
||||
|
||||
models.value.push({
|
||||
mode,
|
||||
id: nanoid(),
|
||||
path,
|
||||
mode,
|
||||
isPreset: true,
|
||||
path: join(presetModelsPath, mode),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
models.value = models.value.map((item) => {
|
||||
const { isPreset, mode } = item
|
||||
|
||||
if (!isPreset) return item
|
||||
|
||||
return {
|
||||
...item,
|
||||
path: join(presetModelsPath, mode),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const matched = models.value.find(item => item.id === currentModel.value?.id)
|
||||
|
||||
if (matched) {
|
||||
return currentModel.value = matched
|
||||
}
|
||||
if (currentModel.value) return
|
||||
|
||||
currentModel.value = models.value[0]
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
models,
|
||||
currentModel,
|
||||
motions,
|
||||
expressions,
|
||||
init,
|
||||
}
|
||||
})
|
||||
|
||||
15
src/utils/dom.ts
Normal file
15
src/utils/dom.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function getImageSize(src: string) {
|
||||
return new Promise<{ width: number, height: number }>((resolve, reject) => {
|
||||
const img = new Image()
|
||||
|
||||
img.src = src
|
||||
|
||||
img.onload = () => {
|
||||
const { naturalWidth, naturalHeight } = img
|
||||
|
||||
resolve({ width: naturalWidth, height: naturalHeight })
|
||||
}
|
||||
|
||||
img.onerror = reject
|
||||
})
|
||||
}
|
||||
110
src/utils/live2d copy.ts
Normal file
110
src/utils/live2d copy.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import type { Cubism4InternalModel } from 'pixi-live2d-display'
|
||||
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { readDir, readTextFile } from '@tauri-apps/plugin-fs'
|
||||
import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'
|
||||
import { Application, Ticker } from 'pixi.js'
|
||||
|
||||
import { join } from './path'
|
||||
|
||||
Live2DModel.registerTicker(Ticker)
|
||||
|
||||
class Live2d {
|
||||
private app: Application | null = null
|
||||
public model: Live2DModel | null = null
|
||||
|
||||
constructor() { }
|
||||
|
||||
private mount() {
|
||||
const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
|
||||
|
||||
this.app = new Application({
|
||||
view,
|
||||
resizeTo: window,
|
||||
backgroundAlpha: 0,
|
||||
autoDensity: true,
|
||||
resolution: devicePixelRatio,
|
||||
})
|
||||
}
|
||||
|
||||
public async load(path: string) {
|
||||
if (!this.app) {
|
||||
this.mount()
|
||||
}
|
||||
|
||||
this.destroy()
|
||||
|
||||
const files = await readDir(path)
|
||||
|
||||
const modelFile = files.find(file => file.name.endsWith('.model3.json'))
|
||||
|
||||
if (!modelFile) {
|
||||
throw new Error('未找到模型主配置文件,请确认模型文件是否完整。')
|
||||
}
|
||||
|
||||
const modelPath = join(path, modelFile.name)
|
||||
|
||||
const modelJSON = JSON.parse(await readTextFile(modelPath))
|
||||
|
||||
const modelSettings = new Cubism4ModelSettings({
|
||||
...modelJSON,
|
||||
url: convertFileSrc(modelPath),
|
||||
})
|
||||
|
||||
modelSettings.replaceFiles((file) => {
|
||||
return convertFileSrc(join(path, file))
|
||||
})
|
||||
|
||||
this.model = await Live2DModel.from(modelSettings)
|
||||
|
||||
this.app?.stage.addChild(this.model)
|
||||
|
||||
const { motions, expressions } = modelSettings
|
||||
|
||||
return {
|
||||
motions,
|
||||
expressions,
|
||||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.model?.destroy()
|
||||
}
|
||||
|
||||
public playMotion(group: string, index: number) {
|
||||
return this.model?.motion(group, index)
|
||||
}
|
||||
|
||||
public playExpressions(index: number) {
|
||||
return this.model?.expression(index)
|
||||
}
|
||||
|
||||
public getCoreModel() {
|
||||
const internalModel = this.model?.internalModel as Cubism4InternalModel
|
||||
|
||||
return internalModel?.coreModel
|
||||
}
|
||||
|
||||
public getParameterRange(id: string) {
|
||||
const coreModel = this.getCoreModel()
|
||||
|
||||
const index = coreModel?.getParameterIndex(id)
|
||||
const min = coreModel?.getParameterMinimumValue(index)
|
||||
const max = coreModel?.getParameterMaximumValue(index)
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
}
|
||||
}
|
||||
|
||||
public setParameterValue(id: string, value: number) {
|
||||
const coreModel = this.getCoreModel()
|
||||
|
||||
return coreModel?.setParameterValueById?.(id, Number(value))
|
||||
}
|
||||
}
|
||||
|
||||
const live2d = new Live2d()
|
||||
|
||||
export default live2d
|
||||
@@ -1,31 +1,34 @@
|
||||
import type { Cubism4InternalModel } from 'pixi-live2d-display'
|
||||
import type { Sprite } from 'pixi.js'
|
||||
|
||||
import { convertFileSrc } from '@tauri-apps/api/core'
|
||||
import { readDir, readTextFile } from '@tauri-apps/plugin-fs'
|
||||
import { Cubism4ModelSettings, Live2DModel } from 'pixi-live2d-display'
|
||||
import { Config, CubismSetting, Live2DSprite, LogLevel } from 'easy-live2d'
|
||||
import { Application, Ticker } from 'pixi.js'
|
||||
|
||||
import { join } from './path'
|
||||
|
||||
Live2DModel.registerTicker(Ticker)
|
||||
Config.MotionGroupIdle = 'Idle'
|
||||
Config.MouseFollow = false
|
||||
Config.CubismLoggingLevel = LogLevel.LogLevel_Off
|
||||
|
||||
class Live2d {
|
||||
private app: Application | null = null
|
||||
public model: Live2DModel | null = null
|
||||
private modelWidth = 0
|
||||
private modelHeight = 0
|
||||
public model: Sprite | null = null
|
||||
|
||||
constructor() { }
|
||||
|
||||
private initApp() {
|
||||
private async initApp() {
|
||||
if (this.app) return
|
||||
|
||||
const view = document.getElementById('live2dCanvas') as HTMLCanvasElement
|
||||
|
||||
this.app = new Application({
|
||||
this.app = new Application()
|
||||
|
||||
await this.app.init({
|
||||
view,
|
||||
resizeTo: window,
|
||||
backgroundAlpha: 0,
|
||||
autoDensity: true,
|
||||
resolution: devicePixelRatio,
|
||||
})
|
||||
}
|
||||
@@ -47,81 +50,64 @@ class Live2d {
|
||||
|
||||
const modelJSON = JSON.parse(await readTextFile(modelPath))
|
||||
|
||||
const modelSettings = new Cubism4ModelSettings({
|
||||
...modelJSON,
|
||||
url: convertFileSrc(modelPath),
|
||||
const modelSetting = new CubismSetting({
|
||||
modelJSON,
|
||||
})
|
||||
|
||||
modelSettings.replaceFiles((file) => {
|
||||
modelSetting.redirectPath(({ file }) => {
|
||||
return convertFileSrc(join(path, file))
|
||||
})
|
||||
|
||||
this.model = await Live2DModel.from(modelSettings)
|
||||
|
||||
const { width, height } = this.model
|
||||
this.modelWidth = width
|
||||
this.modelHeight = height
|
||||
this.model = new Live2DSprite({
|
||||
modelSetting,
|
||||
ticker: Ticker.shared,
|
||||
})
|
||||
|
||||
this.app?.stage.addChild(this.model)
|
||||
|
||||
const { motions, expressions } = modelSettings
|
||||
// const { motions, expressions } = modelSettings
|
||||
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
motions,
|
||||
expressions,
|
||||
}
|
||||
// return {
|
||||
// motions,
|
||||
// expressions,
|
||||
// }
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.model?.destroy()
|
||||
}
|
||||
|
||||
public fitModel() {
|
||||
if (!this.model) return
|
||||
|
||||
const scaleX = innerWidth / this.modelWidth
|
||||
const scaleY = innerHeight / this.modelHeight
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
this.model.scale.set(scale)
|
||||
this.model.x = innerWidth / 2
|
||||
this.model.y = innerHeight / 2
|
||||
this.model.anchor.set(0.5)
|
||||
}
|
||||
|
||||
public playMotion(group: string, index: number) {
|
||||
return this.model?.motion(group, index)
|
||||
// return this.model?.motion(group, index)
|
||||
}
|
||||
|
||||
public playExpressions(index: number) {
|
||||
return this.model?.expression(index)
|
||||
// return this.model?.expression(index)
|
||||
}
|
||||
|
||||
public getCoreModel() {
|
||||
const internalModel = this.model?.internalModel as Cubism4InternalModel
|
||||
// const internalModel = this.model?.internalModel as Cubism4InternalModel
|
||||
|
||||
return internalModel?.coreModel
|
||||
// return internalModel?.coreModel
|
||||
}
|
||||
|
||||
public getParameterRange(id: string) {
|
||||
const coreModel = this.getCoreModel()
|
||||
// const coreModel = this.getCoreModel()
|
||||
|
||||
const index = coreModel?.getParameterIndex(id)
|
||||
const min = coreModel?.getParameterMinimumValue(index)
|
||||
const max = coreModel?.getParameterMaximumValue(index)
|
||||
// const index = coreModel?.getParameterIndex(id)
|
||||
// const min = coreModel?.getParameterMinimumValue(index)
|
||||
// const max = coreModel?.getParameterMaximumValue(index)
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
min: 0,
|
||||
max: 1,
|
||||
}
|
||||
}
|
||||
|
||||
public setParameterValue(id: string, value: number) {
|
||||
const coreModel = this.getCoreModel()
|
||||
// const coreModel = this.getCoreModel()
|
||||
|
||||
return coreModel?.setParameterValueById?.(id, Number(value))
|
||||
// return coreModel?.setParameterValueById?.(id, Number(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user