scene configuration, neko opacity & scale
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
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,
|
||||||
presence_modules::models::ModuleMetadata,
|
presence_modules::models::ModuleMetadata,
|
||||||
sprite,
|
sprite,
|
||||||
@@ -45,3 +46,27 @@ 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 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,
|
doll_editor::open_doll_editor_window,
|
||||||
services::{
|
scene::{get_scene_interactive, set_pet_menu_state, set_scene_interactive},
|
||||||
doll_editor::open_doll_editor_window,
|
|
||||||
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,
|
||||||
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,11 +25,12 @@ 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,
|
CursorMoved, EditDoll, FriendActiveDollChanged, FriendActiveDollSpritesUpdated,
|
||||||
FriendDisconnected, FriendRequestAccepted, FriendRequestDenied, FriendRequestReceived,
|
FriendCursorPositionsUpdated, FriendDisconnected, FriendRequestAccepted,
|
||||||
FriendUserStatusChanged, InteractionDeliveryFailed, InteractionReceived,
|
FriendRequestDenied, FriendRequestReceived, FriendUserStatusChanged,
|
||||||
SceneInteractiveChanged, SetInteractionOverlay, Unfriended, UserStatusChanged,
|
InteractionDeliveryFailed, InteractionReceived, SceneInteractiveChanged,
|
||||||
|
SetInteractionOverlay, Unfriended, UserStatusChanged,
|
||||||
};
|
};
|
||||||
|
|
||||||
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
static APP_HANDLE: std::sync::OnceLock<tauri::AppHandle<tauri::Wry>> = std::sync::OnceLock::new();
|
||||||
@@ -68,6 +67,7 @@ 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_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,12 +102,16 @@ 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,
|
CursorMoved,
|
||||||
SceneInteractiveChanged,
|
SceneInteractiveChanged,
|
||||||
AppDataRefreshed,
|
AppDataRefreshed,
|
||||||
|
AppStateChanged,
|
||||||
ActiveDollSpriteChanged,
|
ActiveDollSpriteChanged,
|
||||||
SetInteractionOverlay,
|
SetInteractionOverlay,
|
||||||
EditDoll,
|
EditDoll,
|
||||||
|
|||||||
39
src-tauri/src/models/app_state.rs
Normal file
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;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -47,6 +48,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>);
|
||||||
|
|||||||
42
src-tauri/src/services/app_state.rs
Normal file
42
src-tauri/src/services/app_state.rs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
use std::sync::{Arc, LazyLock, RwLock};
|
||||||
|
|
||||||
|
use tauri_specta::Event as _;
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
get_app_handle,
|
||||||
|
models::app_state::{AppState, NekoPosition},
|
||||||
|
services::app_events::AppStateChanged,
|
||||||
|
};
|
||||||
|
|
||||||
|
static APP_STATE: LazyLock<Arc<RwLock<AppState>>> =
|
||||||
|
LazyLock::new(|| Arc::new(RwLock::new(AppState::default())));
|
||||||
|
|
||||||
|
pub fn get_snapshot() -> AppState {
|
||||||
|
let guard = APP_STATE.read().expect("app state lock poisoned");
|
||||||
|
guard.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scene_setup_nekos_position(nekos_position: Option<NekoPosition>) {
|
||||||
|
let mut guard = APP_STATE.write().expect("app state lock poisoned");
|
||||||
|
guard.scene_setup.nekos_position = nekos_position;
|
||||||
|
emit_snapshot(&guard);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_scene_setup_nekos_opacity(nekos_opacity: f32) {
|
||||||
|
let mut guard = APP_STATE.write().expect("app state lock poisoned");
|
||||||
|
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 = APP_STATE.write().expect("app state lock poisoned");
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
17
src/assets/icons/image.svelte
Normal file
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
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);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -8,6 +8,9 @@ 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 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 +121,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,6 +139,7 @@ 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,
|
cursorMoved: CursorMoved,
|
||||||
@@ -148,6 +161,7 @@ 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",
|
cursorMoved: "cursor-moved",
|
||||||
@@ -180,6 +194,8 @@ 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 }
|
||||||
@@ -217,9 +233,11 @@ 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 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
|
||||||
|
|||||||
@@ -15,6 +15,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,6 +28,7 @@
|
|||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
try {
|
try {
|
||||||
await startAppData();
|
await startAppData();
|
||||||
|
await startAppState();
|
||||||
await startActiveDollSprite();
|
await startActiveDollSprite();
|
||||||
await startFriendActiveDollSprite();
|
await startFriendActiveDollSprite();
|
||||||
await startCursorTracking();
|
await startCursorTracking();
|
||||||
@@ -47,6 +49,7 @@
|
|||||||
stopSceneInteractive();
|
stopSceneInteractive();
|
||||||
stopInteraction();
|
stopInteraction();
|
||||||
stopUserStatus();
|
stopUserStatus();
|
||||||
|
stopAppState();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
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";
|
||||||
|
|
||||||
let showInteractionOverlay = false;
|
let showInteractionOverlay = false;
|
||||||
|
|
||||||
@@ -46,9 +48,19 @@
|
|||||||
<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
|
||||||
|
|||||||
74
src/routes/app-menu/tabs/scene/neko-reposition.svelte
Normal file
74
src/routes/app-menu/tabs/scene/neko-reposition.svelte
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<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>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={$appState.sceneSetup.nekosPosition !== null}
|
||||||
|
onclick={() =>
|
||||||
|
selectPosition(
|
||||||
|
$appState.sceneSetup.nekosPosition ? null : "bottom-left",
|
||||||
|
)}
|
||||||
|
class="toggle toggle-xl {$appState.sceneSetup.nekosPosition
|
||||||
|
? 'bg-primary/10 toggle-primary'
|
||||||
|
: 'bg-base-200'}"
|
||||||
|
/>
|
||||||
|
</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>
|
||||||
85
src/routes/app-menu/tabs/scene/neko-view.svelte
Normal file
85
src/routes/app-menu/tabs/scene/neko-view.svelte
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<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 bg-base-200/50 w-40 card relative">
|
||||||
|
<div class="size-full absolute">
|
||||||
|
<div
|
||||||
|
class="flex flex-row size-full items-end justify-between text-[8px] opacity-50 p-1 font-mono"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
10
src/routes/app-menu/tabs/scene/scene.svelte
Normal file
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 font-bold">Scene Configuration</p>
|
||||||
|
<NekoView />
|
||||||
|
<NekoReposition />
|
||||||
|
</div>
|
||||||
@@ -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: "",
|
||||||
@@ -109,7 +106,6 @@
|
|||||||
bind:checked={form.debug_mode}
|
bind:checked={form.debug_mode}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if errorMessage}
|
{#if errorMessage}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
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 = false;
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@
|
|||||||
targetX={$cursorPositionOnScreen.raw.x}
|
targetX={$cursorPositionOnScreen.raw.x}
|
||||||
targetY={$cursorPositionOnScreen.raw.y}
|
targetY={$cursorPositionOnScreen.raw.y}
|
||||||
spriteUrl={$activeDollSpriteUrl}
|
spriteUrl={$activeDollSpriteUrl}
|
||||||
|
scale={$appState.sceneSetup.nekosScale}
|
||||||
|
opacity={$appState.sceneSetup.nekosOpacity}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)}
|
{#each Object.entries($friendsCursorPositions) as [friendId, position] (friendId)}
|
||||||
@@ -57,6 +60,8 @@
|
|||||||
spriteUrl={$friendActiveDollSpriteUrls[friendId]}
|
spriteUrl={$friendActiveDollSpriteUrls[friendId]}
|
||||||
initialX={position.raw.x}
|
initialX={position.raw.x}
|
||||||
initialY={position.raw.y}
|
initialY={position.raw.y}
|
||||||
|
scale={$appState.sceneSetup.nekosScale}
|
||||||
|
opacity={$appState.sceneSetup.nekosOpacity}
|
||||||
>
|
>
|
||||||
<PetMenu user={friend!} ariaLabel={`Open ${friend?.name} actions`} />
|
<PetMenu user={friend!} ariaLabel={`Open ${friend?.name} actions`} />
|
||||||
<PetMessagePop userId={friendId} />
|
<PetMessagePop userId={friendId} />
|
||||||
@@ -67,12 +72,12 @@
|
|||||||
{#if debugMode}
|
{#if debugMode}
|
||||||
<div id="debug-bar">
|
<div id="debug-bar">
|
||||||
<DebugBar
|
<DebugBar
|
||||||
isInteractive={$sceneInteractive}
|
isInteractive={$sceneInteractive}
|
||||||
cursorPosition={$cursorPositionOnScreen}
|
cursorPosition={$cursorPositionOnScreen}
|
||||||
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
presenceStatus={$currentPresenceState?.presenceStatus ?? null}
|
||||||
friendsCursorPositions={$friendsCursorPositions}
|
friendsCursorPositions={$friendsCursorPositions}
|
||||||
friends={$appData?.friends ?? []}
|
friends={$appData?.friends ?? []}
|
||||||
friendsPresenceStates={$friendsPresenceStates}
|
friendsPresenceStates={$friendsPresenceStates}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -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">
|
||||||
{@render children?.()}
|
<div
|
||||||
|
bind:this={nekoEl}
|
||||||
|
class="size-8"
|
||||||
|
style="position: absolute; image-rendering: pixelated;"
|
||||||
|
></div>
|
||||||
|
<div class="absolute size-8">
|
||||||
|
{@render children?.()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user