Compare commits
15 Commits
4093b0eb0c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 22bbcaa2df | |||
| c7a5b50223 | |||
| 8a8e77125a | |||
| a945526808 | |||
| 743af7adb6 | |||
| cba5cf1980 | |||
| 5e1009616c | |||
| d55d7e90f0 | |||
| 5d2f2241f3 | |||
| cb0b366e96 | |||
| 53248243e3 | |||
| e7f9633fcc | |||
| 96d1ead1a9 | |||
| 4aa2b00d17 | |||
| 75ab799a7f |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "friendolls-desktop",
|
"name": "friendolls-desktop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"description": "",
|
"description": "",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
API_BASE_URL=http://127.0.0.1:3000
|
|
||||||
19
src-tauri/Cargo.lock
generated
@@ -1240,7 +1240,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "friendolls-desktop"
|
name = "friendolls-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"device_query",
|
"device_query",
|
||||||
@@ -1269,6 +1269,7 @@ dependencies = [
|
|||||||
"strum",
|
"strum",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
|
"tauri-macros",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
@@ -1948,9 +1949,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ico"
|
name = "ico"
|
||||||
version = "0.4.0"
|
version = "0.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98"
|
checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"byteorder",
|
"byteorder",
|
||||||
"png",
|
"png",
|
||||||
@@ -4734,9 +4735,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-codegen"
|
name = "tauri-codegen"
|
||||||
version = "2.5.1"
|
version = "2.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7ef707148f0755110ca54377560ab891d722de4d53297595380a748026f139f"
|
checksum = "d4a24476afd977c5d5d169f72425868613d82747916dd29e0a357c84c4bd6d29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"brotli",
|
"brotli",
|
||||||
@@ -4761,9 +4762,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-macros"
|
name = "tauri-macros"
|
||||||
version = "2.5.1"
|
version = "2.5.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "71664fd715ee6e382c05345ad258d6d1d50f90cf1b58c0aa726638b33c2a075d"
|
checksum = "d39b349a98dadaffebb73f0a40dcd1f23c999211e5a2e744403db384d0c33de7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -5006,9 +5007,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-utils"
|
name = "tauri-utils"
|
||||||
version = "2.8.0"
|
version = "2.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6b8bbe426abdbf52d050e52ed693130dbd68375b9ad82a3fb17efb4c8d85673"
|
checksum = "219a1f983a2af3653f75b5747f76733b0da7ff03069c7a41901a5eb3ace4557d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"brotli",
|
"brotli",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "friendolls-desktop"
|
name = "friendolls-desktop"
|
||||||
version = "0.1.0"
|
version = "0.1.4"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -50,6 +50,7 @@ enigo = { version = "0.6.1", features = ["wayland"] }
|
|||||||
lazy_static = "1.5.0"
|
lazy_static = "1.5.0"
|
||||||
mlua = { version = "0.11", default-features = false, features = ["lua54", "vendored", "serde", "async"] }
|
mlua = { version = "0.11", default-features = false, features = ["lua54", "vendored", "serde", "async"] }
|
||||||
petpet = "2.4.3"
|
petpet = "2.4.3"
|
||||||
|
tauri-macros = "2.5.5"
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-global-shortcut = "2"
|
tauri-plugin-global-shortcut = "2"
|
||||||
tauri-plugin-positioner = "2"
|
tauri-plugin-positioner = "2"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 2.4 KiB |
BIN
src-tauri/icons/64x64.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 903 B After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 4.7 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
|
</adaptive-icon>
|
||||||
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 4.0 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.7 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png
Normal file
|
After Width: | Height: | Size: 34 KiB |
BIN
src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 35 KiB |
BIN
src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#fff</color>
|
||||||
|
</resources>
|
||||||
|
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 10 KiB |
BIN
src-tauri/icons/icon.tray.png
Normal file
|
After Width: | Height: | Size: 287 B |
BIN
src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
|
After Width: | Height: | Size: 6.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
|
After Width: | Height: | Size: 5.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
BIN
src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
|
After Width: | Height: | Size: 116 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
@@ -1,9 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
lock_r,
|
lock_r,
|
||||||
models::app_data::UserData,
|
models::{app_data::UserData, app_state::{AppState, NekoPosition}},
|
||||||
services::{
|
services::{
|
||||||
app_data::{init_app_data_scoped, AppDataRefreshScope},
|
app_data::{init_app_data_scoped, AppDataRefreshScope},
|
||||||
|
app_state,
|
||||||
friends,
|
friends,
|
||||||
|
neko_positions,
|
||||||
presence_modules::models::ModuleMetadata,
|
presence_modules::models::ModuleMetadata,
|
||||||
sprite,
|
sprite,
|
||||||
},
|
},
|
||||||
@@ -45,3 +47,33 @@ pub fn get_friend_active_doll_sprites_base64() -> Result<friends::FriendActiveDo
|
|||||||
friends::sync_active_doll_sprites_from_app_data();
|
friends::sync_active_doll_sprites_from_app_data();
|
||||||
Ok(friends::get_active_doll_sprites_snapshot())
|
Ok(friends::get_active_doll_sprites_snapshot())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_app_state() -> Result<AppState, String> {
|
||||||
|
Ok(app_state::get_snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn get_neko_positions() -> Result<neko_positions::NekoPositionsDto, String> {
|
||||||
|
Ok(neko_positions::get_snapshot())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
||||||
|
app_state::set_scene_setup_nekos_position(nekos_position);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
||||||
|
app_state::set_scene_setup_nekos_opacity(nekos_opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
#[specta::specta]
|
||||||
|
pub fn set_scene_setup_nekos_scale(nekos_scale: f32) {
|
||||||
|
app_state::set_scene_setup_nekos_scale(nekos_scale);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
use crate::{
|
use crate::services::{
|
||||||
commands::app_state::get_modules,
|
|
||||||
services::{
|
|
||||||
doll_editor::open_doll_editor_window,
|
doll_editor::open_doll_editor_window,
|
||||||
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use commands::app::{quit_app, restart_app, retry_connection};
|
use commands::app::{quit_app, restart_app, retry_connection};
|
||||||
use commands::app_state::{
|
use commands::app_state::{
|
||||||
get_active_doll_sprite_base64, get_app_data, get_friend_active_doll_sprites_base64,
|
get_active_doll_sprite_base64, get_app_data, get_app_state, get_neko_positions,
|
||||||
refresh_app_data,
|
get_friend_active_doll_sprites_base64, get_modules, refresh_app_data,
|
||||||
|
set_scene_setup_nekos_opacity, set_scene_setup_nekos_position, set_scene_setup_nekos_scale,
|
||||||
};
|
};
|
||||||
use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth};
|
use commands::auth::{logout_and_restart, start_discord_auth, start_google_auth};
|
||||||
use commands::config::{get_client_config, open_client_config, save_client_config};
|
use commands::config::{get_client_config, open_client_config, save_client_config};
|
||||||
@@ -27,10 +25,10 @@ use tauri::async_runtime;
|
|||||||
use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode};
|
use tauri_specta::{collect_commands, collect_events, Builder as SpectaBuilder, ErrorHandlingMode};
|
||||||
|
|
||||||
use crate::services::app_events::{
|
use crate::services::app_events::{
|
||||||
ActiveDollSpriteChanged, AppDataRefreshed, AuthFlowUpdated, CreateDoll, CursorMoved, EditDoll,
|
ActiveDollSpriteChanged, AppDataRefreshed, AppStateChanged, AuthFlowUpdated, CreateDoll,
|
||||||
FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendCursorPositionsUpdated,
|
EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated, FriendDisconnected,
|
||||||
FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived,
|
FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
||||||
FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived,
|
InteractionDeliveryFailed, InteractionReceived, NekoPositionsUpdated,
|
||||||
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -68,6 +66,8 @@ pub fn run() {
|
|||||||
.error_handling(ErrorHandlingMode::Throw)
|
.error_handling(ErrorHandlingMode::Throw)
|
||||||
.commands(collect_commands![
|
.commands(collect_commands![
|
||||||
get_app_data,
|
get_app_data,
|
||||||
|
get_app_state,
|
||||||
|
get_neko_positions,
|
||||||
get_active_doll_sprite_base64,
|
get_active_doll_sprite_base64,
|
||||||
get_friend_active_doll_sprites_base64,
|
get_friend_active_doll_sprites_base64,
|
||||||
refresh_app_data,
|
refresh_app_data,
|
||||||
@@ -102,18 +102,21 @@ pub fn run() {
|
|||||||
start_discord_auth,
|
start_discord_auth,
|
||||||
logout_and_restart,
|
logout_and_restart,
|
||||||
send_interaction_cmd,
|
send_interaction_cmd,
|
||||||
get_modules
|
get_modules,
|
||||||
|
set_scene_setup_nekos_position,
|
||||||
|
set_scene_setup_nekos_opacity,
|
||||||
|
set_scene_setup_nekos_scale
|
||||||
])
|
])
|
||||||
.events(collect_events![
|
.events(collect_events![
|
||||||
CursorMoved,
|
|
||||||
SceneInteractiveChanged,
|
SceneInteractiveChanged,
|
||||||
AppDataRefreshed,
|
AppDataRefreshed,
|
||||||
|
AppStateChanged,
|
||||||
|
NekoPositionsUpdated,
|
||||||
ActiveDollSpriteChanged,
|
ActiveDollSpriteChanged,
|
||||||
SetInteractionOverlay,
|
SetInteractionOverlay,
|
||||||
EditDoll,
|
EditDoll,
|
||||||
CreateDoll,
|
CreateDoll,
|
||||||
UserStatusChanged,
|
UserStatusChanged,
|
||||||
FriendCursorPositionsUpdated,
|
|
||||||
FriendDisconnected,
|
FriendDisconnected,
|
||||||
FriendActiveDollChanged,
|
FriendActiveDollChanged,
|
||||||
FriendActiveDollSpritesUpdated,
|
FriendActiveDollSpritesUpdated,
|
||||||
|
|||||||
39
src-tauri/src/models/app_state.rs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(rename_all = "kebab-case")]
|
||||||
|
pub enum NekoPosition {
|
||||||
|
TopLeft,
|
||||||
|
Top,
|
||||||
|
TopRight,
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
BottomLeft,
|
||||||
|
Bottom,
|
||||||
|
BottomRight,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SceneSetup {
|
||||||
|
pub nekos_position: Option<NekoPosition>,
|
||||||
|
pub nekos_opacity: f32,
|
||||||
|
pub nekos_scale: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SceneSetup {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
nekos_position: None,
|
||||||
|
nekos_opacity: 1.0,
|
||||||
|
nekos_scale: 1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Type, Default)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AppState {
|
||||||
|
pub scene_setup: SceneSetup,
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod app_data;
|
pub mod app_data;
|
||||||
|
pub mod app_state;
|
||||||
pub mod dolls;
|
pub mod dolls;
|
||||||
pub mod event_payloads;
|
pub mod event_payloads;
|
||||||
pub mod friends;
|
pub mod friends;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
remotes::{dolls::DollsRemote, friends::FriendRemote, user::UserRemote},
|
||||||
services::{
|
services::{
|
||||||
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
|
app_events::{ActiveDollSpriteChanged, AppDataRefreshed},
|
||||||
friends, sprite,
|
friends, neko_positions, sprite,
|
||||||
},
|
},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
};
|
};
|
||||||
@@ -51,6 +51,8 @@ pub async fn init_app_data_scoped(scope: AppDataRefreshScope) {
|
|||||||
Ok(user) => {
|
Ok(user) => {
|
||||||
let mut guard = lock_w!(FDOLL);
|
let mut guard = lock_w!(FDOLL);
|
||||||
guard.user_data.user = Some(user);
|
guard.user_data.user = Some(user);
|
||||||
|
drop(guard);
|
||||||
|
neko_positions::sync_from_app_data();
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
warn!("Failed to fetch user profile: {}", error);
|
warn!("Failed to fetch user profile: {}", error);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use tauri_specta::Event;
|
|||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
app_data::UserData,
|
app_data::UserData,
|
||||||
|
app_state::AppState,
|
||||||
event_payloads::{
|
event_payloads::{
|
||||||
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
FriendActiveDollChangedPayload, FriendDisconnectedPayload,
|
||||||
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
FriendRequestAcceptedPayload, FriendRequestDeniedPayload, FriendRequestReceivedPayload,
|
||||||
@@ -12,10 +13,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
interaction::{InteractionDeliveryFailedDto, InteractionPayloadDto},
|
||||||
},
|
},
|
||||||
services::{
|
services::{friends::FriendActiveDollSpritesDto, neko_positions::NekoPositionsDto},
|
||||||
cursor::CursorPositions,
|
|
||||||
friends::{FriendActiveDollSpritesDto, FriendCursorPositionsDto},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
@@ -35,10 +33,6 @@ pub struct AuthFlowUpdatedPayload {
|
|||||||
pub message: Option<String>,
|
pub message: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
|
||||||
#[tauri_specta(event_name = "cursor-position")]
|
|
||||||
pub struct CursorMoved(pub CursorPositions);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "scene-interactive")]
|
#[tauri_specta(event_name = "scene-interactive")]
|
||||||
pub struct SceneInteractiveChanged(pub bool);
|
pub struct SceneInteractiveChanged(pub bool);
|
||||||
@@ -47,6 +41,10 @@ pub struct SceneInteractiveChanged(pub bool);
|
|||||||
#[tauri_specta(event_name = "app-data-refreshed")]
|
#[tauri_specta(event_name = "app-data-refreshed")]
|
||||||
pub struct AppDataRefreshed(pub UserData);
|
pub struct AppDataRefreshed(pub UserData);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
|
#[tauri_specta(event_name = "app-state-changed")]
|
||||||
|
pub struct AppStateChanged(pub AppState);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "active-doll-sprite-changed")]
|
#[tauri_specta(event_name = "active-doll-sprite-changed")]
|
||||||
pub struct ActiveDollSpriteChanged(pub Option<String>);
|
pub struct ActiveDollSpriteChanged(pub Option<String>);
|
||||||
@@ -68,8 +66,8 @@ pub struct CreateDoll;
|
|||||||
pub struct UserStatusChanged(pub UserStatusPayload);
|
pub struct UserStatusChanged(pub UserStatusPayload);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-cursor-positions")]
|
#[tauri_specta(event_name = "neko-positions")]
|
||||||
pub struct FriendCursorPositionsUpdated(pub FriendCursorPositionsDto);
|
pub struct NekoPositionsUpdated(pub NekoPositionsDto);
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type, Event)]
|
||||||
#[tauri_specta(event_name = "friend-disconnected")]
|
#[tauri_specta(event_name = "friend-disconnected")]
|
||||||
|
|||||||
44
src-tauri/src/services/app_state.rs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
use std::sync::{Arc, LazyLock, RwLock};
|
||||||
|
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
get_app_handle, lock_r, lock_w,
|
||||||
|
models::app_state::{AppState, NekoPosition},
|
||||||
|
services::{app_events::AppStateChanged, neko_positions},
|
||||||
|
};
|
||||||
|
|
||||||
|
static APP_STATE: LazyLock<Arc<RwLock<AppState>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
||||||
|
|
||||||
|
pub fn get_snapshot() -> AppState {
|
||||||
|
let guard = lock_r!(APP_STATE);
|
||||||
|
guard.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
||||||
|
let mut guard = lock_w!(APP_STATE);
|
||||||
|
guard.scene_setup.nekos_position = nekos_position;
|
||||||
|
emit_snapshot(&guard);
|
||||||
|
drop(guard);
|
||||||
|
neko_positions::refresh_from_scene_setup();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
||||||
|
let mut guard = lock_w!(APP_STATE);
|
||||||
|
guard.scene_setup.nekos_opacity = nekos_opacity.clamp(0.1, 1.0);
|
||||||
|
emit_snapshot(&guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scene_setup_nekos_scale(nekos_scale: f32) {
|
||||||
|
let mut guard = lock_w!(APP_STATE);
|
||||||
|
guard.scene_setup.nekos_scale = nekos_scale.clamp(0.5, 2.0);
|
||||||
|
emit_snapshot(&guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_snapshot(app_state: &AppState) {
|
||||||
|
if let Err(error) = AppStateChanged(app_state.clone()).emit(get_app_handle()) {
|
||||||
|
warn!("Failed to emit app-state-changed event: {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,8 +7,11 @@ use std::time::Duration;
|
|||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
use crate::{get_app_handle, lock_r, services::app_events::CursorMoved, state::FDOLL};
|
use crate::{
|
||||||
use tauri_specta::Event as _;
|
lock_r,
|
||||||
|
services::{neko_positions, ws::report_cursor_data},
|
||||||
|
state::FDOLL,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Type)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -59,8 +62,7 @@ pub fn normalized_to_absolute(normalized: &CursorPosition) -> CursorPosition {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize cursor tracking. Broadcasts cursor
|
/// Initialize cursor tracking.
|
||||||
/// position changes via `cursor-position` event.
|
|
||||||
pub async fn init_cursor_tracking() {
|
pub async fn init_cursor_tracking() {
|
||||||
info!("start_cursor_tracking called");
|
info!("start_cursor_tracking called");
|
||||||
|
|
||||||
@@ -88,22 +90,19 @@ async fn init_cursor_tracking_i() -> Result<(), String> {
|
|||||||
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
let (tx, mut rx) = mpsc::channel::<CursorPositions>(100);
|
||||||
|
|
||||||
// Spawn the consumer task
|
// Spawn the consumer task
|
||||||
// This task handles WebSocket reporting and local broadcasting.
|
// This task handles WebSocket reporting and local position projection updates.
|
||||||
// It runs independently of the device event loop.
|
// It runs independently of the device event loop.
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
info!("Cursor event consumer started");
|
info!("Cursor event consumer started");
|
||||||
let app_handle = get_app_handle();
|
|
||||||
|
|
||||||
while let Some(positions) = rx.recv().await {
|
while let Some(positions) = rx.recv().await {
|
||||||
let mapped_for_ws = positions.mapped.clone();
|
let mapped_for_ws = positions.mapped.clone();
|
||||||
|
|
||||||
// 1. WebSocket reporting
|
// 1. WebSocket reporting
|
||||||
crate::services::ws::report_cursor_data(mapped_for_ws).await;
|
report_cursor_data(mapped_for_ws).await;
|
||||||
|
|
||||||
// 2. Broadcast to local windows
|
// 2. Update unified neko positions projection
|
||||||
if let Err(e) = CursorMoved(positions).emit(app_handle) {
|
neko_positions::update_self_cursor(positions);
|
||||||
error!("Failed to emit cursor position event: {:?}", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
warn!("Cursor event consumer stopped (channel closed)");
|
warn!("Cursor event consumer stopped (channel closed)");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use specta::Type;
|
|||||||
use tauri_specta::Event as _;
|
use tauri_specta::Event as _;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
get_app_handle, lock_r,
|
get_app_handle, lock_r, lock_w,
|
||||||
models::{dolls::DollDto, friends::FriendshipResponseDto},
|
models::{dolls::DollDto, friends::FriendshipResponseDto},
|
||||||
services::{app_events::FriendActiveDollSpritesUpdated, sprite},
|
services::{app_events::FriendActiveDollSpritesUpdated, sprite},
|
||||||
state::FDOLL,
|
state::FDOLL,
|
||||||
@@ -29,27 +29,21 @@ pub fn sync_from_app_data() {
|
|||||||
|
|
||||||
let next = build_sprites(&friends);
|
let next = build_sprites(&friends);
|
||||||
|
|
||||||
let mut projection = FRIEND_ACTIVE_DOLL_SPRITES
|
let mut projection = lock_w!(FRIEND_ACTIVE_DOLL_SPRITES);
|
||||||
.write()
|
|
||||||
.expect("friend active doll sprite projection lock poisoned");
|
|
||||||
*projection = next;
|
*projection = next;
|
||||||
|
|
||||||
emit_snapshot(&projection);
|
emit_snapshot(&projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear() {
|
pub fn clear() {
|
||||||
let mut projection = FRIEND_ACTIVE_DOLL_SPRITES
|
let mut projection = lock_w!(FRIEND_ACTIVE_DOLL_SPRITES);
|
||||||
.write()
|
|
||||||
.expect("friend active doll sprite projection lock poisoned");
|
|
||||||
projection.clear();
|
projection.clear();
|
||||||
|
|
||||||
emit_snapshot(&projection);
|
emit_snapshot(&projection);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_friend(user_id: &str) {
|
pub fn remove_friend(user_id: &str) {
|
||||||
let mut projection = FRIEND_ACTIVE_DOLL_SPRITES
|
let mut projection = lock_w!(FRIEND_ACTIVE_DOLL_SPRITES);
|
||||||
.write()
|
|
||||||
.expect("friend active doll sprite projection lock poisoned");
|
|
||||||
|
|
||||||
if projection.remove(user_id).is_some() {
|
if projection.remove(user_id).is_some() {
|
||||||
emit_snapshot(&projection);
|
emit_snapshot(&projection);
|
||||||
@@ -57,9 +51,7 @@ pub fn remove_friend(user_id: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
||||||
let mut projection = FRIEND_ACTIVE_DOLL_SPRITES
|
let mut projection = lock_w!(FRIEND_ACTIVE_DOLL_SPRITES);
|
||||||
.write()
|
|
||||||
.expect("friend active doll sprite projection lock poisoned");
|
|
||||||
|
|
||||||
match doll {
|
match doll {
|
||||||
Some(doll) => match sprite::encode_doll_sprite_base64(doll) {
|
Some(doll) => match sprite::encode_doll_sprite_base64(doll) {
|
||||||
@@ -88,9 +80,7 @@ pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_snapshot() -> FriendActiveDollSpritesDto {
|
pub fn get_snapshot() -> FriendActiveDollSpritesDto {
|
||||||
let projection = FRIEND_ACTIVE_DOLL_SPRITES
|
let projection = lock_r!(FRIEND_ACTIVE_DOLL_SPRITES);
|
||||||
.read()
|
|
||||||
.expect("friend active doll sprite projection lock poisoned");
|
|
||||||
|
|
||||||
FriendActiveDollSpritesDto(projection.clone())
|
FriendActiveDollSpritesDto(projection.clone())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
sync::{Arc, LazyLock, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use specta::Type;
|
|
||||||
use tauri_specta::Event as _;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
get_app_handle, lock_r,
|
|
||||||
services::{app_events::FriendCursorPositionsUpdated, cursor::CursorPositions},
|
|
||||||
state::FDOLL,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct FriendCursorPositionsDto(pub HashMap<String, CursorPositions>);
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
struct FriendCursorProjection {
|
|
||||||
active_dolls: HashMap<String, bool>,
|
|
||||||
positions: HashMap<String, CursorPositions>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static FRIEND_CURSOR_PROJECTION: LazyLock<Arc<RwLock<FriendCursorProjection>>> =
|
|
||||||
LazyLock::new(|| Arc::new(RwLock::new(FriendCursorProjection::default())));
|
|
||||||
|
|
||||||
pub fn sync_from_app_data() {
|
|
||||||
let friends = {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
guard.user_data.friends.clone().unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
projection.active_dolls = friends
|
|
||||||
.into_iter()
|
|
||||||
.filter_map(|friendship| {
|
|
||||||
friendship.friend.map(|friend| {
|
|
||||||
let has_active_doll = friend.active_doll.is_some();
|
|
||||||
(friend.id, has_active_doll)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let active_dolls = projection.active_dolls.clone();
|
|
||||||
projection
|
|
||||||
.positions
|
|
||||||
.retain(|user_id, _| active_dolls.get(user_id) == Some(&true));
|
|
||||||
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear() {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
projection.active_dolls.clear();
|
|
||||||
projection.positions.clear();
|
|
||||||
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_position(user_id: String, position: CursorPositions) {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
if !has_active_doll(&mut projection, &user_id) {
|
|
||||||
if projection.positions.remove(&user_id).is_some() {
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
projection.positions.insert(user_id, position);
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn remove_friend(user_id: &str) {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
let removed_active_doll = projection.active_dolls.remove(user_id).is_some();
|
|
||||||
let removed_position = projection.positions.remove(user_id).is_some();
|
|
||||||
|
|
||||||
if removed_active_doll || removed_position {
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_active_doll(user_id: &str, has_active_doll: bool) {
|
|
||||||
let mut projection = FRIEND_CURSOR_PROJECTION
|
|
||||||
.write()
|
|
||||||
.expect("friend cursor projection lock poisoned");
|
|
||||||
|
|
||||||
projection
|
|
||||||
.active_dolls
|
|
||||||
.insert(user_id.to_string(), has_active_doll);
|
|
||||||
|
|
||||||
if !has_active_doll && projection.positions.remove(user_id).is_some() {
|
|
||||||
emit_snapshot(&projection.positions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_active_doll(projection: &mut FriendCursorProjection, user_id: &str) -> bool {
|
|
||||||
if let Some(has_active_doll) = projection.active_dolls.get(user_id) {
|
|
||||||
return *has_active_doll;
|
|
||||||
}
|
|
||||||
|
|
||||||
let has_active_doll = {
|
|
||||||
let guard = lock_r!(FDOLL);
|
|
||||||
guard
|
|
||||||
.user_data
|
|
||||||
.friends
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|friends| {
|
|
||||||
friends.iter().find_map(|friendship| {
|
|
||||||
let friend = friendship.friend.as_ref()?;
|
|
||||||
(friend.id == user_id).then_some(friend)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.and_then(|friend| friend.active_doll.as_ref())
|
|
||||||
.is_some()
|
|
||||||
};
|
|
||||||
|
|
||||||
projection
|
|
||||||
.active_dolls
|
|
||||||
.insert(user_id.to_string(), has_active_doll);
|
|
||||||
|
|
||||||
has_active_doll
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_snapshot(positions: &HashMap<String, CursorPositions>) {
|
|
||||||
let payload = FriendCursorPositionsDto(positions.clone());
|
|
||||||
|
|
||||||
if let Err(err) = FriendCursorPositionsUpdated(payload).emit(get_app_handle()) {
|
|
||||||
tracing::warn!("Failed to emit friend cursor positions update: {}", err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +1,27 @@
|
|||||||
mod active_doll_sprites;
|
mod active_doll_sprites;
|
||||||
mod cursor_positions;
|
|
||||||
|
|
||||||
use crate::{models::dolls::DollDto, services::cursor::CursorPositions};
|
use crate::{models::dolls::DollDto, services::neko_positions};
|
||||||
|
|
||||||
pub use active_doll_sprites::FriendActiveDollSpritesDto;
|
pub use active_doll_sprites::FriendActiveDollSpritesDto;
|
||||||
pub use cursor_positions::FriendCursorPositionsDto;
|
|
||||||
|
|
||||||
pub fn sync_from_app_data() {
|
pub fn sync_from_app_data() {
|
||||||
active_doll_sprites::sync_from_app_data();
|
active_doll_sprites::sync_from_app_data();
|
||||||
cursor_positions::sync_from_app_data();
|
neko_positions::sync_from_app_data();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear() {
|
pub fn clear() {
|
||||||
active_doll_sprites::clear();
|
active_doll_sprites::clear();
|
||||||
cursor_positions::clear();
|
neko_positions::clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_friend(user_id: &str) {
|
pub fn remove_friend(user_id: &str) {
|
||||||
active_doll_sprites::remove_friend(user_id);
|
active_doll_sprites::remove_friend(user_id);
|
||||||
cursor_positions::remove_friend(user_id);
|
neko_positions::remove_friend(user_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
pub fn set_active_doll(user_id: &str, doll: Option<&DollDto>) {
|
||||||
active_doll_sprites::set_active_doll(user_id, doll);
|
active_doll_sprites::set_active_doll(user_id, doll);
|
||||||
cursor_positions::set_active_doll(user_id, doll.is_some());
|
neko_positions::set_friend_active_doll(user_id, doll.is_some());
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update_cursor_position(user_id: String, position: CursorPositions) {
|
|
||||||
cursor_positions::update_position(user_id, position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sync_active_doll_sprites_from_app_data() {
|
pub fn sync_active_doll_sprites_from_app_data() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
pub mod app_data;
|
pub mod app_data;
|
||||||
pub mod app_events;
|
pub mod app_events;
|
||||||
|
pub mod app_state;
|
||||||
pub mod app_menu;
|
pub mod app_menu;
|
||||||
pub mod app_update;
|
pub mod app_update;
|
||||||
pub mod accelerators;
|
pub mod accelerators;
|
||||||
@@ -11,6 +12,7 @@ pub mod friends;
|
|||||||
pub mod health_manager;
|
pub mod health_manager;
|
||||||
pub mod health_monitor;
|
pub mod health_monitor;
|
||||||
pub mod interaction;
|
pub mod interaction;
|
||||||
|
pub mod neko_positions;
|
||||||
pub mod petpet;
|
pub mod petpet;
|
||||||
pub mod presence_modules;
|
pub mod presence_modules;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
|
|||||||
291
src-tauri/src/services/neko_positions.rs
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, LazyLock, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use specta::Type;
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
get_app_handle, lock_r, lock_w,
|
||||||
|
models::app_state::NekoPosition,
|
||||||
|
services::{
|
||||||
|
app_events::NekoPositionsUpdated,
|
||||||
|
app_state,
|
||||||
|
cursor::{CursorPosition, CursorPositions},
|
||||||
|
},
|
||||||
|
state::FDOLL,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NekoPositionDto {
|
||||||
|
pub user_id: String,
|
||||||
|
pub is_self: bool,
|
||||||
|
pub cursor: CursorPositions,
|
||||||
|
pub target: CursorPosition,
|
||||||
|
pub override_applied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, Type)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct NekoPositionsDto(pub HashMap<String, NekoPositionDto>);
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct NekoPositionsProjection {
|
||||||
|
self_cursor: Option<CursorPositions>,
|
||||||
|
friend_cursors: HashMap<String, CursorPositions>,
|
||||||
|
friend_active_dolls: HashMap<String, bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static NEKO_POSITIONS: LazyLock<Arc<RwLock<NekoPositionsProjection>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(NekoPositionsProjection::default())));
|
||||||
|
|
||||||
|
pub fn sync_from_app_data() {
|
||||||
|
let friends = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard.user_data.friends.clone().unwrap_or_default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut projection = lock_w!(NEKO_POSITIONS);
|
||||||
|
|
||||||
|
projection.friend_active_dolls = friends
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|friendship| {
|
||||||
|
friendship.friend.map(|friend| {
|
||||||
|
let has_active_doll = friend.active_doll.is_some();
|
||||||
|
(friend.id, has_active_doll)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let active_dolls = projection.friend_active_dolls.clone();
|
||||||
|
projection
|
||||||
|
.friend_cursors
|
||||||
|
.retain(|user_id, _| active_dolls.get(user_id) == Some(&true));
|
||||||
|
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear() {
|
||||||
|
let mut projection = lock_w!(NEKO_POSITIONS);
|
||||||
|
|
||||||
|
projection.self_cursor = None;
|
||||||
|
projection.friend_cursors.clear();
|
||||||
|
projection.friend_active_dolls.clear();
|
||||||
|
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_self_cursor(position: CursorPositions) {
|
||||||
|
let mut projection = lock_w!(NEKO_POSITIONS);
|
||||||
|
|
||||||
|
projection.self_cursor = Some(position);
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_friend_cursor(user_id: String, position: CursorPositions) {
|
||||||
|
let mut projection = lock_w!(NEKO_POSITIONS);
|
||||||
|
|
||||||
|
if !has_friend_active_doll(&mut projection, &user_id) {
|
||||||
|
if projection.friend_cursors.remove(&user_id).is_some() {
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projection.friend_cursors.insert(user_id, position);
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_friend(user_id: &str) {
|
||||||
|
let mut projection = lock_w!(NEKO_POSITIONS);
|
||||||
|
|
||||||
|
let removed_active_doll = projection.friend_active_dolls.remove(user_id).is_some();
|
||||||
|
let removed_position = projection.friend_cursors.remove(user_id).is_some();
|
||||||
|
|
||||||
|
if removed_active_doll || removed_position {
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_friend_active_doll(user_id: &str, has_active_doll: bool) {
|
||||||
|
let mut projection = lock_w!(NEKO_POSITIONS);
|
||||||
|
|
||||||
|
projection
|
||||||
|
.friend_active_dolls
|
||||||
|
.insert(user_id.to_string(), has_active_doll);
|
||||||
|
|
||||||
|
if !has_active_doll && projection.friend_cursors.remove(user_id).is_some() {
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh_from_scene_setup() {
|
||||||
|
let projection = lock_r!(NEKO_POSITIONS);
|
||||||
|
emit_snapshot(&projection);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_snapshot() -> NekoPositionsDto {
|
||||||
|
let projection = lock_r!(NEKO_POSITIONS);
|
||||||
|
build_snapshot(&projection)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_friend_active_doll(projection: &mut NekoPositionsProjection, user_id: &str) -> bool {
|
||||||
|
if let Some(has_active_doll) = projection.friend_active_dolls.get(user_id) {
|
||||||
|
return *has_active_doll;
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_active_doll = {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard
|
||||||
|
.user_data
|
||||||
|
.friends
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|friends| {
|
||||||
|
friends.iter().find_map(|friendship| {
|
||||||
|
let friend = friendship.friend.as_ref()?;
|
||||||
|
(friend.id == user_id).then_some(friend)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.and_then(|friend| friend.active_doll.as_ref())
|
||||||
|
.is_some()
|
||||||
|
};
|
||||||
|
|
||||||
|
projection
|
||||||
|
.friend_active_dolls
|
||||||
|
.insert(user_id.to_string(), has_active_doll);
|
||||||
|
|
||||||
|
has_active_doll
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_self_active_doll() -> bool {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard
|
||||||
|
.user_data
|
||||||
|
.user
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|user| user.active_doll_id.as_ref())
|
||||||
|
.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_self_user_id() -> Option<String> {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
guard.user_data.user.as_ref().map(|user| user.id.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_display_size() -> (f64, f64) {
|
||||||
|
let guard = lock_r!(FDOLL);
|
||||||
|
(
|
||||||
|
guard.user_data.scene.display.screen_width as f64,
|
||||||
|
guard.user_data.scene.display.screen_height as f64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_snapshot(projection: &NekoPositionsProjection) {
|
||||||
|
let payload = build_snapshot(projection);
|
||||||
|
|
||||||
|
if let Err(err) = NekoPositionsUpdated(payload).emit(get_app_handle()) {
|
||||||
|
tracing::warn!("Failed to emit neko positions update: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_snapshot(projection: &NekoPositionsProjection) -> NekoPositionsDto {
|
||||||
|
let mut entries: Vec<(String, bool, CursorPositions)> = Vec::new();
|
||||||
|
|
||||||
|
if has_self_active_doll() {
|
||||||
|
if let (Some(self_user_id), Some(self_cursor)) =
|
||||||
|
(get_self_user_id(), projection.self_cursor.clone())
|
||||||
|
{
|
||||||
|
entries.push((self_user_id, true, self_cursor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (user_id, cursor) in &projection.friend_cursors {
|
||||||
|
if projection.friend_active_dolls.get(user_id) == Some(&true) {
|
||||||
|
entries.push((user_id.clone(), false, cursor.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
let app_state = app_state::get_snapshot();
|
||||||
|
let override_anchor = app_state.scene_setup.nekos_position;
|
||||||
|
let (screen_width, screen_height) = get_display_size();
|
||||||
|
|
||||||
|
let total = entries.len();
|
||||||
|
|
||||||
|
NekoPositionsDto(
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, (user_id, is_self, cursor))| {
|
||||||
|
let (target, override_applied) = match &override_anchor {
|
||||||
|
Some(anchor) => (
|
||||||
|
get_cluster_target(
|
||||||
|
anchor.clone(),
|
||||||
|
index,
|
||||||
|
total,
|
||||||
|
screen_width,
|
||||||
|
screen_height,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
None => (cursor.raw.clone(), false),
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
user_id.clone(),
|
||||||
|
NekoPositionDto {
|
||||||
|
user_id,
|
||||||
|
is_self,
|
||||||
|
cursor,
|
||||||
|
target,
|
||||||
|
override_applied,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_cluster_target(
|
||||||
|
anchor: NekoPosition,
|
||||||
|
index: usize,
|
||||||
|
count: usize,
|
||||||
|
screen_width: f64,
|
||||||
|
screen_height: f64,
|
||||||
|
) -> CursorPosition {
|
||||||
|
let spacing = 36.0;
|
||||||
|
let margin = 28.0;
|
||||||
|
|
||||||
|
let columns = (count as f64).sqrt().ceil().max(1.0) as usize;
|
||||||
|
let rows = count.div_ceil(columns).max(1);
|
||||||
|
let col = index % columns;
|
||||||
|
let row = index / columns;
|
||||||
|
|
||||||
|
let block_width = (columns.saturating_sub(1)) as f64 * spacing;
|
||||||
|
let block_height = (rows.saturating_sub(1)) as f64 * spacing;
|
||||||
|
|
||||||
|
let start_x = match anchor {
|
||||||
|
NekoPosition::TopLeft | NekoPosition::Left | NekoPosition::BottomLeft => margin,
|
||||||
|
NekoPosition::Top | NekoPosition::Bottom => (screen_width - block_width) / 2.0,
|
||||||
|
NekoPosition::TopRight | NekoPosition::Right | NekoPosition::BottomRight => {
|
||||||
|
screen_width - margin - block_width
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let start_y = match anchor {
|
||||||
|
NekoPosition::TopLeft | NekoPosition::Top | NekoPosition::TopRight => margin,
|
||||||
|
NekoPosition::Left | NekoPosition::Right => (screen_height - block_height) / 2.0,
|
||||||
|
NekoPosition::BottomLeft | NekoPosition::Bottom | NekoPosition::BottomRight => {
|
||||||
|
screen_height - margin - block_height
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
CursorPosition {
|
||||||
|
x: (start_x + col as f64 * spacing).clamp(0.0, screen_width),
|
||||||
|
y: (start_y + row as f64 * spacing).clamp(0.0, screen_height),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -69,8 +69,12 @@ fn harden_scene_window_on_macos(window: &tauri::Window) {
|
|||||||
pub fn open_splash_window() {
|
pub fn open_splash_window() {
|
||||||
info!("Starting splash window creation...");
|
info!("Starting splash window creation...");
|
||||||
|
|
||||||
let mut config =
|
let url = if cfg!(debug_assertions) {
|
||||||
WindowConfig::accessory(SPLASH_WINDOW_LABEL, "/splash.html", "Friendolls Splash");
|
"/splash.html?debug=Y"
|
||||||
|
} else {
|
||||||
|
"/splash.html"
|
||||||
|
};
|
||||||
|
let mut config = WindowConfig::accessory(SPLASH_WINDOW_LABEL, url, "Friendolls Splash");
|
||||||
config.width = 800.0;
|
config.width = 800.0;
|
||||||
config.height = 400.0;
|
config.height = 400.0;
|
||||||
config.visible = false;
|
config.visible = false;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use crate::services::app_events::{
|
|||||||
};
|
};
|
||||||
use crate::services::{
|
use crate::services::{
|
||||||
cursor::{normalized_to_absolute, CursorPositions},
|
cursor::{normalized_to_absolute, CursorPositions},
|
||||||
friends,
|
friends, neko_positions,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils};
|
use super::{emitter, refresh, types::IncomingFriendCursorPayload, utils};
|
||||||
@@ -62,13 +62,12 @@ pub fn on_friend_cursor_position(payload: Payload, _socket: RawClient) {
|
|||||||
let mapped_pos = &friend_data.position;
|
let mapped_pos = &friend_data.position;
|
||||||
let raw_pos = normalized_to_absolute(mapped_pos);
|
let raw_pos = normalized_to_absolute(mapped_pos);
|
||||||
|
|
||||||
friends::update_cursor_position(
|
let position = CursorPositions {
|
||||||
friend_data.user_id,
|
|
||||||
CursorPositions {
|
|
||||||
raw: raw_pos,
|
raw: raw_pos,
|
||||||
mapped: mapped_pos.clone(),
|
mapped: mapped_pos.clone(),
|
||||||
},
|
};
|
||||||
);
|
|
||||||
|
neko_positions::update_friend_cursor(friend_data.user_id, position);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ pub fn init_network_state() -> NetworkState {
|
|||||||
let http_client = reqwest::ClientBuilder::new()
|
let http_client = reqwest::ClientBuilder::new()
|
||||||
.timeout(std::time::Duration::from_secs(30))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.connect_timeout(std::time::Duration::from_secs(10))
|
.connect_timeout(std::time::Duration::from_secs(10))
|
||||||
.user_agent("friendolls-desktop/0.1.0")
|
.user_agent("friendolls-desktop/0.1.4")
|
||||||
.build()
|
.build()
|
||||||
.expect("Client should build");
|
.expect("Client should build");
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,15 @@ use crate::{
|
|||||||
get_app_handle, lock_r, lock_w, services::app_menu::open_app_menu_window, state::FDOLL,
|
get_app_handle, lock_r, lock_w, services::app_menu::open_app_menu_window, state::FDOLL,
|
||||||
};
|
};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
|
image::Image,
|
||||||
|
include_image,
|
||||||
menu::{Menu, MenuItem},
|
menu::{Menu, MenuItem},
|
||||||
tray::TrayIconBuilder,
|
tray::TrayIconBuilder,
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
const TRAY_ICON: Image<'_> = include_image!("./icons/icon.tray.png");
|
||||||
|
|
||||||
/// Constructs app system tray.
|
/// Constructs app system tray.
|
||||||
/// Uses Tauri.
|
/// Uses Tauri.
|
||||||
pub fn init_system_tray() {
|
pub fn init_system_tray() {
|
||||||
@@ -34,7 +38,7 @@ pub fn init_system_tray() {
|
|||||||
error!("menu item {:?} not handled", event.id);
|
error!("menu item {:?} not handled", event.id);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.icon(app.default_window_icon().unwrap().clone())
|
.icon(TRAY_ICON)
|
||||||
.build(app)
|
.build(app)
|
||||||
.unwrap_or_else(|err| panic!("Failed to build tray: {}", err));
|
.unwrap_or_else(|err| panic!("Failed to build tray: {}", err));
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "friendolls-desktop",
|
"productName": "friendolls-desktop",
|
||||||
"version": "0.1.2",
|
"version": "0.1.4",
|
||||||
"identifier": "com.adamcv.friendolls-desktop",
|
"identifier": "com.adamcv.friendolls-desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
32
src/app.css
@@ -27,9 +27,9 @@
|
|||||||
--color-warning-content: oklch(100% 0 0);
|
--color-warning-content: oklch(100% 0 0);
|
||||||
--color-error: oklch(70% 0.191 22.216);
|
--color-error: oklch(70% 0.191 22.216);
|
||||||
--color-error-content: oklch(100% 0 0);
|
--color-error-content: oklch(100% 0 0);
|
||||||
--radius-selector: 0.25rem;
|
--radius-selector: 0rem;
|
||||||
--radius-field: 0.25rem;
|
--radius-field: 0rem;
|
||||||
--radius-box: 0.25rem;
|
--radius-box: 0rem;
|
||||||
--size-selector: 0.1875rem;
|
--size-selector: 0.1875rem;
|
||||||
--size-field: 0.1875rem;
|
--size-field: 0.1875rem;
|
||||||
--border: 1px;
|
--border: 1px;
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
--color-warning-content: oklch(0% 0 0);
|
--color-warning-content: oklch(0% 0 0);
|
||||||
--color-error: oklch(63% 0.237 25.331);
|
--color-error: oklch(63% 0.237 25.331);
|
||||||
--color-error-content: oklch(100% 0 0);
|
--color-error-content: oklch(100% 0 0);
|
||||||
--radius-selector: 0.25rem;
|
--radius-selector: 0rem;
|
||||||
--radius-field: 0.25rem;
|
--radius-field: 0rem;
|
||||||
--radius-box: 0.25rem;
|
--radius-box: 0rem;
|
||||||
--size-selector: 0.1875rem;
|
--size-selector: 0.1875rem;
|
||||||
--size-field: 0.1875rem;
|
--size-field: 0.1875rem;
|
||||||
--border: 1px;
|
--border: 1px;
|
||||||
@@ -73,10 +73,30 @@
|
|||||||
--noise: 1;
|
--noise: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: "DepartureFontMono";
|
||||||
|
src: url("/DepartureMono-Regular.woff2") format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-custom: "DepartureFontMono", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Standard property */
|
||||||
|
}
|
||||||
|
|
||||||
.pixelated {
|
.pixelated {
|
||||||
image-rendering: pixelated;
|
image-rendering: pixelated;
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/assets/icons/image.svelte
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-image-icon lucide-image"
|
||||||
|
><rect width="18" height="18" x="3" y="3" rx="2" ry="2" /><circle
|
||||||
|
cx="9"
|
||||||
|
cy="9"
|
||||||
|
r="2"
|
||||||
|
/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" /></svg
|
||||||
|
>
|
||||||
|
After Width: | Height: | Size: 413 B |
32
src/events/app-state.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import {
|
||||||
|
commands,
|
||||||
|
events,
|
||||||
|
type AppState,
|
||||||
|
type NekoPosition,
|
||||||
|
} from "$lib/bindings";
|
||||||
|
import { createEventSource } from "./listener-utils";
|
||||||
|
|
||||||
|
export type NeksPosition = NekoPosition;
|
||||||
|
export type { AppState };
|
||||||
|
|
||||||
|
const initialState: AppState = {
|
||||||
|
sceneSetup: {
|
||||||
|
nekosPosition: null,
|
||||||
|
nekosOpacity: 1,
|
||||||
|
nekosScale: 1,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appState = writable<AppState>(initialState);
|
||||||
|
|
||||||
|
export const { start: startAppState, stop: stopAppState } = createEventSource(
|
||||||
|
async (addEventListener) => {
|
||||||
|
appState.set(await commands.getAppState());
|
||||||
|
addEventListener(
|
||||||
|
await events.appStateChanged.listen((event) => {
|
||||||
|
appState.set(event.payload);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
import { writable } from "svelte/store";
|
|
||||||
import { events, type CursorPositions } from "$lib/bindings";
|
|
||||||
import { createEventSource } from "./listener-utils";
|
|
||||||
|
|
||||||
export const cursorPositionOnScreen = writable<CursorPositions>({
|
|
||||||
raw: { x: 0, y: 0 },
|
|
||||||
mapped: { x: 0, y: 0 },
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { start: startCursorTracking, stop: stopCursorTracking } =
|
|
||||||
createEventSource(async (addEventListener) => {
|
|
||||||
addEventListener(
|
|
||||||
await events.cursorMoved.listen((event) => {
|
|
||||||
cursorPositionOnScreen.set(event.payload);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { writable } from "svelte/store";
|
|
||||||
import { events, type CursorPositions } from "$lib/bindings";
|
|
||||||
import { createEventSource } from "./listener-utils";
|
|
||||||
|
|
||||||
export const friendsCursorPositions = writable<Record<string, CursorPositions>>(
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Here for now. Will extract into shared
|
|
||||||
// util when there's more similar cases.
|
|
||||||
function toCursorPositionsRecord(
|
|
||||||
payload: Partial<Record<string, CursorPositions>>,
|
|
||||||
): Record<string, CursorPositions> {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(payload).filter(
|
|
||||||
(entry): entry is [string, CursorPositions] => {
|
|
||||||
return entry[1] !== undefined;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const {
|
|
||||||
start: startFriendCursorTracking,
|
|
||||||
stop: stopFriendCursorTracking,
|
|
||||||
} = createEventSource(async (addEventListener) => {
|
|
||||||
addEventListener(
|
|
||||||
await events.friendCursorPositionsUpdated.listen((event) => {
|
|
||||||
friendsCursorPositions.set(toCursorPositionsRecord(event.payload));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
16
src/events/neko-positions.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { commands, events, type NekoPositionsDto } from "$lib/bindings";
|
||||||
|
import { createEventSource } from "./listener-utils";
|
||||||
|
|
||||||
|
export const nekoPositions = writable<NekoPositionsDto>({});
|
||||||
|
|
||||||
|
export const { start: startNekoPositions, stop: stopNekoPositions } =
|
||||||
|
createEventSource(async (addEventListener) => {
|
||||||
|
nekoPositions.set(await commands.getNekoPositions());
|
||||||
|
|
||||||
|
addEventListener(
|
||||||
|
await events.nekoPositionsUpdated.listen((event) => {
|
||||||
|
nekoPositions.set(event.payload);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -8,6 +8,12 @@ export const commands = {
|
|||||||
async getAppData() : Promise<UserData> {
|
async getAppData() : Promise<UserData> {
|
||||||
return await TAURI_INVOKE("get_app_data");
|
return await TAURI_INVOKE("get_app_data");
|
||||||
},
|
},
|
||||||
|
async getAppState() : Promise<AppState> {
|
||||||
|
return await TAURI_INVOKE("get_app_state");
|
||||||
|
},
|
||||||
|
async getNekoPositions() : Promise<NekoPositionsDto> {
|
||||||
|
return await TAURI_INVOKE("get_neko_positions");
|
||||||
|
},
|
||||||
async getActiveDollSpriteBase64() : Promise<string | null> {
|
async getActiveDollSpriteBase64() : Promise<string | null> {
|
||||||
return await TAURI_INVOKE("get_active_doll_sprite_base64");
|
return await TAURI_INVOKE("get_active_doll_sprite_base64");
|
||||||
},
|
},
|
||||||
@@ -118,6 +124,15 @@ async sendInteractionCmd(dto: SendInteractionDto) : Promise<null> {
|
|||||||
},
|
},
|
||||||
async getModules() : Promise<ModuleMetadata[]> {
|
async getModules() : Promise<ModuleMetadata[]> {
|
||||||
return await TAURI_INVOKE("get_modules");
|
return await TAURI_INVOKE("get_modules");
|
||||||
|
},
|
||||||
|
async setSceneSetupNekosPosition(nekosPosition: NekoPosition | null) : Promise<void> {
|
||||||
|
await TAURI_INVOKE("set_scene_setup_nekos_position", { nekosPosition });
|
||||||
|
},
|
||||||
|
async setSceneSetupNekosOpacity(nekosOpacity: number) : Promise<void> {
|
||||||
|
await TAURI_INVOKE("set_scene_setup_nekos_opacity", { nekosOpacity });
|
||||||
|
},
|
||||||
|
async setSceneSetupNekosScale(nekosScale: number) : Promise<void> {
|
||||||
|
await TAURI_INVOKE("set_scene_setup_nekos_scale", { nekosScale });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,13 +142,12 @@ async getModules() : Promise<ModuleMetadata[]> {
|
|||||||
export const events = __makeEvents__<{
|
export const events = __makeEvents__<{
|
||||||
activeDollSpriteChanged: ActiveDollSpriteChanged,
|
activeDollSpriteChanged: ActiveDollSpriteChanged,
|
||||||
appDataRefreshed: AppDataRefreshed,
|
appDataRefreshed: AppDataRefreshed,
|
||||||
|
appStateChanged: AppStateChanged,
|
||||||
authFlowUpdated: AuthFlowUpdated,
|
authFlowUpdated: AuthFlowUpdated,
|
||||||
createDoll: CreateDoll,
|
createDoll: CreateDoll,
|
||||||
cursorMoved: CursorMoved,
|
|
||||||
editDoll: EditDoll,
|
editDoll: EditDoll,
|
||||||
friendActiveDollChanged: FriendActiveDollChanged,
|
friendActiveDollChanged: FriendActiveDollChanged,
|
||||||
friendActiveDollSpritesUpdated: FriendActiveDollSpritesUpdated,
|
friendActiveDollSpritesUpdated: FriendActiveDollSpritesUpdated,
|
||||||
friendCursorPositionsUpdated: FriendCursorPositionsUpdated,
|
|
||||||
friendDisconnected: FriendDisconnected,
|
friendDisconnected: FriendDisconnected,
|
||||||
friendRequestAccepted: FriendRequestAccepted,
|
friendRequestAccepted: FriendRequestAccepted,
|
||||||
friendRequestDenied: FriendRequestDenied,
|
friendRequestDenied: FriendRequestDenied,
|
||||||
@@ -141,6 +155,7 @@ friendRequestReceived: FriendRequestReceived,
|
|||||||
friendUserStatusChanged: FriendUserStatusChanged,
|
friendUserStatusChanged: FriendUserStatusChanged,
|
||||||
interactionDeliveryFailed: InteractionDeliveryFailed,
|
interactionDeliveryFailed: InteractionDeliveryFailed,
|
||||||
interactionReceived: InteractionReceived,
|
interactionReceived: InteractionReceived,
|
||||||
|
nekoPositionsUpdated: NekoPositionsUpdated,
|
||||||
sceneInteractiveChanged: SceneInteractiveChanged,
|
sceneInteractiveChanged: SceneInteractiveChanged,
|
||||||
setInteractionOverlay: SetInteractionOverlay,
|
setInteractionOverlay: SetInteractionOverlay,
|
||||||
unfriended: Unfriended,
|
unfriended: Unfriended,
|
||||||
@@ -148,13 +163,12 @@ userStatusChanged: UserStatusChanged
|
|||||||
}>({
|
}>({
|
||||||
activeDollSpriteChanged: "active-doll-sprite-changed",
|
activeDollSpriteChanged: "active-doll-sprite-changed",
|
||||||
appDataRefreshed: "app-data-refreshed",
|
appDataRefreshed: "app-data-refreshed",
|
||||||
|
appStateChanged: "app-state-changed",
|
||||||
authFlowUpdated: "auth-flow-updated",
|
authFlowUpdated: "auth-flow-updated",
|
||||||
createDoll: "create-doll",
|
createDoll: "create-doll",
|
||||||
cursorMoved: "cursor-moved",
|
|
||||||
editDoll: "edit-doll",
|
editDoll: "edit-doll",
|
||||||
friendActiveDollChanged: "friend-active-doll-changed",
|
friendActiveDollChanged: "friend-active-doll-changed",
|
||||||
friendActiveDollSpritesUpdated: "friend-active-doll-sprites-updated",
|
friendActiveDollSpritesUpdated: "friend-active-doll-sprites-updated",
|
||||||
friendCursorPositionsUpdated: "friend-cursor-positions-updated",
|
|
||||||
friendDisconnected: "friend-disconnected",
|
friendDisconnected: "friend-disconnected",
|
||||||
friendRequestAccepted: "friend-request-accepted",
|
friendRequestAccepted: "friend-request-accepted",
|
||||||
friendRequestDenied: "friend-request-denied",
|
friendRequestDenied: "friend-request-denied",
|
||||||
@@ -162,6 +176,7 @@ friendRequestReceived: "friend-request-received",
|
|||||||
friendUserStatusChanged: "friend-user-status-changed",
|
friendUserStatusChanged: "friend-user-status-changed",
|
||||||
interactionDeliveryFailed: "interaction-delivery-failed",
|
interactionDeliveryFailed: "interaction-delivery-failed",
|
||||||
interactionReceived: "interaction-received",
|
interactionReceived: "interaction-received",
|
||||||
|
nekoPositionsUpdated: "neko-positions-updated",
|
||||||
sceneInteractiveChanged: "scene-interactive-changed",
|
sceneInteractiveChanged: "scene-interactive-changed",
|
||||||
setInteractionOverlay: "set-interaction-overlay",
|
setInteractionOverlay: "set-interaction-overlay",
|
||||||
unfriended: "unfriended",
|
unfriended: "unfriended",
|
||||||
@@ -180,12 +195,13 @@ export type AcceleratorModifier = "cmd" | "alt" | "ctrl" | "shift"
|
|||||||
export type ActiveDollSpriteChanged = string | null
|
export type ActiveDollSpriteChanged = string | null
|
||||||
export type AppConfig = { api_base_url: string | null; debug_mode: boolean; accelerators?: Partial<{ [key in AcceleratorAction]: KeyboardAccelerator }> }
|
export type AppConfig = { api_base_url: string | null; debug_mode: boolean; accelerators?: Partial<{ [key in AcceleratorAction]: KeyboardAccelerator }> }
|
||||||
export type AppDataRefreshed = UserData
|
export type AppDataRefreshed = UserData
|
||||||
|
export type AppState = { sceneSetup: SceneSetup }
|
||||||
|
export type AppStateChanged = AppState
|
||||||
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
|
export type AuthFlowStatus = "started" | "succeeded" | "failed" | "cancelled"
|
||||||
export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
export type AuthFlowUpdated = AuthFlowUpdatedPayload
|
||||||
export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null }
|
export type AuthFlowUpdatedPayload = { provider: string; status: AuthFlowStatus; message: string | null }
|
||||||
export type CreateDoll = null
|
export type CreateDoll = null
|
||||||
export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null }
|
export type CreateDollDto = { name: string; configuration: DollConfigurationDto | null }
|
||||||
export type CursorMoved = CursorPositions
|
|
||||||
export type CursorPosition = { x: number; y: number }
|
export type CursorPosition = { x: number; y: number }
|
||||||
export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition }
|
export type CursorPositions = { raw: CursorPosition; mapped: CursorPosition }
|
||||||
export type DisplayData = { screen_width: number; screen_height: number; monitor_scale_factor: number }
|
export type DisplayData = { screen_width: number; screen_height: number; monitor_scale_factor: number }
|
||||||
@@ -197,8 +213,6 @@ export type FriendActiveDollChanged = FriendActiveDollChangedPayload
|
|||||||
export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null }
|
export type FriendActiveDollChangedPayload = { friendId: string; doll: DollDto | null }
|
||||||
export type FriendActiveDollSpritesDto = Partial<{ [key in string]: string }>
|
export type FriendActiveDollSpritesDto = Partial<{ [key in string]: string }>
|
||||||
export type FriendActiveDollSpritesUpdated = FriendActiveDollSpritesDto
|
export type FriendActiveDollSpritesUpdated = FriendActiveDollSpritesDto
|
||||||
export type FriendCursorPositionsDto = Partial<{ [key in string]: CursorPositions }>
|
|
||||||
export type FriendCursorPositionsUpdated = FriendCursorPositionsDto
|
|
||||||
export type FriendDisconnected = FriendDisconnectedPayload
|
export type FriendDisconnected = FriendDisconnectedPayload
|
||||||
export type FriendDisconnectedPayload = { userId: string }
|
export type FriendDisconnectedPayload = { userId: string }
|
||||||
export type FriendRequestAccepted = FriendRequestAcceptedPayload
|
export type FriendRequestAccepted = FriendRequestAcceptedPayload
|
||||||
@@ -217,9 +231,14 @@ export type InteractionPayloadDto = { senderUserId: string; senderName: string;
|
|||||||
export type InteractionReceived = InteractionPayloadDto
|
export type InteractionReceived = InteractionPayloadDto
|
||||||
export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null }
|
export type KeyboardAccelerator = { modifiers?: AcceleratorModifier[]; key?: AcceleratorKey | null }
|
||||||
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
|
export type ModuleMetadata = { id: string; name: string; version: string; description: string | null }
|
||||||
|
export type NekoPosition = "top-left" | "top" | "top-right" | "left" | "right" | "bottom-left" | "bottom" | "bottom-right"
|
||||||
|
export type NekoPositionDto = { userId: string; isSelf: boolean; cursor: CursorPositions; target: CursorPosition; overrideApplied: boolean }
|
||||||
|
export type NekoPositionsDto = Partial<{ [key in string]: NekoPositionDto }>
|
||||||
|
export type NekoPositionsUpdated = NekoPositionsDto
|
||||||
export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
|
export type PresenceStatus = { title: string | null; subtitle: string | null; graphicsB64: string | null }
|
||||||
export type SceneData = { display: DisplayData; grid_size: number }
|
export type SceneData = { display: DisplayData; grid_size: number }
|
||||||
export type SceneInteractiveChanged = boolean
|
export type SceneInteractiveChanged = boolean
|
||||||
|
export type SceneSetup = { nekosPosition: NekoPosition | null; nekosOpacity: number; nekosScale: number }
|
||||||
export type SendFriendRequestDto = { receiverId: string }
|
export type SendFriendRequestDto = { receiverId: string }
|
||||||
export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
export type SendInteractionDto = { recipientUserId: string; content: string; type: string }
|
||||||
export type SetInteractionOverlay = boolean
|
export type SetInteractionOverlay = boolean
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
<script>
|
<script>
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { onMount, onDestroy } from "svelte";
|
import { onMount, onDestroy } from "svelte";
|
||||||
import { startCursorTracking, stopCursorTracking } from "../events/cursor";
|
|
||||||
import {
|
import {
|
||||||
startFriendCursorTracking,
|
startNekoPositions,
|
||||||
stopFriendCursorTracking,
|
stopNekoPositions,
|
||||||
} from "../events/friend-cursor";
|
} from "../events/neko-positions";
|
||||||
import {
|
import {
|
||||||
startActiveDollSprite,
|
startActiveDollSprite,
|
||||||
stopActiveDollSprite,
|
stopActiveDollSprite,
|
||||||
@@ -15,6 +14,7 @@
|
|||||||
stopFriendActiveDollSprite,
|
stopFriendActiveDollSprite,
|
||||||
} from "../events/friend-active-doll-sprite";
|
} from "../events/friend-active-doll-sprite";
|
||||||
import { startAppData } from "../events/app-data";
|
import { startAppData } from "../events/app-data";
|
||||||
|
import { startAppState, stopAppState } from "../events/app-state";
|
||||||
import { startInteraction, stopInteraction } from "../events/interaction";
|
import { startInteraction, stopInteraction } from "../events/interaction";
|
||||||
import {
|
import {
|
||||||
startSceneInteractive,
|
startSceneInteractive,
|
||||||
@@ -27,10 +27,10 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await startAppData();
|
await startAppData();
|
||||||
|
await startAppState();
|
||||||
await startActiveDollSprite();
|
await startActiveDollSprite();
|
||||||
await startFriendActiveDollSprite();
|
await startFriendActiveDollSprite();
|
||||||
await startCursorTracking();
|
await startNekoPositions();
|
||||||
await startFriendCursorTracking();
|
|
||||||
await startSceneInteractive();
|
await startSceneInteractive();
|
||||||
await startInteraction();
|
await startInteraction();
|
||||||
await startUserStatus();
|
await startUserStatus();
|
||||||
@@ -40,17 +40,21 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
stopCursorTracking();
|
stopNekoPositions();
|
||||||
stopFriendCursorTracking();
|
|
||||||
stopActiveDollSprite();
|
stopActiveDollSprite();
|
||||||
stopFriendActiveDollSprite();
|
stopFriendActiveDollSprite();
|
||||||
stopSceneInteractive();
|
stopSceneInteractive();
|
||||||
stopInteraction();
|
stopInteraction();
|
||||||
stopUserStatus();
|
stopUserStatus();
|
||||||
|
stopAppState();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("contextmenu", (e) => {
|
||||||
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-screen h-screen bg-transparent">
|
<div class="w-screen h-screen bg-transparent font-custom">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,14 +9,22 @@
|
|||||||
import Users from "../../assets/icons/users.svelte";
|
import Users from "../../assets/icons/users.svelte";
|
||||||
import Settings from "../../assets/icons/settings.svelte";
|
import Settings from "../../assets/icons/settings.svelte";
|
||||||
import Blocks from "../../assets/icons/blocks.svelte";
|
import Blocks from "../../assets/icons/blocks.svelte";
|
||||||
|
import Image from "../../assets/icons/image.svelte";
|
||||||
|
import Scene from "./tabs/scene/scene.svelte";
|
||||||
|
import { getVersion } from "@tauri-apps/api/app";
|
||||||
|
|
||||||
let showInteractionOverlay = false;
|
let showInteractionOverlay = false;
|
||||||
|
let appVersion = "";
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const unlisten = events.setInteractionOverlay.listen((event) => {
|
const unlisten = events.setInteractionOverlay.listen((event) => {
|
||||||
showInteractionOverlay = event.payload as boolean;
|
showInteractionOverlay = event.payload as boolean;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getVersion().then((version) => {
|
||||||
|
appVersion = version;
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlisten.then((u) => u());
|
unlisten.then((u) => u());
|
||||||
};
|
};
|
||||||
@@ -40,15 +48,28 @@
|
|||||||
></div>
|
></div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col gap-2 h-full max-h-full">
|
<div class="flex flex-col gap-2 h-full max-h-full">
|
||||||
<div class="size-full flex flex-col max-h-full gap-2 h-full">
|
<div class="size-full flex flex-col max-h-full gap-2 h-full relative">
|
||||||
|
<div class="absolute top-2 right-2">
|
||||||
|
<p class="text-xs opacity-50">v{appVersion}</p>
|
||||||
|
</div>
|
||||||
<div class="tabs tabs-lift h-full flex-1">
|
<div class="tabs tabs-lift h-full flex-1">
|
||||||
<label class="tab">
|
<label class="tab">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="app_menu_tabs"
|
name="app_menu_tabs"
|
||||||
aria-label="Your Nekos"
|
aria-label="Scene Configuration"
|
||||||
checked
|
checked
|
||||||
/>
|
/>
|
||||||
|
<div class="*:size-4">
|
||||||
|
<Image />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<div class="tab-content bg-base-100 border-base-300 p-4">
|
||||||
|
<Scene />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="tab">
|
||||||
|
<input type="radio" name="app_menu_tabs" aria-label="Your Nekos" />
|
||||||
<div class="*:size-4">
|
<div class="*:size-4">
|
||||||
<PawPrint />
|
<PawPrint />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import type { DollColorSchemeDto } from "$lib/bindings";
|
import type { DollColorSchemeDto } from "$lib/bindings";
|
||||||
|
|
||||||
export let dollColorScheme: DollColorSchemeDto;
|
export let dollColorScheme: DollColorSchemeDto;
|
||||||
|
export let spriteScale = 2;
|
||||||
|
export let spriteOpacity = 1;
|
||||||
|
|
||||||
let previewBase64: string | null = null;
|
let previewBase64: string | null = null;
|
||||||
let error: string | null = null;
|
let error: string | null = null;
|
||||||
@@ -80,7 +82,10 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="scale-200 p-4">
|
<div
|
||||||
|
style="transform: scale({spriteScale}); padding: {spriteScale *
|
||||||
|
10}px; opacity: {spriteOpacity};"
|
||||||
|
>
|
||||||
<div class="size-8">
|
<div class="size-8">
|
||||||
{#if error}
|
{#if error}
|
||||||
<div
|
<div
|
||||||
|
|||||||
68
src/routes/app-menu/tabs/scene/neko-reposition.svelte
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { commands } from "$lib/bindings";
|
||||||
|
import { appState, type NeksPosition } from "../../../../events/app-state";
|
||||||
|
|
||||||
|
const positions: { value: NeksPosition | null; label: string }[] = [
|
||||||
|
{ value: "top-left", label: "Top Left" },
|
||||||
|
{ value: "top", label: "Top" },
|
||||||
|
{ value: "top-right", label: "Top Right" },
|
||||||
|
{ value: "left", label: "Left" },
|
||||||
|
{ value: null, label: "" },
|
||||||
|
{ value: "right", label: "Right" },
|
||||||
|
{ value: "bottom-left", label: "Bottom Left" },
|
||||||
|
{ value: "bottom", label: "Bottom" },
|
||||||
|
{ value: "bottom-right", label: "Bottom Right" },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function selectPosition(position: NeksPosition | null) {
|
||||||
|
await commands.setSceneSetupNekosPosition(
|
||||||
|
$appState.sceneSetup.nekosPosition === position ? null : position,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let selectedLabel = $derived(
|
||||||
|
positions.find((p) => p.value === $appState.sceneSetup.nekosPosition)
|
||||||
|
?.label ?? "",
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="collapse bg-base-100 border-base-300 border">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title py-2 text-sm opacity-70">Neko Reposition</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="flex flex-row gap-4 h-full pt-4 border-t border-base-300">
|
||||||
|
<div class="h-full flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm opacity-50">
|
||||||
|
Choose a corner to gather nekos into a cluster
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm">
|
||||||
|
{$appState.sceneSetup.nekosPosition
|
||||||
|
? selectedLabel
|
||||||
|
: "Click a corner to enable"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card bg-base-200/50 p-1 w-max border border-base-300">
|
||||||
|
<div class="grid grid-cols-3 gap-6 items-center w-max">
|
||||||
|
{#each positions as pos}
|
||||||
|
{#if pos.value === null}
|
||||||
|
<div></div>
|
||||||
|
{:else}
|
||||||
|
<button
|
||||||
|
class={"btn-xs btn btn-square " +
|
||||||
|
($appState.sceneSetup.nekosPosition === pos.value
|
||||||
|
? "btn-primary"
|
||||||
|
: "")}
|
||||||
|
aria-label={pos.label}
|
||||||
|
onclick={() => selectPosition(pos.value)}
|
||||||
|
></button>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
119
src/routes/app-menu/tabs/scene/neko-view.svelte
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { commands } from "$lib/bindings";
|
||||||
|
import { appState } from "../../../../events/app-state";
|
||||||
|
import DollPreview from "../../components/doll-preview.svelte";
|
||||||
|
|
||||||
|
async function updateOpacity(value: number) {
|
||||||
|
await commands.setSceneSetupNekosOpacity(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateScale(value: number) {
|
||||||
|
await commands.setSceneSetupNekosScale(value);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="collapse bg-base-100 border-base-300 border">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title py-2 text-sm opacity-70">Neko View</div>
|
||||||
|
<div class="collapse-content">
|
||||||
|
<div class="pt-4 border-t border-base-300">
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<div
|
||||||
|
class="border border-primary relative shadow-[inset_0_0_8px] bg-primary/5 shadow-primary w-40 card"
|
||||||
|
>
|
||||||
|
<div class="size-full absolute bg-gridded opacity-25"></div>
|
||||||
|
<div class="size-full absolute">
|
||||||
|
<div
|
||||||
|
class="flex flex-row size-full items-end justify-between text-[8px] text-primary p-1"
|
||||||
|
>
|
||||||
|
<div class="text-start flex flex-col">
|
||||||
|
<p>Scale</p>
|
||||||
|
<p>Opacity</p>
|
||||||
|
</div>
|
||||||
|
<div class="text-end flex flex-col">
|
||||||
|
<p>{($appState.sceneSetup.nekosScale * 100).toFixed(0)}%</p>
|
||||||
|
<p>{($appState.sceneSetup.nekosOpacity * 100).toFixed(0)}%</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="size-full flex flex-row -translate-y-2 justify-center items-center"
|
||||||
|
>
|
||||||
|
<DollPreview
|
||||||
|
dollColorScheme={{ body: "b7f2ff", outline: "496065" }}
|
||||||
|
spriteScale={$appState.sceneSetup.nekosScale}
|
||||||
|
spriteOpacity={$appState.sceneSetup.nekosOpacity}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-4 w-full">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="text-xs opacity-70">Opacity</p>
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="range flex-1"
|
||||||
|
min="0.1"
|
||||||
|
max="1"
|
||||||
|
step="0.01"
|
||||||
|
value={$appState.sceneSetup.nekosOpacity}
|
||||||
|
oninput={(event) =>
|
||||||
|
updateOpacity(
|
||||||
|
Number((event.currentTarget as HTMLInputElement).value),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<p class="text-xs opacity-70">Scale</p>
|
||||||
|
<div class="flex flex-row gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="range flex-1"
|
||||||
|
min="0.5"
|
||||||
|
max="2"
|
||||||
|
step="0.25"
|
||||||
|
value={$appState.sceneSetup.nekosScale}
|
||||||
|
oninput={(event) =>
|
||||||
|
updateScale(
|
||||||
|
Number((event.currentTarget as HTMLInputElement).value),
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.bg-gridded {
|
||||||
|
background-image:
|
||||||
|
linear-gradient(
|
||||||
|
0deg,
|
||||||
|
transparent 24%,
|
||||||
|
var(--color-primary) 25%,
|
||||||
|
var(--color-primary) 26%,
|
||||||
|
transparent 27%,
|
||||||
|
transparent 74%,
|
||||||
|
var(--color-primary) 75%,
|
||||||
|
var(--color-primary) 76%,
|
||||||
|
transparent 77%,
|
||||||
|
transparent
|
||||||
|
),
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent 24%,
|
||||||
|
var(--color-primary) 25%,
|
||||||
|
var(--color-primary) 26%,
|
||||||
|
transparent 27%,
|
||||||
|
transparent 74%,
|
||||||
|
var(--color-primary) 75%,
|
||||||
|
var(--color-primary) 76%,
|
||||||
|
transparent 77%,
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
10
src/routes/app-menu/tabs/scene/scene.svelte
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import NekoView from "./neko-view.svelte";
|
||||||
|
import NekoReposition from "./neko-reposition.svelte";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-4 w-full h-full">
|
||||||
|
<p class="text-lg">Scene Configuration</p>
|
||||||
|
<NekoView />
|
||||||
|
<NekoReposition />
|
||||||
|
</div>
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<div class="dolls-page flex flex-col gap-4 max-h-full h-full">
|
<div class="dolls-page flex flex-col gap-4 max-h-full h-full">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<h2 class="text-lg font-bold">Your Nekos</h2>
|
<h2 class="text-lg">Your Nekos</h2>
|
||||||
<button class="btn btn-primary btn-sm" on:click={openCreateModal}>
|
<button class="btn btn-primary btn-sm" on:click={openCreateModal}>
|
||||||
Add a Neko
|
Add a Neko
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import {
|
import { commands, type AppConfig } from "$lib/bindings";
|
||||||
commands,
|
|
||||||
type AppConfig,
|
|
||||||
} from "$lib/bindings";
|
|
||||||
|
|
||||||
let form: AppConfig = {
|
let form: AppConfig = {
|
||||||
api_base_url: "",
|
api_base_url: "",
|
||||||
@@ -87,7 +84,7 @@
|
|||||||
<div class="p-6 flex flex-col gap-4 w-full h-full justify-between">
|
<div class="p-6 flex flex-col gap-4 w-full h-full justify-between">
|
||||||
<div class="flex flex-col gap-4 w-full">
|
<div class="flex flex-col gap-4 w-full">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<p class="text-xl font-semibold">Client Configuration</p>
|
<p class="text-xl">Client Configuration</p>
|
||||||
<p class="opacity-70 text-sm">Set a custom API endpoint.</p>
|
<p class="opacity-70 text-sm">Set a custom API endpoint.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -109,7 +106,6 @@
|
|||||||
bind:checked={form.debug_mode}
|
bind:checked={form.debug_mode}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
<div class="flex flex-col gap-4 size-full justify-between">
|
<div class="flex flex-col gap-4 size-full justify-between">
|
||||||
<div class="flex flex-col gap-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<p class="text-md font-light">Something is not right...</p>
|
<p class="text-md">Something is not right...</p>
|
||||||
<p class="opacity-70 text-3xl font-bold">
|
<p class="opacity-70 text-3xl">
|
||||||
Seems like the server is inaccessible. Check your network?
|
Seems like the server is inaccessible. Check your network?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
<p class="text-xs opacity-70 wrap-break-word font-mono">
|
<p class="text-xs opacity-70 wrap-break-word">
|
||||||
{errorMessage}
|
{errorMessage}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { cursorPositionOnScreen } from "../../events/cursor";
|
import { nekoPositions } from "../../events/neko-positions";
|
||||||
import { friendsCursorPositions } from "../../events/friend-cursor";
|
|
||||||
import { appData } from "../../events/app-data";
|
import { appData } from "../../events/app-data";
|
||||||
import { activeDollSpriteUrl } from "../../events/active-doll-sprite";
|
import { activeDollSpriteUrl } from "../../events/active-doll-sprite";
|
||||||
import { friendActiveDollSpriteUrls } from "../../events/friend-active-doll-sprite";
|
import { friendActiveDollSpriteUrls } from "../../events/friend-active-doll-sprite";
|
||||||
@@ -10,15 +9,16 @@
|
|||||||
friendsPresenceStates,
|
friendsPresenceStates,
|
||||||
currentPresenceState,
|
currentPresenceState,
|
||||||
} from "../../events/user-status";
|
} from "../../events/user-status";
|
||||||
import { commands } from "$lib/bindings";
|
import { commands, type NekoPositionDto } from "$lib/bindings";
|
||||||
import DebugBar from "./components/debug-bar.svelte";
|
import DebugBar from "./components/debug-bar.svelte";
|
||||||
import Neko from "./components/neko/neko.svelte";
|
import Neko from "./components/neko/neko.svelte";
|
||||||
import PetMenu from "./components/pet-menu/pet-menu.svelte";
|
import PetMenu from "./components/pet-menu/pet-menu.svelte";
|
||||||
import PetMessagePop from "./components/pet-message-pop.svelte";
|
import PetMessagePop from "./components/pet-message-pop.svelte";
|
||||||
import PetMessageSend from "./components/pet-message-send.svelte";
|
import PetMessageSend from "./components/pet-message-send.svelte";
|
||||||
import type { UserBasicDto } from "$lib/bindings";
|
import type { UserBasicDto } from "$lib/bindings";
|
||||||
|
import { appState } from "../../events/app-state";
|
||||||
|
|
||||||
let debugMode = false;
|
let debugMode = $state(false);
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
const config = await commands.getClientConfig();
|
const config = await commands.getClientConfig();
|
||||||
@@ -31,6 +31,12 @@
|
|||||||
?.friend ?? undefined
|
?.friend ?? undefined
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let nekoEntries = $derived.by(() => {
|
||||||
|
return Object.entries($nekoPositions).filter(
|
||||||
|
(entry): entry is [string, NekoPositionDto] => entry[1] !== undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="w-svw h-svh p-4 relative overflow-hidden">
|
<div class="w-svw h-svh p-4 relative overflow-hidden">
|
||||||
@@ -41,26 +47,26 @@
|
|||||||
await commands.setSceneInteractive(false, true);
|
await commands.setSceneInteractive(false, true);
|
||||||
}}> </button
|
}}> </button
|
||||||
>
|
>
|
||||||
{#if $appData?.user?.activeDollId}
|
{#each nekoEntries as [userId, position] (userId)}
|
||||||
|
{@const spriteUrl = position.isSelf
|
||||||
|
? $activeDollSpriteUrl
|
||||||
|
: $friendActiveDollSpriteUrls[userId]}
|
||||||
|
{#if spriteUrl}
|
||||||
|
{@const friend = position.isSelf ? undefined : getFriend(userId)}
|
||||||
<Neko
|
<Neko
|
||||||
targetX={$cursorPositionOnScreen.raw.x}
|
targetX={position.target.x}
|
||||||
targetY={$cursorPositionOnScreen.raw.y}
|
targetY={position.target.y}
|
||||||
spriteUrl={$activeDollSpriteUrl}
|
spriteUrl={spriteUrl}
|
||||||
/>
|
initialX={position.target.x}
|
||||||
{/if}
|
initialY={position.target.y}
|
||||||
{#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)}
|
scale={$appState.sceneSetup.nekosScale}
|
||||||
{#if $friendActiveDollSpriteUrls[friendId]}
|
opacity={$appState.sceneSetup.nekosOpacity}
|
||||||
{@const friend = getFriend(friendId)}
|
|
||||||
<Neko
|
|
||||||
targetX={position.raw.x}
|
|
||||||
targetY={position.raw.y}
|
|
||||||
spriteUrl={$friendActiveDollSpriteUrls[friendId]}
|
|
||||||
initialX={position.raw.x}
|
|
||||||
initialY={position.raw.y}
|
|
||||||
>
|
>
|
||||||
<PetMenu user={friend!} ariaLabel={`Open ${friend?.name} actions`} />
|
{#if !position.isSelf && friend}
|
||||||
<PetMessagePop userId={friendId} />
|
<PetMenu user={friend} ariaLabel={`Open ${friend.name} actions`} />
|
||||||
<PetMessageSend userId={friendId} userName={friend?.name ?? "Friend"} />
|
<PetMessagePop userId={userId} />
|
||||||
|
<PetMessageSend userId={userId} userName={friend.name} />
|
||||||
|
{/if}
|
||||||
</Neko>
|
</Neko>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -68,9 +74,8 @@
|
|||||||
<div id="debug-bar">
|
<div id="debug-bar">
|
||||||
<DebugBar
|
<DebugBar
|
||||||
isInteractive={$sceneInteractive}
|
isInteractive={$sceneInteractive}
|
||||||
cursorPosition={$cursorPositionOnScreen}
|
nekoPositions={$nekoPositions}
|
||||||
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
||||||
friendsCursorPositions={$friendsCursorPositions}
|
|
||||||
friends={$appData?.friends ?? []}
|
friends={$appData?.friends ?? []}
|
||||||
friendsPresenceStates={$friendsPresenceStates}
|
friendsPresenceStates={$friendsPresenceStates}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PresenceStatus, UserStatusPayload } from "$lib/bindings";
|
import type {
|
||||||
|
NekoPositionDto,
|
||||||
|
NekoPositionsDto,
|
||||||
|
PresenceStatus,
|
||||||
|
UserStatusPayload,
|
||||||
|
} from "$lib/bindings";
|
||||||
|
|
||||||
interface Friend {
|
interface Friend {
|
||||||
friend?: {
|
friend?: {
|
||||||
@@ -10,22 +15,32 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isInteractive: boolean;
|
isInteractive: boolean;
|
||||||
cursorPosition: { mapped: { x: number; y: number } };
|
nekoPositions: NekoPositionsDto;
|
||||||
presenceStatus: PresenceStatus | null;
|
presenceStatus: PresenceStatus | null;
|
||||||
friendsCursorPositions: Record<string, { mapped: { x: number; y: number } }>;
|
|
||||||
friends: Friend[];
|
friends: Friend[];
|
||||||
friendsPresenceStates: Record<string, UserStatusPayload>;
|
friendsPresenceStates: Record<string, UserStatusPayload>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
isInteractive,
|
isInteractive,
|
||||||
cursorPosition,
|
nekoPositions,
|
||||||
presenceStatus,
|
presenceStatus,
|
||||||
friendsCursorPositions,
|
|
||||||
friends,
|
friends,
|
||||||
friendsPresenceStates,
|
friendsPresenceStates,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let selfCursor = $derived(
|
||||||
|
Object.values(nekoPositions).find((position) => position?.isSelf)?.cursor,
|
||||||
|
);
|
||||||
|
|
||||||
|
let friendEntries = $derived.by(() => {
|
||||||
|
return Object.entries(nekoPositions).filter(
|
||||||
|
(entry): entry is [string, NekoPositionDto] => {
|
||||||
|
return entry[1] !== undefined && !entry[1].isSelf;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
function getFriendById(userId: string) {
|
function getFriendById(userId: string) {
|
||||||
const friend = friends.find((f) => f.friend?.id === userId);
|
const friend = friends.find((f) => f.friend?.id === userId);
|
||||||
return friend?.friend;
|
return friend?.friend;
|
||||||
@@ -37,7 +52,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="size-max mx-auto bg-base-100 border-base-200 border p-1 rounded-lg shadow-md"
|
class="size-max mx-auto bg-base-100 border-base-200 border p-1 rounded-box shadow-md"
|
||||||
>
|
>
|
||||||
<div class="flex flex-row gap-1 items-center text-center">
|
<div class="flex flex-row gap-1 items-center text-center">
|
||||||
<div>
|
<div>
|
||||||
@@ -49,9 +64,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if selfCursor}
|
||||||
<span class="font-mono text-xs badge py-3">
|
<span class="font-mono text-xs badge py-3">
|
||||||
{cursorPosition.mapped.x.toFixed(3)}, {cursorPosition.mapped.y.toFixed(3)}
|
{selfCursor.mapped.x.toFixed(3)}, {selfCursor.mapped.y.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if presenceStatus}
|
{#if presenceStatus}
|
||||||
<span class="font-mono text-xs badge py-3 flex items-center gap-2">
|
<span class="font-mono text-xs badge py-3 flex items-center gap-2">
|
||||||
@@ -66,16 +83,17 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if Object.keys(friendsCursorPositions).length > 0}
|
{#if friendEntries.length > 0}
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div>
|
<div>
|
||||||
{#each Object.entries(friendsCursorPositions) as [userId, position]}
|
{#each friendEntries as [userId, position]}
|
||||||
{@const status = getFriendStatus(userId)}
|
{@const status = getFriendStatus(userId)}
|
||||||
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
<div class="badge py-3 text-xs text-left flex flex-row gap-2">
|
||||||
<span class="font-bold">{getFriendById(userId)?.name}</span>
|
<span class="font-bold">{getFriendById(userId)?.name}</span>
|
||||||
<div class="flex flex-row font-mono gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<span>
|
<span>
|
||||||
{position.mapped.x.toFixed(3)}, {position.mapped.y.toFixed(3)}
|
{position.cursor.mapped.x.toFixed(3)},
|
||||||
|
{position.cursor.mapped.y.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
{#if status}
|
{#if status}
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import { setSprite } from "./sprites";
|
import { setSprite } from "./sprites";
|
||||||
import { calculateDirection, moveTowards, clampPosition } from "./physics";
|
import { calculateDirection, moveTowards, clampPosition } from "./physics";
|
||||||
import { updateIdle } from "./idle";
|
import { updateIdle } from "./idle";
|
||||||
|
import { appState } from "../../../../events/app-state";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
targetX: number;
|
targetX: number;
|
||||||
@@ -11,6 +12,8 @@
|
|||||||
spriteUrl: string;
|
spriteUrl: string;
|
||||||
initialX?: number;
|
initialX?: number;
|
||||||
initialY?: number;
|
initialY?: number;
|
||||||
|
scale?: number;
|
||||||
|
opacity?: number;
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,10 +23,13 @@
|
|||||||
spriteUrl,
|
spriteUrl,
|
||||||
initialX = 32,
|
initialX = 32,
|
||||||
initialY = 32,
|
initialY = 32,
|
||||||
|
scale = 1.0,
|
||||||
|
opacity = 1.0,
|
||||||
children,
|
children,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let nekoEl: HTMLDivElement;
|
let nekoEl: HTMLDivElement;
|
||||||
|
let wrapperEl: HTMLDivElement;
|
||||||
let animationFrameId: number;
|
let animationFrameId: number;
|
||||||
|
|
||||||
let nekoPos = $state({ x: initialX, y: initialY });
|
let nekoPos = $state({ x: initialX, y: initialY });
|
||||||
@@ -86,11 +92,15 @@
|
|||||||
const newPos = moveTowards(nekoPos.x, nekoPos.y, targetPos.x, targetPos.y);
|
const newPos = moveTowards(nekoPos.x, nekoPos.y, targetPos.x, targetPos.y);
|
||||||
nekoPos = newPos;
|
nekoPos = newPos;
|
||||||
|
|
||||||
nekoEl.style.transform = `translate(${nekoPos.x - 16}px, ${nekoPos.y - 16}px)`;
|
nekoEl.style.transform = `scale(${scale ?? 1.0})`;
|
||||||
|
nekoEl.style.opacity = `${opacity ?? 1.0}`;
|
||||||
|
wrapperEl.style.transform = `translate(${nekoPos.x - 16}px, ${nekoPos.y - 16}px)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
nekoEl.style.backgroundImage = `url(${spriteUrl})`;
|
nekoEl.style.backgroundImage = `url(${spriteUrl})`;
|
||||||
|
nekoEl.style.opacity = `${opacity ?? 1.0}`;
|
||||||
|
wrapperEl.style.transform = `translate(${nekoPos.x - 16}px, ${nekoPos.y - 16}px)`;
|
||||||
animationFrameId = requestAnimationFrame(frame);
|
animationFrameId = requestAnimationFrame(frame);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,18 +111,27 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (nekoEl && spriteUrl) {
|
if (nekoEl && spriteUrl && $appState) {
|
||||||
|
nekoEl.style.transform = `scale(${scale ?? 1.0})`;
|
||||||
nekoEl.style.backgroundImage = `url(${spriteUrl})`;
|
nekoEl.style.backgroundImage = `url(${spriteUrl})`;
|
||||||
|
nekoEl.style.opacity = `${opacity ?? 1.0}`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={nekoEl}
|
bind:this={wrapperEl}
|
||||||
class="pointer-events-none fixed z-999 size-8 select-none"
|
class="pointer-events-none fixed z-999 size-8 select-none"
|
||||||
style="width: 32px; height: 32px; position: fixed; image-rendering: pixelated;"
|
style="position: fixed; width: 32px; height: 32px;"
|
||||||
>
|
>
|
||||||
<div class="relative size-full">
|
<div class="relative">
|
||||||
|
<div
|
||||||
|
bind:this={nekoEl}
|
||||||
|
class="size-8"
|
||||||
|
style="position: absolute; image-rendering: pixelated;"
|
||||||
|
></div>
|
||||||
|
<div class="absolute size-8">
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@
|
|||||||
handleActionClick(action);
|
handleActionClick(action);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="text-[11px] font-semibold leading-none">{action.icon}</span>
|
<span class="text-[11px] leading-none">{action.icon}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`absolute inset-0 z-30 rounded-full transition-all duration-200 ease-out focus:outline-none ${
|
class={`absolute inset-0 z-30 transition-all duration-200 ease-out focus:outline-none ${
|
||||||
$sceneInteractive
|
$sceneInteractive
|
||||||
? "cursor-pointer"
|
? "cursor-pointer"
|
||||||
: "pointer-events-none cursor-default"
|
: "pointer-events-none cursor-default"
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
{#if name}
|
{#if name}
|
||||||
<div
|
<div
|
||||||
class={`absolute left-1/2 top-full z-10 mt-3 w-max max-w-32 -translate-x-1/2 rounded-md border border-base-300/80 bg-base-100/90 px-2 py-1 text-center text-[10px] font-medium leading-tight text-base-content/80 shadow-sm backdrop-blur-sm transition-all duration-200 ease-out ${
|
class={`absolute left-1/2 top-full z-10 mt-3 w-max max-w-32 -translate-x-1/2 rounded-field border border-base-300/80 bg-base-100/90 px-2 py-1 text-center text-[10px] font-medium leading-tight text-base-content/80 shadow-sm backdrop-blur-sm transition-all duration-200 ease-out ${
|
||||||
visible
|
visible
|
||||||
? "translate-y-0 opacity-100"
|
? "translate-y-0 opacity-100"
|
||||||
: "pointer-events-none -translate-y-1 opacity-0"
|
: "pointer-events-none -translate-y-1 opacity-0"
|
||||||
|
|||||||
@@ -100,11 +100,9 @@
|
|||||||
<div class="flex flex-col gap-6">
|
<div class="flex flex-col gap-6">
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<p class="text-xl font-light">meow? nyaaa!!</p>
|
<p class="text-xl opacity-70">meow? nyaaa!!</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="opacity-70 text-3xl font-bold">
|
<p class="text-2xl">a cute passive socialization layer!</p>
|
||||||
a cute passive socialization layer!
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<span>Sign in with</span>
|
<span>Sign in with</span>
|
||||||
@@ -118,15 +116,13 @@
|
|||||||
class="flex flex-row justify-start items-center z-1 h-full p-1 pr-0"
|
class="flex flex-row justify-start items-center z-1 h-full p-1 pr-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-linear-to-t from-base-100/50 to-base-100 rounded-sm"
|
class="bg-linear-to-t from-base-100/50 to-base-100 rounded-selector"
|
||||||
>
|
>
|
||||||
<div class="scale-70">
|
<div class="scale-70">
|
||||||
<Google />
|
<Google />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xl font-semibold px-3 text-primary-content"
|
<span class="text-xl px-3 text-primary-content">Google</span>
|
||||||
>Google</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@@ -138,15 +134,13 @@
|
|||||||
class="flex flex-row justify-start items-center z-1 h-full p-1 pr-0"
|
class="flex flex-row justify-start items-center z-1 h-full p-1 pr-0"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="bg-linear-to-t from-base-100/50 to-base-100 rounded-sm"
|
class="bg-linear-to-t from-base-100/50 to-base-100 rounded-selector"
|
||||||
>
|
>
|
||||||
<div class="scale-70">
|
<div class="scale-70">
|
||||||
<Discord />
|
<Discord />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xl font-semibold px-3 text-primary-content"
|
<span class="text-xl px-3 text-primary-content">Discord</span>
|
||||||
>Discord</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
BIN
static/DepartureMono-Regular.woff2
Normal file
@@ -2,22 +2,29 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<title>Friendolls</title>
|
<title>Friendolls Splash</title>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("contextmenu", function (e) {
|
document.addEventListener("contextmenu", function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const debug = urlParams.get("debug") === "Y";
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body style="width: 100%; height: 100%">
|
<body style="width: 100%; height: 100%">
|
||||||
<div id="splash"></div>
|
<div id="splash"></div>
|
||||||
|
<script>
|
||||||
|
document.getElementById("splash").style.backgroundImage = debug
|
||||||
|
? 'url("/splash-dev.jpeg")'
|
||||||
|
: 'url("/splash.jpg")';
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
#splash {
|
#splash {
|
||||||
background-image: url("/splash-dev.jpeg");
|
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
width: 800px;
|
width: 800px;
|
||||||
|
|||||||
BIN
static/splash.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |