Compare commits
No commits in common. "98590f63e77e96f8196dcf25158af3b0cf26fa61a68da8de1d31aec4835544f3" and "9acb688379e9235617bf1ce8f32edb1821c75f99c2a68af86941ac6354e8a71e" have entirely different histories.
98590f63e7
...
9acb688379
|
|
@ -9,8 +9,6 @@ pub mod config;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod loose_props;
|
|
||||||
#[cfg(feature = "ssr")]
|
|
||||||
pub mod props;
|
pub mod props;
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod realms;
|
pub mod realms;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use chattyness_db::{
|
||||||
models::{CreateServerAvatarRequest, ServerAvatar, ServerAvatarSummary, UpdateServerAvatarRequest},
|
models::{CreateServerAvatarRequest, ServerAvatar, ServerAvatarSummary, UpdateServerAvatarRequest},
|
||||||
queries::server_avatars,
|
queries::server_avatars,
|
||||||
};
|
};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -87,7 +87,7 @@ pub async fn get_avatar(
|
||||||
) -> Result<Json<ServerAvatar>, AppError> {
|
) -> Result<Json<ServerAvatar>, AppError> {
|
||||||
let avatar = server_avatars::get_server_avatar_by_id(&pool, avatar_id)
|
let avatar = server_avatars::get_server_avatar_by_id(&pool, avatar_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Avatar")?;
|
.ok_or_else(|| AppError::NotFound("Avatar not found".to_string()))?;
|
||||||
Ok(Json(avatar))
|
Ok(Json(avatar))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ pub async fn update_avatar(
|
||||||
// Check avatar exists
|
// Check avatar exists
|
||||||
let existing = server_avatars::get_server_avatar_by_id(&mut *guard, avatar_id)
|
let existing = server_avatars::get_server_avatar_by_id(&mut *guard, avatar_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Avatar")?;
|
.ok_or_else(|| AppError::NotFound("Avatar not found".to_string()))?;
|
||||||
|
|
||||||
// Update the avatar
|
// Update the avatar
|
||||||
let avatar = server_avatars::update_server_avatar(&mut *guard, avatar_id, &req).await?;
|
let avatar = server_avatars::update_server_avatar(&mut *guard, avatar_id, &req).await?;
|
||||||
|
|
@ -127,7 +127,7 @@ pub async fn delete_avatar(
|
||||||
// Get the avatar first to log its name
|
// Get the avatar first to log its name
|
||||||
let avatar = server_avatars::get_server_avatar_by_id(&mut *guard, avatar_id)
|
let avatar = server_avatars::get_server_avatar_by_id(&mut *guard, avatar_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Avatar")?;
|
.ok_or_else(|| AppError::NotFound("Avatar not found".to_string()))?;
|
||||||
|
|
||||||
// Delete from database
|
// Delete from database
|
||||||
server_avatars::delete_server_avatar(&mut *guard, avatar_id).await?;
|
server_avatars::delete_server_avatar(&mut *guard, avatar_id).await?;
|
||||||
|
|
|
||||||
|
|
@ -1,75 +0,0 @@
|
||||||
//! Loose props management API handlers for admin UI.
|
|
||||||
|
|
||||||
use axum::Json;
|
|
||||||
use axum::extract::Path;
|
|
||||||
use chattyness_db::{models::LooseProp, queries::loose_props};
|
|
||||||
use chattyness_error::{AppError, OptionExt};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::auth::AdminConn;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API Types
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Request to update loose prop scale.
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct UpdateLoosePropScaleRequest {
|
|
||||||
/// Scale factor (0.1 - 10.0).
|
|
||||||
pub scale: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API Handlers
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Get a loose prop by ID.
|
|
||||||
pub async fn get_loose_prop(
|
|
||||||
admin_conn: AdminConn,
|
|
||||||
Path(loose_prop_id): Path<Uuid>,
|
|
||||||
) -> Result<Json<LooseProp>, AppError> {
|
|
||||||
let conn = admin_conn.0;
|
|
||||||
let mut guard = conn.acquire().await;
|
|
||||||
|
|
||||||
let prop = loose_props::get_loose_prop_by_id(&mut *guard, loose_prop_id)
|
|
||||||
.await?
|
|
||||||
.or_not_found("Loose prop (may have expired)")?;
|
|
||||||
|
|
||||||
Ok(Json(prop))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update loose prop scale.
|
|
||||||
///
|
|
||||||
/// Server admins can update any loose prop scale.
|
|
||||||
pub async fn update_loose_prop_scale(
|
|
||||||
admin_conn: AdminConn,
|
|
||||||
Path(loose_prop_id): Path<Uuid>,
|
|
||||||
Json(req): Json<UpdateLoosePropScaleRequest>,
|
|
||||||
) -> Result<Json<LooseProp>, AppError> {
|
|
||||||
let conn = admin_conn.0;
|
|
||||||
let mut guard = conn.acquire().await;
|
|
||||||
|
|
||||||
let prop = loose_props::update_loose_prop_scale(&mut *guard, loose_prop_id, req.scale).await?;
|
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
"Updated loose prop {} scale to {}",
|
|
||||||
loose_prop_id,
|
|
||||||
req.scale
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(Json(prop))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List loose props in a scene/channel.
|
|
||||||
pub async fn list_loose_props(
|
|
||||||
admin_conn: AdminConn,
|
|
||||||
Path(scene_id): Path<Uuid>,
|
|
||||||
) -> Result<Json<Vec<LooseProp>>, AppError> {
|
|
||||||
let conn = admin_conn.0;
|
|
||||||
let mut guard = conn.acquire().await;
|
|
||||||
|
|
||||||
let props = loose_props::list_channel_loose_props(&mut *guard, scene_id).await?;
|
|
||||||
|
|
||||||
Ok(Json(props))
|
|
||||||
}
|
|
||||||
|
|
@ -7,7 +7,7 @@ use chattyness_db::{
|
||||||
models::{CreateServerPropRequest, ServerProp, ServerPropSummary},
|
models::{CreateServerPropRequest, ServerProp, ServerPropSummary},
|
||||||
queries::props,
|
queries::props,
|
||||||
};
|
};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
|
@ -218,7 +218,7 @@ pub async fn get_prop(
|
||||||
) -> Result<Json<ServerProp>, AppError> {
|
) -> Result<Json<ServerProp>, AppError> {
|
||||||
let prop = props::get_server_prop_by_id(&pool, prop_id)
|
let prop = props::get_server_prop_by_id(&pool, prop_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Prop")?;
|
.ok_or_else(|| AppError::NotFound("Prop not found".to_string()))?;
|
||||||
Ok(Json(prop))
|
Ok(Json(prop))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +233,7 @@ pub async fn delete_prop(
|
||||||
// Get the prop first to get the asset path
|
// Get the prop first to get the asset path
|
||||||
let prop = props::get_server_prop_by_id(&mut *guard, prop_id)
|
let prop = props::get_server_prop_by_id(&mut *guard, prop_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Prop")?;
|
.ok_or_else(|| AppError::NotFound("Prop not found".to_string()))?;
|
||||||
|
|
||||||
// Delete from database
|
// Delete from database
|
||||||
props::delete_server_prop(&mut *guard, prop_id).await?;
|
props::delete_server_prop(&mut *guard, prop_id).await?;
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use chattyness_db::{
|
||||||
},
|
},
|
||||||
queries::{owner as queries, realm_avatars},
|
queries::{owner as queries, realm_avatars},
|
||||||
};
|
};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -231,7 +231,7 @@ pub async fn get_realm_avatar(
|
||||||
|
|
||||||
let avatar = realm_avatars::get_realm_avatar_by_id(&pool, avatar_id)
|
let avatar = realm_avatars::get_realm_avatar_by_id(&pool, avatar_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Avatar")?;
|
.ok_or_else(|| AppError::NotFound("Avatar not found".to_string()))?;
|
||||||
Ok(Json(avatar))
|
Ok(Json(avatar))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -254,7 +254,7 @@ pub async fn update_realm_avatar(
|
||||||
// Check avatar exists
|
// Check avatar exists
|
||||||
let existing = realm_avatars::get_realm_avatar_by_id(&mut *guard, avatar_id)
|
let existing = realm_avatars::get_realm_avatar_by_id(&mut *guard, avatar_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Avatar")?;
|
.ok_or_else(|| AppError::NotFound("Avatar not found".to_string()))?;
|
||||||
|
|
||||||
// Update the avatar
|
// Update the avatar
|
||||||
let avatar = realm_avatars::update_realm_avatar(&mut *guard, avatar_id, &req).await?;
|
let avatar = realm_avatars::update_realm_avatar(&mut *guard, avatar_id, &req).await?;
|
||||||
|
|
@ -284,7 +284,7 @@ pub async fn delete_realm_avatar(
|
||||||
// Get the avatar first to log its name
|
// Get the avatar first to log its name
|
||||||
let avatar = realm_avatars::get_realm_avatar_by_id(&mut *guard, avatar_id)
|
let avatar = realm_avatars::get_realm_avatar_by_id(&mut *guard, avatar_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Avatar")?;
|
.ok_or_else(|| AppError::NotFound("Avatar not found".to_string()))?;
|
||||||
|
|
||||||
// Delete from database
|
// Delete from database
|
||||||
realm_avatars::delete_realm_avatar(&mut *guard, avatar_id).await?;
|
realm_avatars::delete_realm_avatar(&mut *guard, avatar_id).await?;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use axum::{
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{auth, avatars, config, dashboard, loose_props, props, realms, scenes, spots, staff, users};
|
use super::{auth, avatars, config, dashboard, props, realms, scenes, spots, staff, users};
|
||||||
use crate::app::AdminAppState;
|
use crate::app::AdminAppState;
|
||||||
|
|
||||||
/// Create the admin API router.
|
/// Create the admin API router.
|
||||||
|
|
@ -85,19 +85,6 @@ pub fn admin_api_router() -> Router<AdminAppState> {
|
||||||
"/props/{prop_id}",
|
"/props/{prop_id}",
|
||||||
get(props::get_prop).delete(props::delete_prop),
|
get(props::get_prop).delete(props::delete_prop),
|
||||||
)
|
)
|
||||||
// API - Loose Props (scene props)
|
|
||||||
.route(
|
|
||||||
"/scenes/{scene_id}/loose_props",
|
|
||||||
get(loose_props::list_loose_props),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/loose_props/{loose_prop_id}",
|
|
||||||
get(loose_props::get_loose_prop),
|
|
||||||
)
|
|
||||||
.route(
|
|
||||||
"/loose_props/{loose_prop_id}/scale",
|
|
||||||
put(loose_props::update_loose_prop_scale),
|
|
||||||
)
|
|
||||||
// API - Server Avatars
|
// API - Server Avatars
|
||||||
.route(
|
.route(
|
||||||
"/avatars",
|
"/avatars",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use chattyness_db::{
|
||||||
models::{CreateSceneRequest, Scene, SceneSummary, UpdateSceneRequest},
|
models::{CreateSceneRequest, Scene, SceneSummary, UpdateSceneRequest},
|
||||||
queries::{realms, scenes},
|
queries::{realms, scenes},
|
||||||
};
|
};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -199,7 +199,7 @@ pub async fn get_scene(
|
||||||
) -> Result<Json<Scene>, AppError> {
|
) -> Result<Json<Scene>, AppError> {
|
||||||
let scene = scenes::get_scene_by_id(&pool, scene_id)
|
let scene = scenes::get_scene_by_id(&pool, scene_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Scene")?;
|
.ok_or_else(|| AppError::NotFound("Scene not found".to_string()))?;
|
||||||
Ok(Json(scene))
|
Ok(Json(scene))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -273,7 +273,7 @@ pub async fn update_scene(
|
||||||
// Get the existing scene to get realm_id
|
// Get the existing scene to get realm_id
|
||||||
let existing_scene = scenes::get_scene_by_id(&mut *guard, scene_id)
|
let existing_scene = scenes::get_scene_by_id(&mut *guard, scene_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Scene")?;
|
.ok_or_else(|| AppError::NotFound("Scene not found".to_string()))?;
|
||||||
|
|
||||||
// Handle clear background image
|
// Handle clear background image
|
||||||
if req.clear_background_image {
|
if req.clear_background_image {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use chattyness_db::{
|
||||||
models::{CreateSpotRequest, Spot, SpotSummary, UpdateSpotRequest},
|
models::{CreateSpotRequest, Spot, SpotSummary, UpdateSpotRequest},
|
||||||
queries::spots,
|
queries::spots,
|
||||||
};
|
};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
@ -31,7 +31,7 @@ pub async fn get_spot(
|
||||||
) -> Result<Json<Spot>, AppError> {
|
) -> Result<Json<Spot>, AppError> {
|
||||||
let spot = spots::get_spot_by_id(&pool, spot_id)
|
let spot = spots::get_spot_by_id(&pool, spot_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Spot")?;
|
.ok_or_else(|| AppError::NotFound("Spot not found".to_string()))?;
|
||||||
Ok(Json(spot))
|
Ok(Json(spot))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ pub async fn update_spot(
|
||||||
if let Some(ref new_slug) = req.slug {
|
if let Some(ref new_slug) = req.slug {
|
||||||
let existing = spots::get_spot_by_id(&mut *guard, spot_id)
|
let existing = spots::get_spot_by_id(&mut *guard, spot_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Spot")?;
|
.ok_or_else(|| AppError::NotFound("Spot not found".to_string()))?;
|
||||||
|
|
||||||
if Some(new_slug.clone()) != existing.slug {
|
if Some(new_slug.clone()) != existing.slug {
|
||||||
let available =
|
let available =
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,6 @@ pub struct PropDetail {
|
||||||
pub default_layer: Option<String>,
|
pub default_layer: Option<String>,
|
||||||
/// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
|
/// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
|
||||||
pub default_position: Option<i16>,
|
pub default_position: Option<i16>,
|
||||||
/// Default scale factor (0.1 - 10.0) applied when prop is dropped to canvas.
|
|
||||||
pub default_scale: f32,
|
|
||||||
pub is_unique: bool,
|
pub is_unique: bool,
|
||||||
pub is_transferable: bool,
|
pub is_transferable: bool,
|
||||||
pub is_portable: bool,
|
pub is_portable: bool,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
|
||||||
use crate::components::{Card, PageHeader};
|
use crate::components::{Card, PageHeader};
|
||||||
use crate::utils::name_to_slug;
|
|
||||||
|
|
||||||
/// Server avatar new page component.
|
/// Server avatar new page component.
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -29,7 +28,14 @@ pub fn AvatarsNewPage() -> impl IntoView {
|
||||||
let new_name = event_target_value(&ev);
|
let new_name = event_target_value(&ev);
|
||||||
set_name.set(new_name.clone());
|
set_name.set(new_name.clone());
|
||||||
if slug_auto.get() {
|
if slug_auto.get() {
|
||||||
set_slug.set(name_to_slug(&new_name));
|
let new_slug = new_name
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('-')
|
||||||
|
.to_string();
|
||||||
|
set_slug.set(new_slug);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,6 @@ fn PropDetailView(prop: PropDetail) -> impl IntoView {
|
||||||
None => "Not set".to_string(),
|
None => "Not set".to_string(),
|
||||||
}}
|
}}
|
||||||
</DetailItem>
|
</DetailItem>
|
||||||
<DetailItem label="Default Scale">
|
|
||||||
{format!("{}%", (prop.default_scale * 100.0) as i32)}
|
|
||||||
</DetailItem>
|
|
||||||
<DetailItem label="Status">
|
<DetailItem label="Status">
|
||||||
{if prop.is_active {
|
{if prop.is_active {
|
||||||
view! { <span class="status-badge status-active">"Active"</span> }.into_any()
|
view! { <span class="status-badge status-active">"Active"</span> }.into_any()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
|
||||||
use crate::components::{Card, PageHeader};
|
use crate::components::{Card, PageHeader};
|
||||||
use crate::utils::name_to_slug;
|
|
||||||
|
|
||||||
/// Prop new page component with file upload.
|
/// Prop new page component with file upload.
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -33,7 +32,14 @@ pub fn PropsNewPage() -> impl IntoView {
|
||||||
let new_name = event_target_value(&ev);
|
let new_name = event_target_value(&ev);
|
||||||
set_name.set(new_name.clone());
|
set_name.set(new_name.clone());
|
||||||
if slug_auto.get() {
|
if slug_auto.get() {
|
||||||
set_slug.set(name_to_slug(&new_name));
|
let new_slug = new_name
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('-')
|
||||||
|
.to_string();
|
||||||
|
set_slug.set(new_slug);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use leptos::task::spawn_local;
|
||||||
use leptos_router::hooks::use_params_map;
|
use leptos_router::hooks::use_params_map;
|
||||||
|
|
||||||
use crate::components::{Card, PageHeader};
|
use crate::components::{Card, PageHeader};
|
||||||
use crate::utils::name_to_slug;
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use crate::utils::get_api_base;
|
use crate::utils::get_api_base;
|
||||||
|
|
||||||
|
|
@ -37,7 +36,14 @@ pub fn RealmAvatarsNewPage() -> impl IntoView {
|
||||||
let new_name = event_target_value(&ev);
|
let new_name = event_target_value(&ev);
|
||||||
set_name.set(new_name.clone());
|
set_name.set(new_name.clone());
|
||||||
if slug_auto.get() {
|
if slug_auto.get() {
|
||||||
set_avatar_slug.set(name_to_slug(&new_name));
|
let new_slug = new_name
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('-')
|
||||||
|
.to_string();
|
||||||
|
set_avatar_slug.set(new_slug);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
|
||||||
use crate::components::{Card, PageHeader};
|
use crate::components::{Card, PageHeader};
|
||||||
use crate::utils::name_to_slug;
|
|
||||||
|
|
||||||
/// Realm new page component.
|
/// Realm new page component.
|
||||||
#[component]
|
#[component]
|
||||||
|
|
@ -41,7 +40,14 @@ pub fn RealmNewPage() -> impl IntoView {
|
||||||
let new_name = event_target_value(&ev);
|
let new_name = event_target_value(&ev);
|
||||||
set_name.set(new_name.clone());
|
set_name.set(new_name.clone());
|
||||||
if slug_auto.get() {
|
if slug_auto.get() {
|
||||||
set_slug.set(name_to_slug(&new_name));
|
let new_slug = new_name
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('-')
|
||||||
|
.to_string();
|
||||||
|
set_slug.set(new_slug);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use leptos::task::spawn_local;
|
||||||
use leptos_router::hooks::use_params_map;
|
use leptos_router::hooks::use_params_map;
|
||||||
|
|
||||||
use crate::components::{Card, PageHeader};
|
use crate::components::{Card, PageHeader};
|
||||||
use crate::utils::name_to_slug;
|
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use crate::utils::fetch_image_dimensions_client;
|
use crate::utils::fetch_image_dimensions_client;
|
||||||
|
|
||||||
|
|
@ -41,7 +40,14 @@ pub fn SceneNewPage() -> impl IntoView {
|
||||||
let new_name = event_target_value(&ev);
|
let new_name = event_target_value(&ev);
|
||||||
set_name.set(new_name.clone());
|
set_name.set(new_name.clone());
|
||||||
if slug_auto.get() {
|
if slug_auto.get() {
|
||||||
set_slug.set(name_to_slug(&new_name));
|
let new_slug = new_name
|
||||||
|
.to_lowercase()
|
||||||
|
.chars()
|
||||||
|
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
||||||
|
.collect::<String>()
|
||||||
|
.trim_matches('-')
|
||||||
|
.to_string();
|
||||||
|
set_slug.set(new_slug);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,5 @@
|
||||||
//! Utility functions for the admin UI.
|
//! Utility functions for the admin UI.
|
||||||
|
|
||||||
/// Generate a URL-friendly slug from a name.
|
|
||||||
///
|
|
||||||
/// Converts to lowercase, replaces non-alphanumeric chars with hyphens,
|
|
||||||
/// and trims leading/trailing hyphens.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// assert_eq!(name_to_slug("My Cool Realm!"), "my-cool-realm-");
|
|
||||||
/// assert_eq!(name_to_slug("Test 123"), "test-123");
|
|
||||||
/// ```
|
|
||||||
pub fn name_to_slug(name: &str) -> String {
|
|
||||||
name.to_lowercase()
|
|
||||||
.chars()
|
|
||||||
.map(|c| if c.is_alphanumeric() { c } else { '-' })
|
|
||||||
.collect::<String>()
|
|
||||||
.trim_matches('-')
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Gets the API base path based on the current URL.
|
/// Gets the API base path based on the current URL.
|
||||||
///
|
///
|
||||||
/// Returns `/api/admin` if the current path starts with `/admin`,
|
/// Returns `/api/admin` if the current path starts with `/admin`,
|
||||||
|
|
|
||||||
|
|
@ -889,8 +889,6 @@ pub struct LooseProp {
|
||||||
pub realm_prop_id: Option<Uuid>,
|
pub realm_prop_id: Option<Uuid>,
|
||||||
pub position_x: f64,
|
pub position_x: f64,
|
||||||
pub position_y: f64,
|
pub position_y: f64,
|
||||||
/// Scale factor (0.1 - 10.0) inherited from prop definition at drop time.
|
|
||||||
pub scale: f32,
|
|
||||||
pub dropped_by: Option<Uuid>,
|
pub dropped_by: Option<Uuid>,
|
||||||
pub expires_at: Option<DateTime<Utc>>,
|
pub expires_at: Option<DateTime<Utc>>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
|
|
@ -898,12 +896,6 @@ pub struct LooseProp {
|
||||||
pub prop_name: String,
|
pub prop_name: String,
|
||||||
/// Asset path for rendering (JOINed from source prop).
|
/// Asset path for rendering (JOINed from source prop).
|
||||||
pub prop_asset_path: String,
|
pub prop_asset_path: String,
|
||||||
/// If true, only moderators can move/scale/pickup this prop.
|
|
||||||
#[serde(default)]
|
|
||||||
pub is_locked: bool,
|
|
||||||
/// User ID of the moderator who locked this prop.
|
|
||||||
#[serde(default)]
|
|
||||||
pub locked_by: Option<Uuid>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A server-wide prop (global library).
|
/// A server-wide prop (global library).
|
||||||
|
|
@ -923,8 +915,6 @@ pub struct ServerProp {
|
||||||
pub default_emotion: Option<EmotionState>,
|
pub default_emotion: Option<EmotionState>,
|
||||||
/// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
|
/// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
|
||||||
pub default_position: Option<i16>,
|
pub default_position: Option<i16>,
|
||||||
/// Default scale factor (0.1 - 10.0) applied when prop is dropped to canvas.
|
|
||||||
pub default_scale: f32,
|
|
||||||
pub is_unique: bool,
|
pub is_unique: bool,
|
||||||
pub is_transferable: bool,
|
pub is_transferable: bool,
|
||||||
pub is_portable: bool,
|
pub is_portable: bool,
|
||||||
|
|
@ -976,9 +966,6 @@ pub struct CreateServerPropRequest {
|
||||||
/// Whether prop appears in the public Server inventory tab.
|
/// Whether prop appears in the public Server inventory tab.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub public: Option<bool>,
|
pub public: Option<bool>,
|
||||||
/// Default scale factor (0.1 - 10.0) applied when prop is dropped to canvas.
|
|
||||||
#[serde(default)]
|
|
||||||
pub default_scale: Option<f32>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
|
|
@ -1012,14 +999,6 @@ impl CreateServerPropRequest {
|
||||||
.to_string(),
|
.to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
// Validate scale range (0.1 - 10.0)
|
|
||||||
if let Some(scale) = self.default_scale {
|
|
||||||
if !(0.1..=10.0).contains(&scale) {
|
|
||||||
return Err(AppError::Validation(
|
|
||||||
"default_scale must be between 0.1 and 10.0".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! Database query modules.
|
//! Database query modules.
|
||||||
|
|
||||||
pub mod avatar_common;
|
|
||||||
pub mod avatars;
|
pub mod avatars;
|
||||||
pub mod channel_members;
|
pub mod channel_members;
|
||||||
pub mod channels;
|
pub mod channels;
|
||||||
|
|
|
||||||
|
|
@ -1,657 +0,0 @@
|
||||||
//! Common avatar query infrastructure.
|
|
||||||
//!
|
|
||||||
//! This module provides shared types and traits for server and realm avatars,
|
|
||||||
//! eliminating duplication between `server_avatars.rs` and `realm_avatars.rs`.
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use sqlx::PgExecutor;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::models::{AvatarRenderData, EmotionState};
|
|
||||||
use chattyness_error::AppError;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Avatar Slots - Array-based representation of avatar prop references
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Number of positions per layer (0-8).
|
|
||||||
pub const LAYER_SIZE: usize = 9;
|
|
||||||
|
|
||||||
/// Number of emotion types.
|
|
||||||
pub const EMOTION_COUNT: usize = 12;
|
|
||||||
|
|
||||||
/// Array-based representation of avatar prop slots.
|
|
||||||
///
|
|
||||||
/// This struct consolidates the 135 individual UUID fields into arrays,
|
|
||||||
/// making the code more maintainable and enabling iteration.
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct AvatarSlots {
|
|
||||||
/// Skin layer positions 0-8 (behind user, body/face).
|
|
||||||
pub skin: [Option<Uuid>; LAYER_SIZE],
|
|
||||||
/// Clothes layer positions 0-8 (with user, worn items).
|
|
||||||
pub clothes: [Option<Uuid>; LAYER_SIZE],
|
|
||||||
/// Accessories layer positions 0-8 (in front of user, held/attached items).
|
|
||||||
pub accessories: [Option<Uuid>; LAYER_SIZE],
|
|
||||||
/// Emotion layers: 12 emotions × 9 positions each.
|
|
||||||
/// Index by EmotionState ordinal: neutral=0, happy=1, sad=2, etc.
|
|
||||||
pub emotions: [[Option<Uuid>; LAYER_SIZE]; EMOTION_COUNT],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AvatarSlots {
|
|
||||||
/// Get the emotion layer for a specific emotion state.
|
|
||||||
pub fn emotion_layer(&self, emotion: EmotionState) -> &[Option<Uuid>; LAYER_SIZE] {
|
|
||||||
&self.emotions[emotion.as_index()]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect all non-null prop UUIDs from all layers.
|
|
||||||
pub fn collect_all_prop_ids(&self) -> Vec<Uuid> {
|
|
||||||
let mut ids = Vec::new();
|
|
||||||
|
|
||||||
// Content layers
|
|
||||||
for id in self.skin.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
for id in self.clothes.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
for id in self.accessories.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// All emotion layers
|
|
||||||
for emotion_layer in &self.emotions {
|
|
||||||
for id in emotion_layer.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ids
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Collect prop UUIDs for content layers + specific emotion.
|
|
||||||
pub fn collect_render_prop_ids(&self, emotion: EmotionState) -> Vec<Uuid> {
|
|
||||||
let mut ids = Vec::new();
|
|
||||||
|
|
||||||
// Content layers
|
|
||||||
for id in self.skin.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
for id in self.clothes.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
for id in self.accessories.iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Specific emotion layer
|
|
||||||
for id in self.emotion_layer(emotion).iter().flatten() {
|
|
||||||
ids.push(*id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ids
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extension trait for EmotionState to get array indices.
|
|
||||||
pub trait EmotionIndex {
|
|
||||||
fn as_index(&self) -> usize;
|
|
||||||
fn from_index(index: usize) -> Option<Self>
|
|
||||||
where
|
|
||||||
Self: Sized;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmotionIndex for EmotionState {
|
|
||||||
fn as_index(&self) -> usize {
|
|
||||||
match self {
|
|
||||||
EmotionState::Neutral => 0,
|
|
||||||
EmotionState::Happy => 1,
|
|
||||||
EmotionState::Sad => 2,
|
|
||||||
EmotionState::Angry => 3,
|
|
||||||
EmotionState::Surprised => 4,
|
|
||||||
EmotionState::Thinking => 5,
|
|
||||||
EmotionState::Laughing => 6,
|
|
||||||
EmotionState::Crying => 7,
|
|
||||||
EmotionState::Love => 8,
|
|
||||||
EmotionState::Confused => 9,
|
|
||||||
EmotionState::Sleeping => 10,
|
|
||||||
EmotionState::Wink => 11,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_index(index: usize) -> Option<Self> {
|
|
||||||
match index {
|
|
||||||
0 => Some(EmotionState::Neutral),
|
|
||||||
1 => Some(EmotionState::Happy),
|
|
||||||
2 => Some(EmotionState::Sad),
|
|
||||||
3 => Some(EmotionState::Angry),
|
|
||||||
4 => Some(EmotionState::Surprised),
|
|
||||||
5 => Some(EmotionState::Thinking),
|
|
||||||
6 => Some(EmotionState::Laughing),
|
|
||||||
7 => Some(EmotionState::Crying),
|
|
||||||
8 => Some(EmotionState::Love),
|
|
||||||
9 => Some(EmotionState::Confused),
|
|
||||||
10 => Some(EmotionState::Sleeping),
|
|
||||||
11 => Some(EmotionState::Wink),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Shared Row Types for Database Queries
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Row type for avatar with resolved asset paths.
|
|
||||||
/// Used by both server and realm avatar queries.
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct AvatarWithPathsRow {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub slug: Option<String>, // Server avatars have slug, realm might not in some queries
|
|
||||||
pub name: String,
|
|
||||||
pub description: Option<String>,
|
|
||||||
// Skin layer paths
|
|
||||||
pub skin_0: Option<String>,
|
|
||||||
pub skin_1: Option<String>,
|
|
||||||
pub skin_2: Option<String>,
|
|
||||||
pub skin_3: Option<String>,
|
|
||||||
pub skin_4: Option<String>,
|
|
||||||
pub skin_5: Option<String>,
|
|
||||||
pub skin_6: Option<String>,
|
|
||||||
pub skin_7: Option<String>,
|
|
||||||
pub skin_8: Option<String>,
|
|
||||||
// Clothes layer paths
|
|
||||||
pub clothes_0: Option<String>,
|
|
||||||
pub clothes_1: Option<String>,
|
|
||||||
pub clothes_2: Option<String>,
|
|
||||||
pub clothes_3: Option<String>,
|
|
||||||
pub clothes_4: Option<String>,
|
|
||||||
pub clothes_5: Option<String>,
|
|
||||||
pub clothes_6: Option<String>,
|
|
||||||
pub clothes_7: Option<String>,
|
|
||||||
pub clothes_8: Option<String>,
|
|
||||||
// Accessories layer paths
|
|
||||||
pub accessories_0: Option<String>,
|
|
||||||
pub accessories_1: Option<String>,
|
|
||||||
pub accessories_2: Option<String>,
|
|
||||||
pub accessories_3: Option<String>,
|
|
||||||
pub accessories_4: Option<String>,
|
|
||||||
pub accessories_5: Option<String>,
|
|
||||||
pub accessories_6: Option<String>,
|
|
||||||
pub accessories_7: Option<String>,
|
|
||||||
pub accessories_8: Option<String>,
|
|
||||||
// Happy emotion layer paths (e1 - for store display)
|
|
||||||
pub emotion_0: Option<String>,
|
|
||||||
pub emotion_1: Option<String>,
|
|
||||||
pub emotion_2: Option<String>,
|
|
||||||
pub emotion_3: Option<String>,
|
|
||||||
pub emotion_4: Option<String>,
|
|
||||||
pub emotion_5: Option<String>,
|
|
||||||
pub emotion_6: Option<String>,
|
|
||||||
pub emotion_7: Option<String>,
|
|
||||||
pub emotion_8: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AvatarWithPathsRow {
|
|
||||||
/// Extract skin layer as array.
|
|
||||||
pub fn skin_layer(&self) -> [Option<String>; LAYER_SIZE] {
|
|
||||||
[
|
|
||||||
self.skin_0.clone(),
|
|
||||||
self.skin_1.clone(),
|
|
||||||
self.skin_2.clone(),
|
|
||||||
self.skin_3.clone(),
|
|
||||||
self.skin_4.clone(),
|
|
||||||
self.skin_5.clone(),
|
|
||||||
self.skin_6.clone(),
|
|
||||||
self.skin_7.clone(),
|
|
||||||
self.skin_8.clone(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract clothes layer as array.
|
|
||||||
pub fn clothes_layer(&self) -> [Option<String>; LAYER_SIZE] {
|
|
||||||
[
|
|
||||||
self.clothes_0.clone(),
|
|
||||||
self.clothes_1.clone(),
|
|
||||||
self.clothes_2.clone(),
|
|
||||||
self.clothes_3.clone(),
|
|
||||||
self.clothes_4.clone(),
|
|
||||||
self.clothes_5.clone(),
|
|
||||||
self.clothes_6.clone(),
|
|
||||||
self.clothes_7.clone(),
|
|
||||||
self.clothes_8.clone(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract accessories layer as array.
|
|
||||||
pub fn accessories_layer(&self) -> [Option<String>; LAYER_SIZE] {
|
|
||||||
[
|
|
||||||
self.accessories_0.clone(),
|
|
||||||
self.accessories_1.clone(),
|
|
||||||
self.accessories_2.clone(),
|
|
||||||
self.accessories_3.clone(),
|
|
||||||
self.accessories_4.clone(),
|
|
||||||
self.accessories_5.clone(),
|
|
||||||
self.accessories_6.clone(),
|
|
||||||
self.accessories_7.clone(),
|
|
||||||
self.accessories_8.clone(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extract emotion layer as array.
|
|
||||||
pub fn emotion_layer(&self) -> [Option<String>; LAYER_SIZE] {
|
|
||||||
[
|
|
||||||
self.emotion_0.clone(),
|
|
||||||
self.emotion_1.clone(),
|
|
||||||
self.emotion_2.clone(),
|
|
||||||
self.emotion_3.clone(),
|
|
||||||
self.emotion_4.clone(),
|
|
||||||
self.emotion_5.clone(),
|
|
||||||
self.emotion_6.clone(),
|
|
||||||
self.emotion_7.clone(),
|
|
||||||
self.emotion_8.clone(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Row type for prop asset lookup.
|
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
|
||||||
pub struct PropAssetRow {
|
|
||||||
pub id: Uuid,
|
|
||||||
pub asset_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Generic Query Helpers
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Build a prop UUID to asset path lookup map.
|
|
||||||
pub async fn build_prop_map<'e>(
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
prop_ids: &[Uuid],
|
|
||||||
props_table: &str,
|
|
||||||
) -> Result<HashMap<Uuid, String>, AppError> {
|
|
||||||
if prop_ids.is_empty() {
|
|
||||||
return Ok(HashMap::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
let query = format!(
|
|
||||||
r#"
|
|
||||||
SELECT id, asset_path
|
|
||||||
FROM {}
|
|
||||||
WHERE id = ANY($1)
|
|
||||||
"#,
|
|
||||||
props_table
|
|
||||||
);
|
|
||||||
|
|
||||||
let rows = sqlx::query_as::<_, PropAssetRow>(&query)
|
|
||||||
.bind(prop_ids)
|
|
||||||
.fetch_all(executor)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(rows.into_iter().map(|r| (r.id, r.asset_path)).collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve avatar slots to render data using a prop map.
|
|
||||||
pub fn resolve_slots_to_render_data(
|
|
||||||
avatar_id: Uuid,
|
|
||||||
slots: &AvatarSlots,
|
|
||||||
current_emotion: EmotionState,
|
|
||||||
prop_map: &HashMap<Uuid, String>,
|
|
||||||
) -> AvatarRenderData {
|
|
||||||
let get_path = |id: Option<Uuid>| -> Option<String> {
|
|
||||||
id.and_then(|id| prop_map.get(&id).cloned())
|
|
||||||
};
|
|
||||||
|
|
||||||
let emotion_layer = slots.emotion_layer(current_emotion);
|
|
||||||
|
|
||||||
AvatarRenderData {
|
|
||||||
avatar_id,
|
|
||||||
current_emotion,
|
|
||||||
skin_layer: [
|
|
||||||
get_path(slots.skin[0]),
|
|
||||||
get_path(slots.skin[1]),
|
|
||||||
get_path(slots.skin[2]),
|
|
||||||
get_path(slots.skin[3]),
|
|
||||||
get_path(slots.skin[4]),
|
|
||||||
get_path(slots.skin[5]),
|
|
||||||
get_path(slots.skin[6]),
|
|
||||||
get_path(slots.skin[7]),
|
|
||||||
get_path(slots.skin[8]),
|
|
||||||
],
|
|
||||||
clothes_layer: [
|
|
||||||
get_path(slots.clothes[0]),
|
|
||||||
get_path(slots.clothes[1]),
|
|
||||||
get_path(slots.clothes[2]),
|
|
||||||
get_path(slots.clothes[3]),
|
|
||||||
get_path(slots.clothes[4]),
|
|
||||||
get_path(slots.clothes[5]),
|
|
||||||
get_path(slots.clothes[6]),
|
|
||||||
get_path(slots.clothes[7]),
|
|
||||||
get_path(slots.clothes[8]),
|
|
||||||
],
|
|
||||||
accessories_layer: [
|
|
||||||
get_path(slots.accessories[0]),
|
|
||||||
get_path(slots.accessories[1]),
|
|
||||||
get_path(slots.accessories[2]),
|
|
||||||
get_path(slots.accessories[3]),
|
|
||||||
get_path(slots.accessories[4]),
|
|
||||||
get_path(slots.accessories[5]),
|
|
||||||
get_path(slots.accessories[6]),
|
|
||||||
get_path(slots.accessories[7]),
|
|
||||||
get_path(slots.accessories[8]),
|
|
||||||
],
|
|
||||||
emotion_layer: [
|
|
||||||
get_path(emotion_layer[0]),
|
|
||||||
get_path(emotion_layer[1]),
|
|
||||||
get_path(emotion_layer[2]),
|
|
||||||
get_path(emotion_layer[3]),
|
|
||||||
get_path(emotion_layer[4]),
|
|
||||||
get_path(emotion_layer[5]),
|
|
||||||
get_path(emotion_layer[6]),
|
|
||||||
get_path(emotion_layer[7]),
|
|
||||||
get_path(emotion_layer[8]),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// SQL Generation Helpers
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Generate the SELECT clause for avatar paths query.
|
|
||||||
/// Returns the column selections for joining props to get asset paths.
|
|
||||||
pub fn avatar_paths_select_clause() -> &'static str {
|
|
||||||
r#"
|
|
||||||
a.id,
|
|
||||||
a.name,
|
|
||||||
a.description,
|
|
||||||
-- Skin layer
|
|
||||||
p_skin_0.asset_path AS skin_0,
|
|
||||||
p_skin_1.asset_path AS skin_1,
|
|
||||||
p_skin_2.asset_path AS skin_2,
|
|
||||||
p_skin_3.asset_path AS skin_3,
|
|
||||||
p_skin_4.asset_path AS skin_4,
|
|
||||||
p_skin_5.asset_path AS skin_5,
|
|
||||||
p_skin_6.asset_path AS skin_6,
|
|
||||||
p_skin_7.asset_path AS skin_7,
|
|
||||||
p_skin_8.asset_path AS skin_8,
|
|
||||||
-- Clothes layer
|
|
||||||
p_clothes_0.asset_path AS clothes_0,
|
|
||||||
p_clothes_1.asset_path AS clothes_1,
|
|
||||||
p_clothes_2.asset_path AS clothes_2,
|
|
||||||
p_clothes_3.asset_path AS clothes_3,
|
|
||||||
p_clothes_4.asset_path AS clothes_4,
|
|
||||||
p_clothes_5.asset_path AS clothes_5,
|
|
||||||
p_clothes_6.asset_path AS clothes_6,
|
|
||||||
p_clothes_7.asset_path AS clothes_7,
|
|
||||||
p_clothes_8.asset_path AS clothes_8,
|
|
||||||
-- Accessories layer
|
|
||||||
p_acc_0.asset_path AS accessories_0,
|
|
||||||
p_acc_1.asset_path AS accessories_1,
|
|
||||||
p_acc_2.asset_path AS accessories_2,
|
|
||||||
p_acc_3.asset_path AS accessories_3,
|
|
||||||
p_acc_4.asset_path AS accessories_4,
|
|
||||||
p_acc_5.asset_path AS accessories_5,
|
|
||||||
p_acc_6.asset_path AS accessories_6,
|
|
||||||
p_acc_7.asset_path AS accessories_7,
|
|
||||||
p_acc_8.asset_path AS accessories_8,
|
|
||||||
-- Happy emotion layer (e1 - more inviting for store display)
|
|
||||||
p_emo_0.asset_path AS emotion_0,
|
|
||||||
p_emo_1.asset_path AS emotion_1,
|
|
||||||
p_emo_2.asset_path AS emotion_2,
|
|
||||||
p_emo_3.asset_path AS emotion_3,
|
|
||||||
p_emo_4.asset_path AS emotion_4,
|
|
||||||
p_emo_5.asset_path AS emotion_5,
|
|
||||||
p_emo_6.asset_path AS emotion_6,
|
|
||||||
p_emo_7.asset_path AS emotion_7,
|
|
||||||
p_emo_8.asset_path AS emotion_8
|
|
||||||
"#
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Generate the JOIN clause for avatar paths query.
|
|
||||||
/// `props_table` should be "server.props" or "realm.props".
|
|
||||||
pub fn avatar_paths_join_clause(props_table: &str) -> String {
|
|
||||||
format!(
|
|
||||||
r#"
|
|
||||||
-- Skin layer joins
|
|
||||||
LEFT JOIN {props} p_skin_0 ON a.l_skin_0 = p_skin_0.id
|
|
||||||
LEFT JOIN {props} p_skin_1 ON a.l_skin_1 = p_skin_1.id
|
|
||||||
LEFT JOIN {props} p_skin_2 ON a.l_skin_2 = p_skin_2.id
|
|
||||||
LEFT JOIN {props} p_skin_3 ON a.l_skin_3 = p_skin_3.id
|
|
||||||
LEFT JOIN {props} p_skin_4 ON a.l_skin_4 = p_skin_4.id
|
|
||||||
LEFT JOIN {props} p_skin_5 ON a.l_skin_5 = p_skin_5.id
|
|
||||||
LEFT JOIN {props} p_skin_6 ON a.l_skin_6 = p_skin_6.id
|
|
||||||
LEFT JOIN {props} p_skin_7 ON a.l_skin_7 = p_skin_7.id
|
|
||||||
LEFT JOIN {props} p_skin_8 ON a.l_skin_8 = p_skin_8.id
|
|
||||||
-- Clothes layer joins
|
|
||||||
LEFT JOIN {props} p_clothes_0 ON a.l_clothes_0 = p_clothes_0.id
|
|
||||||
LEFT JOIN {props} p_clothes_1 ON a.l_clothes_1 = p_clothes_1.id
|
|
||||||
LEFT JOIN {props} p_clothes_2 ON a.l_clothes_2 = p_clothes_2.id
|
|
||||||
LEFT JOIN {props} p_clothes_3 ON a.l_clothes_3 = p_clothes_3.id
|
|
||||||
LEFT JOIN {props} p_clothes_4 ON a.l_clothes_4 = p_clothes_4.id
|
|
||||||
LEFT JOIN {props} p_clothes_5 ON a.l_clothes_5 = p_clothes_5.id
|
|
||||||
LEFT JOIN {props} p_clothes_6 ON a.l_clothes_6 = p_clothes_6.id
|
|
||||||
LEFT JOIN {props} p_clothes_7 ON a.l_clothes_7 = p_clothes_7.id
|
|
||||||
LEFT JOIN {props} p_clothes_8 ON a.l_clothes_8 = p_clothes_8.id
|
|
||||||
-- Accessories layer joins
|
|
||||||
LEFT JOIN {props} p_acc_0 ON a.l_accessories_0 = p_acc_0.id
|
|
||||||
LEFT JOIN {props} p_acc_1 ON a.l_accessories_1 = p_acc_1.id
|
|
||||||
LEFT JOIN {props} p_acc_2 ON a.l_accessories_2 = p_acc_2.id
|
|
||||||
LEFT JOIN {props} p_acc_3 ON a.l_accessories_3 = p_acc_3.id
|
|
||||||
LEFT JOIN {props} p_acc_4 ON a.l_accessories_4 = p_acc_4.id
|
|
||||||
LEFT JOIN {props} p_acc_5 ON a.l_accessories_5 = p_acc_5.id
|
|
||||||
LEFT JOIN {props} p_acc_6 ON a.l_accessories_6 = p_acc_6.id
|
|
||||||
LEFT JOIN {props} p_acc_7 ON a.l_accessories_7 = p_acc_7.id
|
|
||||||
LEFT JOIN {props} p_acc_8 ON a.l_accessories_8 = p_acc_8.id
|
|
||||||
-- Happy emotion layer joins (e1 - more inviting for store display)
|
|
||||||
LEFT JOIN {props} p_emo_0 ON a.e_happy_0 = p_emo_0.id
|
|
||||||
LEFT JOIN {props} p_emo_1 ON a.e_happy_1 = p_emo_1.id
|
|
||||||
LEFT JOIN {props} p_emo_2 ON a.e_happy_2 = p_emo_2.id
|
|
||||||
LEFT JOIN {props} p_emo_3 ON a.e_happy_3 = p_emo_3.id
|
|
||||||
LEFT JOIN {props} p_emo_4 ON a.e_happy_4 = p_emo_4.id
|
|
||||||
LEFT JOIN {props} p_emo_5 ON a.e_happy_5 = p_emo_5.id
|
|
||||||
LEFT JOIN {props} p_emo_6 ON a.e_happy_6 = p_emo_6.id
|
|
||||||
LEFT JOIN {props} p_emo_7 ON a.e_happy_7 = p_emo_7.id
|
|
||||||
LEFT JOIN {props} p_emo_8 ON a.e_happy_8 = p_emo_8.id
|
|
||||||
"#,
|
|
||||||
props = props_table
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Macros for Avatar Slot Extraction
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Extract AvatarSlots from an avatar struct with the standard field naming.
|
|
||||||
/// Works with both ServerAvatar and RealmAvatar.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! extract_avatar_slots {
|
|
||||||
($avatar:expr) => {{
|
|
||||||
use $crate::queries::avatar_common::AvatarSlots;
|
|
||||||
|
|
||||||
AvatarSlots {
|
|
||||||
skin: [
|
|
||||||
$avatar.l_skin_0,
|
|
||||||
$avatar.l_skin_1,
|
|
||||||
$avatar.l_skin_2,
|
|
||||||
$avatar.l_skin_3,
|
|
||||||
$avatar.l_skin_4,
|
|
||||||
$avatar.l_skin_5,
|
|
||||||
$avatar.l_skin_6,
|
|
||||||
$avatar.l_skin_7,
|
|
||||||
$avatar.l_skin_8,
|
|
||||||
],
|
|
||||||
clothes: [
|
|
||||||
$avatar.l_clothes_0,
|
|
||||||
$avatar.l_clothes_1,
|
|
||||||
$avatar.l_clothes_2,
|
|
||||||
$avatar.l_clothes_3,
|
|
||||||
$avatar.l_clothes_4,
|
|
||||||
$avatar.l_clothes_5,
|
|
||||||
$avatar.l_clothes_6,
|
|
||||||
$avatar.l_clothes_7,
|
|
||||||
$avatar.l_clothes_8,
|
|
||||||
],
|
|
||||||
accessories: [
|
|
||||||
$avatar.l_accessories_0,
|
|
||||||
$avatar.l_accessories_1,
|
|
||||||
$avatar.l_accessories_2,
|
|
||||||
$avatar.l_accessories_3,
|
|
||||||
$avatar.l_accessories_4,
|
|
||||||
$avatar.l_accessories_5,
|
|
||||||
$avatar.l_accessories_6,
|
|
||||||
$avatar.l_accessories_7,
|
|
||||||
$avatar.l_accessories_8,
|
|
||||||
],
|
|
||||||
emotions: [
|
|
||||||
// Neutral (e0)
|
|
||||||
[
|
|
||||||
$avatar.e_neutral_0,
|
|
||||||
$avatar.e_neutral_1,
|
|
||||||
$avatar.e_neutral_2,
|
|
||||||
$avatar.e_neutral_3,
|
|
||||||
$avatar.e_neutral_4,
|
|
||||||
$avatar.e_neutral_5,
|
|
||||||
$avatar.e_neutral_6,
|
|
||||||
$avatar.e_neutral_7,
|
|
||||||
$avatar.e_neutral_8,
|
|
||||||
],
|
|
||||||
// Happy (e1)
|
|
||||||
[
|
|
||||||
$avatar.e_happy_0,
|
|
||||||
$avatar.e_happy_1,
|
|
||||||
$avatar.e_happy_2,
|
|
||||||
$avatar.e_happy_3,
|
|
||||||
$avatar.e_happy_4,
|
|
||||||
$avatar.e_happy_5,
|
|
||||||
$avatar.e_happy_6,
|
|
||||||
$avatar.e_happy_7,
|
|
||||||
$avatar.e_happy_8,
|
|
||||||
],
|
|
||||||
// Sad (e2)
|
|
||||||
[
|
|
||||||
$avatar.e_sad_0,
|
|
||||||
$avatar.e_sad_1,
|
|
||||||
$avatar.e_sad_2,
|
|
||||||
$avatar.e_sad_3,
|
|
||||||
$avatar.e_sad_4,
|
|
||||||
$avatar.e_sad_5,
|
|
||||||
$avatar.e_sad_6,
|
|
||||||
$avatar.e_sad_7,
|
|
||||||
$avatar.e_sad_8,
|
|
||||||
],
|
|
||||||
// Angry (e3)
|
|
||||||
[
|
|
||||||
$avatar.e_angry_0,
|
|
||||||
$avatar.e_angry_1,
|
|
||||||
$avatar.e_angry_2,
|
|
||||||
$avatar.e_angry_3,
|
|
||||||
$avatar.e_angry_4,
|
|
||||||
$avatar.e_angry_5,
|
|
||||||
$avatar.e_angry_6,
|
|
||||||
$avatar.e_angry_7,
|
|
||||||
$avatar.e_angry_8,
|
|
||||||
],
|
|
||||||
// Surprised (e4)
|
|
||||||
[
|
|
||||||
$avatar.e_surprised_0,
|
|
||||||
$avatar.e_surprised_1,
|
|
||||||
$avatar.e_surprised_2,
|
|
||||||
$avatar.e_surprised_3,
|
|
||||||
$avatar.e_surprised_4,
|
|
||||||
$avatar.e_surprised_5,
|
|
||||||
$avatar.e_surprised_6,
|
|
||||||
$avatar.e_surprised_7,
|
|
||||||
$avatar.e_surprised_8,
|
|
||||||
],
|
|
||||||
// Thinking (e5)
|
|
||||||
[
|
|
||||||
$avatar.e_thinking_0,
|
|
||||||
$avatar.e_thinking_1,
|
|
||||||
$avatar.e_thinking_2,
|
|
||||||
$avatar.e_thinking_3,
|
|
||||||
$avatar.e_thinking_4,
|
|
||||||
$avatar.e_thinking_5,
|
|
||||||
$avatar.e_thinking_6,
|
|
||||||
$avatar.e_thinking_7,
|
|
||||||
$avatar.e_thinking_8,
|
|
||||||
],
|
|
||||||
// Laughing (e6)
|
|
||||||
[
|
|
||||||
$avatar.e_laughing_0,
|
|
||||||
$avatar.e_laughing_1,
|
|
||||||
$avatar.e_laughing_2,
|
|
||||||
$avatar.e_laughing_3,
|
|
||||||
$avatar.e_laughing_4,
|
|
||||||
$avatar.e_laughing_5,
|
|
||||||
$avatar.e_laughing_6,
|
|
||||||
$avatar.e_laughing_7,
|
|
||||||
$avatar.e_laughing_8,
|
|
||||||
],
|
|
||||||
// Crying (e7)
|
|
||||||
[
|
|
||||||
$avatar.e_crying_0,
|
|
||||||
$avatar.e_crying_1,
|
|
||||||
$avatar.e_crying_2,
|
|
||||||
$avatar.e_crying_3,
|
|
||||||
$avatar.e_crying_4,
|
|
||||||
$avatar.e_crying_5,
|
|
||||||
$avatar.e_crying_6,
|
|
||||||
$avatar.e_crying_7,
|
|
||||||
$avatar.e_crying_8,
|
|
||||||
],
|
|
||||||
// Love (e8)
|
|
||||||
[
|
|
||||||
$avatar.e_love_0,
|
|
||||||
$avatar.e_love_1,
|
|
||||||
$avatar.e_love_2,
|
|
||||||
$avatar.e_love_3,
|
|
||||||
$avatar.e_love_4,
|
|
||||||
$avatar.e_love_5,
|
|
||||||
$avatar.e_love_6,
|
|
||||||
$avatar.e_love_7,
|
|
||||||
$avatar.e_love_8,
|
|
||||||
],
|
|
||||||
// Confused (e9)
|
|
||||||
[
|
|
||||||
$avatar.e_confused_0,
|
|
||||||
$avatar.e_confused_1,
|
|
||||||
$avatar.e_confused_2,
|
|
||||||
$avatar.e_confused_3,
|
|
||||||
$avatar.e_confused_4,
|
|
||||||
$avatar.e_confused_5,
|
|
||||||
$avatar.e_confused_6,
|
|
||||||
$avatar.e_confused_7,
|
|
||||||
$avatar.e_confused_8,
|
|
||||||
],
|
|
||||||
// Sleeping (e10)
|
|
||||||
[
|
|
||||||
$avatar.e_sleeping_0,
|
|
||||||
$avatar.e_sleeping_1,
|
|
||||||
$avatar.e_sleeping_2,
|
|
||||||
$avatar.e_sleeping_3,
|
|
||||||
$avatar.e_sleeping_4,
|
|
||||||
$avatar.e_sleeping_5,
|
|
||||||
$avatar.e_sleeping_6,
|
|
||||||
$avatar.e_sleeping_7,
|
|
||||||
$avatar.e_sleeping_8,
|
|
||||||
],
|
|
||||||
// Wink (e11)
|
|
||||||
[
|
|
||||||
$avatar.e_wink_0,
|
|
||||||
$avatar.e_wink_1,
|
|
||||||
$avatar.e_wink_2,
|
|
||||||
$avatar.e_wink_3,
|
|
||||||
$avatar.e_wink_4,
|
|
||||||
$avatar.e_wink_5,
|
|
||||||
$avatar.e_wink_6,
|
|
||||||
$avatar.e_wink_7,
|
|
||||||
$avatar.e_wink_8,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use extract_avatar_slots;
|
|
||||||
|
|
@ -6,7 +6,7 @@ use sqlx::PgExecutor;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::{InventoryItem, LooseProp};
|
use crate::models::{InventoryItem, LooseProp};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
|
|
||||||
/// Ensure an instance exists for a scene.
|
/// Ensure an instance exists for a scene.
|
||||||
///
|
///
|
||||||
|
|
@ -46,14 +46,11 @@ pub async fn list_channel_loose_props<'e>(
|
||||||
lp.realm_prop_id,
|
lp.realm_prop_id,
|
||||||
ST_X(lp.position) as position_x,
|
ST_X(lp.position) as position_x,
|
||||||
ST_Y(lp.position) as position_y,
|
ST_Y(lp.position) as position_y,
|
||||||
lp.scale,
|
|
||||||
lp.dropped_by,
|
lp.dropped_by,
|
||||||
lp.expires_at,
|
lp.expires_at,
|
||||||
lp.created_at,
|
lp.created_at,
|
||||||
COALESCE(sp.name, rp.name) as prop_name,
|
COALESCE(sp.name, rp.name) as prop_name,
|
||||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
|
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path
|
||||||
lp.is_locked,
|
|
||||||
lp.locked_by
|
|
||||||
FROM scene.loose_props lp
|
FROM scene.loose_props lp
|
||||||
LEFT JOIN server.props sp ON lp.server_prop_id = sp.id
|
LEFT JOIN server.props sp ON lp.server_prop_id = sp.id
|
||||||
LEFT JOIN realm.props rp ON lp.realm_prop_id = rp.id
|
LEFT JOIN realm.props rp ON lp.realm_prop_id = rp.id
|
||||||
|
|
@ -84,7 +81,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
) -> Result<LooseProp, AppError> {
|
) -> Result<LooseProp, AppError> {
|
||||||
// Single CTE that checks existence/droppability and performs the operation atomically.
|
// Single CTE that checks existence/droppability and performs the operation atomically.
|
||||||
// Returns status flags plus the LooseProp data (if successful).
|
// Returns status flags plus the LooseProp data (if successful).
|
||||||
// Includes scale inherited from the source prop's default_scale.
|
|
||||||
let result: Option<(
|
let result: Option<(
|
||||||
bool,
|
bool,
|
||||||
bool,
|
bool,
|
||||||
|
|
@ -95,7 +91,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
Option<f32>,
|
Option<f32>,
|
||||||
Option<f32>,
|
Option<f32>,
|
||||||
Option<f32>,
|
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
Option<chrono::DateTime<chrono::Utc>>,
|
Option<chrono::DateTime<chrono::Utc>>,
|
||||||
Option<chrono::DateTime<chrono::Utc>>,
|
Option<chrono::DateTime<chrono::Utc>>,
|
||||||
|
|
@ -104,18 +99,9 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
)> = sqlx::query_as(
|
)> = sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
WITH item_info AS (
|
WITH item_info AS (
|
||||||
SELECT
|
SELECT id, is_droppable, server_prop_id, realm_prop_id, prop_name, prop_asset_path
|
||||||
inv.id,
|
FROM auth.inventory
|
||||||
inv.is_droppable,
|
WHERE id = $1 AND user_id = $2
|
||||||
inv.server_prop_id,
|
|
||||||
inv.realm_prop_id,
|
|
||||||
inv.prop_name,
|
|
||||||
inv.prop_asset_path,
|
|
||||||
COALESCE(sp.default_scale, rp.default_scale, 1.0) as default_scale
|
|
||||||
FROM auth.inventory inv
|
|
||||||
LEFT JOIN server.props sp ON inv.server_prop_id = sp.id
|
|
||||||
LEFT JOIN realm.props rp ON inv.realm_prop_id = rp.id
|
|
||||||
WHERE inv.id = $1 AND inv.user_id = $2
|
|
||||||
),
|
),
|
||||||
deleted_item AS (
|
deleted_item AS (
|
||||||
DELETE FROM auth.inventory
|
DELETE FROM auth.inventory
|
||||||
|
|
@ -128,7 +114,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
server_prop_id,
|
server_prop_id,
|
||||||
realm_prop_id,
|
realm_prop_id,
|
||||||
position,
|
position,
|
||||||
scale,
|
|
||||||
dropped_by,
|
dropped_by,
|
||||||
expires_at
|
expires_at
|
||||||
)
|
)
|
||||||
|
|
@ -137,7 +122,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
di.server_prop_id,
|
di.server_prop_id,
|
||||||
di.realm_prop_id,
|
di.realm_prop_id,
|
||||||
public.make_virtual_point($4::real, $5::real),
|
public.make_virtual_point($4::real, $5::real),
|
||||||
(SELECT default_scale FROM item_info),
|
|
||||||
$2,
|
$2,
|
||||||
now() + interval '30 minutes'
|
now() + interval '30 minutes'
|
||||||
FROM deleted_item di
|
FROM deleted_item di
|
||||||
|
|
@ -148,7 +132,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
realm_prop_id,
|
realm_prop_id,
|
||||||
ST_X(position)::real as position_x,
|
ST_X(position)::real as position_x,
|
||||||
ST_Y(position)::real as position_y,
|
ST_Y(position)::real as position_y,
|
||||||
scale,
|
|
||||||
dropped_by,
|
dropped_by,
|
||||||
expires_at,
|
expires_at,
|
||||||
created_at
|
created_at
|
||||||
|
|
@ -163,7 +146,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
ip.realm_prop_id,
|
ip.realm_prop_id,
|
||||||
ip.position_x,
|
ip.position_x,
|
||||||
ip.position_y,
|
ip.position_y,
|
||||||
ip.scale,
|
|
||||||
ip.dropped_by,
|
ip.dropped_by,
|
||||||
ip.expires_at,
|
ip.expires_at,
|
||||||
ip.created_at,
|
ip.created_at,
|
||||||
|
|
@ -189,19 +171,19 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
"Unexpected error dropping prop to canvas".to_string(),
|
"Unexpected error dropping prop to canvas".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Some((false, _, _, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
Some((false, _, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||||
// Item didn't exist
|
// Item didn't exist
|
||||||
Err(AppError::NotFound(
|
Err(AppError::NotFound(
|
||||||
"Inventory item not found or not owned by user".to_string(),
|
"Inventory item not found or not owned by user".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Some((true, false, _, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
Some((true, false, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||||
// Item existed but is not droppable
|
// Item existed but is not droppable
|
||||||
Err(AppError::Forbidden(
|
Err(AppError::Forbidden(
|
||||||
"This prop cannot be dropped - it is an essential prop".to_string(),
|
"This prop cannot be dropped - it is an essential prop".to_string(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
Some((true, true, false, _, _, _, _, _, _, _, _, _, _, _, _)) => {
|
Some((true, true, false, _, _, _, _, _, _, _, _, _, _, _)) => {
|
||||||
// Item was droppable but delete failed (shouldn't happen)
|
// Item was droppable but delete failed (shouldn't happen)
|
||||||
Err(AppError::Internal(
|
Err(AppError::Internal(
|
||||||
"Unexpected error dropping prop to canvas".to_string(),
|
"Unexpected error dropping prop to canvas".to_string(),
|
||||||
|
|
@ -217,7 +199,6 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
realm_prop_id,
|
realm_prop_id,
|
||||||
Some(position_x),
|
Some(position_x),
|
||||||
Some(position_y),
|
Some(position_y),
|
||||||
Some(scale),
|
|
||||||
dropped_by,
|
dropped_by,
|
||||||
Some(expires_at),
|
Some(expires_at),
|
||||||
Some(created_at),
|
Some(created_at),
|
||||||
|
|
@ -232,14 +213,11 @@ pub async fn drop_prop_to_canvas<'e>(
|
||||||
realm_prop_id,
|
realm_prop_id,
|
||||||
position_x: position_x.into(),
|
position_x: position_x.into(),
|
||||||
position_y: position_y.into(),
|
position_y: position_y.into(),
|
||||||
scale,
|
|
||||||
dropped_by,
|
dropped_by,
|
||||||
expires_at: Some(expires_at),
|
expires_at: Some(expires_at),
|
||||||
created_at,
|
created_at,
|
||||||
prop_name,
|
prop_name,
|
||||||
prop_asset_path,
|
prop_asset_path,
|
||||||
is_locked: false,
|
|
||||||
locked_by: None,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
|
|
@ -330,281 +308,11 @@ pub async fn pick_up_loose_prop<'e>(
|
||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Loose prop (may have expired)")?;
|
.ok_or_else(|| AppError::NotFound("Loose prop not found or has expired".to_string()))?;
|
||||||
|
|
||||||
Ok(item)
|
Ok(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the scale of a loose prop.
|
|
||||||
///
|
|
||||||
/// Server admins can update any loose prop.
|
|
||||||
/// Realm admins can update loose props in their realm.
|
|
||||||
pub async fn update_loose_prop_scale<'e>(
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
scale: f32,
|
|
||||||
) -> Result<LooseProp, AppError> {
|
|
||||||
// Validate scale range
|
|
||||||
if !(0.1..=10.0).contains(&scale) {
|
|
||||||
return Err(AppError::Validation(
|
|
||||||
"Scale must be between 0.1 and 10.0".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let prop = sqlx::query_as::<_, LooseProp>(
|
|
||||||
r#"
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE scene.loose_props
|
|
||||||
SET scale = $2
|
|
||||||
WHERE id = $1
|
|
||||||
AND (expires_at IS NULL OR expires_at > now())
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
instance_id as channel_id,
|
|
||||||
server_prop_id,
|
|
||||||
realm_prop_id,
|
|
||||||
ST_X(position) as position_x,
|
|
||||||
ST_Y(position) as position_y,
|
|
||||||
scale,
|
|
||||||
dropped_by,
|
|
||||||
expires_at,
|
|
||||||
created_at,
|
|
||||||
is_locked,
|
|
||||||
locked_by
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
u.channel_id,
|
|
||||||
u.server_prop_id,
|
|
||||||
u.realm_prop_id,
|
|
||||||
u.position_x,
|
|
||||||
u.position_y,
|
|
||||||
u.scale,
|
|
||||||
u.dropped_by,
|
|
||||||
u.expires_at,
|
|
||||||
u.created_at,
|
|
||||||
COALESCE(sp.name, rp.name) as prop_name,
|
|
||||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
|
|
||||||
u.is_locked,
|
|
||||||
u.locked_by
|
|
||||||
FROM updated u
|
|
||||||
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
|
|
||||||
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(loose_prop_id)
|
|
||||||
.bind(scale)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await?
|
|
||||||
.or_not_found("Loose prop (may have expired)")?;
|
|
||||||
|
|
||||||
Ok(prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a loose prop by ID.
|
|
||||||
pub async fn get_loose_prop_by_id<'e>(
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
) -> Result<Option<LooseProp>, AppError> {
|
|
||||||
let prop = sqlx::query_as::<_, LooseProp>(
|
|
||||||
r#"
|
|
||||||
SELECT
|
|
||||||
lp.id,
|
|
||||||
lp.instance_id as channel_id,
|
|
||||||
lp.server_prop_id,
|
|
||||||
lp.realm_prop_id,
|
|
||||||
ST_X(lp.position) as position_x,
|
|
||||||
ST_Y(lp.position) as position_y,
|
|
||||||
lp.scale,
|
|
||||||
lp.dropped_by,
|
|
||||||
lp.expires_at,
|
|
||||||
lp.created_at,
|
|
||||||
COALESCE(sp.name, rp.name) as prop_name,
|
|
||||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
|
|
||||||
lp.is_locked,
|
|
||||||
lp.locked_by
|
|
||||||
FROM scene.loose_props lp
|
|
||||||
LEFT JOIN server.props sp ON lp.server_prop_id = sp.id
|
|
||||||
LEFT JOIN realm.props rp ON lp.realm_prop_id = rp.id
|
|
||||||
WHERE lp.id = $1
|
|
||||||
AND (lp.expires_at IS NULL OR lp.expires_at > now())
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(loose_prop_id)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Move a loose prop to a new position.
|
|
||||||
pub async fn move_loose_prop<'e>(
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
x: f64,
|
|
||||||
y: f64,
|
|
||||||
) -> Result<LooseProp, AppError> {
|
|
||||||
let prop = sqlx::query_as::<_, LooseProp>(
|
|
||||||
r#"
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE scene.loose_props
|
|
||||||
SET position = public.make_virtual_point($2::real, $3::real)
|
|
||||||
WHERE id = $1
|
|
||||||
AND (expires_at IS NULL OR expires_at > now())
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
instance_id as channel_id,
|
|
||||||
server_prop_id,
|
|
||||||
realm_prop_id,
|
|
||||||
ST_X(position) as position_x,
|
|
||||||
ST_Y(position) as position_y,
|
|
||||||
scale,
|
|
||||||
dropped_by,
|
|
||||||
expires_at,
|
|
||||||
created_at,
|
|
||||||
is_locked,
|
|
||||||
locked_by
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
u.channel_id,
|
|
||||||
u.server_prop_id,
|
|
||||||
u.realm_prop_id,
|
|
||||||
u.position_x,
|
|
||||||
u.position_y,
|
|
||||||
u.scale,
|
|
||||||
u.dropped_by,
|
|
||||||
u.expires_at,
|
|
||||||
u.created_at,
|
|
||||||
COALESCE(sp.name, rp.name) as prop_name,
|
|
||||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
|
|
||||||
u.is_locked,
|
|
||||||
u.locked_by
|
|
||||||
FROM updated u
|
|
||||||
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
|
|
||||||
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(loose_prop_id)
|
|
||||||
.bind(x as f32)
|
|
||||||
.bind(y as f32)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await?
|
|
||||||
.or_not_found("Loose prop (may have expired)")?;
|
|
||||||
|
|
||||||
Ok(prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lock a loose prop (moderator only).
|
|
||||||
pub async fn lock_loose_prop<'e>(
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
locked_by: Uuid,
|
|
||||||
) -> Result<LooseProp, AppError> {
|
|
||||||
let prop = sqlx::query_as::<_, LooseProp>(
|
|
||||||
r#"
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE scene.loose_props
|
|
||||||
SET is_locked = true, locked_by = $2
|
|
||||||
WHERE id = $1
|
|
||||||
AND (expires_at IS NULL OR expires_at > now())
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
instance_id as channel_id,
|
|
||||||
server_prop_id,
|
|
||||||
realm_prop_id,
|
|
||||||
ST_X(position) as position_x,
|
|
||||||
ST_Y(position) as position_y,
|
|
||||||
scale,
|
|
||||||
dropped_by,
|
|
||||||
expires_at,
|
|
||||||
created_at,
|
|
||||||
is_locked,
|
|
||||||
locked_by
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
u.channel_id,
|
|
||||||
u.server_prop_id,
|
|
||||||
u.realm_prop_id,
|
|
||||||
u.position_x,
|
|
||||||
u.position_y,
|
|
||||||
u.scale,
|
|
||||||
u.dropped_by,
|
|
||||||
u.expires_at,
|
|
||||||
u.created_at,
|
|
||||||
COALESCE(sp.name, rp.name) as prop_name,
|
|
||||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
|
|
||||||
u.is_locked,
|
|
||||||
u.locked_by
|
|
||||||
FROM updated u
|
|
||||||
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
|
|
||||||
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(loose_prop_id)
|
|
||||||
.bind(locked_by)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await?
|
|
||||||
.or_not_found("Loose prop (may have expired)")?;
|
|
||||||
|
|
||||||
Ok(prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unlock a loose prop (moderator only).
|
|
||||||
pub async fn unlock_loose_prop<'e>(
|
|
||||||
executor: impl PgExecutor<'e>,
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
) -> Result<LooseProp, AppError> {
|
|
||||||
let prop = sqlx::query_as::<_, LooseProp>(
|
|
||||||
r#"
|
|
||||||
WITH updated AS (
|
|
||||||
UPDATE scene.loose_props
|
|
||||||
SET is_locked = false, locked_by = NULL
|
|
||||||
WHERE id = $1
|
|
||||||
AND (expires_at IS NULL OR expires_at > now())
|
|
||||||
RETURNING
|
|
||||||
id,
|
|
||||||
instance_id as channel_id,
|
|
||||||
server_prop_id,
|
|
||||||
realm_prop_id,
|
|
||||||
ST_X(position) as position_x,
|
|
||||||
ST_Y(position) as position_y,
|
|
||||||
scale,
|
|
||||||
dropped_by,
|
|
||||||
expires_at,
|
|
||||||
created_at,
|
|
||||||
is_locked,
|
|
||||||
locked_by
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
u.channel_id,
|
|
||||||
u.server_prop_id,
|
|
||||||
u.realm_prop_id,
|
|
||||||
u.position_x,
|
|
||||||
u.position_y,
|
|
||||||
u.scale,
|
|
||||||
u.dropped_by,
|
|
||||||
u.expires_at,
|
|
||||||
u.created_at,
|
|
||||||
COALESCE(sp.name, rp.name) as prop_name,
|
|
||||||
COALESCE(sp.asset_path, rp.asset_path) as prop_asset_path,
|
|
||||||
u.is_locked,
|
|
||||||
u.locked_by
|
|
||||||
FROM updated u
|
|
||||||
LEFT JOIN server.props sp ON u.server_prop_id = sp.id
|
|
||||||
LEFT JOIN realm.props rp ON u.realm_prop_id = rp.id
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.bind(loose_prop_id)
|
|
||||||
.fetch_optional(executor)
|
|
||||||
.await?
|
|
||||||
.or_not_found("Loose prop (may have expired)")?;
|
|
||||||
|
|
||||||
Ok(prop)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete expired loose props.
|
/// Delete expired loose props.
|
||||||
///
|
///
|
||||||
/// Returns the number of props deleted.
|
/// Returns the number of props deleted.
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ pub async fn get_server_prop_by_id<'e>(
|
||||||
default_layer,
|
default_layer,
|
||||||
default_emotion,
|
default_emotion,
|
||||||
default_position,
|
default_position,
|
||||||
default_scale,
|
|
||||||
is_unique,
|
is_unique,
|
||||||
is_transferable,
|
is_transferable,
|
||||||
is_portable,
|
is_portable,
|
||||||
|
|
@ -117,23 +116,20 @@ pub async fn create_server_prop<'e>(
|
||||||
|
|
||||||
let is_droppable = req.droppable.unwrap_or(true);
|
let is_droppable = req.droppable.unwrap_or(true);
|
||||||
let is_public = req.public.unwrap_or(false);
|
let is_public = req.public.unwrap_or(false);
|
||||||
let default_scale = req.default_scale.unwrap_or(1.0);
|
|
||||||
|
|
||||||
let prop = sqlx::query_as::<_, ServerProp>(
|
let prop = sqlx::query_as::<_, ServerProp>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO server.props (
|
INSERT INTO server.props (
|
||||||
name, slug, description, tags, asset_path,
|
name, slug, description, tags, asset_path,
|
||||||
default_layer, default_emotion, default_position,
|
default_layer, default_emotion, default_position,
|
||||||
default_scale,
|
|
||||||
is_droppable, is_public,
|
is_droppable, is_public,
|
||||||
created_by
|
created_by
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6::server.avatar_layer, $7::server.emotion_state, $8,
|
$6::server.avatar_layer, $7::server.emotion_state, $8,
|
||||||
$9,
|
$9, $10,
|
||||||
$10, $11,
|
$11
|
||||||
$12
|
|
||||||
)
|
)
|
||||||
RETURNING
|
RETURNING
|
||||||
id,
|
id,
|
||||||
|
|
@ -146,7 +142,6 @@ pub async fn create_server_prop<'e>(
|
||||||
default_layer,
|
default_layer,
|
||||||
default_emotion,
|
default_emotion,
|
||||||
default_position,
|
default_position,
|
||||||
default_scale,
|
|
||||||
is_unique,
|
is_unique,
|
||||||
is_transferable,
|
is_transferable,
|
||||||
is_portable,
|
is_portable,
|
||||||
|
|
@ -168,7 +163,6 @@ pub async fn create_server_prop<'e>(
|
||||||
.bind(&default_layer)
|
.bind(&default_layer)
|
||||||
.bind(&default_emotion)
|
.bind(&default_emotion)
|
||||||
.bind(default_position)
|
.bind(default_position)
|
||||||
.bind(default_scale)
|
|
||||||
.bind(is_droppable)
|
.bind(is_droppable)
|
||||||
.bind(is_public)
|
.bind(is_public)
|
||||||
.bind(created_by)
|
.bind(created_by)
|
||||||
|
|
@ -213,23 +207,20 @@ pub async fn upsert_server_prop<'e>(
|
||||||
|
|
||||||
let is_droppable = req.droppable.unwrap_or(true);
|
let is_droppable = req.droppable.unwrap_or(true);
|
||||||
let is_public = req.public.unwrap_or(false);
|
let is_public = req.public.unwrap_or(false);
|
||||||
let default_scale = req.default_scale.unwrap_or(1.0);
|
|
||||||
|
|
||||||
let prop = sqlx::query_as::<_, ServerProp>(
|
let prop = sqlx::query_as::<_, ServerProp>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO server.props (
|
INSERT INTO server.props (
|
||||||
name, slug, description, tags, asset_path,
|
name, slug, description, tags, asset_path,
|
||||||
default_layer, default_emotion, default_position,
|
default_layer, default_emotion, default_position,
|
||||||
default_scale,
|
|
||||||
is_droppable, is_public,
|
is_droppable, is_public,
|
||||||
created_by
|
created_by
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6::server.avatar_layer, $7::server.emotion_state, $8,
|
$6::server.avatar_layer, $7::server.emotion_state, $8,
|
||||||
$9,
|
$9, $10,
|
||||||
$10, $11,
|
$11
|
||||||
$12
|
|
||||||
)
|
)
|
||||||
ON CONFLICT (slug) DO UPDATE SET
|
ON CONFLICT (slug) DO UPDATE SET
|
||||||
name = EXCLUDED.name,
|
name = EXCLUDED.name,
|
||||||
|
|
@ -239,7 +230,6 @@ pub async fn upsert_server_prop<'e>(
|
||||||
default_layer = EXCLUDED.default_layer,
|
default_layer = EXCLUDED.default_layer,
|
||||||
default_emotion = EXCLUDED.default_emotion,
|
default_emotion = EXCLUDED.default_emotion,
|
||||||
default_position = EXCLUDED.default_position,
|
default_position = EXCLUDED.default_position,
|
||||||
default_scale = EXCLUDED.default_scale,
|
|
||||||
is_droppable = EXCLUDED.is_droppable,
|
is_droppable = EXCLUDED.is_droppable,
|
||||||
is_public = EXCLUDED.is_public,
|
is_public = EXCLUDED.is_public,
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
|
|
@ -254,7 +244,6 @@ pub async fn upsert_server_prop<'e>(
|
||||||
default_layer,
|
default_layer,
|
||||||
default_emotion,
|
default_emotion,
|
||||||
default_position,
|
default_position,
|
||||||
default_scale,
|
|
||||||
is_unique,
|
is_unique,
|
||||||
is_transferable,
|
is_transferable,
|
||||||
is_portable,
|
is_portable,
|
||||||
|
|
@ -276,7 +265,6 @@ pub async fn upsert_server_prop<'e>(
|
||||||
.bind(&default_layer)
|
.bind(&default_layer)
|
||||||
.bind(&default_emotion)
|
.bind(&default_emotion)
|
||||||
.bind(default_position)
|
.bind(default_position)
|
||||||
.bind(default_scale)
|
|
||||||
.bind(is_droppable)
|
.bind(is_droppable)
|
||||||
.bind(is_public)
|
.bind(is_public)
|
||||||
.bind(created_by)
|
.bind(created_by)
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,15 @@
|
||||||
//! Realm avatars are pre-configured avatar configurations specific to a realm.
|
//! Realm avatars are pre-configured avatar configurations specific to a realm.
|
||||||
//! They reference realm.props directly (not inventory items).
|
//! They reference realm.props directly (not inventory items).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use sqlx::PgExecutor;
|
use sqlx::PgExecutor;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::extract_avatar_slots;
|
use crate::models::{AvatarRenderData, EmotionState, RealmAvatar};
|
||||||
use crate::models::{AvatarRenderData, EmotionState, RealmAvatar, RealmAvatarWithPaths};
|
|
||||||
use crate::queries::avatar_common::{
|
|
||||||
avatar_paths_join_clause, avatar_paths_select_clause, build_prop_map,
|
|
||||||
resolve_slots_to_render_data,
|
|
||||||
};
|
|
||||||
use chattyness_error::AppError;
|
use chattyness_error::AppError;
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Basic Queries
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Get a realm avatar by slug within a realm.
|
/// Get a realm avatar by slug within a realm.
|
||||||
pub async fn get_realm_avatar_by_slug<'e>(
|
pub async fn get_realm_avatar_by_slug<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
|
|
@ -79,9 +72,7 @@ pub async fn list_public_realm_avatars<'e>(
|
||||||
Ok(avatars)
|
Ok(avatars)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
use crate::models::RealmAvatarWithPaths;
|
||||||
// Avatar with Paths Queries
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Row type for realm avatar with paths query.
|
/// Row type for realm avatar with paths query.
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
|
|
@ -119,7 +110,7 @@ struct RealmAvatarWithPathsRow {
|
||||||
accessories_6: Option<String>,
|
accessories_6: Option<String>,
|
||||||
accessories_7: Option<String>,
|
accessories_7: Option<String>,
|
||||||
accessories_8: Option<String>,
|
accessories_8: Option<String>,
|
||||||
// Happy emotion layer paths
|
// Happy emotion layer paths (e1 - more inviting for store display)
|
||||||
emotion_0: Option<String>,
|
emotion_0: Option<String>,
|
||||||
emotion_1: Option<String>,
|
emotion_1: Option<String>,
|
||||||
emotion_2: Option<String>,
|
emotion_2: Option<String>,
|
||||||
|
|
@ -162,25 +153,104 @@ impl From<RealmAvatarWithPathsRow> for RealmAvatarWithPaths {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all active public realm avatars with resolved asset paths.
|
/// List all active public realm avatars with resolved asset paths.
|
||||||
|
///
|
||||||
|
/// Joins with the props table to resolve prop UUIDs to asset paths,
|
||||||
|
/// suitable for client-side rendering without additional lookups.
|
||||||
pub async fn list_public_realm_avatars_with_paths<'e>(
|
pub async fn list_public_realm_avatars_with_paths<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
realm_id: Uuid,
|
realm_id: Uuid,
|
||||||
) -> Result<Vec<RealmAvatarWithPaths>, AppError> {
|
) -> Result<Vec<RealmAvatarWithPaths>, AppError> {
|
||||||
let join_clause = avatar_paths_join_clause("realm.props");
|
let rows = sqlx::query_as::<_, RealmAvatarWithPathsRow>(
|
||||||
let query = format!(
|
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
{}
|
a.id,
|
||||||
|
a.name,
|
||||||
|
a.description,
|
||||||
|
-- Skin layer
|
||||||
|
p_skin_0.asset_path AS skin_0,
|
||||||
|
p_skin_1.asset_path AS skin_1,
|
||||||
|
p_skin_2.asset_path AS skin_2,
|
||||||
|
p_skin_3.asset_path AS skin_3,
|
||||||
|
p_skin_4.asset_path AS skin_4,
|
||||||
|
p_skin_5.asset_path AS skin_5,
|
||||||
|
p_skin_6.asset_path AS skin_6,
|
||||||
|
p_skin_7.asset_path AS skin_7,
|
||||||
|
p_skin_8.asset_path AS skin_8,
|
||||||
|
-- Clothes layer
|
||||||
|
p_clothes_0.asset_path AS clothes_0,
|
||||||
|
p_clothes_1.asset_path AS clothes_1,
|
||||||
|
p_clothes_2.asset_path AS clothes_2,
|
||||||
|
p_clothes_3.asset_path AS clothes_3,
|
||||||
|
p_clothes_4.asset_path AS clothes_4,
|
||||||
|
p_clothes_5.asset_path AS clothes_5,
|
||||||
|
p_clothes_6.asset_path AS clothes_6,
|
||||||
|
p_clothes_7.asset_path AS clothes_7,
|
||||||
|
p_clothes_8.asset_path AS clothes_8,
|
||||||
|
-- Accessories layer
|
||||||
|
p_acc_0.asset_path AS accessories_0,
|
||||||
|
p_acc_1.asset_path AS accessories_1,
|
||||||
|
p_acc_2.asset_path AS accessories_2,
|
||||||
|
p_acc_3.asset_path AS accessories_3,
|
||||||
|
p_acc_4.asset_path AS accessories_4,
|
||||||
|
p_acc_5.asset_path AS accessories_5,
|
||||||
|
p_acc_6.asset_path AS accessories_6,
|
||||||
|
p_acc_7.asset_path AS accessories_7,
|
||||||
|
p_acc_8.asset_path AS accessories_8,
|
||||||
|
-- Happy emotion layer (e1 - more inviting for store display)
|
||||||
|
p_emo_0.asset_path AS emotion_0,
|
||||||
|
p_emo_1.asset_path AS emotion_1,
|
||||||
|
p_emo_2.asset_path AS emotion_2,
|
||||||
|
p_emo_3.asset_path AS emotion_3,
|
||||||
|
p_emo_4.asset_path AS emotion_4,
|
||||||
|
p_emo_5.asset_path AS emotion_5,
|
||||||
|
p_emo_6.asset_path AS emotion_6,
|
||||||
|
p_emo_7.asset_path AS emotion_7,
|
||||||
|
p_emo_8.asset_path AS emotion_8
|
||||||
FROM realm.avatars a
|
FROM realm.avatars a
|
||||||
{}
|
-- Skin layer joins
|
||||||
|
LEFT JOIN realm.props p_skin_0 ON a.l_skin_0 = p_skin_0.id
|
||||||
|
LEFT JOIN realm.props p_skin_1 ON a.l_skin_1 = p_skin_1.id
|
||||||
|
LEFT JOIN realm.props p_skin_2 ON a.l_skin_2 = p_skin_2.id
|
||||||
|
LEFT JOIN realm.props p_skin_3 ON a.l_skin_3 = p_skin_3.id
|
||||||
|
LEFT JOIN realm.props p_skin_4 ON a.l_skin_4 = p_skin_4.id
|
||||||
|
LEFT JOIN realm.props p_skin_5 ON a.l_skin_5 = p_skin_5.id
|
||||||
|
LEFT JOIN realm.props p_skin_6 ON a.l_skin_6 = p_skin_6.id
|
||||||
|
LEFT JOIN realm.props p_skin_7 ON a.l_skin_7 = p_skin_7.id
|
||||||
|
LEFT JOIN realm.props p_skin_8 ON a.l_skin_8 = p_skin_8.id
|
||||||
|
-- Clothes layer joins
|
||||||
|
LEFT JOIN realm.props p_clothes_0 ON a.l_clothes_0 = p_clothes_0.id
|
||||||
|
LEFT JOIN realm.props p_clothes_1 ON a.l_clothes_1 = p_clothes_1.id
|
||||||
|
LEFT JOIN realm.props p_clothes_2 ON a.l_clothes_2 = p_clothes_2.id
|
||||||
|
LEFT JOIN realm.props p_clothes_3 ON a.l_clothes_3 = p_clothes_3.id
|
||||||
|
LEFT JOIN realm.props p_clothes_4 ON a.l_clothes_4 = p_clothes_4.id
|
||||||
|
LEFT JOIN realm.props p_clothes_5 ON a.l_clothes_5 = p_clothes_5.id
|
||||||
|
LEFT JOIN realm.props p_clothes_6 ON a.l_clothes_6 = p_clothes_6.id
|
||||||
|
LEFT JOIN realm.props p_clothes_7 ON a.l_clothes_7 = p_clothes_7.id
|
||||||
|
LEFT JOIN realm.props p_clothes_8 ON a.l_clothes_8 = p_clothes_8.id
|
||||||
|
-- Accessories layer joins
|
||||||
|
LEFT JOIN realm.props p_acc_0 ON a.l_accessories_0 = p_acc_0.id
|
||||||
|
LEFT JOIN realm.props p_acc_1 ON a.l_accessories_1 = p_acc_1.id
|
||||||
|
LEFT JOIN realm.props p_acc_2 ON a.l_accessories_2 = p_acc_2.id
|
||||||
|
LEFT JOIN realm.props p_acc_3 ON a.l_accessories_3 = p_acc_3.id
|
||||||
|
LEFT JOIN realm.props p_acc_4 ON a.l_accessories_4 = p_acc_4.id
|
||||||
|
LEFT JOIN realm.props p_acc_5 ON a.l_accessories_5 = p_acc_5.id
|
||||||
|
LEFT JOIN realm.props p_acc_6 ON a.l_accessories_6 = p_acc_6.id
|
||||||
|
LEFT JOIN realm.props p_acc_7 ON a.l_accessories_7 = p_acc_7.id
|
||||||
|
LEFT JOIN realm.props p_acc_8 ON a.l_accessories_8 = p_acc_8.id
|
||||||
|
-- Happy emotion layer joins (e1 - more inviting for store display)
|
||||||
|
LEFT JOIN realm.props p_emo_0 ON a.e_happy_0 = p_emo_0.id
|
||||||
|
LEFT JOIN realm.props p_emo_1 ON a.e_happy_1 = p_emo_1.id
|
||||||
|
LEFT JOIN realm.props p_emo_2 ON a.e_happy_2 = p_emo_2.id
|
||||||
|
LEFT JOIN realm.props p_emo_3 ON a.e_happy_3 = p_emo_3.id
|
||||||
|
LEFT JOIN realm.props p_emo_4 ON a.e_happy_4 = p_emo_4.id
|
||||||
|
LEFT JOIN realm.props p_emo_5 ON a.e_happy_5 = p_emo_5.id
|
||||||
|
LEFT JOIN realm.props p_emo_6 ON a.e_happy_6 = p_emo_6.id
|
||||||
|
LEFT JOIN realm.props p_emo_7 ON a.e_happy_7 = p_emo_7.id
|
||||||
|
LEFT JOIN realm.props p_emo_8 ON a.e_happy_8 = p_emo_8.id
|
||||||
WHERE a.realm_id = $1 AND a.is_active = true AND a.is_public = true
|
WHERE a.realm_id = $1 AND a.is_active = true AND a.is_public = true
|
||||||
ORDER BY a.name ASC
|
ORDER BY a.name ASC
|
||||||
"#,
|
"#,
|
||||||
avatar_paths_select_clause(),
|
)
|
||||||
join_clause
|
|
||||||
);
|
|
||||||
|
|
||||||
let rows = sqlx::query_as::<_, RealmAvatarWithPathsRow>(&query)
|
|
||||||
.bind(realm_id)
|
.bind(realm_id)
|
||||||
.fetch_all(executor)
|
.fetch_all(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -188,25 +258,130 @@ pub async fn list_public_realm_avatars_with_paths<'e>(
|
||||||
Ok(rows.into_iter().map(RealmAvatarWithPaths::from).collect())
|
Ok(rows.into_iter().map(RealmAvatarWithPaths::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
/// Row type for prop asset lookup.
|
||||||
// Render Data Resolution
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
// =============================================================================
|
struct PropAssetRow {
|
||||||
|
id: Uuid,
|
||||||
|
asset_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a realm avatar to render data.
|
/// Resolve a realm avatar to render data.
|
||||||
|
/// Joins the avatar's prop UUIDs with realm.props to get asset paths.
|
||||||
pub async fn resolve_realm_avatar_to_render_data<'e>(
|
pub async fn resolve_realm_avatar_to_render_data<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
avatar: &RealmAvatar,
|
avatar: &RealmAvatar,
|
||||||
current_emotion: EmotionState,
|
current_emotion: EmotionState,
|
||||||
) -> Result<AvatarRenderData, AppError> {
|
) -> Result<AvatarRenderData, AppError> {
|
||||||
let slots = extract_avatar_slots!(avatar);
|
// Collect all non-null prop UUIDs
|
||||||
let prop_ids = slots.collect_render_prop_ids(current_emotion);
|
let mut prop_ids: Vec<Uuid> = Vec::new();
|
||||||
let prop_map = build_prop_map(executor, &prop_ids, "realm.props").await?;
|
|
||||||
Ok(resolve_slots_to_render_data(avatar.id, &slots, current_emotion, &prop_map))
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// Content layers
|
||||||
// Forced Avatar Management
|
for id in [
|
||||||
// =============================================================================
|
avatar.l_skin_0, avatar.l_skin_1, avatar.l_skin_2,
|
||||||
|
avatar.l_skin_3, avatar.l_skin_4, avatar.l_skin_5,
|
||||||
|
avatar.l_skin_6, avatar.l_skin_7, avatar.l_skin_8,
|
||||||
|
avatar.l_clothes_0, avatar.l_clothes_1, avatar.l_clothes_2,
|
||||||
|
avatar.l_clothes_3, avatar.l_clothes_4, avatar.l_clothes_5,
|
||||||
|
avatar.l_clothes_6, avatar.l_clothes_7, avatar.l_clothes_8,
|
||||||
|
avatar.l_accessories_0, avatar.l_accessories_1, avatar.l_accessories_2,
|
||||||
|
avatar.l_accessories_3, avatar.l_accessories_4, avatar.l_accessories_5,
|
||||||
|
avatar.l_accessories_6, avatar.l_accessories_7, avatar.l_accessories_8,
|
||||||
|
].iter().flatten() {
|
||||||
|
prop_ids.push(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get emotion layer slots based on current emotion
|
||||||
|
let emotion_slots: [Option<Uuid>; 9] = match current_emotion {
|
||||||
|
EmotionState::Neutral => [avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2,
|
||||||
|
avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5,
|
||||||
|
avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8],
|
||||||
|
EmotionState::Happy => [avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2,
|
||||||
|
avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5,
|
||||||
|
avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8],
|
||||||
|
EmotionState::Sad => [avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2,
|
||||||
|
avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5,
|
||||||
|
avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8],
|
||||||
|
EmotionState::Angry => [avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2,
|
||||||
|
avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5,
|
||||||
|
avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8],
|
||||||
|
EmotionState::Surprised => [avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2,
|
||||||
|
avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5,
|
||||||
|
avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8],
|
||||||
|
EmotionState::Thinking => [avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2,
|
||||||
|
avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5,
|
||||||
|
avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8],
|
||||||
|
EmotionState::Laughing => [avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2,
|
||||||
|
avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5,
|
||||||
|
avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8],
|
||||||
|
EmotionState::Crying => [avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2,
|
||||||
|
avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5,
|
||||||
|
avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8],
|
||||||
|
EmotionState::Love => [avatar.e_love_0, avatar.e_love_1, avatar.e_love_2,
|
||||||
|
avatar.e_love_3, avatar.e_love_4, avatar.e_love_5,
|
||||||
|
avatar.e_love_6, avatar.e_love_7, avatar.e_love_8],
|
||||||
|
EmotionState::Confused => [avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2,
|
||||||
|
avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5,
|
||||||
|
avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8],
|
||||||
|
EmotionState::Sleeping => [avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2,
|
||||||
|
avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5,
|
||||||
|
avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8],
|
||||||
|
EmotionState::Wink => [avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2,
|
||||||
|
avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5,
|
||||||
|
avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8],
|
||||||
|
};
|
||||||
|
|
||||||
|
for id in emotion_slots.iter().flatten() {
|
||||||
|
prop_ids.push(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk lookup all prop asset paths from realm.props
|
||||||
|
let prop_map: HashMap<Uuid, String> = if prop_ids.is_empty() {
|
||||||
|
HashMap::new()
|
||||||
|
} else {
|
||||||
|
let rows = sqlx::query_as::<_, PropAssetRow>(
|
||||||
|
r#"
|
||||||
|
SELECT id, asset_path
|
||||||
|
FROM realm.props
|
||||||
|
WHERE id = ANY($1)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&prop_ids)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
rows.into_iter().map(|r| (r.id, r.asset_path)).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to look up path
|
||||||
|
let get_path = |id: Option<Uuid>| -> Option<String> {
|
||||||
|
id.and_then(|id| prop_map.get(&id).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AvatarRenderData {
|
||||||
|
avatar_id: avatar.id,
|
||||||
|
current_emotion,
|
||||||
|
skin_layer: [
|
||||||
|
get_path(avatar.l_skin_0), get_path(avatar.l_skin_1), get_path(avatar.l_skin_2),
|
||||||
|
get_path(avatar.l_skin_3), get_path(avatar.l_skin_4), get_path(avatar.l_skin_5),
|
||||||
|
get_path(avatar.l_skin_6), get_path(avatar.l_skin_7), get_path(avatar.l_skin_8),
|
||||||
|
],
|
||||||
|
clothes_layer: [
|
||||||
|
get_path(avatar.l_clothes_0), get_path(avatar.l_clothes_1), get_path(avatar.l_clothes_2),
|
||||||
|
get_path(avatar.l_clothes_3), get_path(avatar.l_clothes_4), get_path(avatar.l_clothes_5),
|
||||||
|
get_path(avatar.l_clothes_6), get_path(avatar.l_clothes_7), get_path(avatar.l_clothes_8),
|
||||||
|
],
|
||||||
|
accessories_layer: [
|
||||||
|
get_path(avatar.l_accessories_0), get_path(avatar.l_accessories_1), get_path(avatar.l_accessories_2),
|
||||||
|
get_path(avatar.l_accessories_3), get_path(avatar.l_accessories_4), get_path(avatar.l_accessories_5),
|
||||||
|
get_path(avatar.l_accessories_6), get_path(avatar.l_accessories_7), get_path(avatar.l_accessories_8),
|
||||||
|
],
|
||||||
|
emotion_layer: [
|
||||||
|
get_path(emotion_slots[0]), get_path(emotion_slots[1]), get_path(emotion_slots[2]),
|
||||||
|
get_path(emotion_slots[3]), get_path(emotion_slots[4]), get_path(emotion_slots[5]),
|
||||||
|
get_path(emotion_slots[6]), get_path(emotion_slots[7]), get_path(emotion_slots[8]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a forced realm avatar to a user.
|
/// Apply a forced realm avatar to a user.
|
||||||
pub async fn apply_forced_realm_avatar<'e>(
|
pub async fn apply_forced_realm_avatar<'e>(
|
||||||
|
|
@ -401,65 +576,155 @@ pub async fn create_realm_avatar<'e>(
|
||||||
.bind(&req.thumbnail_path)
|
.bind(&req.thumbnail_path)
|
||||||
.bind(created_by)
|
.bind(created_by)
|
||||||
// Skin layer
|
// Skin layer
|
||||||
.bind(req.l_skin_0).bind(req.l_skin_1).bind(req.l_skin_2)
|
.bind(req.l_skin_0)
|
||||||
.bind(req.l_skin_3).bind(req.l_skin_4).bind(req.l_skin_5)
|
.bind(req.l_skin_1)
|
||||||
.bind(req.l_skin_6).bind(req.l_skin_7).bind(req.l_skin_8)
|
.bind(req.l_skin_2)
|
||||||
|
.bind(req.l_skin_3)
|
||||||
|
.bind(req.l_skin_4)
|
||||||
|
.bind(req.l_skin_5)
|
||||||
|
.bind(req.l_skin_6)
|
||||||
|
.bind(req.l_skin_7)
|
||||||
|
.bind(req.l_skin_8)
|
||||||
// Clothes layer
|
// Clothes layer
|
||||||
.bind(req.l_clothes_0).bind(req.l_clothes_1).bind(req.l_clothes_2)
|
.bind(req.l_clothes_0)
|
||||||
.bind(req.l_clothes_3).bind(req.l_clothes_4).bind(req.l_clothes_5)
|
.bind(req.l_clothes_1)
|
||||||
.bind(req.l_clothes_6).bind(req.l_clothes_7).bind(req.l_clothes_8)
|
.bind(req.l_clothes_2)
|
||||||
|
.bind(req.l_clothes_3)
|
||||||
|
.bind(req.l_clothes_4)
|
||||||
|
.bind(req.l_clothes_5)
|
||||||
|
.bind(req.l_clothes_6)
|
||||||
|
.bind(req.l_clothes_7)
|
||||||
|
.bind(req.l_clothes_8)
|
||||||
// Accessories layer
|
// Accessories layer
|
||||||
.bind(req.l_accessories_0).bind(req.l_accessories_1).bind(req.l_accessories_2)
|
.bind(req.l_accessories_0)
|
||||||
.bind(req.l_accessories_3).bind(req.l_accessories_4).bind(req.l_accessories_5)
|
.bind(req.l_accessories_1)
|
||||||
.bind(req.l_accessories_6).bind(req.l_accessories_7).bind(req.l_accessories_8)
|
.bind(req.l_accessories_2)
|
||||||
|
.bind(req.l_accessories_3)
|
||||||
|
.bind(req.l_accessories_4)
|
||||||
|
.bind(req.l_accessories_5)
|
||||||
|
.bind(req.l_accessories_6)
|
||||||
|
.bind(req.l_accessories_7)
|
||||||
|
.bind(req.l_accessories_8)
|
||||||
// Neutral emotion
|
// Neutral emotion
|
||||||
.bind(req.e_neutral_0).bind(req.e_neutral_1).bind(req.e_neutral_2)
|
.bind(req.e_neutral_0)
|
||||||
.bind(req.e_neutral_3).bind(req.e_neutral_4).bind(req.e_neutral_5)
|
.bind(req.e_neutral_1)
|
||||||
.bind(req.e_neutral_6).bind(req.e_neutral_7).bind(req.e_neutral_8)
|
.bind(req.e_neutral_2)
|
||||||
|
.bind(req.e_neutral_3)
|
||||||
|
.bind(req.e_neutral_4)
|
||||||
|
.bind(req.e_neutral_5)
|
||||||
|
.bind(req.e_neutral_6)
|
||||||
|
.bind(req.e_neutral_7)
|
||||||
|
.bind(req.e_neutral_8)
|
||||||
// Happy emotion
|
// Happy emotion
|
||||||
.bind(req.e_happy_0).bind(req.e_happy_1).bind(req.e_happy_2)
|
.bind(req.e_happy_0)
|
||||||
.bind(req.e_happy_3).bind(req.e_happy_4).bind(req.e_happy_5)
|
.bind(req.e_happy_1)
|
||||||
.bind(req.e_happy_6).bind(req.e_happy_7).bind(req.e_happy_8)
|
.bind(req.e_happy_2)
|
||||||
|
.bind(req.e_happy_3)
|
||||||
|
.bind(req.e_happy_4)
|
||||||
|
.bind(req.e_happy_5)
|
||||||
|
.bind(req.e_happy_6)
|
||||||
|
.bind(req.e_happy_7)
|
||||||
|
.bind(req.e_happy_8)
|
||||||
// Sad emotion
|
// Sad emotion
|
||||||
.bind(req.e_sad_0).bind(req.e_sad_1).bind(req.e_sad_2)
|
.bind(req.e_sad_0)
|
||||||
.bind(req.e_sad_3).bind(req.e_sad_4).bind(req.e_sad_5)
|
.bind(req.e_sad_1)
|
||||||
.bind(req.e_sad_6).bind(req.e_sad_7).bind(req.e_sad_8)
|
.bind(req.e_sad_2)
|
||||||
|
.bind(req.e_sad_3)
|
||||||
|
.bind(req.e_sad_4)
|
||||||
|
.bind(req.e_sad_5)
|
||||||
|
.bind(req.e_sad_6)
|
||||||
|
.bind(req.e_sad_7)
|
||||||
|
.bind(req.e_sad_8)
|
||||||
// Angry emotion
|
// Angry emotion
|
||||||
.bind(req.e_angry_0).bind(req.e_angry_1).bind(req.e_angry_2)
|
.bind(req.e_angry_0)
|
||||||
.bind(req.e_angry_3).bind(req.e_angry_4).bind(req.e_angry_5)
|
.bind(req.e_angry_1)
|
||||||
.bind(req.e_angry_6).bind(req.e_angry_7).bind(req.e_angry_8)
|
.bind(req.e_angry_2)
|
||||||
|
.bind(req.e_angry_3)
|
||||||
|
.bind(req.e_angry_4)
|
||||||
|
.bind(req.e_angry_5)
|
||||||
|
.bind(req.e_angry_6)
|
||||||
|
.bind(req.e_angry_7)
|
||||||
|
.bind(req.e_angry_8)
|
||||||
// Surprised emotion
|
// Surprised emotion
|
||||||
.bind(req.e_surprised_0).bind(req.e_surprised_1).bind(req.e_surprised_2)
|
.bind(req.e_surprised_0)
|
||||||
.bind(req.e_surprised_3).bind(req.e_surprised_4).bind(req.e_surprised_5)
|
.bind(req.e_surprised_1)
|
||||||
.bind(req.e_surprised_6).bind(req.e_surprised_7).bind(req.e_surprised_8)
|
.bind(req.e_surprised_2)
|
||||||
|
.bind(req.e_surprised_3)
|
||||||
|
.bind(req.e_surprised_4)
|
||||||
|
.bind(req.e_surprised_5)
|
||||||
|
.bind(req.e_surprised_6)
|
||||||
|
.bind(req.e_surprised_7)
|
||||||
|
.bind(req.e_surprised_8)
|
||||||
// Thinking emotion
|
// Thinking emotion
|
||||||
.bind(req.e_thinking_0).bind(req.e_thinking_1).bind(req.e_thinking_2)
|
.bind(req.e_thinking_0)
|
||||||
.bind(req.e_thinking_3).bind(req.e_thinking_4).bind(req.e_thinking_5)
|
.bind(req.e_thinking_1)
|
||||||
.bind(req.e_thinking_6).bind(req.e_thinking_7).bind(req.e_thinking_8)
|
.bind(req.e_thinking_2)
|
||||||
|
.bind(req.e_thinking_3)
|
||||||
|
.bind(req.e_thinking_4)
|
||||||
|
.bind(req.e_thinking_5)
|
||||||
|
.bind(req.e_thinking_6)
|
||||||
|
.bind(req.e_thinking_7)
|
||||||
|
.bind(req.e_thinking_8)
|
||||||
// Laughing emotion
|
// Laughing emotion
|
||||||
.bind(req.e_laughing_0).bind(req.e_laughing_1).bind(req.e_laughing_2)
|
.bind(req.e_laughing_0)
|
||||||
.bind(req.e_laughing_3).bind(req.e_laughing_4).bind(req.e_laughing_5)
|
.bind(req.e_laughing_1)
|
||||||
.bind(req.e_laughing_6).bind(req.e_laughing_7).bind(req.e_laughing_8)
|
.bind(req.e_laughing_2)
|
||||||
|
.bind(req.e_laughing_3)
|
||||||
|
.bind(req.e_laughing_4)
|
||||||
|
.bind(req.e_laughing_5)
|
||||||
|
.bind(req.e_laughing_6)
|
||||||
|
.bind(req.e_laughing_7)
|
||||||
|
.bind(req.e_laughing_8)
|
||||||
// Crying emotion
|
// Crying emotion
|
||||||
.bind(req.e_crying_0).bind(req.e_crying_1).bind(req.e_crying_2)
|
.bind(req.e_crying_0)
|
||||||
.bind(req.e_crying_3).bind(req.e_crying_4).bind(req.e_crying_5)
|
.bind(req.e_crying_1)
|
||||||
.bind(req.e_crying_6).bind(req.e_crying_7).bind(req.e_crying_8)
|
.bind(req.e_crying_2)
|
||||||
|
.bind(req.e_crying_3)
|
||||||
|
.bind(req.e_crying_4)
|
||||||
|
.bind(req.e_crying_5)
|
||||||
|
.bind(req.e_crying_6)
|
||||||
|
.bind(req.e_crying_7)
|
||||||
|
.bind(req.e_crying_8)
|
||||||
// Love emotion
|
// Love emotion
|
||||||
.bind(req.e_love_0).bind(req.e_love_1).bind(req.e_love_2)
|
.bind(req.e_love_0)
|
||||||
.bind(req.e_love_3).bind(req.e_love_4).bind(req.e_love_5)
|
.bind(req.e_love_1)
|
||||||
.bind(req.e_love_6).bind(req.e_love_7).bind(req.e_love_8)
|
.bind(req.e_love_2)
|
||||||
|
.bind(req.e_love_3)
|
||||||
|
.bind(req.e_love_4)
|
||||||
|
.bind(req.e_love_5)
|
||||||
|
.bind(req.e_love_6)
|
||||||
|
.bind(req.e_love_7)
|
||||||
|
.bind(req.e_love_8)
|
||||||
// Confused emotion
|
// Confused emotion
|
||||||
.bind(req.e_confused_0).bind(req.e_confused_1).bind(req.e_confused_2)
|
.bind(req.e_confused_0)
|
||||||
.bind(req.e_confused_3).bind(req.e_confused_4).bind(req.e_confused_5)
|
.bind(req.e_confused_1)
|
||||||
.bind(req.e_confused_6).bind(req.e_confused_7).bind(req.e_confused_8)
|
.bind(req.e_confused_2)
|
||||||
|
.bind(req.e_confused_3)
|
||||||
|
.bind(req.e_confused_4)
|
||||||
|
.bind(req.e_confused_5)
|
||||||
|
.bind(req.e_confused_6)
|
||||||
|
.bind(req.e_confused_7)
|
||||||
|
.bind(req.e_confused_8)
|
||||||
// Sleeping emotion
|
// Sleeping emotion
|
||||||
.bind(req.e_sleeping_0).bind(req.e_sleeping_1).bind(req.e_sleeping_2)
|
.bind(req.e_sleeping_0)
|
||||||
.bind(req.e_sleeping_3).bind(req.e_sleeping_4).bind(req.e_sleeping_5)
|
.bind(req.e_sleeping_1)
|
||||||
.bind(req.e_sleeping_6).bind(req.e_sleeping_7).bind(req.e_sleeping_8)
|
.bind(req.e_sleeping_2)
|
||||||
|
.bind(req.e_sleeping_3)
|
||||||
|
.bind(req.e_sleeping_4)
|
||||||
|
.bind(req.e_sleeping_5)
|
||||||
|
.bind(req.e_sleeping_6)
|
||||||
|
.bind(req.e_sleeping_7)
|
||||||
|
.bind(req.e_sleeping_8)
|
||||||
// Wink emotion
|
// Wink emotion
|
||||||
.bind(req.e_wink_0).bind(req.e_wink_1).bind(req.e_wink_2)
|
.bind(req.e_wink_0)
|
||||||
.bind(req.e_wink_3).bind(req.e_wink_4).bind(req.e_wink_5)
|
.bind(req.e_wink_1)
|
||||||
.bind(req.e_wink_6).bind(req.e_wink_7).bind(req.e_wink_8)
|
.bind(req.e_wink_2)
|
||||||
|
.bind(req.e_wink_3)
|
||||||
|
.bind(req.e_wink_4)
|
||||||
|
.bind(req.e_wink_5)
|
||||||
|
.bind(req.e_wink_6)
|
||||||
|
.bind(req.e_wink_7)
|
||||||
|
.bind(req.e_wink_8)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -480,51 +745,141 @@ pub async fn update_realm_avatar<'e>(
|
||||||
is_public = COALESCE($4, is_public),
|
is_public = COALESCE($4, is_public),
|
||||||
is_active = COALESCE($5, is_active),
|
is_active = COALESCE($5, is_active),
|
||||||
thumbnail_path = COALESCE($6, thumbnail_path),
|
thumbnail_path = COALESCE($6, thumbnail_path),
|
||||||
l_skin_0 = COALESCE($7, l_skin_0), l_skin_1 = COALESCE($8, l_skin_1), l_skin_2 = COALESCE($9, l_skin_2),
|
l_skin_0 = COALESCE($7, l_skin_0),
|
||||||
l_skin_3 = COALESCE($10, l_skin_3), l_skin_4 = COALESCE($11, l_skin_4), l_skin_5 = COALESCE($12, l_skin_5),
|
l_skin_1 = COALESCE($8, l_skin_1),
|
||||||
l_skin_6 = COALESCE($13, l_skin_6), l_skin_7 = COALESCE($14, l_skin_7), l_skin_8 = COALESCE($15, l_skin_8),
|
l_skin_2 = COALESCE($9, l_skin_2),
|
||||||
l_clothes_0 = COALESCE($16, l_clothes_0), l_clothes_1 = COALESCE($17, l_clothes_1), l_clothes_2 = COALESCE($18, l_clothes_2),
|
l_skin_3 = COALESCE($10, l_skin_3),
|
||||||
l_clothes_3 = COALESCE($19, l_clothes_3), l_clothes_4 = COALESCE($20, l_clothes_4), l_clothes_5 = COALESCE($21, l_clothes_5),
|
l_skin_4 = COALESCE($11, l_skin_4),
|
||||||
l_clothes_6 = COALESCE($22, l_clothes_6), l_clothes_7 = COALESCE($23, l_clothes_7), l_clothes_8 = COALESCE($24, l_clothes_8),
|
l_skin_5 = COALESCE($12, l_skin_5),
|
||||||
l_accessories_0 = COALESCE($25, l_accessories_0), l_accessories_1 = COALESCE($26, l_accessories_1), l_accessories_2 = COALESCE($27, l_accessories_2),
|
l_skin_6 = COALESCE($13, l_skin_6),
|
||||||
l_accessories_3 = COALESCE($28, l_accessories_3), l_accessories_4 = COALESCE($29, l_accessories_4), l_accessories_5 = COALESCE($30, l_accessories_5),
|
l_skin_7 = COALESCE($14, l_skin_7),
|
||||||
l_accessories_6 = COALESCE($31, l_accessories_6), l_accessories_7 = COALESCE($32, l_accessories_7), l_accessories_8 = COALESCE($33, l_accessories_8),
|
l_skin_8 = COALESCE($15, l_skin_8),
|
||||||
e_neutral_0 = COALESCE($34, e_neutral_0), e_neutral_1 = COALESCE($35, e_neutral_1), e_neutral_2 = COALESCE($36, e_neutral_2),
|
l_clothes_0 = COALESCE($16, l_clothes_0),
|
||||||
e_neutral_3 = COALESCE($37, e_neutral_3), e_neutral_4 = COALESCE($38, e_neutral_4), e_neutral_5 = COALESCE($39, e_neutral_5),
|
l_clothes_1 = COALESCE($17, l_clothes_1),
|
||||||
e_neutral_6 = COALESCE($40, e_neutral_6), e_neutral_7 = COALESCE($41, e_neutral_7), e_neutral_8 = COALESCE($42, e_neutral_8),
|
l_clothes_2 = COALESCE($18, l_clothes_2),
|
||||||
e_happy_0 = COALESCE($43, e_happy_0), e_happy_1 = COALESCE($44, e_happy_1), e_happy_2 = COALESCE($45, e_happy_2),
|
l_clothes_3 = COALESCE($19, l_clothes_3),
|
||||||
e_happy_3 = COALESCE($46, e_happy_3), e_happy_4 = COALESCE($47, e_happy_4), e_happy_5 = COALESCE($48, e_happy_5),
|
l_clothes_4 = COALESCE($20, l_clothes_4),
|
||||||
e_happy_6 = COALESCE($49, e_happy_6), e_happy_7 = COALESCE($50, e_happy_7), e_happy_8 = COALESCE($51, e_happy_8),
|
l_clothes_5 = COALESCE($21, l_clothes_5),
|
||||||
e_sad_0 = COALESCE($52, e_sad_0), e_sad_1 = COALESCE($53, e_sad_1), e_sad_2 = COALESCE($54, e_sad_2),
|
l_clothes_6 = COALESCE($22, l_clothes_6),
|
||||||
e_sad_3 = COALESCE($55, e_sad_3), e_sad_4 = COALESCE($56, e_sad_4), e_sad_5 = COALESCE($57, e_sad_5),
|
l_clothes_7 = COALESCE($23, l_clothes_7),
|
||||||
e_sad_6 = COALESCE($58, e_sad_6), e_sad_7 = COALESCE($59, e_sad_7), e_sad_8 = COALESCE($60, e_sad_8),
|
l_clothes_8 = COALESCE($24, l_clothes_8),
|
||||||
e_angry_0 = COALESCE($61, e_angry_0), e_angry_1 = COALESCE($62, e_angry_1), e_angry_2 = COALESCE($63, e_angry_2),
|
l_accessories_0 = COALESCE($25, l_accessories_0),
|
||||||
e_angry_3 = COALESCE($64, e_angry_3), e_angry_4 = COALESCE($65, e_angry_4), e_angry_5 = COALESCE($66, e_angry_5),
|
l_accessories_1 = COALESCE($26, l_accessories_1),
|
||||||
e_angry_6 = COALESCE($67, e_angry_6), e_angry_7 = COALESCE($68, e_angry_7), e_angry_8 = COALESCE($69, e_angry_8),
|
l_accessories_2 = COALESCE($27, l_accessories_2),
|
||||||
e_surprised_0 = COALESCE($70, e_surprised_0), e_surprised_1 = COALESCE($71, e_surprised_1), e_surprised_2 = COALESCE($72, e_surprised_2),
|
l_accessories_3 = COALESCE($28, l_accessories_3),
|
||||||
e_surprised_3 = COALESCE($73, e_surprised_3), e_surprised_4 = COALESCE($74, e_surprised_4), e_surprised_5 = COALESCE($75, e_surprised_5),
|
l_accessories_4 = COALESCE($29, l_accessories_4),
|
||||||
e_surprised_6 = COALESCE($76, e_surprised_6), e_surprised_7 = COALESCE($77, e_surprised_7), e_surprised_8 = COALESCE($78, e_surprised_8),
|
l_accessories_5 = COALESCE($30, l_accessories_5),
|
||||||
e_thinking_0 = COALESCE($79, e_thinking_0), e_thinking_1 = COALESCE($80, e_thinking_1), e_thinking_2 = COALESCE($81, e_thinking_2),
|
l_accessories_6 = COALESCE($31, l_accessories_6),
|
||||||
e_thinking_3 = COALESCE($82, e_thinking_3), e_thinking_4 = COALESCE($83, e_thinking_4), e_thinking_5 = COALESCE($84, e_thinking_5),
|
l_accessories_7 = COALESCE($32, l_accessories_7),
|
||||||
e_thinking_6 = COALESCE($85, e_thinking_6), e_thinking_7 = COALESCE($86, e_thinking_7), e_thinking_8 = COALESCE($87, e_thinking_8),
|
l_accessories_8 = COALESCE($33, l_accessories_8),
|
||||||
e_laughing_0 = COALESCE($88, e_laughing_0), e_laughing_1 = COALESCE($89, e_laughing_1), e_laughing_2 = COALESCE($90, e_laughing_2),
|
e_neutral_0 = COALESCE($34, e_neutral_0),
|
||||||
e_laughing_3 = COALESCE($91, e_laughing_3), e_laughing_4 = COALESCE($92, e_laughing_4), e_laughing_5 = COALESCE($93, e_laughing_5),
|
e_neutral_1 = COALESCE($35, e_neutral_1),
|
||||||
e_laughing_6 = COALESCE($94, e_laughing_6), e_laughing_7 = COALESCE($95, e_laughing_7), e_laughing_8 = COALESCE($96, e_laughing_8),
|
e_neutral_2 = COALESCE($36, e_neutral_2),
|
||||||
e_crying_0 = COALESCE($97, e_crying_0), e_crying_1 = COALESCE($98, e_crying_1), e_crying_2 = COALESCE($99, e_crying_2),
|
e_neutral_3 = COALESCE($37, e_neutral_3),
|
||||||
e_crying_3 = COALESCE($100, e_crying_3), e_crying_4 = COALESCE($101, e_crying_4), e_crying_5 = COALESCE($102, e_crying_5),
|
e_neutral_4 = COALESCE($38, e_neutral_4),
|
||||||
e_crying_6 = COALESCE($103, e_crying_6), e_crying_7 = COALESCE($104, e_crying_7), e_crying_8 = COALESCE($105, e_crying_8),
|
e_neutral_5 = COALESCE($39, e_neutral_5),
|
||||||
e_love_0 = COALESCE($106, e_love_0), e_love_1 = COALESCE($107, e_love_1), e_love_2 = COALESCE($108, e_love_2),
|
e_neutral_6 = COALESCE($40, e_neutral_6),
|
||||||
e_love_3 = COALESCE($109, e_love_3), e_love_4 = COALESCE($110, e_love_4), e_love_5 = COALESCE($111, e_love_5),
|
e_neutral_7 = COALESCE($41, e_neutral_7),
|
||||||
e_love_6 = COALESCE($112, e_love_6), e_love_7 = COALESCE($113, e_love_7), e_love_8 = COALESCE($114, e_love_8),
|
e_neutral_8 = COALESCE($42, e_neutral_8),
|
||||||
e_confused_0 = COALESCE($115, e_confused_0), e_confused_1 = COALESCE($116, e_confused_1), e_confused_2 = COALESCE($117, e_confused_2),
|
e_happy_0 = COALESCE($43, e_happy_0),
|
||||||
e_confused_3 = COALESCE($118, e_confused_3), e_confused_4 = COALESCE($119, e_confused_4), e_confused_5 = COALESCE($120, e_confused_5),
|
e_happy_1 = COALESCE($44, e_happy_1),
|
||||||
e_confused_6 = COALESCE($121, e_confused_6), e_confused_7 = COALESCE($122, e_confused_7), e_confused_8 = COALESCE($123, e_confused_8),
|
e_happy_2 = COALESCE($45, e_happy_2),
|
||||||
e_sleeping_0 = COALESCE($124, e_sleeping_0), e_sleeping_1 = COALESCE($125, e_sleeping_1), e_sleeping_2 = COALESCE($126, e_sleeping_2),
|
e_happy_3 = COALESCE($46, e_happy_3),
|
||||||
e_sleeping_3 = COALESCE($127, e_sleeping_3), e_sleeping_4 = COALESCE($128, e_sleeping_4), e_sleeping_5 = COALESCE($129, e_sleeping_5),
|
e_happy_4 = COALESCE($47, e_happy_4),
|
||||||
e_sleeping_6 = COALESCE($130, e_sleeping_6), e_sleeping_7 = COALESCE($131, e_sleeping_7), e_sleeping_8 = COALESCE($132, e_sleeping_8),
|
e_happy_5 = COALESCE($48, e_happy_5),
|
||||||
e_wink_0 = COALESCE($133, e_wink_0), e_wink_1 = COALESCE($134, e_wink_1), e_wink_2 = COALESCE($135, e_wink_2),
|
e_happy_6 = COALESCE($49, e_happy_6),
|
||||||
e_wink_3 = COALESCE($136, e_wink_3), e_wink_4 = COALESCE($137, e_wink_4), e_wink_5 = COALESCE($138, e_wink_5),
|
e_happy_7 = COALESCE($50, e_happy_7),
|
||||||
e_wink_6 = COALESCE($139, e_wink_6), e_wink_7 = COALESCE($140, e_wink_7), e_wink_8 = COALESCE($141, e_wink_8),
|
e_happy_8 = COALESCE($51, e_happy_8),
|
||||||
|
e_sad_0 = COALESCE($52, e_sad_0),
|
||||||
|
e_sad_1 = COALESCE($53, e_sad_1),
|
||||||
|
e_sad_2 = COALESCE($54, e_sad_2),
|
||||||
|
e_sad_3 = COALESCE($55, e_sad_3),
|
||||||
|
e_sad_4 = COALESCE($56, e_sad_4),
|
||||||
|
e_sad_5 = COALESCE($57, e_sad_5),
|
||||||
|
e_sad_6 = COALESCE($58, e_sad_6),
|
||||||
|
e_sad_7 = COALESCE($59, e_sad_7),
|
||||||
|
e_sad_8 = COALESCE($60, e_sad_8),
|
||||||
|
e_angry_0 = COALESCE($61, e_angry_0),
|
||||||
|
e_angry_1 = COALESCE($62, e_angry_1),
|
||||||
|
e_angry_2 = COALESCE($63, e_angry_2),
|
||||||
|
e_angry_3 = COALESCE($64, e_angry_3),
|
||||||
|
e_angry_4 = COALESCE($65, e_angry_4),
|
||||||
|
e_angry_5 = COALESCE($66, e_angry_5),
|
||||||
|
e_angry_6 = COALESCE($67, e_angry_6),
|
||||||
|
e_angry_7 = COALESCE($68, e_angry_7),
|
||||||
|
e_angry_8 = COALESCE($69, e_angry_8),
|
||||||
|
e_surprised_0 = COALESCE($70, e_surprised_0),
|
||||||
|
e_surprised_1 = COALESCE($71, e_surprised_1),
|
||||||
|
e_surprised_2 = COALESCE($72, e_surprised_2),
|
||||||
|
e_surprised_3 = COALESCE($73, e_surprised_3),
|
||||||
|
e_surprised_4 = COALESCE($74, e_surprised_4),
|
||||||
|
e_surprised_5 = COALESCE($75, e_surprised_5),
|
||||||
|
e_surprised_6 = COALESCE($76, e_surprised_6),
|
||||||
|
e_surprised_7 = COALESCE($77, e_surprised_7),
|
||||||
|
e_surprised_8 = COALESCE($78, e_surprised_8),
|
||||||
|
e_thinking_0 = COALESCE($79, e_thinking_0),
|
||||||
|
e_thinking_1 = COALESCE($80, e_thinking_1),
|
||||||
|
e_thinking_2 = COALESCE($81, e_thinking_2),
|
||||||
|
e_thinking_3 = COALESCE($82, e_thinking_3),
|
||||||
|
e_thinking_4 = COALESCE($83, e_thinking_4),
|
||||||
|
e_thinking_5 = COALESCE($84, e_thinking_5),
|
||||||
|
e_thinking_6 = COALESCE($85, e_thinking_6),
|
||||||
|
e_thinking_7 = COALESCE($86, e_thinking_7),
|
||||||
|
e_thinking_8 = COALESCE($87, e_thinking_8),
|
||||||
|
e_laughing_0 = COALESCE($88, e_laughing_0),
|
||||||
|
e_laughing_1 = COALESCE($89, e_laughing_1),
|
||||||
|
e_laughing_2 = COALESCE($90, e_laughing_2),
|
||||||
|
e_laughing_3 = COALESCE($91, e_laughing_3),
|
||||||
|
e_laughing_4 = COALESCE($92, e_laughing_4),
|
||||||
|
e_laughing_5 = COALESCE($93, e_laughing_5),
|
||||||
|
e_laughing_6 = COALESCE($94, e_laughing_6),
|
||||||
|
e_laughing_7 = COALESCE($95, e_laughing_7),
|
||||||
|
e_laughing_8 = COALESCE($96, e_laughing_8),
|
||||||
|
e_crying_0 = COALESCE($97, e_crying_0),
|
||||||
|
e_crying_1 = COALESCE($98, e_crying_1),
|
||||||
|
e_crying_2 = COALESCE($99, e_crying_2),
|
||||||
|
e_crying_3 = COALESCE($100, e_crying_3),
|
||||||
|
e_crying_4 = COALESCE($101, e_crying_4),
|
||||||
|
e_crying_5 = COALESCE($102, e_crying_5),
|
||||||
|
e_crying_6 = COALESCE($103, e_crying_6),
|
||||||
|
e_crying_7 = COALESCE($104, e_crying_7),
|
||||||
|
e_crying_8 = COALESCE($105, e_crying_8),
|
||||||
|
e_love_0 = COALESCE($106, e_love_0),
|
||||||
|
e_love_1 = COALESCE($107, e_love_1),
|
||||||
|
e_love_2 = COALESCE($108, e_love_2),
|
||||||
|
e_love_3 = COALESCE($109, e_love_3),
|
||||||
|
e_love_4 = COALESCE($110, e_love_4),
|
||||||
|
e_love_5 = COALESCE($111, e_love_5),
|
||||||
|
e_love_6 = COALESCE($112, e_love_6),
|
||||||
|
e_love_7 = COALESCE($113, e_love_7),
|
||||||
|
e_love_8 = COALESCE($114, e_love_8),
|
||||||
|
e_confused_0 = COALESCE($115, e_confused_0),
|
||||||
|
e_confused_1 = COALESCE($116, e_confused_1),
|
||||||
|
e_confused_2 = COALESCE($117, e_confused_2),
|
||||||
|
e_confused_3 = COALESCE($118, e_confused_3),
|
||||||
|
e_confused_4 = COALESCE($119, e_confused_4),
|
||||||
|
e_confused_5 = COALESCE($120, e_confused_5),
|
||||||
|
e_confused_6 = COALESCE($121, e_confused_6),
|
||||||
|
e_confused_7 = COALESCE($122, e_confused_7),
|
||||||
|
e_confused_8 = COALESCE($123, e_confused_8),
|
||||||
|
e_sleeping_0 = COALESCE($124, e_sleeping_0),
|
||||||
|
e_sleeping_1 = COALESCE($125, e_sleeping_1),
|
||||||
|
e_sleeping_2 = COALESCE($126, e_sleeping_2),
|
||||||
|
e_sleeping_3 = COALESCE($127, e_sleeping_3),
|
||||||
|
e_sleeping_4 = COALESCE($128, e_sleeping_4),
|
||||||
|
e_sleeping_5 = COALESCE($129, e_sleeping_5),
|
||||||
|
e_sleeping_6 = COALESCE($130, e_sleeping_6),
|
||||||
|
e_sleeping_7 = COALESCE($131, e_sleeping_7),
|
||||||
|
e_sleeping_8 = COALESCE($132, e_sleeping_8),
|
||||||
|
e_wink_0 = COALESCE($133, e_wink_0),
|
||||||
|
e_wink_1 = COALESCE($134, e_wink_1),
|
||||||
|
e_wink_2 = COALESCE($135, e_wink_2),
|
||||||
|
e_wink_3 = COALESCE($136, e_wink_3),
|
||||||
|
e_wink_4 = COALESCE($137, e_wink_4),
|
||||||
|
e_wink_5 = COALESCE($138, e_wink_5),
|
||||||
|
e_wink_6 = COALESCE($139, e_wink_6),
|
||||||
|
e_wink_7 = COALESCE($140, e_wink_7),
|
||||||
|
e_wink_8 = COALESCE($141, e_wink_8),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING *
|
RETURNING *
|
||||||
|
|
@ -537,65 +892,155 @@ pub async fn update_realm_avatar<'e>(
|
||||||
.bind(req.is_active)
|
.bind(req.is_active)
|
||||||
.bind(&req.thumbnail_path)
|
.bind(&req.thumbnail_path)
|
||||||
// Skin layer
|
// Skin layer
|
||||||
.bind(req.l_skin_0).bind(req.l_skin_1).bind(req.l_skin_2)
|
.bind(req.l_skin_0)
|
||||||
.bind(req.l_skin_3).bind(req.l_skin_4).bind(req.l_skin_5)
|
.bind(req.l_skin_1)
|
||||||
.bind(req.l_skin_6).bind(req.l_skin_7).bind(req.l_skin_8)
|
.bind(req.l_skin_2)
|
||||||
|
.bind(req.l_skin_3)
|
||||||
|
.bind(req.l_skin_4)
|
||||||
|
.bind(req.l_skin_5)
|
||||||
|
.bind(req.l_skin_6)
|
||||||
|
.bind(req.l_skin_7)
|
||||||
|
.bind(req.l_skin_8)
|
||||||
// Clothes layer
|
// Clothes layer
|
||||||
.bind(req.l_clothes_0).bind(req.l_clothes_1).bind(req.l_clothes_2)
|
.bind(req.l_clothes_0)
|
||||||
.bind(req.l_clothes_3).bind(req.l_clothes_4).bind(req.l_clothes_5)
|
.bind(req.l_clothes_1)
|
||||||
.bind(req.l_clothes_6).bind(req.l_clothes_7).bind(req.l_clothes_8)
|
.bind(req.l_clothes_2)
|
||||||
|
.bind(req.l_clothes_3)
|
||||||
|
.bind(req.l_clothes_4)
|
||||||
|
.bind(req.l_clothes_5)
|
||||||
|
.bind(req.l_clothes_6)
|
||||||
|
.bind(req.l_clothes_7)
|
||||||
|
.bind(req.l_clothes_8)
|
||||||
// Accessories layer
|
// Accessories layer
|
||||||
.bind(req.l_accessories_0).bind(req.l_accessories_1).bind(req.l_accessories_2)
|
.bind(req.l_accessories_0)
|
||||||
.bind(req.l_accessories_3).bind(req.l_accessories_4).bind(req.l_accessories_5)
|
.bind(req.l_accessories_1)
|
||||||
.bind(req.l_accessories_6).bind(req.l_accessories_7).bind(req.l_accessories_8)
|
.bind(req.l_accessories_2)
|
||||||
|
.bind(req.l_accessories_3)
|
||||||
|
.bind(req.l_accessories_4)
|
||||||
|
.bind(req.l_accessories_5)
|
||||||
|
.bind(req.l_accessories_6)
|
||||||
|
.bind(req.l_accessories_7)
|
||||||
|
.bind(req.l_accessories_8)
|
||||||
// Neutral emotion
|
// Neutral emotion
|
||||||
.bind(req.e_neutral_0).bind(req.e_neutral_1).bind(req.e_neutral_2)
|
.bind(req.e_neutral_0)
|
||||||
.bind(req.e_neutral_3).bind(req.e_neutral_4).bind(req.e_neutral_5)
|
.bind(req.e_neutral_1)
|
||||||
.bind(req.e_neutral_6).bind(req.e_neutral_7).bind(req.e_neutral_8)
|
.bind(req.e_neutral_2)
|
||||||
|
.bind(req.e_neutral_3)
|
||||||
|
.bind(req.e_neutral_4)
|
||||||
|
.bind(req.e_neutral_5)
|
||||||
|
.bind(req.e_neutral_6)
|
||||||
|
.bind(req.e_neutral_7)
|
||||||
|
.bind(req.e_neutral_8)
|
||||||
// Happy emotion
|
// Happy emotion
|
||||||
.bind(req.e_happy_0).bind(req.e_happy_1).bind(req.e_happy_2)
|
.bind(req.e_happy_0)
|
||||||
.bind(req.e_happy_3).bind(req.e_happy_4).bind(req.e_happy_5)
|
.bind(req.e_happy_1)
|
||||||
.bind(req.e_happy_6).bind(req.e_happy_7).bind(req.e_happy_8)
|
.bind(req.e_happy_2)
|
||||||
|
.bind(req.e_happy_3)
|
||||||
|
.bind(req.e_happy_4)
|
||||||
|
.bind(req.e_happy_5)
|
||||||
|
.bind(req.e_happy_6)
|
||||||
|
.bind(req.e_happy_7)
|
||||||
|
.bind(req.e_happy_8)
|
||||||
// Sad emotion
|
// Sad emotion
|
||||||
.bind(req.e_sad_0).bind(req.e_sad_1).bind(req.e_sad_2)
|
.bind(req.e_sad_0)
|
||||||
.bind(req.e_sad_3).bind(req.e_sad_4).bind(req.e_sad_5)
|
.bind(req.e_sad_1)
|
||||||
.bind(req.e_sad_6).bind(req.e_sad_7).bind(req.e_sad_8)
|
.bind(req.e_sad_2)
|
||||||
|
.bind(req.e_sad_3)
|
||||||
|
.bind(req.e_sad_4)
|
||||||
|
.bind(req.e_sad_5)
|
||||||
|
.bind(req.e_sad_6)
|
||||||
|
.bind(req.e_sad_7)
|
||||||
|
.bind(req.e_sad_8)
|
||||||
// Angry emotion
|
// Angry emotion
|
||||||
.bind(req.e_angry_0).bind(req.e_angry_1).bind(req.e_angry_2)
|
.bind(req.e_angry_0)
|
||||||
.bind(req.e_angry_3).bind(req.e_angry_4).bind(req.e_angry_5)
|
.bind(req.e_angry_1)
|
||||||
.bind(req.e_angry_6).bind(req.e_angry_7).bind(req.e_angry_8)
|
.bind(req.e_angry_2)
|
||||||
|
.bind(req.e_angry_3)
|
||||||
|
.bind(req.e_angry_4)
|
||||||
|
.bind(req.e_angry_5)
|
||||||
|
.bind(req.e_angry_6)
|
||||||
|
.bind(req.e_angry_7)
|
||||||
|
.bind(req.e_angry_8)
|
||||||
// Surprised emotion
|
// Surprised emotion
|
||||||
.bind(req.e_surprised_0).bind(req.e_surprised_1).bind(req.e_surprised_2)
|
.bind(req.e_surprised_0)
|
||||||
.bind(req.e_surprised_3).bind(req.e_surprised_4).bind(req.e_surprised_5)
|
.bind(req.e_surprised_1)
|
||||||
.bind(req.e_surprised_6).bind(req.e_surprised_7).bind(req.e_surprised_8)
|
.bind(req.e_surprised_2)
|
||||||
|
.bind(req.e_surprised_3)
|
||||||
|
.bind(req.e_surprised_4)
|
||||||
|
.bind(req.e_surprised_5)
|
||||||
|
.bind(req.e_surprised_6)
|
||||||
|
.bind(req.e_surprised_7)
|
||||||
|
.bind(req.e_surprised_8)
|
||||||
// Thinking emotion
|
// Thinking emotion
|
||||||
.bind(req.e_thinking_0).bind(req.e_thinking_1).bind(req.e_thinking_2)
|
.bind(req.e_thinking_0)
|
||||||
.bind(req.e_thinking_3).bind(req.e_thinking_4).bind(req.e_thinking_5)
|
.bind(req.e_thinking_1)
|
||||||
.bind(req.e_thinking_6).bind(req.e_thinking_7).bind(req.e_thinking_8)
|
.bind(req.e_thinking_2)
|
||||||
|
.bind(req.e_thinking_3)
|
||||||
|
.bind(req.e_thinking_4)
|
||||||
|
.bind(req.e_thinking_5)
|
||||||
|
.bind(req.e_thinking_6)
|
||||||
|
.bind(req.e_thinking_7)
|
||||||
|
.bind(req.e_thinking_8)
|
||||||
// Laughing emotion
|
// Laughing emotion
|
||||||
.bind(req.e_laughing_0).bind(req.e_laughing_1).bind(req.e_laughing_2)
|
.bind(req.e_laughing_0)
|
||||||
.bind(req.e_laughing_3).bind(req.e_laughing_4).bind(req.e_laughing_5)
|
.bind(req.e_laughing_1)
|
||||||
.bind(req.e_laughing_6).bind(req.e_laughing_7).bind(req.e_laughing_8)
|
.bind(req.e_laughing_2)
|
||||||
|
.bind(req.e_laughing_3)
|
||||||
|
.bind(req.e_laughing_4)
|
||||||
|
.bind(req.e_laughing_5)
|
||||||
|
.bind(req.e_laughing_6)
|
||||||
|
.bind(req.e_laughing_7)
|
||||||
|
.bind(req.e_laughing_8)
|
||||||
// Crying emotion
|
// Crying emotion
|
||||||
.bind(req.e_crying_0).bind(req.e_crying_1).bind(req.e_crying_2)
|
.bind(req.e_crying_0)
|
||||||
.bind(req.e_crying_3).bind(req.e_crying_4).bind(req.e_crying_5)
|
.bind(req.e_crying_1)
|
||||||
.bind(req.e_crying_6).bind(req.e_crying_7).bind(req.e_crying_8)
|
.bind(req.e_crying_2)
|
||||||
|
.bind(req.e_crying_3)
|
||||||
|
.bind(req.e_crying_4)
|
||||||
|
.bind(req.e_crying_5)
|
||||||
|
.bind(req.e_crying_6)
|
||||||
|
.bind(req.e_crying_7)
|
||||||
|
.bind(req.e_crying_8)
|
||||||
// Love emotion
|
// Love emotion
|
||||||
.bind(req.e_love_0).bind(req.e_love_1).bind(req.e_love_2)
|
.bind(req.e_love_0)
|
||||||
.bind(req.e_love_3).bind(req.e_love_4).bind(req.e_love_5)
|
.bind(req.e_love_1)
|
||||||
.bind(req.e_love_6).bind(req.e_love_7).bind(req.e_love_8)
|
.bind(req.e_love_2)
|
||||||
|
.bind(req.e_love_3)
|
||||||
|
.bind(req.e_love_4)
|
||||||
|
.bind(req.e_love_5)
|
||||||
|
.bind(req.e_love_6)
|
||||||
|
.bind(req.e_love_7)
|
||||||
|
.bind(req.e_love_8)
|
||||||
// Confused emotion
|
// Confused emotion
|
||||||
.bind(req.e_confused_0).bind(req.e_confused_1).bind(req.e_confused_2)
|
.bind(req.e_confused_0)
|
||||||
.bind(req.e_confused_3).bind(req.e_confused_4).bind(req.e_confused_5)
|
.bind(req.e_confused_1)
|
||||||
.bind(req.e_confused_6).bind(req.e_confused_7).bind(req.e_confused_8)
|
.bind(req.e_confused_2)
|
||||||
|
.bind(req.e_confused_3)
|
||||||
|
.bind(req.e_confused_4)
|
||||||
|
.bind(req.e_confused_5)
|
||||||
|
.bind(req.e_confused_6)
|
||||||
|
.bind(req.e_confused_7)
|
||||||
|
.bind(req.e_confused_8)
|
||||||
// Sleeping emotion
|
// Sleeping emotion
|
||||||
.bind(req.e_sleeping_0).bind(req.e_sleeping_1).bind(req.e_sleeping_2)
|
.bind(req.e_sleeping_0)
|
||||||
.bind(req.e_sleeping_3).bind(req.e_sleeping_4).bind(req.e_sleeping_5)
|
.bind(req.e_sleeping_1)
|
||||||
.bind(req.e_sleeping_6).bind(req.e_sleeping_7).bind(req.e_sleeping_8)
|
.bind(req.e_sleeping_2)
|
||||||
|
.bind(req.e_sleeping_3)
|
||||||
|
.bind(req.e_sleeping_4)
|
||||||
|
.bind(req.e_sleeping_5)
|
||||||
|
.bind(req.e_sleeping_6)
|
||||||
|
.bind(req.e_sleeping_7)
|
||||||
|
.bind(req.e_sleeping_8)
|
||||||
// Wink emotion
|
// Wink emotion
|
||||||
.bind(req.e_wink_0).bind(req.e_wink_1).bind(req.e_wink_2)
|
.bind(req.e_wink_0)
|
||||||
.bind(req.e_wink_3).bind(req.e_wink_4).bind(req.e_wink_5)
|
.bind(req.e_wink_1)
|
||||||
.bind(req.e_wink_6).bind(req.e_wink_7).bind(req.e_wink_8)
|
.bind(req.e_wink_2)
|
||||||
|
.bind(req.e_wink_3)
|
||||||
|
.bind(req.e_wink_4)
|
||||||
|
.bind(req.e_wink_5)
|
||||||
|
.bind(req.e_wink_6)
|
||||||
|
.bind(req.e_wink_7)
|
||||||
|
.bind(req.e_wink_8)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use sqlx::PgExecutor;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::{CreateSceneRequest, Scene, SceneSummary, UpdateSceneRequest};
|
use crate::models::{CreateSceneRequest, Scene, SceneSummary, UpdateSceneRequest};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
|
|
||||||
/// List all scenes for a realm.
|
/// List all scenes for a realm.
|
||||||
pub async fn list_scenes_for_realm<'e>(
|
pub async fn list_scenes_for_realm<'e>(
|
||||||
|
|
@ -374,7 +374,7 @@ pub async fn update_scene<'e>(
|
||||||
let scene = query_builder
|
let scene = query_builder
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Scene")?;
|
.ok_or_else(|| AppError::NotFound("Scene not found".to_string()))?;
|
||||||
|
|
||||||
Ok(scene)
|
Ok(scene)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,15 @@
|
||||||
//! Server avatars are pre-configured avatar configurations available globally
|
//! Server avatars are pre-configured avatar configurations available globally
|
||||||
//! across all realms. They reference server.props directly (not inventory items).
|
//! across all realms. They reference server.props directly (not inventory items).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use sqlx::PgExecutor;
|
use sqlx::PgExecutor;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::extract_avatar_slots;
|
use crate::models::{AvatarRenderData, EmotionState, ServerAvatar};
|
||||||
use crate::models::{AvatarRenderData, EmotionState, ServerAvatar, ServerAvatarWithPaths};
|
|
||||||
use crate::queries::avatar_common::{
|
|
||||||
avatar_paths_join_clause, avatar_paths_select_clause, build_prop_map,
|
|
||||||
resolve_slots_to_render_data,
|
|
||||||
};
|
|
||||||
use chattyness_error::AppError;
|
use chattyness_error::AppError;
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Basic Queries
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Get a server avatar by slug.
|
/// Get a server avatar by slug.
|
||||||
pub async fn get_server_avatar_by_slug<'e>(
|
pub async fn get_server_avatar_by_slug<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
|
|
@ -75,11 +68,9 @@ pub async fn list_public_server_avatars<'e>(
|
||||||
Ok(avatars)
|
Ok(avatars)
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
use crate::models::ServerAvatarWithPaths;
|
||||||
// Avatar with Paths Queries
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/// Row type for server avatar with paths query (includes slug).
|
/// Row type for server avatar with paths query.
|
||||||
#[derive(Debug, sqlx::FromRow)]
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
struct ServerAvatarWithPathsRow {
|
struct ServerAvatarWithPathsRow {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
|
|
@ -116,7 +107,7 @@ struct ServerAvatarWithPathsRow {
|
||||||
accessories_6: Option<String>,
|
accessories_6: Option<String>,
|
||||||
accessories_7: Option<String>,
|
accessories_7: Option<String>,
|
||||||
accessories_8: Option<String>,
|
accessories_8: Option<String>,
|
||||||
// Happy emotion layer paths
|
// Happy emotion layer paths (e1 - more inviting for store display)
|
||||||
emotion_0: Option<String>,
|
emotion_0: Option<String>,
|
||||||
emotion_1: Option<String>,
|
emotion_1: Option<String>,
|
||||||
emotion_2: Option<String>,
|
emotion_2: Option<String>,
|
||||||
|
|
@ -160,51 +151,234 @@ impl From<ServerAvatarWithPathsRow> for ServerAvatarWithPaths {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List all active public server avatars with resolved asset paths.
|
/// List all active public server avatars with resolved asset paths.
|
||||||
|
///
|
||||||
|
/// Joins with the props table to resolve prop UUIDs to asset paths,
|
||||||
|
/// suitable for client-side rendering without additional lookups.
|
||||||
pub async fn list_public_server_avatars_with_paths<'e>(
|
pub async fn list_public_server_avatars_with_paths<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
) -> Result<Vec<ServerAvatarWithPaths>, AppError> {
|
) -> Result<Vec<ServerAvatarWithPaths>, AppError> {
|
||||||
let join_clause = avatar_paths_join_clause("server.props");
|
let rows = sqlx::query_as::<_, ServerAvatarWithPathsRow>(
|
||||||
let query = format!(
|
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
a.id,
|
a.id,
|
||||||
a.slug,
|
a.slug,
|
||||||
{}
|
a.name,
|
||||||
|
a.description,
|
||||||
|
-- Skin layer
|
||||||
|
p_skin_0.asset_path AS skin_0,
|
||||||
|
p_skin_1.asset_path AS skin_1,
|
||||||
|
p_skin_2.asset_path AS skin_2,
|
||||||
|
p_skin_3.asset_path AS skin_3,
|
||||||
|
p_skin_4.asset_path AS skin_4,
|
||||||
|
p_skin_5.asset_path AS skin_5,
|
||||||
|
p_skin_6.asset_path AS skin_6,
|
||||||
|
p_skin_7.asset_path AS skin_7,
|
||||||
|
p_skin_8.asset_path AS skin_8,
|
||||||
|
-- Clothes layer
|
||||||
|
p_clothes_0.asset_path AS clothes_0,
|
||||||
|
p_clothes_1.asset_path AS clothes_1,
|
||||||
|
p_clothes_2.asset_path AS clothes_2,
|
||||||
|
p_clothes_3.asset_path AS clothes_3,
|
||||||
|
p_clothes_4.asset_path AS clothes_4,
|
||||||
|
p_clothes_5.asset_path AS clothes_5,
|
||||||
|
p_clothes_6.asset_path AS clothes_6,
|
||||||
|
p_clothes_7.asset_path AS clothes_7,
|
||||||
|
p_clothes_8.asset_path AS clothes_8,
|
||||||
|
-- Accessories layer
|
||||||
|
p_acc_0.asset_path AS accessories_0,
|
||||||
|
p_acc_1.asset_path AS accessories_1,
|
||||||
|
p_acc_2.asset_path AS accessories_2,
|
||||||
|
p_acc_3.asset_path AS accessories_3,
|
||||||
|
p_acc_4.asset_path AS accessories_4,
|
||||||
|
p_acc_5.asset_path AS accessories_5,
|
||||||
|
p_acc_6.asset_path AS accessories_6,
|
||||||
|
p_acc_7.asset_path AS accessories_7,
|
||||||
|
p_acc_8.asset_path AS accessories_8,
|
||||||
|
-- Happy emotion layer (e1 - more inviting for store display)
|
||||||
|
p_emo_0.asset_path AS emotion_0,
|
||||||
|
p_emo_1.asset_path AS emotion_1,
|
||||||
|
p_emo_2.asset_path AS emotion_2,
|
||||||
|
p_emo_3.asset_path AS emotion_3,
|
||||||
|
p_emo_4.asset_path AS emotion_4,
|
||||||
|
p_emo_5.asset_path AS emotion_5,
|
||||||
|
p_emo_6.asset_path AS emotion_6,
|
||||||
|
p_emo_7.asset_path AS emotion_7,
|
||||||
|
p_emo_8.asset_path AS emotion_8
|
||||||
FROM server.avatars a
|
FROM server.avatars a
|
||||||
{}
|
-- Skin layer joins
|
||||||
|
LEFT JOIN server.props p_skin_0 ON a.l_skin_0 = p_skin_0.id
|
||||||
|
LEFT JOIN server.props p_skin_1 ON a.l_skin_1 = p_skin_1.id
|
||||||
|
LEFT JOIN server.props p_skin_2 ON a.l_skin_2 = p_skin_2.id
|
||||||
|
LEFT JOIN server.props p_skin_3 ON a.l_skin_3 = p_skin_3.id
|
||||||
|
LEFT JOIN server.props p_skin_4 ON a.l_skin_4 = p_skin_4.id
|
||||||
|
LEFT JOIN server.props p_skin_5 ON a.l_skin_5 = p_skin_5.id
|
||||||
|
LEFT JOIN server.props p_skin_6 ON a.l_skin_6 = p_skin_6.id
|
||||||
|
LEFT JOIN server.props p_skin_7 ON a.l_skin_7 = p_skin_7.id
|
||||||
|
LEFT JOIN server.props p_skin_8 ON a.l_skin_8 = p_skin_8.id
|
||||||
|
-- Clothes layer joins
|
||||||
|
LEFT JOIN server.props p_clothes_0 ON a.l_clothes_0 = p_clothes_0.id
|
||||||
|
LEFT JOIN server.props p_clothes_1 ON a.l_clothes_1 = p_clothes_1.id
|
||||||
|
LEFT JOIN server.props p_clothes_2 ON a.l_clothes_2 = p_clothes_2.id
|
||||||
|
LEFT JOIN server.props p_clothes_3 ON a.l_clothes_3 = p_clothes_3.id
|
||||||
|
LEFT JOIN server.props p_clothes_4 ON a.l_clothes_4 = p_clothes_4.id
|
||||||
|
LEFT JOIN server.props p_clothes_5 ON a.l_clothes_5 = p_clothes_5.id
|
||||||
|
LEFT JOIN server.props p_clothes_6 ON a.l_clothes_6 = p_clothes_6.id
|
||||||
|
LEFT JOIN server.props p_clothes_7 ON a.l_clothes_7 = p_clothes_7.id
|
||||||
|
LEFT JOIN server.props p_clothes_8 ON a.l_clothes_8 = p_clothes_8.id
|
||||||
|
-- Accessories layer joins
|
||||||
|
LEFT JOIN server.props p_acc_0 ON a.l_accessories_0 = p_acc_0.id
|
||||||
|
LEFT JOIN server.props p_acc_1 ON a.l_accessories_1 = p_acc_1.id
|
||||||
|
LEFT JOIN server.props p_acc_2 ON a.l_accessories_2 = p_acc_2.id
|
||||||
|
LEFT JOIN server.props p_acc_3 ON a.l_accessories_3 = p_acc_3.id
|
||||||
|
LEFT JOIN server.props p_acc_4 ON a.l_accessories_4 = p_acc_4.id
|
||||||
|
LEFT JOIN server.props p_acc_5 ON a.l_accessories_5 = p_acc_5.id
|
||||||
|
LEFT JOIN server.props p_acc_6 ON a.l_accessories_6 = p_acc_6.id
|
||||||
|
LEFT JOIN server.props p_acc_7 ON a.l_accessories_7 = p_acc_7.id
|
||||||
|
LEFT JOIN server.props p_acc_8 ON a.l_accessories_8 = p_acc_8.id
|
||||||
|
-- Happy emotion layer joins (e1 - more inviting for store display)
|
||||||
|
LEFT JOIN server.props p_emo_0 ON a.e_happy_0 = p_emo_0.id
|
||||||
|
LEFT JOIN server.props p_emo_1 ON a.e_happy_1 = p_emo_1.id
|
||||||
|
LEFT JOIN server.props p_emo_2 ON a.e_happy_2 = p_emo_2.id
|
||||||
|
LEFT JOIN server.props p_emo_3 ON a.e_happy_3 = p_emo_3.id
|
||||||
|
LEFT JOIN server.props p_emo_4 ON a.e_happy_4 = p_emo_4.id
|
||||||
|
LEFT JOIN server.props p_emo_5 ON a.e_happy_5 = p_emo_5.id
|
||||||
|
LEFT JOIN server.props p_emo_6 ON a.e_happy_6 = p_emo_6.id
|
||||||
|
LEFT JOIN server.props p_emo_7 ON a.e_happy_7 = p_emo_7.id
|
||||||
|
LEFT JOIN server.props p_emo_8 ON a.e_happy_8 = p_emo_8.id
|
||||||
WHERE a.is_active = true AND a.is_public = true
|
WHERE a.is_active = true AND a.is_public = true
|
||||||
ORDER BY a.name ASC
|
ORDER BY a.name ASC
|
||||||
"#,
|
"#,
|
||||||
avatar_paths_select_clause(),
|
)
|
||||||
join_clause
|
|
||||||
);
|
|
||||||
|
|
||||||
let rows = sqlx::query_as::<_, ServerAvatarWithPathsRow>(&query)
|
|
||||||
.fetch_all(executor)
|
.fetch_all(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(rows.into_iter().map(ServerAvatarWithPaths::from).collect())
|
Ok(rows.into_iter().map(ServerAvatarWithPaths::from).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
/// Row type for prop asset lookup.
|
||||||
// Render Data Resolution
|
#[derive(Debug, sqlx::FromRow)]
|
||||||
// =============================================================================
|
struct PropAssetRow {
|
||||||
|
id: Uuid,
|
||||||
|
asset_path: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Resolve a server avatar to render data.
|
/// Resolve a server avatar to render data.
|
||||||
|
/// Joins the avatar's prop UUIDs with server.props to get asset paths.
|
||||||
pub async fn resolve_server_avatar_to_render_data<'e>(
|
pub async fn resolve_server_avatar_to_render_data<'e>(
|
||||||
executor: impl PgExecutor<'e>,
|
executor: impl PgExecutor<'e>,
|
||||||
avatar: &ServerAvatar,
|
avatar: &ServerAvatar,
|
||||||
current_emotion: EmotionState,
|
current_emotion: EmotionState,
|
||||||
) -> Result<AvatarRenderData, AppError> {
|
) -> Result<AvatarRenderData, AppError> {
|
||||||
let slots = extract_avatar_slots!(avatar);
|
// Collect all non-null prop UUIDs
|
||||||
let prop_ids = slots.collect_render_prop_ids(current_emotion);
|
let mut prop_ids: Vec<Uuid> = Vec::new();
|
||||||
let prop_map = build_prop_map(executor, &prop_ids, "server.props").await?;
|
|
||||||
Ok(resolve_slots_to_render_data(avatar.id, &slots, current_emotion, &prop_map))
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// Content layers
|
||||||
// Forced Avatar Management
|
for id in [
|
||||||
// =============================================================================
|
avatar.l_skin_0, avatar.l_skin_1, avatar.l_skin_2,
|
||||||
|
avatar.l_skin_3, avatar.l_skin_4, avatar.l_skin_5,
|
||||||
|
avatar.l_skin_6, avatar.l_skin_7, avatar.l_skin_8,
|
||||||
|
avatar.l_clothes_0, avatar.l_clothes_1, avatar.l_clothes_2,
|
||||||
|
avatar.l_clothes_3, avatar.l_clothes_4, avatar.l_clothes_5,
|
||||||
|
avatar.l_clothes_6, avatar.l_clothes_7, avatar.l_clothes_8,
|
||||||
|
avatar.l_accessories_0, avatar.l_accessories_1, avatar.l_accessories_2,
|
||||||
|
avatar.l_accessories_3, avatar.l_accessories_4, avatar.l_accessories_5,
|
||||||
|
avatar.l_accessories_6, avatar.l_accessories_7, avatar.l_accessories_8,
|
||||||
|
].iter().flatten() {
|
||||||
|
prop_ids.push(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get emotion layer slots based on current emotion
|
||||||
|
let emotion_slots: [Option<Uuid>; 9] = match current_emotion {
|
||||||
|
EmotionState::Neutral => [avatar.e_neutral_0, avatar.e_neutral_1, avatar.e_neutral_2,
|
||||||
|
avatar.e_neutral_3, avatar.e_neutral_4, avatar.e_neutral_5,
|
||||||
|
avatar.e_neutral_6, avatar.e_neutral_7, avatar.e_neutral_8],
|
||||||
|
EmotionState::Happy => [avatar.e_happy_0, avatar.e_happy_1, avatar.e_happy_2,
|
||||||
|
avatar.e_happy_3, avatar.e_happy_4, avatar.e_happy_5,
|
||||||
|
avatar.e_happy_6, avatar.e_happy_7, avatar.e_happy_8],
|
||||||
|
EmotionState::Sad => [avatar.e_sad_0, avatar.e_sad_1, avatar.e_sad_2,
|
||||||
|
avatar.e_sad_3, avatar.e_sad_4, avatar.e_sad_5,
|
||||||
|
avatar.e_sad_6, avatar.e_sad_7, avatar.e_sad_8],
|
||||||
|
EmotionState::Angry => [avatar.e_angry_0, avatar.e_angry_1, avatar.e_angry_2,
|
||||||
|
avatar.e_angry_3, avatar.e_angry_4, avatar.e_angry_5,
|
||||||
|
avatar.e_angry_6, avatar.e_angry_7, avatar.e_angry_8],
|
||||||
|
EmotionState::Surprised => [avatar.e_surprised_0, avatar.e_surprised_1, avatar.e_surprised_2,
|
||||||
|
avatar.e_surprised_3, avatar.e_surprised_4, avatar.e_surprised_5,
|
||||||
|
avatar.e_surprised_6, avatar.e_surprised_7, avatar.e_surprised_8],
|
||||||
|
EmotionState::Thinking => [avatar.e_thinking_0, avatar.e_thinking_1, avatar.e_thinking_2,
|
||||||
|
avatar.e_thinking_3, avatar.e_thinking_4, avatar.e_thinking_5,
|
||||||
|
avatar.e_thinking_6, avatar.e_thinking_7, avatar.e_thinking_8],
|
||||||
|
EmotionState::Laughing => [avatar.e_laughing_0, avatar.e_laughing_1, avatar.e_laughing_2,
|
||||||
|
avatar.e_laughing_3, avatar.e_laughing_4, avatar.e_laughing_5,
|
||||||
|
avatar.e_laughing_6, avatar.e_laughing_7, avatar.e_laughing_8],
|
||||||
|
EmotionState::Crying => [avatar.e_crying_0, avatar.e_crying_1, avatar.e_crying_2,
|
||||||
|
avatar.e_crying_3, avatar.e_crying_4, avatar.e_crying_5,
|
||||||
|
avatar.e_crying_6, avatar.e_crying_7, avatar.e_crying_8],
|
||||||
|
EmotionState::Love => [avatar.e_love_0, avatar.e_love_1, avatar.e_love_2,
|
||||||
|
avatar.e_love_3, avatar.e_love_4, avatar.e_love_5,
|
||||||
|
avatar.e_love_6, avatar.e_love_7, avatar.e_love_8],
|
||||||
|
EmotionState::Confused => [avatar.e_confused_0, avatar.e_confused_1, avatar.e_confused_2,
|
||||||
|
avatar.e_confused_3, avatar.e_confused_4, avatar.e_confused_5,
|
||||||
|
avatar.e_confused_6, avatar.e_confused_7, avatar.e_confused_8],
|
||||||
|
EmotionState::Sleeping => [avatar.e_sleeping_0, avatar.e_sleeping_1, avatar.e_sleeping_2,
|
||||||
|
avatar.e_sleeping_3, avatar.e_sleeping_4, avatar.e_sleeping_5,
|
||||||
|
avatar.e_sleeping_6, avatar.e_sleeping_7, avatar.e_sleeping_8],
|
||||||
|
EmotionState::Wink => [avatar.e_wink_0, avatar.e_wink_1, avatar.e_wink_2,
|
||||||
|
avatar.e_wink_3, avatar.e_wink_4, avatar.e_wink_5,
|
||||||
|
avatar.e_wink_6, avatar.e_wink_7, avatar.e_wink_8],
|
||||||
|
};
|
||||||
|
|
||||||
|
for id in emotion_slots.iter().flatten() {
|
||||||
|
prop_ids.push(*id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bulk lookup all prop asset paths
|
||||||
|
let prop_map: HashMap<Uuid, String> = if prop_ids.is_empty() {
|
||||||
|
HashMap::new()
|
||||||
|
} else {
|
||||||
|
let rows = sqlx::query_as::<_, PropAssetRow>(
|
||||||
|
r#"
|
||||||
|
SELECT id, asset_path
|
||||||
|
FROM server.props
|
||||||
|
WHERE id = ANY($1)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&prop_ids)
|
||||||
|
.fetch_all(executor)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
rows.into_iter().map(|r| (r.id, r.asset_path)).collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to look up path
|
||||||
|
let get_path = |id: Option<Uuid>| -> Option<String> {
|
||||||
|
id.and_then(|id| prop_map.get(&id).cloned())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(AvatarRenderData {
|
||||||
|
avatar_id: avatar.id,
|
||||||
|
current_emotion,
|
||||||
|
skin_layer: [
|
||||||
|
get_path(avatar.l_skin_0), get_path(avatar.l_skin_1), get_path(avatar.l_skin_2),
|
||||||
|
get_path(avatar.l_skin_3), get_path(avatar.l_skin_4), get_path(avatar.l_skin_5),
|
||||||
|
get_path(avatar.l_skin_6), get_path(avatar.l_skin_7), get_path(avatar.l_skin_8),
|
||||||
|
],
|
||||||
|
clothes_layer: [
|
||||||
|
get_path(avatar.l_clothes_0), get_path(avatar.l_clothes_1), get_path(avatar.l_clothes_2),
|
||||||
|
get_path(avatar.l_clothes_3), get_path(avatar.l_clothes_4), get_path(avatar.l_clothes_5),
|
||||||
|
get_path(avatar.l_clothes_6), get_path(avatar.l_clothes_7), get_path(avatar.l_clothes_8),
|
||||||
|
],
|
||||||
|
accessories_layer: [
|
||||||
|
get_path(avatar.l_accessories_0), get_path(avatar.l_accessories_1), get_path(avatar.l_accessories_2),
|
||||||
|
get_path(avatar.l_accessories_3), get_path(avatar.l_accessories_4), get_path(avatar.l_accessories_5),
|
||||||
|
get_path(avatar.l_accessories_6), get_path(avatar.l_accessories_7), get_path(avatar.l_accessories_8),
|
||||||
|
],
|
||||||
|
emotion_layer: [
|
||||||
|
get_path(emotion_slots[0]), get_path(emotion_slots[1]), get_path(emotion_slots[2]),
|
||||||
|
get_path(emotion_slots[3]), get_path(emotion_slots[4]), get_path(emotion_slots[5]),
|
||||||
|
get_path(emotion_slots[6]), get_path(emotion_slots[7]), get_path(emotion_slots[8]),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a forced server avatar to a user.
|
/// Apply a forced server avatar to a user.
|
||||||
pub async fn apply_forced_server_avatar<'e>(
|
pub async fn apply_forced_server_avatar<'e>(
|
||||||
|
|
@ -425,65 +599,155 @@ pub async fn create_server_avatar<'e>(
|
||||||
.bind(&req.thumbnail_path)
|
.bind(&req.thumbnail_path)
|
||||||
.bind(created_by)
|
.bind(created_by)
|
||||||
// Skin layer
|
// Skin layer
|
||||||
.bind(req.l_skin_0).bind(req.l_skin_1).bind(req.l_skin_2)
|
.bind(req.l_skin_0)
|
||||||
.bind(req.l_skin_3).bind(req.l_skin_4).bind(req.l_skin_5)
|
.bind(req.l_skin_1)
|
||||||
.bind(req.l_skin_6).bind(req.l_skin_7).bind(req.l_skin_8)
|
.bind(req.l_skin_2)
|
||||||
|
.bind(req.l_skin_3)
|
||||||
|
.bind(req.l_skin_4)
|
||||||
|
.bind(req.l_skin_5)
|
||||||
|
.bind(req.l_skin_6)
|
||||||
|
.bind(req.l_skin_7)
|
||||||
|
.bind(req.l_skin_8)
|
||||||
// Clothes layer
|
// Clothes layer
|
||||||
.bind(req.l_clothes_0).bind(req.l_clothes_1).bind(req.l_clothes_2)
|
.bind(req.l_clothes_0)
|
||||||
.bind(req.l_clothes_3).bind(req.l_clothes_4).bind(req.l_clothes_5)
|
.bind(req.l_clothes_1)
|
||||||
.bind(req.l_clothes_6).bind(req.l_clothes_7).bind(req.l_clothes_8)
|
.bind(req.l_clothes_2)
|
||||||
|
.bind(req.l_clothes_3)
|
||||||
|
.bind(req.l_clothes_4)
|
||||||
|
.bind(req.l_clothes_5)
|
||||||
|
.bind(req.l_clothes_6)
|
||||||
|
.bind(req.l_clothes_7)
|
||||||
|
.bind(req.l_clothes_8)
|
||||||
// Accessories layer
|
// Accessories layer
|
||||||
.bind(req.l_accessories_0).bind(req.l_accessories_1).bind(req.l_accessories_2)
|
.bind(req.l_accessories_0)
|
||||||
.bind(req.l_accessories_3).bind(req.l_accessories_4).bind(req.l_accessories_5)
|
.bind(req.l_accessories_1)
|
||||||
.bind(req.l_accessories_6).bind(req.l_accessories_7).bind(req.l_accessories_8)
|
.bind(req.l_accessories_2)
|
||||||
|
.bind(req.l_accessories_3)
|
||||||
|
.bind(req.l_accessories_4)
|
||||||
|
.bind(req.l_accessories_5)
|
||||||
|
.bind(req.l_accessories_6)
|
||||||
|
.bind(req.l_accessories_7)
|
||||||
|
.bind(req.l_accessories_8)
|
||||||
// Neutral emotion
|
// Neutral emotion
|
||||||
.bind(req.e_neutral_0).bind(req.e_neutral_1).bind(req.e_neutral_2)
|
.bind(req.e_neutral_0)
|
||||||
.bind(req.e_neutral_3).bind(req.e_neutral_4).bind(req.e_neutral_5)
|
.bind(req.e_neutral_1)
|
||||||
.bind(req.e_neutral_6).bind(req.e_neutral_7).bind(req.e_neutral_8)
|
.bind(req.e_neutral_2)
|
||||||
|
.bind(req.e_neutral_3)
|
||||||
|
.bind(req.e_neutral_4)
|
||||||
|
.bind(req.e_neutral_5)
|
||||||
|
.bind(req.e_neutral_6)
|
||||||
|
.bind(req.e_neutral_7)
|
||||||
|
.bind(req.e_neutral_8)
|
||||||
// Happy emotion
|
// Happy emotion
|
||||||
.bind(req.e_happy_0).bind(req.e_happy_1).bind(req.e_happy_2)
|
.bind(req.e_happy_0)
|
||||||
.bind(req.e_happy_3).bind(req.e_happy_4).bind(req.e_happy_5)
|
.bind(req.e_happy_1)
|
||||||
.bind(req.e_happy_6).bind(req.e_happy_7).bind(req.e_happy_8)
|
.bind(req.e_happy_2)
|
||||||
|
.bind(req.e_happy_3)
|
||||||
|
.bind(req.e_happy_4)
|
||||||
|
.bind(req.e_happy_5)
|
||||||
|
.bind(req.e_happy_6)
|
||||||
|
.bind(req.e_happy_7)
|
||||||
|
.bind(req.e_happy_8)
|
||||||
// Sad emotion
|
// Sad emotion
|
||||||
.bind(req.e_sad_0).bind(req.e_sad_1).bind(req.e_sad_2)
|
.bind(req.e_sad_0)
|
||||||
.bind(req.e_sad_3).bind(req.e_sad_4).bind(req.e_sad_5)
|
.bind(req.e_sad_1)
|
||||||
.bind(req.e_sad_6).bind(req.e_sad_7).bind(req.e_sad_8)
|
.bind(req.e_sad_2)
|
||||||
|
.bind(req.e_sad_3)
|
||||||
|
.bind(req.e_sad_4)
|
||||||
|
.bind(req.e_sad_5)
|
||||||
|
.bind(req.e_sad_6)
|
||||||
|
.bind(req.e_sad_7)
|
||||||
|
.bind(req.e_sad_8)
|
||||||
// Angry emotion
|
// Angry emotion
|
||||||
.bind(req.e_angry_0).bind(req.e_angry_1).bind(req.e_angry_2)
|
.bind(req.e_angry_0)
|
||||||
.bind(req.e_angry_3).bind(req.e_angry_4).bind(req.e_angry_5)
|
.bind(req.e_angry_1)
|
||||||
.bind(req.e_angry_6).bind(req.e_angry_7).bind(req.e_angry_8)
|
.bind(req.e_angry_2)
|
||||||
|
.bind(req.e_angry_3)
|
||||||
|
.bind(req.e_angry_4)
|
||||||
|
.bind(req.e_angry_5)
|
||||||
|
.bind(req.e_angry_6)
|
||||||
|
.bind(req.e_angry_7)
|
||||||
|
.bind(req.e_angry_8)
|
||||||
// Surprised emotion
|
// Surprised emotion
|
||||||
.bind(req.e_surprised_0).bind(req.e_surprised_1).bind(req.e_surprised_2)
|
.bind(req.e_surprised_0)
|
||||||
.bind(req.e_surprised_3).bind(req.e_surprised_4).bind(req.e_surprised_5)
|
.bind(req.e_surprised_1)
|
||||||
.bind(req.e_surprised_6).bind(req.e_surprised_7).bind(req.e_surprised_8)
|
.bind(req.e_surprised_2)
|
||||||
|
.bind(req.e_surprised_3)
|
||||||
|
.bind(req.e_surprised_4)
|
||||||
|
.bind(req.e_surprised_5)
|
||||||
|
.bind(req.e_surprised_6)
|
||||||
|
.bind(req.e_surprised_7)
|
||||||
|
.bind(req.e_surprised_8)
|
||||||
// Thinking emotion
|
// Thinking emotion
|
||||||
.bind(req.e_thinking_0).bind(req.e_thinking_1).bind(req.e_thinking_2)
|
.bind(req.e_thinking_0)
|
||||||
.bind(req.e_thinking_3).bind(req.e_thinking_4).bind(req.e_thinking_5)
|
.bind(req.e_thinking_1)
|
||||||
.bind(req.e_thinking_6).bind(req.e_thinking_7).bind(req.e_thinking_8)
|
.bind(req.e_thinking_2)
|
||||||
|
.bind(req.e_thinking_3)
|
||||||
|
.bind(req.e_thinking_4)
|
||||||
|
.bind(req.e_thinking_5)
|
||||||
|
.bind(req.e_thinking_6)
|
||||||
|
.bind(req.e_thinking_7)
|
||||||
|
.bind(req.e_thinking_8)
|
||||||
// Laughing emotion
|
// Laughing emotion
|
||||||
.bind(req.e_laughing_0).bind(req.e_laughing_1).bind(req.e_laughing_2)
|
.bind(req.e_laughing_0)
|
||||||
.bind(req.e_laughing_3).bind(req.e_laughing_4).bind(req.e_laughing_5)
|
.bind(req.e_laughing_1)
|
||||||
.bind(req.e_laughing_6).bind(req.e_laughing_7).bind(req.e_laughing_8)
|
.bind(req.e_laughing_2)
|
||||||
|
.bind(req.e_laughing_3)
|
||||||
|
.bind(req.e_laughing_4)
|
||||||
|
.bind(req.e_laughing_5)
|
||||||
|
.bind(req.e_laughing_6)
|
||||||
|
.bind(req.e_laughing_7)
|
||||||
|
.bind(req.e_laughing_8)
|
||||||
// Crying emotion
|
// Crying emotion
|
||||||
.bind(req.e_crying_0).bind(req.e_crying_1).bind(req.e_crying_2)
|
.bind(req.e_crying_0)
|
||||||
.bind(req.e_crying_3).bind(req.e_crying_4).bind(req.e_crying_5)
|
.bind(req.e_crying_1)
|
||||||
.bind(req.e_crying_6).bind(req.e_crying_7).bind(req.e_crying_8)
|
.bind(req.e_crying_2)
|
||||||
|
.bind(req.e_crying_3)
|
||||||
|
.bind(req.e_crying_4)
|
||||||
|
.bind(req.e_crying_5)
|
||||||
|
.bind(req.e_crying_6)
|
||||||
|
.bind(req.e_crying_7)
|
||||||
|
.bind(req.e_crying_8)
|
||||||
// Love emotion
|
// Love emotion
|
||||||
.bind(req.e_love_0).bind(req.e_love_1).bind(req.e_love_2)
|
.bind(req.e_love_0)
|
||||||
.bind(req.e_love_3).bind(req.e_love_4).bind(req.e_love_5)
|
.bind(req.e_love_1)
|
||||||
.bind(req.e_love_6).bind(req.e_love_7).bind(req.e_love_8)
|
.bind(req.e_love_2)
|
||||||
|
.bind(req.e_love_3)
|
||||||
|
.bind(req.e_love_4)
|
||||||
|
.bind(req.e_love_5)
|
||||||
|
.bind(req.e_love_6)
|
||||||
|
.bind(req.e_love_7)
|
||||||
|
.bind(req.e_love_8)
|
||||||
// Confused emotion
|
// Confused emotion
|
||||||
.bind(req.e_confused_0).bind(req.e_confused_1).bind(req.e_confused_2)
|
.bind(req.e_confused_0)
|
||||||
.bind(req.e_confused_3).bind(req.e_confused_4).bind(req.e_confused_5)
|
.bind(req.e_confused_1)
|
||||||
.bind(req.e_confused_6).bind(req.e_confused_7).bind(req.e_confused_8)
|
.bind(req.e_confused_2)
|
||||||
|
.bind(req.e_confused_3)
|
||||||
|
.bind(req.e_confused_4)
|
||||||
|
.bind(req.e_confused_5)
|
||||||
|
.bind(req.e_confused_6)
|
||||||
|
.bind(req.e_confused_7)
|
||||||
|
.bind(req.e_confused_8)
|
||||||
// Sleeping emotion
|
// Sleeping emotion
|
||||||
.bind(req.e_sleeping_0).bind(req.e_sleeping_1).bind(req.e_sleeping_2)
|
.bind(req.e_sleeping_0)
|
||||||
.bind(req.e_sleeping_3).bind(req.e_sleeping_4).bind(req.e_sleeping_5)
|
.bind(req.e_sleeping_1)
|
||||||
.bind(req.e_sleeping_6).bind(req.e_sleeping_7).bind(req.e_sleeping_8)
|
.bind(req.e_sleeping_2)
|
||||||
|
.bind(req.e_sleeping_3)
|
||||||
|
.bind(req.e_sleeping_4)
|
||||||
|
.bind(req.e_sleeping_5)
|
||||||
|
.bind(req.e_sleeping_6)
|
||||||
|
.bind(req.e_sleeping_7)
|
||||||
|
.bind(req.e_sleeping_8)
|
||||||
// Wink emotion
|
// Wink emotion
|
||||||
.bind(req.e_wink_0).bind(req.e_wink_1).bind(req.e_wink_2)
|
.bind(req.e_wink_0)
|
||||||
.bind(req.e_wink_3).bind(req.e_wink_4).bind(req.e_wink_5)
|
.bind(req.e_wink_1)
|
||||||
.bind(req.e_wink_6).bind(req.e_wink_7).bind(req.e_wink_8)
|
.bind(req.e_wink_2)
|
||||||
|
.bind(req.e_wink_3)
|
||||||
|
.bind(req.e_wink_4)
|
||||||
|
.bind(req.e_wink_5)
|
||||||
|
.bind(req.e_wink_6)
|
||||||
|
.bind(req.e_wink_7)
|
||||||
|
.bind(req.e_wink_8)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -504,51 +768,141 @@ pub async fn update_server_avatar<'e>(
|
||||||
is_public = COALESCE($4, is_public),
|
is_public = COALESCE($4, is_public),
|
||||||
is_active = COALESCE($5, is_active),
|
is_active = COALESCE($5, is_active),
|
||||||
thumbnail_path = COALESCE($6, thumbnail_path),
|
thumbnail_path = COALESCE($6, thumbnail_path),
|
||||||
l_skin_0 = COALESCE($7, l_skin_0), l_skin_1 = COALESCE($8, l_skin_1), l_skin_2 = COALESCE($9, l_skin_2),
|
l_skin_0 = COALESCE($7, l_skin_0),
|
||||||
l_skin_3 = COALESCE($10, l_skin_3), l_skin_4 = COALESCE($11, l_skin_4), l_skin_5 = COALESCE($12, l_skin_5),
|
l_skin_1 = COALESCE($8, l_skin_1),
|
||||||
l_skin_6 = COALESCE($13, l_skin_6), l_skin_7 = COALESCE($14, l_skin_7), l_skin_8 = COALESCE($15, l_skin_8),
|
l_skin_2 = COALESCE($9, l_skin_2),
|
||||||
l_clothes_0 = COALESCE($16, l_clothes_0), l_clothes_1 = COALESCE($17, l_clothes_1), l_clothes_2 = COALESCE($18, l_clothes_2),
|
l_skin_3 = COALESCE($10, l_skin_3),
|
||||||
l_clothes_3 = COALESCE($19, l_clothes_3), l_clothes_4 = COALESCE($20, l_clothes_4), l_clothes_5 = COALESCE($21, l_clothes_5),
|
l_skin_4 = COALESCE($11, l_skin_4),
|
||||||
l_clothes_6 = COALESCE($22, l_clothes_6), l_clothes_7 = COALESCE($23, l_clothes_7), l_clothes_8 = COALESCE($24, l_clothes_8),
|
l_skin_5 = COALESCE($12, l_skin_5),
|
||||||
l_accessories_0 = COALESCE($25, l_accessories_0), l_accessories_1 = COALESCE($26, l_accessories_1), l_accessories_2 = COALESCE($27, l_accessories_2),
|
l_skin_6 = COALESCE($13, l_skin_6),
|
||||||
l_accessories_3 = COALESCE($28, l_accessories_3), l_accessories_4 = COALESCE($29, l_accessories_4), l_accessories_5 = COALESCE($30, l_accessories_5),
|
l_skin_7 = COALESCE($14, l_skin_7),
|
||||||
l_accessories_6 = COALESCE($31, l_accessories_6), l_accessories_7 = COALESCE($32, l_accessories_7), l_accessories_8 = COALESCE($33, l_accessories_8),
|
l_skin_8 = COALESCE($15, l_skin_8),
|
||||||
e_neutral_0 = COALESCE($34, e_neutral_0), e_neutral_1 = COALESCE($35, e_neutral_1), e_neutral_2 = COALESCE($36, e_neutral_2),
|
l_clothes_0 = COALESCE($16, l_clothes_0),
|
||||||
e_neutral_3 = COALESCE($37, e_neutral_3), e_neutral_4 = COALESCE($38, e_neutral_4), e_neutral_5 = COALESCE($39, e_neutral_5),
|
l_clothes_1 = COALESCE($17, l_clothes_1),
|
||||||
e_neutral_6 = COALESCE($40, e_neutral_6), e_neutral_7 = COALESCE($41, e_neutral_7), e_neutral_8 = COALESCE($42, e_neutral_8),
|
l_clothes_2 = COALESCE($18, l_clothes_2),
|
||||||
e_happy_0 = COALESCE($43, e_happy_0), e_happy_1 = COALESCE($44, e_happy_1), e_happy_2 = COALESCE($45, e_happy_2),
|
l_clothes_3 = COALESCE($19, l_clothes_3),
|
||||||
e_happy_3 = COALESCE($46, e_happy_3), e_happy_4 = COALESCE($47, e_happy_4), e_happy_5 = COALESCE($48, e_happy_5),
|
l_clothes_4 = COALESCE($20, l_clothes_4),
|
||||||
e_happy_6 = COALESCE($49, e_happy_6), e_happy_7 = COALESCE($50, e_happy_7), e_happy_8 = COALESCE($51, e_happy_8),
|
l_clothes_5 = COALESCE($21, l_clothes_5),
|
||||||
e_sad_0 = COALESCE($52, e_sad_0), e_sad_1 = COALESCE($53, e_sad_1), e_sad_2 = COALESCE($54, e_sad_2),
|
l_clothes_6 = COALESCE($22, l_clothes_6),
|
||||||
e_sad_3 = COALESCE($55, e_sad_3), e_sad_4 = COALESCE($56, e_sad_4), e_sad_5 = COALESCE($57, e_sad_5),
|
l_clothes_7 = COALESCE($23, l_clothes_7),
|
||||||
e_sad_6 = COALESCE($58, e_sad_6), e_sad_7 = COALESCE($59, e_sad_7), e_sad_8 = COALESCE($60, e_sad_8),
|
l_clothes_8 = COALESCE($24, l_clothes_8),
|
||||||
e_angry_0 = COALESCE($61, e_angry_0), e_angry_1 = COALESCE($62, e_angry_1), e_angry_2 = COALESCE($63, e_angry_2),
|
l_accessories_0 = COALESCE($25, l_accessories_0),
|
||||||
e_angry_3 = COALESCE($64, e_angry_3), e_angry_4 = COALESCE($65, e_angry_4), e_angry_5 = COALESCE($66, e_angry_5),
|
l_accessories_1 = COALESCE($26, l_accessories_1),
|
||||||
e_angry_6 = COALESCE($67, e_angry_6), e_angry_7 = COALESCE($68, e_angry_7), e_angry_8 = COALESCE($69, e_angry_8),
|
l_accessories_2 = COALESCE($27, l_accessories_2),
|
||||||
e_surprised_0 = COALESCE($70, e_surprised_0), e_surprised_1 = COALESCE($71, e_surprised_1), e_surprised_2 = COALESCE($72, e_surprised_2),
|
l_accessories_3 = COALESCE($28, l_accessories_3),
|
||||||
e_surprised_3 = COALESCE($73, e_surprised_3), e_surprised_4 = COALESCE($74, e_surprised_4), e_surprised_5 = COALESCE($75, e_surprised_5),
|
l_accessories_4 = COALESCE($29, l_accessories_4),
|
||||||
e_surprised_6 = COALESCE($76, e_surprised_6), e_surprised_7 = COALESCE($77, e_surprised_7), e_surprised_8 = COALESCE($78, e_surprised_8),
|
l_accessories_5 = COALESCE($30, l_accessories_5),
|
||||||
e_thinking_0 = COALESCE($79, e_thinking_0), e_thinking_1 = COALESCE($80, e_thinking_1), e_thinking_2 = COALESCE($81, e_thinking_2),
|
l_accessories_6 = COALESCE($31, l_accessories_6),
|
||||||
e_thinking_3 = COALESCE($82, e_thinking_3), e_thinking_4 = COALESCE($83, e_thinking_4), e_thinking_5 = COALESCE($84, e_thinking_5),
|
l_accessories_7 = COALESCE($32, l_accessories_7),
|
||||||
e_thinking_6 = COALESCE($85, e_thinking_6), e_thinking_7 = COALESCE($86, e_thinking_7), e_thinking_8 = COALESCE($87, e_thinking_8),
|
l_accessories_8 = COALESCE($33, l_accessories_8),
|
||||||
e_laughing_0 = COALESCE($88, e_laughing_0), e_laughing_1 = COALESCE($89, e_laughing_1), e_laughing_2 = COALESCE($90, e_laughing_2),
|
e_neutral_0 = COALESCE($34, e_neutral_0),
|
||||||
e_laughing_3 = COALESCE($91, e_laughing_3), e_laughing_4 = COALESCE($92, e_laughing_4), e_laughing_5 = COALESCE($93, e_laughing_5),
|
e_neutral_1 = COALESCE($35, e_neutral_1),
|
||||||
e_laughing_6 = COALESCE($94, e_laughing_6), e_laughing_7 = COALESCE($95, e_laughing_7), e_laughing_8 = COALESCE($96, e_laughing_8),
|
e_neutral_2 = COALESCE($36, e_neutral_2),
|
||||||
e_crying_0 = COALESCE($97, e_crying_0), e_crying_1 = COALESCE($98, e_crying_1), e_crying_2 = COALESCE($99, e_crying_2),
|
e_neutral_3 = COALESCE($37, e_neutral_3),
|
||||||
e_crying_3 = COALESCE($100, e_crying_3), e_crying_4 = COALESCE($101, e_crying_4), e_crying_5 = COALESCE($102, e_crying_5),
|
e_neutral_4 = COALESCE($38, e_neutral_4),
|
||||||
e_crying_6 = COALESCE($103, e_crying_6), e_crying_7 = COALESCE($104, e_crying_7), e_crying_8 = COALESCE($105, e_crying_8),
|
e_neutral_5 = COALESCE($39, e_neutral_5),
|
||||||
e_love_0 = COALESCE($106, e_love_0), e_love_1 = COALESCE($107, e_love_1), e_love_2 = COALESCE($108, e_love_2),
|
e_neutral_6 = COALESCE($40, e_neutral_6),
|
||||||
e_love_3 = COALESCE($109, e_love_3), e_love_4 = COALESCE($110, e_love_4), e_love_5 = COALESCE($111, e_love_5),
|
e_neutral_7 = COALESCE($41, e_neutral_7),
|
||||||
e_love_6 = COALESCE($112, e_love_6), e_love_7 = COALESCE($113, e_love_7), e_love_8 = COALESCE($114, e_love_8),
|
e_neutral_8 = COALESCE($42, e_neutral_8),
|
||||||
e_confused_0 = COALESCE($115, e_confused_0), e_confused_1 = COALESCE($116, e_confused_1), e_confused_2 = COALESCE($117, e_confused_2),
|
e_happy_0 = COALESCE($43, e_happy_0),
|
||||||
e_confused_3 = COALESCE($118, e_confused_3), e_confused_4 = COALESCE($119, e_confused_4), e_confused_5 = COALESCE($120, e_confused_5),
|
e_happy_1 = COALESCE($44, e_happy_1),
|
||||||
e_confused_6 = COALESCE($121, e_confused_6), e_confused_7 = COALESCE($122, e_confused_7), e_confused_8 = COALESCE($123, e_confused_8),
|
e_happy_2 = COALESCE($45, e_happy_2),
|
||||||
e_sleeping_0 = COALESCE($124, e_sleeping_0), e_sleeping_1 = COALESCE($125, e_sleeping_1), e_sleeping_2 = COALESCE($126, e_sleeping_2),
|
e_happy_3 = COALESCE($46, e_happy_3),
|
||||||
e_sleeping_3 = COALESCE($127, e_sleeping_3), e_sleeping_4 = COALESCE($128, e_sleeping_4), e_sleeping_5 = COALESCE($129, e_sleeping_5),
|
e_happy_4 = COALESCE($47, e_happy_4),
|
||||||
e_sleeping_6 = COALESCE($130, e_sleeping_6), e_sleeping_7 = COALESCE($131, e_sleeping_7), e_sleeping_8 = COALESCE($132, e_sleeping_8),
|
e_happy_5 = COALESCE($48, e_happy_5),
|
||||||
e_wink_0 = COALESCE($133, e_wink_0), e_wink_1 = COALESCE($134, e_wink_1), e_wink_2 = COALESCE($135, e_wink_2),
|
e_happy_6 = COALESCE($49, e_happy_6),
|
||||||
e_wink_3 = COALESCE($136, e_wink_3), e_wink_4 = COALESCE($137, e_wink_4), e_wink_5 = COALESCE($138, e_wink_5),
|
e_happy_7 = COALESCE($50, e_happy_7),
|
||||||
e_wink_6 = COALESCE($139, e_wink_6), e_wink_7 = COALESCE($140, e_wink_7), e_wink_8 = COALESCE($141, e_wink_8),
|
e_happy_8 = COALESCE($51, e_happy_8),
|
||||||
|
e_sad_0 = COALESCE($52, e_sad_0),
|
||||||
|
e_sad_1 = COALESCE($53, e_sad_1),
|
||||||
|
e_sad_2 = COALESCE($54, e_sad_2),
|
||||||
|
e_sad_3 = COALESCE($55, e_sad_3),
|
||||||
|
e_sad_4 = COALESCE($56, e_sad_4),
|
||||||
|
e_sad_5 = COALESCE($57, e_sad_5),
|
||||||
|
e_sad_6 = COALESCE($58, e_sad_6),
|
||||||
|
e_sad_7 = COALESCE($59, e_sad_7),
|
||||||
|
e_sad_8 = COALESCE($60, e_sad_8),
|
||||||
|
e_angry_0 = COALESCE($61, e_angry_0),
|
||||||
|
e_angry_1 = COALESCE($62, e_angry_1),
|
||||||
|
e_angry_2 = COALESCE($63, e_angry_2),
|
||||||
|
e_angry_3 = COALESCE($64, e_angry_3),
|
||||||
|
e_angry_4 = COALESCE($65, e_angry_4),
|
||||||
|
e_angry_5 = COALESCE($66, e_angry_5),
|
||||||
|
e_angry_6 = COALESCE($67, e_angry_6),
|
||||||
|
e_angry_7 = COALESCE($68, e_angry_7),
|
||||||
|
e_angry_8 = COALESCE($69, e_angry_8),
|
||||||
|
e_surprised_0 = COALESCE($70, e_surprised_0),
|
||||||
|
e_surprised_1 = COALESCE($71, e_surprised_1),
|
||||||
|
e_surprised_2 = COALESCE($72, e_surprised_2),
|
||||||
|
e_surprised_3 = COALESCE($73, e_surprised_3),
|
||||||
|
e_surprised_4 = COALESCE($74, e_surprised_4),
|
||||||
|
e_surprised_5 = COALESCE($75, e_surprised_5),
|
||||||
|
e_surprised_6 = COALESCE($76, e_surprised_6),
|
||||||
|
e_surprised_7 = COALESCE($77, e_surprised_7),
|
||||||
|
e_surprised_8 = COALESCE($78, e_surprised_8),
|
||||||
|
e_thinking_0 = COALESCE($79, e_thinking_0),
|
||||||
|
e_thinking_1 = COALESCE($80, e_thinking_1),
|
||||||
|
e_thinking_2 = COALESCE($81, e_thinking_2),
|
||||||
|
e_thinking_3 = COALESCE($82, e_thinking_3),
|
||||||
|
e_thinking_4 = COALESCE($83, e_thinking_4),
|
||||||
|
e_thinking_5 = COALESCE($84, e_thinking_5),
|
||||||
|
e_thinking_6 = COALESCE($85, e_thinking_6),
|
||||||
|
e_thinking_7 = COALESCE($86, e_thinking_7),
|
||||||
|
e_thinking_8 = COALESCE($87, e_thinking_8),
|
||||||
|
e_laughing_0 = COALESCE($88, e_laughing_0),
|
||||||
|
e_laughing_1 = COALESCE($89, e_laughing_1),
|
||||||
|
e_laughing_2 = COALESCE($90, e_laughing_2),
|
||||||
|
e_laughing_3 = COALESCE($91, e_laughing_3),
|
||||||
|
e_laughing_4 = COALESCE($92, e_laughing_4),
|
||||||
|
e_laughing_5 = COALESCE($93, e_laughing_5),
|
||||||
|
e_laughing_6 = COALESCE($94, e_laughing_6),
|
||||||
|
e_laughing_7 = COALESCE($95, e_laughing_7),
|
||||||
|
e_laughing_8 = COALESCE($96, e_laughing_8),
|
||||||
|
e_crying_0 = COALESCE($97, e_crying_0),
|
||||||
|
e_crying_1 = COALESCE($98, e_crying_1),
|
||||||
|
e_crying_2 = COALESCE($99, e_crying_2),
|
||||||
|
e_crying_3 = COALESCE($100, e_crying_3),
|
||||||
|
e_crying_4 = COALESCE($101, e_crying_4),
|
||||||
|
e_crying_5 = COALESCE($102, e_crying_5),
|
||||||
|
e_crying_6 = COALESCE($103, e_crying_6),
|
||||||
|
e_crying_7 = COALESCE($104, e_crying_7),
|
||||||
|
e_crying_8 = COALESCE($105, e_crying_8),
|
||||||
|
e_love_0 = COALESCE($106, e_love_0),
|
||||||
|
e_love_1 = COALESCE($107, e_love_1),
|
||||||
|
e_love_2 = COALESCE($108, e_love_2),
|
||||||
|
e_love_3 = COALESCE($109, e_love_3),
|
||||||
|
e_love_4 = COALESCE($110, e_love_4),
|
||||||
|
e_love_5 = COALESCE($111, e_love_5),
|
||||||
|
e_love_6 = COALESCE($112, e_love_6),
|
||||||
|
e_love_7 = COALESCE($113, e_love_7),
|
||||||
|
e_love_8 = COALESCE($114, e_love_8),
|
||||||
|
e_confused_0 = COALESCE($115, e_confused_0),
|
||||||
|
e_confused_1 = COALESCE($116, e_confused_1),
|
||||||
|
e_confused_2 = COALESCE($117, e_confused_2),
|
||||||
|
e_confused_3 = COALESCE($118, e_confused_3),
|
||||||
|
e_confused_4 = COALESCE($119, e_confused_4),
|
||||||
|
e_confused_5 = COALESCE($120, e_confused_5),
|
||||||
|
e_confused_6 = COALESCE($121, e_confused_6),
|
||||||
|
e_confused_7 = COALESCE($122, e_confused_7),
|
||||||
|
e_confused_8 = COALESCE($123, e_confused_8),
|
||||||
|
e_sleeping_0 = COALESCE($124, e_sleeping_0),
|
||||||
|
e_sleeping_1 = COALESCE($125, e_sleeping_1),
|
||||||
|
e_sleeping_2 = COALESCE($126, e_sleeping_2),
|
||||||
|
e_sleeping_3 = COALESCE($127, e_sleeping_3),
|
||||||
|
e_sleeping_4 = COALESCE($128, e_sleeping_4),
|
||||||
|
e_sleeping_5 = COALESCE($129, e_sleeping_5),
|
||||||
|
e_sleeping_6 = COALESCE($130, e_sleeping_6),
|
||||||
|
e_sleeping_7 = COALESCE($131, e_sleeping_7),
|
||||||
|
e_sleeping_8 = COALESCE($132, e_sleeping_8),
|
||||||
|
e_wink_0 = COALESCE($133, e_wink_0),
|
||||||
|
e_wink_1 = COALESCE($134, e_wink_1),
|
||||||
|
e_wink_2 = COALESCE($135, e_wink_2),
|
||||||
|
e_wink_3 = COALESCE($136, e_wink_3),
|
||||||
|
e_wink_4 = COALESCE($137, e_wink_4),
|
||||||
|
e_wink_5 = COALESCE($138, e_wink_5),
|
||||||
|
e_wink_6 = COALESCE($139, e_wink_6),
|
||||||
|
e_wink_7 = COALESCE($140, e_wink_7),
|
||||||
|
e_wink_8 = COALESCE($141, e_wink_8),
|
||||||
updated_at = now()
|
updated_at = now()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING *
|
RETURNING *
|
||||||
|
|
@ -561,65 +915,155 @@ pub async fn update_server_avatar<'e>(
|
||||||
.bind(req.is_active)
|
.bind(req.is_active)
|
||||||
.bind(&req.thumbnail_path)
|
.bind(&req.thumbnail_path)
|
||||||
// Skin layer
|
// Skin layer
|
||||||
.bind(req.l_skin_0).bind(req.l_skin_1).bind(req.l_skin_2)
|
.bind(req.l_skin_0)
|
||||||
.bind(req.l_skin_3).bind(req.l_skin_4).bind(req.l_skin_5)
|
.bind(req.l_skin_1)
|
||||||
.bind(req.l_skin_6).bind(req.l_skin_7).bind(req.l_skin_8)
|
.bind(req.l_skin_2)
|
||||||
|
.bind(req.l_skin_3)
|
||||||
|
.bind(req.l_skin_4)
|
||||||
|
.bind(req.l_skin_5)
|
||||||
|
.bind(req.l_skin_6)
|
||||||
|
.bind(req.l_skin_7)
|
||||||
|
.bind(req.l_skin_8)
|
||||||
// Clothes layer
|
// Clothes layer
|
||||||
.bind(req.l_clothes_0).bind(req.l_clothes_1).bind(req.l_clothes_2)
|
.bind(req.l_clothes_0)
|
||||||
.bind(req.l_clothes_3).bind(req.l_clothes_4).bind(req.l_clothes_5)
|
.bind(req.l_clothes_1)
|
||||||
.bind(req.l_clothes_6).bind(req.l_clothes_7).bind(req.l_clothes_8)
|
.bind(req.l_clothes_2)
|
||||||
|
.bind(req.l_clothes_3)
|
||||||
|
.bind(req.l_clothes_4)
|
||||||
|
.bind(req.l_clothes_5)
|
||||||
|
.bind(req.l_clothes_6)
|
||||||
|
.bind(req.l_clothes_7)
|
||||||
|
.bind(req.l_clothes_8)
|
||||||
// Accessories layer
|
// Accessories layer
|
||||||
.bind(req.l_accessories_0).bind(req.l_accessories_1).bind(req.l_accessories_2)
|
.bind(req.l_accessories_0)
|
||||||
.bind(req.l_accessories_3).bind(req.l_accessories_4).bind(req.l_accessories_5)
|
.bind(req.l_accessories_1)
|
||||||
.bind(req.l_accessories_6).bind(req.l_accessories_7).bind(req.l_accessories_8)
|
.bind(req.l_accessories_2)
|
||||||
|
.bind(req.l_accessories_3)
|
||||||
|
.bind(req.l_accessories_4)
|
||||||
|
.bind(req.l_accessories_5)
|
||||||
|
.bind(req.l_accessories_6)
|
||||||
|
.bind(req.l_accessories_7)
|
||||||
|
.bind(req.l_accessories_8)
|
||||||
// Neutral emotion
|
// Neutral emotion
|
||||||
.bind(req.e_neutral_0).bind(req.e_neutral_1).bind(req.e_neutral_2)
|
.bind(req.e_neutral_0)
|
||||||
.bind(req.e_neutral_3).bind(req.e_neutral_4).bind(req.e_neutral_5)
|
.bind(req.e_neutral_1)
|
||||||
.bind(req.e_neutral_6).bind(req.e_neutral_7).bind(req.e_neutral_8)
|
.bind(req.e_neutral_2)
|
||||||
|
.bind(req.e_neutral_3)
|
||||||
|
.bind(req.e_neutral_4)
|
||||||
|
.bind(req.e_neutral_5)
|
||||||
|
.bind(req.e_neutral_6)
|
||||||
|
.bind(req.e_neutral_7)
|
||||||
|
.bind(req.e_neutral_8)
|
||||||
// Happy emotion
|
// Happy emotion
|
||||||
.bind(req.e_happy_0).bind(req.e_happy_1).bind(req.e_happy_2)
|
.bind(req.e_happy_0)
|
||||||
.bind(req.e_happy_3).bind(req.e_happy_4).bind(req.e_happy_5)
|
.bind(req.e_happy_1)
|
||||||
.bind(req.e_happy_6).bind(req.e_happy_7).bind(req.e_happy_8)
|
.bind(req.e_happy_2)
|
||||||
|
.bind(req.e_happy_3)
|
||||||
|
.bind(req.e_happy_4)
|
||||||
|
.bind(req.e_happy_5)
|
||||||
|
.bind(req.e_happy_6)
|
||||||
|
.bind(req.e_happy_7)
|
||||||
|
.bind(req.e_happy_8)
|
||||||
// Sad emotion
|
// Sad emotion
|
||||||
.bind(req.e_sad_0).bind(req.e_sad_1).bind(req.e_sad_2)
|
.bind(req.e_sad_0)
|
||||||
.bind(req.e_sad_3).bind(req.e_sad_4).bind(req.e_sad_5)
|
.bind(req.e_sad_1)
|
||||||
.bind(req.e_sad_6).bind(req.e_sad_7).bind(req.e_sad_8)
|
.bind(req.e_sad_2)
|
||||||
|
.bind(req.e_sad_3)
|
||||||
|
.bind(req.e_sad_4)
|
||||||
|
.bind(req.e_sad_5)
|
||||||
|
.bind(req.e_sad_6)
|
||||||
|
.bind(req.e_sad_7)
|
||||||
|
.bind(req.e_sad_8)
|
||||||
// Angry emotion
|
// Angry emotion
|
||||||
.bind(req.e_angry_0).bind(req.e_angry_1).bind(req.e_angry_2)
|
.bind(req.e_angry_0)
|
||||||
.bind(req.e_angry_3).bind(req.e_angry_4).bind(req.e_angry_5)
|
.bind(req.e_angry_1)
|
||||||
.bind(req.e_angry_6).bind(req.e_angry_7).bind(req.e_angry_8)
|
.bind(req.e_angry_2)
|
||||||
|
.bind(req.e_angry_3)
|
||||||
|
.bind(req.e_angry_4)
|
||||||
|
.bind(req.e_angry_5)
|
||||||
|
.bind(req.e_angry_6)
|
||||||
|
.bind(req.e_angry_7)
|
||||||
|
.bind(req.e_angry_8)
|
||||||
// Surprised emotion
|
// Surprised emotion
|
||||||
.bind(req.e_surprised_0).bind(req.e_surprised_1).bind(req.e_surprised_2)
|
.bind(req.e_surprised_0)
|
||||||
.bind(req.e_surprised_3).bind(req.e_surprised_4).bind(req.e_surprised_5)
|
.bind(req.e_surprised_1)
|
||||||
.bind(req.e_surprised_6).bind(req.e_surprised_7).bind(req.e_surprised_8)
|
.bind(req.e_surprised_2)
|
||||||
|
.bind(req.e_surprised_3)
|
||||||
|
.bind(req.e_surprised_4)
|
||||||
|
.bind(req.e_surprised_5)
|
||||||
|
.bind(req.e_surprised_6)
|
||||||
|
.bind(req.e_surprised_7)
|
||||||
|
.bind(req.e_surprised_8)
|
||||||
// Thinking emotion
|
// Thinking emotion
|
||||||
.bind(req.e_thinking_0).bind(req.e_thinking_1).bind(req.e_thinking_2)
|
.bind(req.e_thinking_0)
|
||||||
.bind(req.e_thinking_3).bind(req.e_thinking_4).bind(req.e_thinking_5)
|
.bind(req.e_thinking_1)
|
||||||
.bind(req.e_thinking_6).bind(req.e_thinking_7).bind(req.e_thinking_8)
|
.bind(req.e_thinking_2)
|
||||||
|
.bind(req.e_thinking_3)
|
||||||
|
.bind(req.e_thinking_4)
|
||||||
|
.bind(req.e_thinking_5)
|
||||||
|
.bind(req.e_thinking_6)
|
||||||
|
.bind(req.e_thinking_7)
|
||||||
|
.bind(req.e_thinking_8)
|
||||||
// Laughing emotion
|
// Laughing emotion
|
||||||
.bind(req.e_laughing_0).bind(req.e_laughing_1).bind(req.e_laughing_2)
|
.bind(req.e_laughing_0)
|
||||||
.bind(req.e_laughing_3).bind(req.e_laughing_4).bind(req.e_laughing_5)
|
.bind(req.e_laughing_1)
|
||||||
.bind(req.e_laughing_6).bind(req.e_laughing_7).bind(req.e_laughing_8)
|
.bind(req.e_laughing_2)
|
||||||
|
.bind(req.e_laughing_3)
|
||||||
|
.bind(req.e_laughing_4)
|
||||||
|
.bind(req.e_laughing_5)
|
||||||
|
.bind(req.e_laughing_6)
|
||||||
|
.bind(req.e_laughing_7)
|
||||||
|
.bind(req.e_laughing_8)
|
||||||
// Crying emotion
|
// Crying emotion
|
||||||
.bind(req.e_crying_0).bind(req.e_crying_1).bind(req.e_crying_2)
|
.bind(req.e_crying_0)
|
||||||
.bind(req.e_crying_3).bind(req.e_crying_4).bind(req.e_crying_5)
|
.bind(req.e_crying_1)
|
||||||
.bind(req.e_crying_6).bind(req.e_crying_7).bind(req.e_crying_8)
|
.bind(req.e_crying_2)
|
||||||
|
.bind(req.e_crying_3)
|
||||||
|
.bind(req.e_crying_4)
|
||||||
|
.bind(req.e_crying_5)
|
||||||
|
.bind(req.e_crying_6)
|
||||||
|
.bind(req.e_crying_7)
|
||||||
|
.bind(req.e_crying_8)
|
||||||
// Love emotion
|
// Love emotion
|
||||||
.bind(req.e_love_0).bind(req.e_love_1).bind(req.e_love_2)
|
.bind(req.e_love_0)
|
||||||
.bind(req.e_love_3).bind(req.e_love_4).bind(req.e_love_5)
|
.bind(req.e_love_1)
|
||||||
.bind(req.e_love_6).bind(req.e_love_7).bind(req.e_love_8)
|
.bind(req.e_love_2)
|
||||||
|
.bind(req.e_love_3)
|
||||||
|
.bind(req.e_love_4)
|
||||||
|
.bind(req.e_love_5)
|
||||||
|
.bind(req.e_love_6)
|
||||||
|
.bind(req.e_love_7)
|
||||||
|
.bind(req.e_love_8)
|
||||||
// Confused emotion
|
// Confused emotion
|
||||||
.bind(req.e_confused_0).bind(req.e_confused_1).bind(req.e_confused_2)
|
.bind(req.e_confused_0)
|
||||||
.bind(req.e_confused_3).bind(req.e_confused_4).bind(req.e_confused_5)
|
.bind(req.e_confused_1)
|
||||||
.bind(req.e_confused_6).bind(req.e_confused_7).bind(req.e_confused_8)
|
.bind(req.e_confused_2)
|
||||||
|
.bind(req.e_confused_3)
|
||||||
|
.bind(req.e_confused_4)
|
||||||
|
.bind(req.e_confused_5)
|
||||||
|
.bind(req.e_confused_6)
|
||||||
|
.bind(req.e_confused_7)
|
||||||
|
.bind(req.e_confused_8)
|
||||||
// Sleeping emotion
|
// Sleeping emotion
|
||||||
.bind(req.e_sleeping_0).bind(req.e_sleeping_1).bind(req.e_sleeping_2)
|
.bind(req.e_sleeping_0)
|
||||||
.bind(req.e_sleeping_3).bind(req.e_sleeping_4).bind(req.e_sleeping_5)
|
.bind(req.e_sleeping_1)
|
||||||
.bind(req.e_sleeping_6).bind(req.e_sleeping_7).bind(req.e_sleeping_8)
|
.bind(req.e_sleeping_2)
|
||||||
|
.bind(req.e_sleeping_3)
|
||||||
|
.bind(req.e_sleeping_4)
|
||||||
|
.bind(req.e_sleeping_5)
|
||||||
|
.bind(req.e_sleeping_6)
|
||||||
|
.bind(req.e_sleeping_7)
|
||||||
|
.bind(req.e_sleeping_8)
|
||||||
// Wink emotion
|
// Wink emotion
|
||||||
.bind(req.e_wink_0).bind(req.e_wink_1).bind(req.e_wink_2)
|
.bind(req.e_wink_0)
|
||||||
.bind(req.e_wink_3).bind(req.e_wink_4).bind(req.e_wink_5)
|
.bind(req.e_wink_1)
|
||||||
.bind(req.e_wink_6).bind(req.e_wink_7).bind(req.e_wink_8)
|
.bind(req.e_wink_2)
|
||||||
|
.bind(req.e_wink_3)
|
||||||
|
.bind(req.e_wink_4)
|
||||||
|
.bind(req.e_wink_5)
|
||||||
|
.bind(req.e_wink_6)
|
||||||
|
.bind(req.e_wink_7)
|
||||||
|
.bind(req.e_wink_8)
|
||||||
.fetch_one(executor)
|
.fetch_one(executor)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ use sqlx::PgExecutor;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::models::{CreateSpotRequest, Spot, SpotSummary, UpdateSpotRequest};
|
use crate::models::{CreateSpotRequest, Spot, SpotSummary, UpdateSpotRequest};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
|
|
||||||
/// List all spots for a scene.
|
/// List all spots for a scene.
|
||||||
pub async fn list_spots_for_scene<'e>(
|
pub async fn list_spots_for_scene<'e>(
|
||||||
|
|
@ -289,7 +289,7 @@ pub async fn update_spot<'e>(
|
||||||
let spot = query_builder
|
let spot = query_builder
|
||||||
.fetch_optional(executor)
|
.fetch_optional(executor)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Spot")?;
|
.ok_or_else(|| AppError::NotFound("Spot not found".to_string()))?;
|
||||||
|
|
||||||
Ok(spot)
|
Ok(spot)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,42 +104,6 @@ pub enum ClientMessage {
|
||||||
/// Request to refresh identity after registration (guest → user conversion).
|
/// Request to refresh identity after registration (guest → user conversion).
|
||||||
/// Server will fetch updated user data and broadcast to all members.
|
/// Server will fetch updated user data and broadcast to all members.
|
||||||
RefreshIdentity,
|
RefreshIdentity,
|
||||||
|
|
||||||
/// Update a loose prop's scale (moderator only).
|
|
||||||
UpdateProp {
|
|
||||||
/// The loose prop ID to update.
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
/// New scale factor (0.1 - 10.0).
|
|
||||||
scale: f32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Move a loose prop to a new position.
|
|
||||||
MoveProp {
|
|
||||||
/// The loose prop ID to move.
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
/// New X coordinate in scene space.
|
|
||||||
x: f64,
|
|
||||||
/// New Y coordinate in scene space.
|
|
||||||
y: f64,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Lock a loose prop (moderator only).
|
|
||||||
LockProp {
|
|
||||||
/// The loose prop ID to lock.
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Unlock a loose prop (moderator only).
|
|
||||||
UnlockProp {
|
|
||||||
/// The loose prop ID to unlock.
|
|
||||||
loose_prop_id: Uuid,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Permanently delete a prop from inventory (does not drop to scene).
|
|
||||||
DeleteProp {
|
|
||||||
/// Inventory item ID to delete.
|
|
||||||
inventory_item_id: Uuid,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Server-to-client WebSocket messages.
|
/// Server-to-client WebSocket messages.
|
||||||
|
|
@ -257,18 +221,6 @@ pub enum ServerMessage {
|
||||||
prop_id: Uuid,
|
prop_id: Uuid,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// A prop was permanently deleted from inventory.
|
|
||||||
PropDeleted {
|
|
||||||
/// ID of the deleted inventory item.
|
|
||||||
inventory_item_id: Uuid,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A prop was updated (scale changed) - clients should update their local copy.
|
|
||||||
PropRefresh {
|
|
||||||
/// The updated prop with all current values.
|
|
||||||
prop: LooseProp,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A member updated their avatar appearance.
|
/// A member updated their avatar appearance.
|
||||||
AvatarUpdated {
|
AvatarUpdated {
|
||||||
/// User ID of the member.
|
/// User ID of the member.
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,6 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
/// Extension trait for Option to convert to AppError::NotFound.
|
|
||||||
///
|
|
||||||
/// Reduces boilerplate when fetching entities that may not exist.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
/// ```rust
|
|
||||||
/// use chattyness_error::OptionExt;
|
|
||||||
///
|
|
||||||
/// let scene = get_scene_by_id(&pool, id).await?.or_not_found("Scene")?;
|
|
||||||
/// ```
|
|
||||||
pub trait OptionExt<T> {
|
|
||||||
/// Convert None to AppError::NotFound with a descriptive message.
|
|
||||||
fn or_not_found(self, entity: &str) -> Result<T, AppError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> OptionExt<T> for Option<T> {
|
|
||||||
fn or_not_found(self, entity: &str) -> Result<T, AppError> {
|
|
||||||
self.ok_or_else(|| AppError::NotFound(format!("{} not found", entity)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Application error types for chattyness.
|
/// Application error types for chattyness.
|
||||||
///
|
///
|
||||||
/// All errors derive From for automatic conversion where applicable.
|
/// All errors derive From for automatic conversion where applicable.
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use chattyness_db::{
|
||||||
models::{Scene, SceneSummary, Spot, SpotSummary},
|
models::{Scene, SceneSummary, Spot, SpotSummary},
|
||||||
queries::{realms, scenes, spots},
|
queries::{realms, scenes, spots},
|
||||||
};
|
};
|
||||||
use chattyness_error::{AppError, OptionExt};
|
use chattyness_error::AppError;
|
||||||
|
|
||||||
/// Get the entry scene for a realm.
|
/// Get the entry scene for a realm.
|
||||||
///
|
///
|
||||||
|
|
@ -86,7 +86,7 @@ pub async fn get_spot(
|
||||||
) -> Result<Json<Spot>, AppError> {
|
) -> Result<Json<Spot>, AppError> {
|
||||||
let spot = spots::get_spot_by_id(&pool, spot_id)
|
let spot = spots::get_spot_by_id(&pool, spot_id)
|
||||||
.await?
|
.await?
|
||||||
.or_not_found("Spot")?;
|
.ok_or_else(|| AppError::NotFound("Spot not found".to_string()))?;
|
||||||
|
|
||||||
Ok(Json(spot))
|
Ok(Json(spot))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use chattyness_db::{
|
use chattyness_db::{
|
||||||
models::{ActionType, AvatarRenderData, ChannelMemberWithAvatar, EmotionState, ForcedAvatarReason, User},
|
models::{ActionType, AvatarRenderData, ChannelMemberWithAvatar, EmotionState, ForcedAvatarReason, User},
|
||||||
queries::{avatars, channel_members, inventory, loose_props, memberships, moderation, realm_avatars, realms, scenes, server_avatars, users},
|
queries::{avatars, channel_members, loose_props, memberships, moderation, realm_avatars, realms, scenes, server_avatars, users},
|
||||||
ws_messages::{close_codes, ClientMessage, DisconnectReason, ServerMessage, WsConfig},
|
ws_messages::{close_codes, ClientMessage, DisconnectReason, ServerMessage, WsConfig},
|
||||||
};
|
};
|
||||||
use chattyness_error::AppError;
|
use chattyness_error::AppError;
|
||||||
|
|
@ -711,57 +711,7 @@ async fn handle_socket(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientMessage::DeleteProp { inventory_item_id } => {
|
|
||||||
match inventory::drop_inventory_item(
|
|
||||||
&mut *recv_conn,
|
|
||||||
user_id,
|
|
||||||
inventory_item_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(()) => {
|
|
||||||
let _ = direct_tx.send(ServerMessage::PropDeleted {
|
|
||||||
inventory_item_id,
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let (code, message) = match &e {
|
|
||||||
chattyness_error::AppError::Forbidden(msg) => (
|
|
||||||
"PROP_NOT_DELETABLE".to_string(),
|
|
||||||
msg.clone(),
|
|
||||||
),
|
|
||||||
chattyness_error::AppError::NotFound(msg) => {
|
|
||||||
("PROP_NOT_FOUND".to_string(), msg.clone())
|
|
||||||
}
|
|
||||||
_ => (
|
|
||||||
"DELETE_FAILED".to_string(),
|
|
||||||
format!("{:?}", e),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error { code, message }).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientMessage::PickUpProp { loose_prop_id } => {
|
ClientMessage::PickUpProp { loose_prop_id } => {
|
||||||
// Check if prop is locked
|
|
||||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Failed to check moderator status: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Ok(Some(prop)) = loose_props::get_loose_prop_by_id(&mut *recv_conn, loose_prop_id).await {
|
|
||||||
if prop.is_locked && !is_mod {
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "PROP_LOCKED".to_string(),
|
|
||||||
message: "This prop is locked and cannot be picked up".to_string(),
|
|
||||||
}).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match loose_props::pick_up_loose_prop(
|
match loose_props::pick_up_loose_prop(
|
||||||
&mut *recv_conn,
|
&mut *recv_conn,
|
||||||
loose_prop_id,
|
loose_prop_id,
|
||||||
|
|
@ -1469,195 +1419,6 @@ async fn handle_socket(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientMessage::UpdateProp { loose_prop_id, scale } => {
|
|
||||||
// Check if user is a moderator
|
|
||||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Failed to check moderator status: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_mod {
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "NOT_MODERATOR".to_string(),
|
|
||||||
message: "You do not have permission to update props".to_string(),
|
|
||||||
}).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if prop is locked (for non-mods)
|
|
||||||
if let Ok(Some(prop)) = loose_props::get_loose_prop_by_id(&mut *recv_conn, loose_prop_id).await {
|
|
||||||
if prop.is_locked && !is_mod {
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "PROP_LOCKED".to_string(),
|
|
||||||
message: "This prop is locked and cannot be modified".to_string(),
|
|
||||||
}).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the prop scale
|
|
||||||
match loose_props::update_loose_prop_scale(
|
|
||||||
&mut *recv_conn,
|
|
||||||
loose_prop_id,
|
|
||||||
scale,
|
|
||||||
).await {
|
|
||||||
Ok(updated_prop) => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
tracing::debug!(
|
|
||||||
"[WS] User {} updated prop {} scale to {}",
|
|
||||||
user_id,
|
|
||||||
loose_prop_id,
|
|
||||||
scale
|
|
||||||
);
|
|
||||||
// Broadcast the updated prop to all users in the channel
|
|
||||||
let _ = tx.send(ServerMessage::PropRefresh { prop: updated_prop });
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Update prop failed: {:?}", e);
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "UPDATE_PROP_FAILED".to_string(),
|
|
||||||
message: format!("{:?}", e),
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientMessage::MoveProp { loose_prop_id, x, y } => {
|
|
||||||
// Check if user is a moderator (needed for locked props)
|
|
||||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Failed to check moderator status: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if prop is locked
|
|
||||||
if let Ok(Some(prop)) = loose_props::get_loose_prop_by_id(&mut *recv_conn, loose_prop_id).await {
|
|
||||||
if prop.is_locked && !is_mod {
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "PROP_LOCKED".to_string(),
|
|
||||||
message: "This prop is locked and cannot be moved".to_string(),
|
|
||||||
}).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move the prop
|
|
||||||
match loose_props::move_loose_prop(
|
|
||||||
&mut *recv_conn,
|
|
||||||
loose_prop_id,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
).await {
|
|
||||||
Ok(updated_prop) => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
tracing::debug!(
|
|
||||||
"[WS] User {} moved prop {} to ({}, {})",
|
|
||||||
user_id,
|
|
||||||
loose_prop_id,
|
|
||||||
x,
|
|
||||||
y
|
|
||||||
);
|
|
||||||
// Broadcast the updated prop to all users in the channel
|
|
||||||
let _ = tx.send(ServerMessage::PropRefresh { prop: updated_prop });
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Move prop failed: {:?}", e);
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "MOVE_PROP_FAILED".to_string(),
|
|
||||||
message: format!("{:?}", e),
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientMessage::LockProp { loose_prop_id } => {
|
|
||||||
// Check if user is a moderator
|
|
||||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Failed to check moderator status: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_mod {
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "NOT_MODERATOR".to_string(),
|
|
||||||
message: "You do not have permission to lock props".to_string(),
|
|
||||||
}).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lock the prop
|
|
||||||
match loose_props::lock_loose_prop(
|
|
||||||
&mut *recv_conn,
|
|
||||||
loose_prop_id,
|
|
||||||
user_id,
|
|
||||||
).await {
|
|
||||||
Ok(updated_prop) => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
tracing::debug!(
|
|
||||||
"[WS] User {} locked prop {}",
|
|
||||||
user_id,
|
|
||||||
loose_prop_id
|
|
||||||
);
|
|
||||||
// Broadcast the updated prop to all users in the channel
|
|
||||||
let _ = tx.send(ServerMessage::PropRefresh { prop: updated_prop });
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Lock prop failed: {:?}", e);
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "LOCK_PROP_FAILED".to_string(),
|
|
||||||
message: format!("{:?}", e),
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ClientMessage::UnlockProp { loose_prop_id } => {
|
|
||||||
// Check if user is a moderator
|
|
||||||
let is_mod = match memberships::is_moderator(&pool, user_id, realm_id).await {
|
|
||||||
Ok(result) => result,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Failed to check moderator status: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if !is_mod {
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "NOT_MODERATOR".to_string(),
|
|
||||||
message: "You do not have permission to unlock props".to_string(),
|
|
||||||
}).await;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock the prop
|
|
||||||
match loose_props::unlock_loose_prop(
|
|
||||||
&mut *recv_conn,
|
|
||||||
loose_prop_id,
|
|
||||||
).await {
|
|
||||||
Ok(updated_prop) => {
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
tracing::debug!(
|
|
||||||
"[WS] User {} unlocked prop {}",
|
|
||||||
user_id,
|
|
||||||
loose_prop_id
|
|
||||||
);
|
|
||||||
// Broadcast the updated prop to all users in the channel
|
|
||||||
let _ = tx.send(ServerMessage::PropRefresh { prop: updated_prop });
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("[WS] Unlock prop failed: {:?}", e);
|
|
||||||
let _ = direct_tx.send(ServerMessage::Error {
|
|
||||||
code: "UNLOCK_PROP_FAILED".to_string(),
|
|
||||||
message: format!("{:?}", e),
|
|
||||||
}).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Close(close_frame) => {
|
Message::Close(close_frame) => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
pub mod avatar_canvas;
|
pub mod avatar_canvas;
|
||||||
pub mod avatar_editor;
|
pub mod avatar_editor;
|
||||||
pub mod canvas_utils;
|
|
||||||
pub mod avatar_store;
|
pub mod avatar_store;
|
||||||
pub mod avatar_thumbnail;
|
pub mod avatar_thumbnail;
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
|
|
@ -18,7 +17,6 @@ pub mod keybindings;
|
||||||
pub mod keybindings_popup;
|
pub mod keybindings_popup;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod log_popup;
|
pub mod log_popup;
|
||||||
pub mod loose_prop_canvas;
|
|
||||||
pub mod modals;
|
pub mod modals;
|
||||||
pub mod notifications;
|
pub mod notifications;
|
||||||
pub mod register_modal;
|
pub mod register_modal;
|
||||||
|
|
@ -33,7 +31,6 @@ pub mod ws_client;
|
||||||
pub use avatar_canvas::*;
|
pub use avatar_canvas::*;
|
||||||
pub use avatar_editor::*;
|
pub use avatar_editor::*;
|
||||||
pub use avatar_store::*;
|
pub use avatar_store::*;
|
||||||
pub use canvas_utils::*;
|
|
||||||
pub use avatar_thumbnail::*;
|
pub use avatar_thumbnail::*;
|
||||||
pub use chat::*;
|
pub use chat::*;
|
||||||
pub use chat_types::*;
|
pub use chat_types::*;
|
||||||
|
|
@ -48,7 +45,6 @@ pub use keybindings::*;
|
||||||
pub use keybindings_popup::*;
|
pub use keybindings_popup::*;
|
||||||
pub use layout::*;
|
pub use layout::*;
|
||||||
pub use log_popup::*;
|
pub use log_popup::*;
|
||||||
pub use loose_prop_canvas::*;
|
|
||||||
pub use modals::*;
|
pub use modals::*;
|
||||||
pub use notifications::*;
|
pub use notifications::*;
|
||||||
pub use register_modal::*;
|
pub use register_modal::*;
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,6 @@ use uuid::Uuid;
|
||||||
|
|
||||||
use chattyness_db::models::{ChannelMemberWithAvatar, EmotionState};
|
use chattyness_db::models::{ChannelMemberWithAvatar, EmotionState};
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub use super::canvas_utils::hit_test_canvas;
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
use super::canvas_utils::normalize_asset_path;
|
|
||||||
use super::chat_types::{ActiveBubble, emotion_bubble_colors};
|
use super::chat_types::{ActiveBubble, emotion_bubble_colors};
|
||||||
|
|
||||||
/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4
|
/// Base text size multiplier. Text at 100% slider = base_sizes * 1.4
|
||||||
|
|
@ -806,6 +802,15 @@ pub fn AvatarCanvas(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Normalize an asset path to be absolute, prefixing with /static/ if needed.
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
fn normalize_asset_path(path: &str) -> String {
|
||||||
|
if path.starts_with('/') {
|
||||||
|
path.to_string()
|
||||||
|
} else {
|
||||||
|
format!("/static/{}", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw a speech bubble using the unified CanvasLayout.
|
/// Draw a speech bubble using the unified CanvasLayout.
|
||||||
///
|
///
|
||||||
|
|
@ -967,6 +972,68 @@ fn wrap_text(ctx: &web_sys::CanvasRenderingContext2d, text: &str, max_width: f64
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test if a click at the given client coordinates hits a non-transparent pixel.
|
||||||
|
///
|
||||||
|
/// Returns true if the alpha channel at the clicked pixel is > 0.
|
||||||
|
/// This enables pixel-perfect hit detection on avatar canvases.
|
||||||
|
#[cfg(feature = "hydrate")]
|
||||||
|
pub fn hit_test_canvas(canvas: &web_sys::HtmlCanvasElement, client_x: f64, client_y: f64) -> bool {
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
|
||||||
|
// Get the canvas bounding rect to transform client coords to canvas coords
|
||||||
|
let rect = canvas.get_bounding_client_rect();
|
||||||
|
|
||||||
|
// Calculate click position relative to the canvas element
|
||||||
|
let relative_x = client_x - rect.left();
|
||||||
|
let relative_y = client_y - rect.top();
|
||||||
|
|
||||||
|
// Check if click is within canvas bounds
|
||||||
|
if relative_x < 0.0
|
||||||
|
|| relative_y < 0.0
|
||||||
|
|| relative_x >= rect.width()
|
||||||
|
|| relative_y >= rect.height()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform to canvas pixel coordinates (accounting for CSS scaling)
|
||||||
|
let canvas_width = canvas.width() as f64;
|
||||||
|
let canvas_height = canvas.height() as f64;
|
||||||
|
|
||||||
|
// Avoid division by zero
|
||||||
|
if rect.width() == 0.0 || rect.height() == 0.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scale_x = canvas_width / rect.width();
|
||||||
|
let scale_y = canvas_height / rect.height();
|
||||||
|
|
||||||
|
let pixel_x = (relative_x * scale_x) as f64;
|
||||||
|
let pixel_y = (relative_y * scale_y) as f64;
|
||||||
|
|
||||||
|
// Get the 2D context and read the pixel data using JavaScript interop
|
||||||
|
if let Ok(Some(ctx)) = canvas.get_context("2d") {
|
||||||
|
let ctx: web_sys::CanvasRenderingContext2d = ctx.dyn_into().unwrap();
|
||||||
|
|
||||||
|
// Use web_sys::CanvasRenderingContext2d::get_image_data with proper error handling
|
||||||
|
match ctx.get_image_data(pixel_x, pixel_y, 1.0, 1.0) {
|
||||||
|
Ok(image_data) => {
|
||||||
|
// Get the pixel data as Clamped<Vec<u8>>
|
||||||
|
let data = image_data.data();
|
||||||
|
// Alpha channel is the 4th value (index 3)
|
||||||
|
if data.len() >= 4 {
|
||||||
|
return data[3] > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Security error or other issue with getImageData - assume no hit
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
/// Draw a rounded rectangle path.
|
/// Draw a rounded rectangle path.
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
|
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
//! Shared canvas utilities for avatar and prop rendering.
|
|
||||||
//!
|
|
||||||
//! Common functions used by both AvatarCanvas and LoosePropCanvas components.
|
|
||||||
|
|
||||||
/// Normalize an asset path to be absolute, prefixing with /static/ if needed.
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn normalize_asset_path(path: &str) -> String {
|
|
||||||
if path.starts_with('/') {
|
|
||||||
path.to_string()
|
|
||||||
} else {
|
|
||||||
format!("/static/{}", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test if a click at the given client coordinates hits a non-transparent pixel.
|
|
||||||
///
|
|
||||||
/// Returns true if the alpha channel at the clicked pixel is > 0.
|
|
||||||
/// This enables pixel-perfect hit detection on canvas elements.
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn hit_test_canvas(
|
|
||||||
canvas: &web_sys::HtmlCanvasElement,
|
|
||||||
client_x: f64,
|
|
||||||
client_y: f64,
|
|
||||||
) -> bool {
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
// Get the canvas bounding rect to transform client coords to canvas coords
|
|
||||||
let rect = canvas.get_bounding_client_rect();
|
|
||||||
|
|
||||||
// Calculate click position relative to the canvas element
|
|
||||||
let relative_x = client_x - rect.left();
|
|
||||||
let relative_y = client_y - rect.top();
|
|
||||||
|
|
||||||
// Check if click is within canvas bounds
|
|
||||||
if relative_x < 0.0
|
|
||||||
|| relative_y < 0.0
|
|
||||||
|| relative_x >= rect.width()
|
|
||||||
|| relative_y >= rect.height()
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform to canvas pixel coordinates (accounting for CSS scaling)
|
|
||||||
let canvas_width = canvas.width() as f64;
|
|
||||||
let canvas_height = canvas.height() as f64;
|
|
||||||
|
|
||||||
// Avoid division by zero
|
|
||||||
if rect.width() == 0.0 || rect.height() == 0.0 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let scale_x = canvas_width / rect.width();
|
|
||||||
let scale_y = canvas_height / rect.height();
|
|
||||||
|
|
||||||
let pixel_x = relative_x * scale_x;
|
|
||||||
let pixel_y = relative_y * scale_y;
|
|
||||||
|
|
||||||
// Get the 2D context and read the pixel data
|
|
||||||
if let Ok(Some(ctx)) = canvas.get_context("2d") {
|
|
||||||
let ctx: web_sys::CanvasRenderingContext2d = ctx.dyn_into().unwrap();
|
|
||||||
|
|
||||||
match ctx.get_image_data(pixel_x, pixel_y, 1.0, 1.0) {
|
|
||||||
Ok(image_data) => {
|
|
||||||
let data = image_data.data();
|
|
||||||
// Alpha channel is the 4th value (index 3)
|
|
||||||
if data.len() >= 4 {
|
|
||||||
return data[3] > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
@ -8,7 +8,7 @@ use chattyness_db::models::{InventoryItem, PropAcquisitionInfo};
|
||||||
#[cfg(feature = "hydrate")]
|
#[cfg(feature = "hydrate")]
|
||||||
use chattyness_db::ws_messages::ClientMessage;
|
use chattyness_db::ws_messages::ClientMessage;
|
||||||
|
|
||||||
use super::modals::{ConfirmModal, GuestLockedOverlay, Modal};
|
use super::modals::{GuestLockedOverlay, Modal};
|
||||||
use super::tabs::{Tab, TabBar};
|
use super::tabs::{Tab, TabBar};
|
||||||
use super::ws_client::WsSender;
|
use super::ws_client::WsSender;
|
||||||
|
|
||||||
|
|
@ -45,8 +45,6 @@ pub fn InventoryPopup(
|
||||||
let (error, set_error) = signal(Option::<String>::None);
|
let (error, set_error) = signal(Option::<String>::None);
|
||||||
let (selected_item, set_selected_item) = signal(Option::<Uuid>::None);
|
let (selected_item, set_selected_item) = signal(Option::<Uuid>::None);
|
||||||
let (dropping, set_dropping) = signal(false);
|
let (dropping, set_dropping) = signal(false);
|
||||||
let (deleting, set_deleting) = signal(false);
|
|
||||||
let (delete_confirm_item, set_delete_confirm_item) = signal(Option::<(Uuid, String)>::None);
|
|
||||||
|
|
||||||
// Server props state (with acquisition info for authenticated users)
|
// Server props state (with acquisition info for authenticated users)
|
||||||
let (server_props, set_server_props) = signal(Vec::<PropAcquisitionInfo>::new());
|
let (server_props, set_server_props) = signal(Vec::<PropAcquisitionInfo>::new());
|
||||||
|
|
@ -246,33 +244,6 @@ pub fn InventoryPopup(
|
||||||
#[cfg(not(feature = "hydrate"))]
|
#[cfg(not(feature = "hydrate"))]
|
||||||
let handle_drop = |_item_id: Uuid| {};
|
let handle_drop = |_item_id: Uuid| {};
|
||||||
|
|
||||||
// Handle delete action via WebSocket (permanent deletion)
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
let handle_delete = {
|
|
||||||
move |item_id: Uuid| {
|
|
||||||
set_deleting.set(true);
|
|
||||||
ws_sender.with_value(|sender| {
|
|
||||||
if let Some(send_fn) = sender {
|
|
||||||
send_fn(ClientMessage::DeleteProp {
|
|
||||||
inventory_item_id: item_id,
|
|
||||||
});
|
|
||||||
// Optimistically remove from local list
|
|
||||||
set_items.update(|items| {
|
|
||||||
items.retain(|i| i.id != item_id);
|
|
||||||
});
|
|
||||||
set_selected_item.set(None);
|
|
||||||
set_delete_confirm_item.set(None);
|
|
||||||
} else {
|
|
||||||
set_error.set(Some("Not connected to server".to_string()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
set_deleting.set(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let handle_delete = |_item_id: Uuid| {};
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Modal
|
<Modal
|
||||||
open=open
|
open=open
|
||||||
|
|
@ -306,10 +277,6 @@ pub fn InventoryPopup(
|
||||||
set_selected_item=set_selected_item
|
set_selected_item=set_selected_item
|
||||||
dropping=dropping
|
dropping=dropping
|
||||||
on_drop=Callback::new(handle_drop)
|
on_drop=Callback::new(handle_drop)
|
||||||
deleting=deleting
|
|
||||||
on_delete_request=Callback::new(move |(id, name)| {
|
|
||||||
set_delete_confirm_item.set(Some((id, name)));
|
|
||||||
})
|
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
|
|
@ -357,25 +324,6 @@ pub fn InventoryPopup(
|
||||||
<Show when=move || is_guest.get()>
|
<Show when=move || is_guest.get()>
|
||||||
<GuestLockedOverlay />
|
<GuestLockedOverlay />
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
// Delete confirmation modal
|
|
||||||
{move || {
|
|
||||||
delete_confirm_item.get().map(|(item_id, item_name)| {
|
|
||||||
view! {
|
|
||||||
<ConfirmModal
|
|
||||||
open=Signal::derive(|| true)
|
|
||||||
title="Delete Prop?"
|
|
||||||
message=format!("Permanently delete '{}'? This cannot be undone.", item_name)
|
|
||||||
confirm_text="Delete"
|
|
||||||
cancel_text="Cancel"
|
|
||||||
destructive=true
|
|
||||||
pending=Signal::derive(move || deleting.get())
|
|
||||||
on_confirm=Callback::new(move |_| handle_delete(item_id))
|
|
||||||
on_cancel=Callback::new(move |_| set_delete_confirm_item.set(None))
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
}
|
}
|
||||||
|
|
@ -391,8 +339,6 @@ fn MyInventoryTab(
|
||||||
set_selected_item: WriteSignal<Option<Uuid>>,
|
set_selected_item: WriteSignal<Option<Uuid>>,
|
||||||
#[prop(into)] dropping: Signal<bool>,
|
#[prop(into)] dropping: Signal<bool>,
|
||||||
#[prop(into)] on_drop: Callback<Uuid>,
|
#[prop(into)] on_drop: Callback<Uuid>,
|
||||||
#[prop(into)] deleting: Signal<bool>,
|
|
||||||
#[prop(into)] on_delete_request: Callback<(Uuid, String)>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
// Loading state
|
// Loading state
|
||||||
|
|
@ -477,11 +423,8 @@ fn MyInventoryTab(
|
||||||
let item_id = selected_item.get()?;
|
let item_id = selected_item.get()?;
|
||||||
let item = items.get().into_iter().find(|i| i.id == item_id)?;
|
let item = items.get().into_iter().find(|i| i.id == item_id)?;
|
||||||
let on_drop = on_drop.clone();
|
let on_drop = on_drop.clone();
|
||||||
let on_delete_request = on_delete_request.clone();
|
|
||||||
let is_dropping = dropping.get();
|
let is_dropping = dropping.get();
|
||||||
let is_deleting = deleting.get();
|
|
||||||
let is_droppable = item.is_droppable;
|
let is_droppable = item.is_droppable;
|
||||||
let item_name = item.prop_name.clone();
|
|
||||||
|
|
||||||
Some(view! {
|
Some(view! {
|
||||||
<div class="mt-4 pt-4 border-t border-gray-700">
|
<div class="mt-4 pt-4 border-t border-gray-700">
|
||||||
|
|
@ -509,25 +452,10 @@ fn MyInventoryTab(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
disabled=is_dropping || !is_droppable
|
disabled=is_dropping || !is_droppable
|
||||||
title=if is_droppable { "Drop prop to scene canvas" } else { "Essential prop cannot be dropped" }
|
title=if is_droppable { "" } else { "Essential prop cannot be dropped" }
|
||||||
>
|
>
|
||||||
{if is_dropping { "Dropping..." } else { "Drop" }}
|
{if is_dropping { "Dropping..." } else { "Drop" }}
|
||||||
</button>
|
</button>
|
||||||
// Delete button - only shown for droppable props
|
|
||||||
<Show when=move || is_droppable>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="px-4 py-2 bg-red-800 hover:bg-red-900 text-white rounded-lg transition-colors disabled:opacity-50"
|
|
||||||
on:click={
|
|
||||||
let name = item_name.clone();
|
|
||||||
move |_| on_delete_request.run((item_id, name.clone()))
|
|
||||||
}
|
|
||||||
disabled=is_dropping || is_deleting
|
|
||||||
title="Permanently delete this prop"
|
|
||||||
>
|
|
||||||
"Delete"
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
// Transfer button (disabled for now)
|
// Transfer button (disabled for now)
|
||||||
<Show when=move || item.is_transferable>
|
<Show when=move || item.is_transferable>
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -1,195 +0,0 @@
|
||||||
//! Individual loose prop canvas component for per-prop rendering.
|
|
||||||
//!
|
|
||||||
//! Each loose prop gets its own canvas element positioned via CSS transforms.
|
|
||||||
//! This enables pixel-perfect hit detection using getImageData().
|
|
||||||
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use chattyness_db::models::LooseProp;
|
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub use super::canvas_utils::hit_test_canvas;
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
use super::canvas_utils::normalize_asset_path;
|
|
||||||
use super::settings::{BASE_AVATAR_SCALE, BASE_PROP_SCALE, BASE_PROP_SIZE};
|
|
||||||
|
|
||||||
/// Get a unique key for a loose prop (for Leptos For keying).
|
|
||||||
pub fn loose_prop_key(p: &LooseProp) -> Uuid {
|
|
||||||
p.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Individual loose prop canvas component.
|
|
||||||
///
|
|
||||||
/// Renders a single prop with:
|
|
||||||
/// - CSS transform for position (GPU-accelerated, no redraw on move)
|
|
||||||
/// - Canvas for prop sprite (redraws only on appearance change)
|
|
||||||
/// - Pixel-perfect hit detection via getImageData()
|
|
||||||
#[component]
|
|
||||||
pub fn LoosePropCanvas(
|
|
||||||
/// The prop data (as a signal for reactive updates).
|
|
||||||
prop: Signal<LooseProp>,
|
|
||||||
/// X scale factor for coordinate conversion.
|
|
||||||
scale_x: Signal<f64>,
|
|
||||||
/// Y scale factor for coordinate conversion.
|
|
||||||
scale_y: Signal<f64>,
|
|
||||||
/// X offset for coordinate conversion.
|
|
||||||
offset_x: Signal<f64>,
|
|
||||||
/// Y offset for coordinate conversion.
|
|
||||||
offset_y: Signal<f64>,
|
|
||||||
/// Base prop size in screen pixels (already includes viewport scaling).
|
|
||||||
base_prop_size: Signal<f64>,
|
|
||||||
/// Z-index for stacking order.
|
|
||||||
z_index: i32,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let canvas_ref = NodeRef::<leptos::html::Canvas>::new();
|
|
||||||
|
|
||||||
// Reactive style for CSS positioning (GPU-accelerated transforms)
|
|
||||||
let style = move || {
|
|
||||||
let p = prop.get();
|
|
||||||
let sx = scale_x.get();
|
|
||||||
let sy = scale_y.get();
|
|
||||||
let ox = offset_x.get();
|
|
||||||
let oy = offset_y.get();
|
|
||||||
let base_size = base_prop_size.get();
|
|
||||||
|
|
||||||
// Calculate rendered prop size
|
|
||||||
let prop_scale_ratio = BASE_PROP_SCALE / BASE_AVATAR_SCALE;
|
|
||||||
let prop_size = base_size * prop_scale_ratio * p.scale as f64;
|
|
||||||
|
|
||||||
// Screen position (center of prop)
|
|
||||||
let screen_x = p.position_x * sx + ox;
|
|
||||||
let screen_y = p.position_y * sy + oy;
|
|
||||||
|
|
||||||
// Canvas positioned at top-left corner
|
|
||||||
let canvas_x = screen_x - prop_size / 2.0;
|
|
||||||
let canvas_y = screen_y - prop_size / 2.0;
|
|
||||||
|
|
||||||
// Add amber dashed border for locked props
|
|
||||||
let border_style = if p.is_locked {
|
|
||||||
"border: 2px dashed #f59e0b; box-sizing: border-box;"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
};
|
|
||||||
|
|
||||||
format!(
|
|
||||||
"position: absolute; \
|
|
||||||
left: 0; top: 0; \
|
|
||||||
transform: translate({}px, {}px); \
|
|
||||||
z-index: {}; \
|
|
||||||
pointer-events: auto; \
|
|
||||||
width: {}px; \
|
|
||||||
height: {}px; {}",
|
|
||||||
canvas_x, canvas_y, z_index, prop_size, prop_size, border_style
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use wasm_bindgen::closure::Closure;
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
// Image cache for this prop
|
|
||||||
let image_cache: Rc<RefCell<Option<web_sys::HtmlImageElement>>> =
|
|
||||||
Rc::new(RefCell::new(None));
|
|
||||||
|
|
||||||
// Redraw trigger - incremented when image loads
|
|
||||||
let (redraw_trigger, set_redraw_trigger) = signal(0u32);
|
|
||||||
|
|
||||||
// Effect to draw the prop when canvas is ready or appearance changes
|
|
||||||
Effect::new(move |_| {
|
|
||||||
// Subscribe to redraw trigger
|
|
||||||
let _ = redraw_trigger.get();
|
|
||||||
|
|
||||||
let p = prop.get();
|
|
||||||
let base_size = base_prop_size.get();
|
|
||||||
|
|
||||||
let Some(canvas) = canvas_ref.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate rendered prop size
|
|
||||||
let prop_scale_ratio = BASE_PROP_SCALE / BASE_AVATAR_SCALE;
|
|
||||||
let prop_size = base_size * prop_scale_ratio * p.scale as f64;
|
|
||||||
|
|
||||||
let canvas_el: &web_sys::HtmlCanvasElement = &canvas;
|
|
||||||
|
|
||||||
// Set canvas resolution
|
|
||||||
canvas_el.set_width(prop_size as u32);
|
|
||||||
canvas_el.set_height(prop_size as u32);
|
|
||||||
|
|
||||||
let Ok(Some(ctx)) = canvas_el.get_context("2d") else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let ctx: web_sys::CanvasRenderingContext2d = ctx.dyn_into().unwrap();
|
|
||||||
|
|
||||||
// Clear canvas
|
|
||||||
ctx.clear_rect(0.0, 0.0, prop_size, prop_size);
|
|
||||||
|
|
||||||
// Draw prop sprite if asset path available
|
|
||||||
if !p.prop_asset_path.is_empty() {
|
|
||||||
let normalized_path = normalize_asset_path(&p.prop_asset_path);
|
|
||||||
let mut cache = image_cache.borrow_mut();
|
|
||||||
|
|
||||||
if let Some(ref img) = *cache {
|
|
||||||
// Image in cache - draw if loaded
|
|
||||||
if img.complete() && img.natural_width() > 0 {
|
|
||||||
let _ = ctx.draw_image_with_html_image_element_and_dw_and_dh(
|
|
||||||
img, 0.0, 0.0, prop_size, prop_size,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Not in cache - create and load
|
|
||||||
let img = web_sys::HtmlImageElement::new().unwrap();
|
|
||||||
|
|
||||||
let trigger = set_redraw_trigger;
|
|
||||||
let onload = Closure::once(Box::new(move || {
|
|
||||||
trigger.update(|v| *v += 1);
|
|
||||||
}) as Box<dyn FnOnce()>);
|
|
||||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
|
||||||
onload.forget();
|
|
||||||
|
|
||||||
img.set_src(&normalized_path);
|
|
||||||
*cache = Some(img);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fallback: draw placeholder circle with prop name
|
|
||||||
ctx.begin_path();
|
|
||||||
let _ = ctx.arc(
|
|
||||||
prop_size / 2.0,
|
|
||||||
prop_size / 2.0,
|
|
||||||
prop_size / 2.0 - 2.0,
|
|
||||||
0.0,
|
|
||||||
std::f64::consts::PI * 2.0,
|
|
||||||
);
|
|
||||||
ctx.set_fill_style_str("#f59e0b");
|
|
||||||
ctx.fill();
|
|
||||||
ctx.set_stroke_style_str("#d97706");
|
|
||||||
ctx.set_line_width(2.0);
|
|
||||||
ctx.stroke();
|
|
||||||
|
|
||||||
// Draw prop name
|
|
||||||
let text_scale = prop_size / (BASE_PROP_SIZE * BASE_PROP_SCALE);
|
|
||||||
ctx.set_fill_style_str("#fff");
|
|
||||||
ctx.set_font(&format!("{}px sans-serif", 10.0 * text_scale));
|
|
||||||
ctx.set_text_align("center");
|
|
||||||
ctx.set_text_baseline("middle");
|
|
||||||
let _ = ctx.fill_text(&p.prop_name, prop_size / 2.0, prop_size / 2.0);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute data-prop-id reactively
|
|
||||||
let data_prop_id = move || prop.get().id.to_string();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<canvas
|
|
||||||
node_ref=canvas_ref
|
|
||||||
style=style
|
|
||||||
data-prop-id=data_prop_id
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,96 +0,0 @@
|
||||||
//! Coordinate conversion utilities for scene viewer.
|
|
||||||
//!
|
|
||||||
//! Handles conversions between:
|
|
||||||
//! - Scene coordinates (native scene dimensions)
|
|
||||||
//! - Canvas coordinates (scaled/offset for display)
|
|
||||||
//! - Viewport/client coordinates (browser window)
|
|
||||||
|
|
||||||
/// Coordinate transform state for converting between scene and canvas coordinates.
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
|
||||||
pub struct CoordinateTransform {
|
|
||||||
pub scale_x: f64,
|
|
||||||
pub scale_y: f64,
|
|
||||||
pub offset_x: f64,
|
|
||||||
pub offset_y: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoordinateTransform {
|
|
||||||
/// Create a new transform with the given scale and offset values.
|
|
||||||
pub fn new(scale_x: f64, scale_y: f64, offset_x: f64, offset_y: f64) -> Self {
|
|
||||||
Self {
|
|
||||||
scale_x,
|
|
||||||
scale_y,
|
|
||||||
offset_x,
|
|
||||||
offset_y,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert canvas coordinates to scene coordinates.
|
|
||||||
pub fn canvas_to_scene(&self, canvas_x: f64, canvas_y: f64) -> (f64, f64) {
|
|
||||||
if self.scale_x > 0.0 && self.scale_y > 0.0 {
|
|
||||||
let scene_x = (canvas_x - self.offset_x) / self.scale_x;
|
|
||||||
let scene_y = (canvas_y - self.offset_y) / self.scale_y;
|
|
||||||
(scene_x, scene_y)
|
|
||||||
} else {
|
|
||||||
(0.0, 0.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert scene coordinates to canvas coordinates.
|
|
||||||
pub fn scene_to_canvas(&self, scene_x: f64, scene_y: f64) -> (f64, f64) {
|
|
||||||
let canvas_x = scene_x * self.scale_x + self.offset_x;
|
|
||||||
let canvas_y = scene_y * self.scale_y + self.offset_y;
|
|
||||||
(canvas_x, canvas_y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clamp scene coordinates to scene bounds.
|
|
||||||
pub fn clamp_to_scene(&self, x: f64, y: f64, scene_width: f64, scene_height: f64) -> (f64, f64) {
|
|
||||||
(x.max(0.0).min(scene_width), y.max(0.0).min(scene_height))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the transform has valid (non-zero) scales.
|
|
||||||
pub fn is_valid(&self) -> bool {
|
|
||||||
self.scale_x > 0.0 && self.scale_y > 0.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the aspect-ratio preserving scale and offset for fit mode.
|
|
||||||
///
|
|
||||||
/// Returns (draw_width, draw_height, offset_x, offset_y, scale_x, scale_y).
|
|
||||||
pub fn calculate_fit_transform(
|
|
||||||
display_width: f64,
|
|
||||||
display_height: f64,
|
|
||||||
scene_width: f64,
|
|
||||||
scene_height: f64,
|
|
||||||
) -> CoordinateTransform {
|
|
||||||
if display_width == 0.0 || display_height == 0.0 {
|
|
||||||
return CoordinateTransform::default();
|
|
||||||
}
|
|
||||||
|
|
||||||
let canvas_aspect = display_width / display_height;
|
|
||||||
let scene_aspect = scene_width / scene_height;
|
|
||||||
|
|
||||||
let (draw_width, draw_height, offset_x, offset_y) = if canvas_aspect > scene_aspect {
|
|
||||||
// Canvas is wider than scene - letterbox on sides
|
|
||||||
let h = display_height;
|
|
||||||
let w = h * scene_aspect;
|
|
||||||
let x = (display_width - w) / 2.0;
|
|
||||||
(w, h, x, 0.0)
|
|
||||||
} else {
|
|
||||||
// Canvas is taller than scene - letterbox on top/bottom
|
|
||||||
let w = display_width;
|
|
||||||
let h = w / scene_aspect;
|
|
||||||
let y = (display_height - h) / 2.0;
|
|
||||||
(w, h, 0.0, y)
|
|
||||||
};
|
|
||||||
|
|
||||||
let scale_x = draw_width / scene_width;
|
|
||||||
let scale_y = draw_height / scene_height;
|
|
||||||
|
|
||||||
CoordinateTransform::new(scale_x, scale_y, offset_x, offset_y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate transform for pan mode (native resolution * zoom).
|
|
||||||
pub fn calculate_pan_transform(zoom: f64) -> CoordinateTransform {
|
|
||||||
CoordinateTransform::new(zoom, zoom, 0.0, 0.0)
|
|
||||||
}
|
|
||||||
|
|
@ -1,392 +0,0 @@
|
||||||
//! Effect functions for scene viewer background drawing and pan handling.
|
|
||||||
|
|
||||||
use leptos::prelude::*;
|
|
||||||
|
|
||||||
/// Set up viewport dimension tracking effect.
|
|
||||||
///
|
|
||||||
/// Tracks the outer container size and updates the provided signal.
|
|
||||||
/// Also listens for window resize events.
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn setup_viewport_tracking(
|
|
||||||
outer_container_ref: NodeRef<leptos::html::Div>,
|
|
||||||
is_pan_mode: Signal<bool>,
|
|
||||||
set_viewport_dimensions: WriteSignal<(f64, f64)>,
|
|
||||||
) {
|
|
||||||
use wasm_bindgen::{JsCast, closure::Closure};
|
|
||||||
|
|
||||||
Effect::new(move |_| {
|
|
||||||
// Track pan mode to re-run when it changes
|
|
||||||
let _ = is_pan_mode.get();
|
|
||||||
|
|
||||||
let Some(container) = outer_container_ref.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let container_el: web_sys::HtmlElement = container.into();
|
|
||||||
|
|
||||||
// Measure and update dimensions
|
|
||||||
let measure_container = {
|
|
||||||
let container_el = container_el.clone();
|
|
||||||
move || {
|
|
||||||
let width = container_el.client_width() as f64;
|
|
||||||
let height = container_el.client_height() as f64;
|
|
||||||
if width > 0.0 && height > 0.0 {
|
|
||||||
set_viewport_dimensions.set((width, height));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Measure immediately
|
|
||||||
measure_container();
|
|
||||||
|
|
||||||
// Also measure on window resize
|
|
||||||
let resize_handler = Closure::wrap(Box::new({
|
|
||||||
let container_el = container_el.clone();
|
|
||||||
move |_: web_sys::Event| {
|
|
||||||
let width = container_el.client_width() as f64;
|
|
||||||
let height = container_el.client_height() as f64;
|
|
||||||
if width > 0.0 && height > 0.0 {
|
|
||||||
set_viewport_dimensions.set((width, height));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as Box<dyn Fn(web_sys::Event)>);
|
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let _ = window.add_event_listener_with_callback(
|
|
||||||
"resize",
|
|
||||||
resize_handler.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keep the closure alive
|
|
||||||
resize_handler.forget();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set up middle mouse button drag-to-pan effect.
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn setup_middle_mouse_pan(
|
|
||||||
outer_container_ref: NodeRef<leptos::html::Div>,
|
|
||||||
is_pan_mode: Signal<bool>,
|
|
||||||
) {
|
|
||||||
use std::cell::Cell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use wasm_bindgen::{JsCast, closure::Closure};
|
|
||||||
|
|
||||||
Effect::new(move |_| {
|
|
||||||
let pan_mode_enabled = is_pan_mode.get();
|
|
||||||
|
|
||||||
let Some(container) = outer_container_ref.get() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let container_el: web_sys::HtmlElement = container.into();
|
|
||||||
|
|
||||||
if !pan_mode_enabled {
|
|
||||||
// Reset cursor when not in pan mode
|
|
||||||
let _ = container_el.style().set_property("cursor", "");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_dragging = Rc::new(Cell::new(false));
|
|
||||||
let last_x = Rc::new(Cell::new(0i32));
|
|
||||||
let last_y = Rc::new(Cell::new(0i32));
|
|
||||||
|
|
||||||
let container_for_move = container_el.clone();
|
|
||||||
let is_dragging_move = is_dragging.clone();
|
|
||||||
let last_x_move = last_x.clone();
|
|
||||||
let last_y_move = last_y.clone();
|
|
||||||
|
|
||||||
let container_for_down = container_el.clone();
|
|
||||||
let is_dragging_down = is_dragging.clone();
|
|
||||||
let last_x_down = last_x.clone();
|
|
||||||
let last_y_down = last_y.clone();
|
|
||||||
|
|
||||||
// Middle mouse down - start drag
|
|
||||||
let onmousedown =
|
|
||||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
|
||||||
if ev.button() == 1 {
|
|
||||||
is_dragging_down.set(true);
|
|
||||||
last_x_down.set(ev.client_x());
|
|
||||||
last_y_down.set(ev.client_y());
|
|
||||||
let _ = container_for_down.style().set_property("cursor", "grabbing");
|
|
||||||
ev.prevent_default();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mouse move - drag scroll
|
|
||||||
let onmousemove =
|
|
||||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
|
||||||
if is_dragging_move.get() {
|
|
||||||
let dx = last_x_move.get() - ev.client_x();
|
|
||||||
let dy = last_y_move.get() - ev.client_y();
|
|
||||||
last_x_move.set(ev.client_x());
|
|
||||||
last_y_move.set(ev.client_y());
|
|
||||||
container_for_move.set_scroll_left(container_for_move.scroll_left() + dx);
|
|
||||||
container_for_move.set_scroll_top(container_for_move.scroll_top() + dy);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let container_for_up = container_el.clone();
|
|
||||||
let is_dragging_up = is_dragging.clone();
|
|
||||||
|
|
||||||
// Mouse up - stop drag
|
|
||||||
let onmouseup =
|
|
||||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
|
|
||||||
if is_dragging_up.get() {
|
|
||||||
is_dragging_up.set(false);
|
|
||||||
let _ = container_for_up.style().set_property("cursor", "");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add event listeners
|
|
||||||
let _ = container_el.add_event_listener_with_callback(
|
|
||||||
"mousedown",
|
|
||||||
onmousedown.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
let _ = container_el.add_event_listener_with_callback(
|
|
||||||
"mousemove",
|
|
||||||
onmousemove.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
let _ = container_el
|
|
||||||
.add_event_listener_with_callback("mouseup", onmouseup.as_ref().unchecked_ref());
|
|
||||||
|
|
||||||
// Also listen for mouseup on window
|
|
||||||
if let Some(window) = web_sys::window() {
|
|
||||||
let is_dragging_window = is_dragging.clone();
|
|
||||||
let container_for_window = container_el.clone();
|
|
||||||
let onmouseup_window =
|
|
||||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |_ev: web_sys::MouseEvent| {
|
|
||||||
if is_dragging_window.get() {
|
|
||||||
is_dragging_window.set(false);
|
|
||||||
let _ = container_for_window.style().set_property("cursor", "");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let _ = window.add_event_listener_with_callback(
|
|
||||||
"mouseup",
|
|
||||||
onmouseup_window.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
onmouseup_window.forget();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent context menu on middle click
|
|
||||||
let oncontextmenu =
|
|
||||||
Closure::<dyn Fn(web_sys::MouseEvent)>::new(move |ev: web_sys::MouseEvent| {
|
|
||||||
if ev.button() == 1 {
|
|
||||||
ev.prevent_default();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let _ = container_el.add_event_listener_with_callback(
|
|
||||||
"auxclick",
|
|
||||||
oncontextmenu.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Keep closures alive
|
|
||||||
onmousedown.forget();
|
|
||||||
onmousemove.forget();
|
|
||||||
onmouseup.forget();
|
|
||||||
oncontextmenu.forget();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set up wheel zoom effect for pan mode.
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn setup_wheel_zoom(
|
|
||||||
outer_container_ref: NodeRef<leptos::html::Div>,
|
|
||||||
is_pan_mode: Signal<bool>,
|
|
||||||
on_zoom_change: Option<Callback<f64>>,
|
|
||||||
) {
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use wasm_bindgen::{JsCast, closure::Closure};
|
|
||||||
|
|
||||||
let wheel_closure: Rc<RefCell<Option<Closure<dyn Fn(web_sys::WheelEvent)>>>> =
|
|
||||||
Rc::new(RefCell::new(None));
|
|
||||||
let wheel_closure_clone = wheel_closure.clone();
|
|
||||||
|
|
||||||
Effect::new(move |_| {
|
|
||||||
let pan_mode = is_pan_mode.get();
|
|
||||||
|
|
||||||
if let Some(container) = outer_container_ref.get() {
|
|
||||||
let element: &web_sys::Element = &container;
|
|
||||||
|
|
||||||
// Remove existing listener if any
|
|
||||||
if let Some(closure) = wheel_closure_clone.borrow().as_ref() {
|
|
||||||
let _ = element.remove_event_listener_with_callback(
|
|
||||||
"wheel",
|
|
||||||
closure.as_ref().unchecked_ref(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if pan_mode {
|
|
||||||
// Add non-passive wheel listener for zoom
|
|
||||||
let closure = Closure::new(move |ev: web_sys::WheelEvent| {
|
|
||||||
if !ev.ctrl_key() {
|
|
||||||
if let Some(zoom_callback) = on_zoom_change {
|
|
||||||
let delta_y = ev.delta_y();
|
|
||||||
let zoom_delta = if delta_y < 0.0 { 0.1 } else { -0.1 };
|
|
||||||
zoom_callback.run(zoom_delta);
|
|
||||||
ev.prevent_default();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let options = web_sys::AddEventListenerOptions::new();
|
|
||||||
options.set_passive(false);
|
|
||||||
|
|
||||||
let _ = element.add_event_listener_with_callback_and_add_event_listener_options(
|
|
||||||
"wheel",
|
|
||||||
closure.as_ref().unchecked_ref(),
|
|
||||||
&options,
|
|
||||||
);
|
|
||||||
|
|
||||||
*wheel_closure_clone.borrow_mut() = Some(closure);
|
|
||||||
} else {
|
|
||||||
*wheel_closure_clone.borrow_mut() = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw background to canvas (handles both pan and fit modes).
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
pub fn draw_background(
|
|
||||||
canvas_el: &web_sys::HtmlCanvasElement,
|
|
||||||
bg_color: &str,
|
|
||||||
image_path: &str,
|
|
||||||
has_background_image: bool,
|
|
||||||
scene_width: f64,
|
|
||||||
scene_height: f64,
|
|
||||||
is_pan_mode: bool,
|
|
||||||
zoom: f64,
|
|
||||||
set_scale_x: WriteSignal<f64>,
|
|
||||||
set_scale_y: WriteSignal<f64>,
|
|
||||||
set_offset_x: WriteSignal<f64>,
|
|
||||||
set_offset_y: WriteSignal<f64>,
|
|
||||||
set_scales_ready: WriteSignal<bool>,
|
|
||||||
) {
|
|
||||||
use wasm_bindgen::{JsCast, closure::Closure};
|
|
||||||
|
|
||||||
let canvas_el = canvas_el.clone();
|
|
||||||
let bg_color = bg_color.to_string();
|
|
||||||
let image_path = image_path.to_string();
|
|
||||||
|
|
||||||
let draw_bg = Closure::once(Box::new(move || {
|
|
||||||
if is_pan_mode {
|
|
||||||
// Pan mode: canvas at native resolution * zoom
|
|
||||||
let canvas_width = (scene_width * zoom) as u32;
|
|
||||||
let canvas_height = (scene_height * zoom) as u32;
|
|
||||||
|
|
||||||
canvas_el.set_width(canvas_width);
|
|
||||||
canvas_el.set_height(canvas_height);
|
|
||||||
|
|
||||||
set_scale_x.set(zoom);
|
|
||||||
set_scale_y.set(zoom);
|
|
||||||
set_offset_x.set(0.0);
|
|
||||||
set_offset_y.set(0.0);
|
|
||||||
set_scales_ready.set(true);
|
|
||||||
|
|
||||||
if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
|
|
||||||
let ctx: web_sys::CanvasRenderingContext2d =
|
|
||||||
ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();
|
|
||||||
|
|
||||||
ctx.set_fill_style_str(&bg_color);
|
|
||||||
ctx.fill_rect(0.0, 0.0, canvas_width as f64, canvas_height as f64);
|
|
||||||
|
|
||||||
if has_background_image && !image_path.is_empty() {
|
|
||||||
let img = web_sys::HtmlImageElement::new().unwrap();
|
|
||||||
let img_clone = img.clone();
|
|
||||||
let ctx_clone = ctx.clone();
|
|
||||||
|
|
||||||
let onload = Closure::once(Box::new(move || {
|
|
||||||
let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
|
|
||||||
&img_clone,
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
canvas_width as f64,
|
|
||||||
canvas_height as f64,
|
|
||||||
);
|
|
||||||
}) as Box<dyn FnOnce()>);
|
|
||||||
|
|
||||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
|
||||||
onload.forget();
|
|
||||||
img.set_src(&image_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Fit mode: scale to viewport with letterboxing
|
|
||||||
let display_width = canvas_el.client_width() as u32;
|
|
||||||
let display_height = canvas_el.client_height() as u32;
|
|
||||||
|
|
||||||
if display_width == 0 || display_height == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas_el.set_width(display_width);
|
|
||||||
canvas_el.set_height(display_height);
|
|
||||||
|
|
||||||
let canvas_aspect = display_width as f64 / display_height as f64;
|
|
||||||
let scene_aspect = scene_width / scene_height;
|
|
||||||
|
|
||||||
let (draw_width, draw_height, draw_x, draw_y) = if canvas_aspect > scene_aspect {
|
|
||||||
let h = display_height as f64;
|
|
||||||
let w = h * scene_aspect;
|
|
||||||
let x = (display_width as f64 - w) / 2.0;
|
|
||||||
(w, h, x, 0.0)
|
|
||||||
} else {
|
|
||||||
let w = display_width as f64;
|
|
||||||
let h = w / scene_aspect;
|
|
||||||
let y = (display_height as f64 - h) / 2.0;
|
|
||||||
(w, h, 0.0, y)
|
|
||||||
};
|
|
||||||
|
|
||||||
let sx = draw_width / scene_width;
|
|
||||||
let sy = draw_height / scene_height;
|
|
||||||
set_scale_x.set(sx);
|
|
||||||
set_scale_y.set(sy);
|
|
||||||
set_offset_x.set(draw_x);
|
|
||||||
set_offset_y.set(draw_y);
|
|
||||||
set_scales_ready.set(true);
|
|
||||||
|
|
||||||
if let Ok(Some(ctx)) = canvas_el.get_context("2d") {
|
|
||||||
let ctx: web_sys::CanvasRenderingContext2d =
|
|
||||||
ctx.dyn_into::<web_sys::CanvasRenderingContext2d>().unwrap();
|
|
||||||
|
|
||||||
// Fill letterbox area with black
|
|
||||||
ctx.set_fill_style_str("#000");
|
|
||||||
ctx.fill_rect(0.0, 0.0, display_width as f64, display_height as f64);
|
|
||||||
|
|
||||||
// Fill scene area with background color
|
|
||||||
ctx.set_fill_style_str(&bg_color);
|
|
||||||
ctx.fill_rect(draw_x, draw_y, draw_width, draw_height);
|
|
||||||
|
|
||||||
if has_background_image && !image_path.is_empty() {
|
|
||||||
let img = web_sys::HtmlImageElement::new().unwrap();
|
|
||||||
let img_clone = img.clone();
|
|
||||||
let ctx_clone = ctx.clone();
|
|
||||||
|
|
||||||
let onload = Closure::once(Box::new(move || {
|
|
||||||
let _ = ctx_clone.draw_image_with_html_image_element_and_dw_and_dh(
|
|
||||||
&img_clone,
|
|
||||||
draw_x,
|
|
||||||
draw_y,
|
|
||||||
draw_width,
|
|
||||||
draw_height,
|
|
||||||
);
|
|
||||||
}) as Box<dyn FnOnce()>);
|
|
||||||
|
|
||||||
img.set_onload(Some(onload.as_ref().unchecked_ref()));
|
|
||||||
onload.forget();
|
|
||||||
img.set_src(&image_path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as Box<dyn FnOnce()>);
|
|
||||||
|
|
||||||
// Use setTimeout with small delay to ensure canvas is in DOM
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(
|
|
||||||
draw_bg.as_ref().unchecked_ref(),
|
|
||||||
100,
|
|
||||||
);
|
|
||||||
draw_bg.forget();
|
|
||||||
}
|
|
||||||
|
|
@ -1,306 +0,0 @@
|
||||||
//! Overlay components for prop editing in scene viewer.
|
|
||||||
//!
|
|
||||||
//! Contains ScaleOverlay and MoveOverlay components used when
|
|
||||||
//! moderators edit prop scale or position.
|
|
||||||
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use chattyness_db::models::LooseProp;
|
|
||||||
|
|
||||||
use super::super::settings::{BASE_AVATAR_SCALE, BASE_PROP_SCALE};
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
use super::super::canvas_utils::normalize_asset_path;
|
|
||||||
|
|
||||||
/// Overlay shown when editing a prop's scale.
|
|
||||||
///
|
|
||||||
/// Allows dragging from the prop center to adjust scale.
|
|
||||||
#[component]
|
|
||||||
pub fn ScaleOverlay(
|
|
||||||
#[prop(into)] active: Signal<bool>,
|
|
||||||
#[prop(into)] prop_id: Signal<Option<Uuid>>,
|
|
||||||
#[prop(into)] preview_scale: RwSignal<f32>,
|
|
||||||
#[prop(into)] prop_center: Signal<(f64, f64)>,
|
|
||||||
#[prop(into)] loose_props: Signal<Vec<LooseProp>>,
|
|
||||||
#[prop(into)] prop_size: Signal<f64>,
|
|
||||||
#[prop(optional)] on_apply: Option<Callback<(Uuid, f32)>>,
|
|
||||||
#[prop(optional)] on_cancel: Option<Callback<()>>,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let (_center_x, _center_y) = prop_center.get_untracked();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<Show when=move || active.get()>
|
|
||||||
{move || {
|
|
||||||
let current_prop_id = prop_id.get();
|
|
||||||
let current_preview_scale = preview_scale.get();
|
|
||||||
let (center_x, center_y) = prop_center.get();
|
|
||||||
|
|
||||||
// Find the prop to get its dimensions
|
|
||||||
let prop_data = current_prop_id.and_then(|id| {
|
|
||||||
loose_props.get().iter().find(|p| p.id == id).cloned()
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 z-50 cursor-crosshair"
|
|
||||||
style="background: rgba(0,0,0,0.3);"
|
|
||||||
on:mousemove=move |ev| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let ev: web_sys::MouseEvent = ev.dyn_into().unwrap();
|
|
||||||
let mouse_x = ev.client_x() as f64;
|
|
||||||
let mouse_y = ev.client_y() as f64;
|
|
||||||
let (cx, cy) = prop_center.get();
|
|
||||||
let dx = mouse_x - cx;
|
|
||||||
let dy = mouse_y - cy;
|
|
||||||
let distance = (dx * dx + dy * dy).sqrt();
|
|
||||||
// Scale formula: distance / 40 gives 1x at 40px
|
|
||||||
let new_scale = (distance / 40.0).clamp(0.1, 10.0) as f32;
|
|
||||||
preview_scale.set(new_scale);
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let _ = ev;
|
|
||||||
}
|
|
||||||
on:mouseup=move |ev| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
if let (Some(pid), Some(ref callback)) = (prop_id.get(), on_apply.as_ref()) {
|
|
||||||
let final_scale = preview_scale.get();
|
|
||||||
callback.run((pid, final_scale));
|
|
||||||
}
|
|
||||||
if let Some(ref callback) = on_cancel {
|
|
||||||
callback.run(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let _ = ev;
|
|
||||||
}
|
|
||||||
on:keydown=move |ev| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let ev: web_sys::KeyboardEvent = ev.dyn_into().unwrap();
|
|
||||||
if ev.key() == "Escape" {
|
|
||||||
ev.prevent_default();
|
|
||||||
if let Some(ref callback) = on_cancel {
|
|
||||||
callback.run(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let _ = ev;
|
|
||||||
}
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
// Visual feedback: dashed border around prop
|
|
||||||
{move || {
|
|
||||||
if let Some(ref _prop) = prop_data {
|
|
||||||
let base_size = prop_size.get();
|
|
||||||
let prop_scale_ratio = BASE_PROP_SCALE / BASE_AVATAR_SCALE;
|
|
||||||
let preview_prop_size = base_size * prop_scale_ratio * current_preview_scale as f64;
|
|
||||||
let half_size = preview_prop_size / 2.0;
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="absolute pointer-events-none"
|
|
||||||
style=format!(
|
|
||||||
"left: {}px; top: {}px; width: {}px; height: {}px; \
|
|
||||||
border: 2px dashed #fbbf24; \
|
|
||||||
transform: translate(-50%, -50%); \
|
|
||||||
box-sizing: border-box;",
|
|
||||||
center_x, center_y, preview_prop_size, preview_prop_size
|
|
||||||
)
|
|
||||||
/>
|
|
||||||
// Scale indicator
|
|
||||||
<div
|
|
||||||
class="absolute bg-gray-900/90 text-yellow-400 px-2 py-1 rounded text-sm font-mono pointer-events-none"
|
|
||||||
style=format!(
|
|
||||||
"left: {}px; top: {}px; transform: translate(-50%, 8px);",
|
|
||||||
center_x, center_y + half_size
|
|
||||||
)
|
|
||||||
>
|
|
||||||
{format!("{:.2}x", current_preview_scale)}
|
|
||||||
</div>
|
|
||||||
}.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// Instructions
|
|
||||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-gray-900/90 text-white px-4 py-2 rounded text-sm">
|
|
||||||
"Drag to resize • Release to apply • Escape to cancel"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Overlay shown when moving a prop to a new position.
|
|
||||||
#[component]
|
|
||||||
pub fn MoveOverlay(
|
|
||||||
#[prop(into)] active: Signal<bool>,
|
|
||||||
#[prop(into)] prop_id: Signal<Option<Uuid>>,
|
|
||||||
#[prop(into)] preview_position: RwSignal<(f64, f64)>,
|
|
||||||
#[prop(into)] prop_scale: Signal<f32>,
|
|
||||||
#[prop(into)] loose_props: Signal<Vec<LooseProp>>,
|
|
||||||
#[prop(into)] prop_size: Signal<f64>,
|
|
||||||
#[prop(into)] scale_x: Signal<f64>,
|
|
||||||
#[prop(into)] scale_y: Signal<f64>,
|
|
||||||
#[prop(into)] offset_x: Signal<f64>,
|
|
||||||
#[prop(into)] offset_y: Signal<f64>,
|
|
||||||
#[prop(optional)] on_apply: Option<Callback<(Uuid, f64, f64)>>,
|
|
||||||
#[prop(optional)] on_cancel: Option<Callback<()>>,
|
|
||||||
) -> impl IntoView {
|
|
||||||
view! {
|
|
||||||
<Show when=move || active.get()>
|
|
||||||
{move || {
|
|
||||||
let current_prop_id = prop_id.get();
|
|
||||||
let (_preview_x, _preview_y) = preview_position.get();
|
|
||||||
let current_prop_scale = prop_scale.get();
|
|
||||||
|
|
||||||
// Find the prop to get its asset path
|
|
||||||
let prop_data = current_prop_id.and_then(|id| {
|
|
||||||
loose_props.get().iter().find(|p| p.id == id).cloned()
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate ghost size
|
|
||||||
let base_size = prop_size.get();
|
|
||||||
let prop_scale_ratio = BASE_PROP_SCALE / BASE_AVATAR_SCALE;
|
|
||||||
let ghost_size = base_size * prop_scale_ratio * current_prop_scale as f64;
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="fixed inset-0 z-50 cursor-crosshair"
|
|
||||||
style="background: rgba(0,0,0,0.3);"
|
|
||||||
on:mousemove=move |ev| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let ev: web_sys::MouseEvent = ev.dyn_into().unwrap();
|
|
||||||
let mouse_x = ev.client_x() as f64;
|
|
||||||
let mouse_y = ev.client_y() as f64;
|
|
||||||
|
|
||||||
// Get scene viewer's position
|
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
|
||||||
if let Some(viewer) = document.query_selector(".scene-viewer-container").ok().flatten() {
|
|
||||||
let rect = viewer.get_bounding_client_rect();
|
|
||||||
let viewer_x = mouse_x - rect.left();
|
|
||||||
let viewer_y = mouse_y - rect.top();
|
|
||||||
|
|
||||||
let sx = scale_x.get();
|
|
||||||
let sy = scale_y.get();
|
|
||||||
let ox = offset_x.get();
|
|
||||||
let oy = offset_y.get();
|
|
||||||
|
|
||||||
if sx > 0.0 && sy > 0.0 {
|
|
||||||
let scene_x = (viewer_x - ox) / sx;
|
|
||||||
let scene_y = (viewer_y - oy) / sy;
|
|
||||||
preview_position.set((scene_x, scene_y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let _ = ev;
|
|
||||||
}
|
|
||||||
on:click=move |ev| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
if let (Some(pid), Some(ref callback)) = (prop_id.get(), on_apply.as_ref()) {
|
|
||||||
let (final_x, final_y) = preview_position.get();
|
|
||||||
callback.run((pid, final_x, final_y));
|
|
||||||
}
|
|
||||||
if let Some(ref callback) = on_cancel {
|
|
||||||
callback.run(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let _ = ev;
|
|
||||||
}
|
|
||||||
on:keydown=move |ev| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
let ev: web_sys::KeyboardEvent = ev.dyn_into().unwrap();
|
|
||||||
if ev.key() == "Escape" {
|
|
||||||
ev.prevent_default();
|
|
||||||
if let Some(ref callback) = on_cancel {
|
|
||||||
callback.run(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
let _ = ev;
|
|
||||||
}
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
// Ghost prop at cursor position
|
|
||||||
{move || {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
{
|
|
||||||
if let Some(ref prop) = prop_data {
|
|
||||||
let (preview_x, preview_y) = preview_position.get();
|
|
||||||
let sx = scale_x.get();
|
|
||||||
let sy = scale_y.get();
|
|
||||||
let ox = offset_x.get();
|
|
||||||
let oy = offset_y.get();
|
|
||||||
|
|
||||||
// Get scene viewer position in viewport
|
|
||||||
let document = web_sys::window().unwrap().document().unwrap();
|
|
||||||
let viewer_offset = document
|
|
||||||
.query_selector(".scene-viewer-container")
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.map(|v| {
|
|
||||||
let rect = v.get_bounding_client_rect();
|
|
||||||
(rect.left(), rect.top())
|
|
||||||
})
|
|
||||||
.unwrap_or((0.0, 0.0));
|
|
||||||
|
|
||||||
// Convert scene coords to viewport coords
|
|
||||||
let viewer_x = preview_x * sx + ox;
|
|
||||||
let viewer_y = preview_y * sy + oy;
|
|
||||||
let viewport_x = viewer_x + viewer_offset.0;
|
|
||||||
let viewport_y = viewer_y + viewer_offset.1;
|
|
||||||
|
|
||||||
let normalized_path = normalize_asset_path(&prop.prop_asset_path);
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="absolute pointer-events-none"
|
|
||||||
style=format!(
|
|
||||||
"left: {}px; top: {}px; width: {}px; height: {}px; \
|
|
||||||
transform: translate(-50%, -50%); \
|
|
||||||
border: 2px dashed #10b981; \
|
|
||||||
background: rgba(16, 185, 129, 0.2); \
|
|
||||||
box-sizing: border-box;",
|
|
||||||
viewport_x, viewport_y, ghost_size, ghost_size
|
|
||||||
)
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src=normalized_path
|
|
||||||
class="w-full h-full object-contain opacity-50"
|
|
||||||
style="pointer-events: none;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}.into_any()
|
|
||||||
} else {
|
|
||||||
().into_any()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(not(feature = "hydrate"))]
|
|
||||||
{
|
|
||||||
().into_any()
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// Instructions
|
|
||||||
<div class="absolute bottom-4 left-1/2 -translate-x-1/2 bg-gray-900/90 text-white px-4 py-2 rounded text-sm">
|
|
||||||
"Click to place • Escape to cancel"
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -8,17 +8,8 @@ use crate::utils::LocalStoragePersist;
|
||||||
pub const REFERENCE_WIDTH: f64 = 1920.0;
|
pub const REFERENCE_WIDTH: f64 = 1920.0;
|
||||||
pub const REFERENCE_HEIGHT: f64 = 1080.0;
|
pub const REFERENCE_HEIGHT: f64 = 1080.0;
|
||||||
|
|
||||||
/// Base size for props/avatars in scene coordinates.
|
/// Base size for props and avatars in scene space.
|
||||||
/// SVG assets are 120x120 pixels - this is the native/full size.
|
pub const BASE_PROP_SIZE: f64 = 60.0;
|
||||||
pub const BASE_PROP_SIZE: f64 = 120.0;
|
|
||||||
|
|
||||||
/// Scale factor for avatar rendering relative to BASE_PROP_SIZE.
|
|
||||||
/// Avatars render at 50% (60px cells) to allow merit-based scaling up later.
|
|
||||||
pub const BASE_AVATAR_SCALE: f64 = 0.5;
|
|
||||||
|
|
||||||
/// Scale factor for dropped loose props relative to BASE_PROP_SIZE.
|
|
||||||
/// Props render at 75% (90px) at default scale=1.0.
|
|
||||||
pub const BASE_PROP_SCALE: f64 = 0.75;
|
|
||||||
|
|
||||||
/// Minimum zoom level (25%).
|
/// Minimum zoom level (25%).
|
||||||
pub const ZOOM_MIN: f64 = 0.25;
|
pub const ZOOM_MIN: f64 = 0.25;
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,6 @@ pub enum WsEvent {
|
||||||
PropDropped(LooseProp),
|
PropDropped(LooseProp),
|
||||||
/// A prop was picked up (by prop ID).
|
/// A prop was picked up (by prop ID).
|
||||||
PropPickedUp(uuid::Uuid),
|
PropPickedUp(uuid::Uuid),
|
||||||
/// A prop was updated (scale changed).
|
|
||||||
PropRefresh(LooseProp),
|
|
||||||
/// A member started fading out (timeout disconnect).
|
/// A member started fading out (timeout disconnect).
|
||||||
MemberFading(FadingMember),
|
MemberFading(FadingMember),
|
||||||
/// Welcome message received with current user info.
|
/// Welcome message received with current user info.
|
||||||
|
|
@ -198,7 +196,7 @@ pub fn use_channel_websocket(
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use wasm_bindgen::{JsCast, closure::Closure};
|
use wasm_bindgen::{JsCast, closure::Closure};
|
||||||
use web_sys::{CloseEvent, MessageEvent, WebSocket};
|
use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket};
|
||||||
|
|
||||||
let (ws_state, set_ws_state) = signal(WsState::Disconnected);
|
let (ws_state, set_ws_state) = signal(WsState::Disconnected);
|
||||||
|
|
||||||
|
|
@ -256,9 +254,6 @@ pub fn use_channel_websocket(
|
||||||
// Close with SCENE_CHANGE code so onclose handler knows this was intentional
|
// Close with SCENE_CHANGE code so onclose handler knows this was intentional
|
||||||
let _ = old_ws.close_with_code_and_reason(close_codes::SCENE_CHANGE, "scene change");
|
let _ = old_ws.close_with_code_and_reason(close_codes::SCENE_CHANGE, "scene change");
|
||||||
}
|
}
|
||||||
// Reset the intentional close flag for the new connection.
|
|
||||||
// This ensures the new connection's handlers don't see a stale flag.
|
|
||||||
state.is_intentional_close = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(ch_id) = ch_id else {
|
let Some(ch_id) = ch_id else {
|
||||||
|
|
@ -381,19 +376,17 @@ pub fn use_channel_websocket(
|
||||||
onmessage.forget();
|
onmessage.forget();
|
||||||
|
|
||||||
// onerror
|
// onerror
|
||||||
// Note: WebSocket.onerror receives a generic Event, not ErrorEvent.
|
|
||||||
// The event has no useful error details - just indicates an error occurred.
|
|
||||||
let set_ws_state_err = set_ws_state;
|
let set_ws_state_err = set_ws_state;
|
||||||
let ws_state_for_err = ws_state;
|
let ws_state_for_err = ws_state;
|
||||||
let reconnect_trigger_for_error = reconnect_trigger;
|
let reconnect_trigger_for_error = reconnect_trigger;
|
||||||
let is_disposed_for_err = is_disposed_for_effect.clone();
|
let is_disposed_for_err = is_disposed_for_effect.clone();
|
||||||
let onerror = Closure::wrap(Box::new(move |_e: web_sys::Event| {
|
let onerror = Closure::wrap(Box::new(move |e: ErrorEvent| {
|
||||||
// Skip if component has been disposed
|
// Skip if component has been disposed
|
||||||
if is_disposed_for_err.load(Ordering::Relaxed) {
|
if is_disposed_for_err.load(Ordering::Relaxed) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
web_sys::console::error_1(&"[WS] Error occurred".into());
|
web_sys::console::error_1(&format!("[WS] Error: {:?}", e.message()).into());
|
||||||
|
|
||||||
// Check if we're in silent reconnection mode
|
// Check if we're in silent reconnection mode
|
||||||
let current_state = ws_state_for_err.get_untracked();
|
let current_state = ws_state_for_err.get_untracked();
|
||||||
|
|
@ -427,7 +420,7 @@ pub fn use_channel_websocket(
|
||||||
} else {
|
} else {
|
||||||
set_ws_state_err.set(WsState::Error);
|
set_ws_state_err.set(WsState::Error);
|
||||||
}
|
}
|
||||||
}) as Box<dyn FnMut(web_sys::Event)>);
|
}) as Box<dyn FnMut(ErrorEvent)>);
|
||||||
ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
|
ws.set_onerror(Some(onerror.as_ref().unchecked_ref()));
|
||||||
onerror.forget();
|
onerror.forget();
|
||||||
|
|
||||||
|
|
@ -523,7 +516,6 @@ fn handle_server_message(
|
||||||
LoosePropsSync(Vec<LooseProp>),
|
LoosePropsSync(Vec<LooseProp>),
|
||||||
PropDropped(LooseProp),
|
PropDropped(LooseProp),
|
||||||
PropPickedUp(uuid::Uuid),
|
PropPickedUp(uuid::Uuid),
|
||||||
PropRefresh(LooseProp),
|
|
||||||
Error(WsError),
|
Error(WsError),
|
||||||
TeleportApproved(TeleportInfo),
|
TeleportApproved(TeleportInfo),
|
||||||
Summoned(SummonInfo),
|
Summoned(SummonInfo),
|
||||||
|
|
@ -662,14 +654,6 @@ fn handle_server_message(
|
||||||
// Treat expired props the same as picked up (remove from display)
|
// Treat expired props the same as picked up (remove from display)
|
||||||
PostAction::PropPickedUp(prop_id)
|
PostAction::PropPickedUp(prop_id)
|
||||||
}
|
}
|
||||||
ServerMessage::PropDeleted { inventory_item_id: _ } => {
|
|
||||||
// Inventory deletion is handled optimistically in the UI
|
|
||||||
// No scene state change needed
|
|
||||||
PostAction::None
|
|
||||||
}
|
|
||||||
ServerMessage::PropRefresh { prop } => {
|
|
||||||
PostAction::PropRefresh(prop)
|
|
||||||
}
|
|
||||||
ServerMessage::AvatarUpdated { user_id, avatar } => {
|
ServerMessage::AvatarUpdated { user_id, avatar } => {
|
||||||
// Find member and update their avatar layers
|
// Find member and update their avatar layers
|
||||||
if let Some(m) = state.members
|
if let Some(m) = state.members
|
||||||
|
|
@ -786,9 +770,6 @@ fn handle_server_message(
|
||||||
PostAction::PropPickedUp(prop_id) => {
|
PostAction::PropPickedUp(prop_id) => {
|
||||||
on_event.run(WsEvent::PropPickedUp(prop_id));
|
on_event.run(WsEvent::PropPickedUp(prop_id));
|
||||||
}
|
}
|
||||||
PostAction::PropRefresh(prop) => {
|
|
||||||
on_event.run(WsEvent::PropRefresh(prop));
|
|
||||||
}
|
|
||||||
PostAction::Error(err) => {
|
PostAction::Error(err) => {
|
||||||
on_event.run(WsEvent::Error(err));
|
on_event.run(WsEvent::Error(err));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -476,14 +476,6 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
WsEvent::PropRefresh(prop) => {
|
|
||||||
// Update the prop in the loose_props list (replace existing or ignore if not found)
|
|
||||||
set_loose_props.update(|props| {
|
|
||||||
if let Some(existing) = props.iter_mut().find(|p| p.id == prop.id) {
|
|
||||||
*existing = prop;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1210,40 +1202,6 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
let is_moderator_signal = Signal::derive(move || is_moderator.get());
|
let is_moderator_signal = Signal::derive(move || is_moderator.get());
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
let ws_for_prop_scale = ws_sender_clone.clone();
|
|
||||||
let on_prop_scale_update_cb = Callback::new(move |(prop_id, scale): (Uuid, f32)| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
ws_for_prop_scale.with_value(|sender| {
|
|
||||||
if let Some(send_fn) = sender {
|
|
||||||
send_fn(ClientMessage::UpdateProp { loose_prop_id: prop_id, scale });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
let ws_for_prop_move = ws_sender_clone.clone();
|
|
||||||
let on_prop_move_cb = Callback::new(move |(prop_id, x, y): (Uuid, f64, f64)| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
ws_for_prop_move.with_value(|sender| {
|
|
||||||
if let Some(send_fn) = sender {
|
|
||||||
send_fn(ClientMessage::MoveProp { loose_prop_id: prop_id, x, y });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
let ws_for_prop_lock = ws_sender_clone.clone();
|
|
||||||
let on_prop_lock_toggle_cb = Callback::new(move |(prop_id, lock): (Uuid, bool)| {
|
|
||||||
#[cfg(feature = "hydrate")]
|
|
||||||
ws_for_prop_lock.with_value(|sender| {
|
|
||||||
if let Some(send_fn) = sender {
|
|
||||||
if lock {
|
|
||||||
send_fn(ClientMessage::LockProp { loose_prop_id: prop_id });
|
|
||||||
} else {
|
|
||||||
send_fn(ClientMessage::UnlockProp { loose_prop_id: prop_id });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
view! {
|
view! {
|
||||||
<div class="relative w-full">
|
<div class="relative w-full">
|
||||||
<RealmSceneViewer
|
<RealmSceneViewer
|
||||||
|
|
@ -1265,10 +1223,6 @@ pub fn RealmPage() -> impl IntoView {
|
||||||
current_user_id=Signal::derive(move || current_user_id.get())
|
current_user_id=Signal::derive(move || current_user_id.get())
|
||||||
is_guest=Signal::derive(move || is_guest.get())
|
is_guest=Signal::derive(move || is_guest.get())
|
||||||
on_whisper_request=on_whisper_request_cb
|
on_whisper_request=on_whisper_request_cb
|
||||||
is_moderator=is_moderator_signal
|
|
||||||
on_prop_scale_update=on_prop_scale_update_cb
|
|
||||||
on_prop_move=on_prop_move_cb
|
|
||||||
on_prop_lock_toggle=on_prop_lock_toggle_cb
|
|
||||||
/>
|
/>
|
||||||
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
|
<div class="absolute bottom-0 left-0 right-0 z-10 pb-4 px-4 pointer-events-none">
|
||||||
<ChatInput
|
<ChatInput
|
||||||
|
|
|
||||||
|
|
@ -279,9 +279,6 @@ CREATE TABLE server.props (
|
||||||
default_emotion server.emotion_state,
|
default_emotion server.emotion_state,
|
||||||
default_position SMALLINT CHECK (default_position IS NULL OR default_position BETWEEN 0 AND 8),
|
default_position SMALLINT CHECK (default_position IS NULL OR default_position BETWEEN 0 AND 8),
|
||||||
|
|
||||||
-- Default scale factor for dropped props (10% - 1000%)
|
|
||||||
default_scale REAL NOT NULL DEFAULT 1.0 CHECK (default_scale >= 0.1 AND default_scale <= 10.0),
|
|
||||||
|
|
||||||
is_unique BOOLEAN NOT NULL DEFAULT false,
|
is_unique BOOLEAN NOT NULL DEFAULT false,
|
||||||
is_transferable BOOLEAN NOT NULL DEFAULT true,
|
is_transferable BOOLEAN NOT NULL DEFAULT true,
|
||||||
is_portable BOOLEAN NOT NULL DEFAULT true,
|
is_portable BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
|
||||||
|
|
@ -194,9 +194,6 @@ CREATE TABLE realm.props (
|
||||||
default_emotion server.emotion_state,
|
default_emotion server.emotion_state,
|
||||||
default_position SMALLINT CHECK (default_position IS NULL OR default_position BETWEEN 0 AND 8),
|
default_position SMALLINT CHECK (default_position IS NULL OR default_position BETWEEN 0 AND 8),
|
||||||
|
|
||||||
-- Default scale factor for dropped props (10% - 1000%)
|
|
||||||
default_scale REAL NOT NULL DEFAULT 1.0 CHECK (default_scale >= 0.1 AND default_scale <= 10.0),
|
|
||||||
|
|
||||||
is_unique BOOLEAN NOT NULL DEFAULT false,
|
is_unique BOOLEAN NOT NULL DEFAULT false,
|
||||||
is_transferable BOOLEAN NOT NULL DEFAULT true,
|
is_transferable BOOLEAN NOT NULL DEFAULT true,
|
||||||
is_droppable BOOLEAN NOT NULL DEFAULT true,
|
is_droppable BOOLEAN NOT NULL DEFAULT true,
|
||||||
|
|
|
||||||
|
|
@ -210,9 +210,6 @@ CREATE TABLE scene.loose_props (
|
||||||
-- Position in scene (PostGIS point, SRID 0)
|
-- Position in scene (PostGIS point, SRID 0)
|
||||||
position public.virtual_point NOT NULL,
|
position public.virtual_point NOT NULL,
|
||||||
|
|
||||||
-- Scale factor (10% - 1000%), inherited from prop definition at drop time
|
|
||||||
scale REAL NOT NULL DEFAULT 1.0 CHECK (scale >= 0.1 AND scale <= 10.0),
|
|
||||||
|
|
||||||
-- Who dropped it (NULL = spawned by system/script)
|
-- Who dropped it (NULL = spawned by system/script)
|
||||||
dropped_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
dropped_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
|
||||||
|
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 125 KiB |
|
|
@ -1,26 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="armGradient" cx="30%" cy="30%" r="70%">
|
|
||||||
<stop offset="0%" stop-color="#FFB347"/>
|
|
||||||
<stop offset="100%" stop-color="#FF8C00"/>
|
|
||||||
</radialGradient>
|
|
||||||
<filter id="armShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Left arm - stubby sausage shape reaching up and out -->
|
|
||||||
<path d="M 120 90
|
|
||||||
Q 100 85 80 60
|
|
||||||
Q 65 40 50 25
|
|
||||||
Q 40 15 30 20
|
|
||||||
Q 20 25 25 40
|
|
||||||
Q 30 55 50 70
|
|
||||||
Q 70 85 95 95
|
|
||||||
Q 110 100 120 95
|
|
||||||
Z"
|
|
||||||
fill="url(#armGradient)" filter="url(#armShadow)"/>
|
|
||||||
|
|
||||||
<!-- Arm highlight -->
|
|
||||||
<path d="M 90 70 Q 70 50 55 35" stroke="#FFCC80" stroke-width="8" fill="none" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 960 B |
|
|
@ -1,26 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="armGradient" cx="70%" cy="30%" r="70%">
|
|
||||||
<stop offset="0%" stop-color="#FFB347"/>
|
|
||||||
<stop offset="100%" stop-color="#FF8C00"/>
|
|
||||||
</radialGradient>
|
|
||||||
<filter id="armShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Right arm - stubby sausage shape reaching up and out (mirrored) -->
|
|
||||||
<path d="M 0 90
|
|
||||||
Q 20 85 40 60
|
|
||||||
Q 55 40 70 25
|
|
||||||
Q 80 15 90 20
|
|
||||||
Q 100 25 95 40
|
|
||||||
Q 90 55 70 70
|
|
||||||
Q 50 85 25 95
|
|
||||||
Q 10 100 0 95
|
|
||||||
Z"
|
|
||||||
fill="url(#armGradient)" filter="url(#armShadow)"/>
|
|
||||||
|
|
||||||
<!-- Arm highlight -->
|
|
||||||
<path d="M 30 70 Q 50 50 65 35" stroke="#FFCC80" stroke-width="8" fill="none" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 967 B |
|
|
@ -1,32 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="handGradient" cx="40%" cy="40%" r="60%">
|
|
||||||
<stop offset="0%" stop-color="#FFB347"/>
|
|
||||||
<stop offset="100%" stop-color="#FF8C00"/>
|
|
||||||
</radialGradient>
|
|
||||||
<filter id="handShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Waving hand - palm open, fingers spread -->
|
|
||||||
<!-- Palm -->
|
|
||||||
<ellipse cx="70" cy="75" rx="28" ry="25" fill="url(#handGradient)" filter="url(#handShadow)"/>
|
|
||||||
|
|
||||||
<!-- Thumb -->
|
|
||||||
<ellipse cx="95" cy="90" rx="12" ry="10" fill="url(#handGradient)" filter="url(#handShadow)"/>
|
|
||||||
|
|
||||||
<!-- Fingers - spread out in wave -->
|
|
||||||
<ellipse cx="45" cy="50" rx="10" ry="20" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-30 45 50)"/>
|
|
||||||
<ellipse cx="62" cy="42" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-10 62 42)"/>
|
|
||||||
<ellipse cx="80" cy="40" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(10 80 40)"/>
|
|
||||||
<ellipse cx="96" cy="48" rx="8" ry="18" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(25 96 48)"/>
|
|
||||||
|
|
||||||
<!-- Palm highlight -->
|
|
||||||
<ellipse cx="65" cy="70" rx="12" ry="10" fill="#FFCC80" opacity="0.5"/>
|
|
||||||
|
|
||||||
<!-- Motion lines for waving -->
|
|
||||||
<path d="M 20 30 Q 15 35 20 40" stroke="#4cc9f0" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<path d="M 12 45 Q 7 50 12 55" stroke="#4cc9f0" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.5"/>
|
|
||||||
<path d="M 25 55 Q 20 60 25 65" stroke="#4cc9f0" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,32 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<radialGradient id="handGradient" cx="60%" cy="40%" r="60%">
|
|
||||||
<stop offset="0%" stop-color="#FFB347"/>
|
|
||||||
<stop offset="100%" stop-color="#FF8C00"/>
|
|
||||||
</radialGradient>
|
|
||||||
<filter id="handShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.2"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Waving hand - palm open, fingers spread (mirrored) -->
|
|
||||||
<!-- Palm -->
|
|
||||||
<ellipse cx="50" cy="75" rx="28" ry="25" fill="url(#handGradient)" filter="url(#handShadow)"/>
|
|
||||||
|
|
||||||
<!-- Thumb -->
|
|
||||||
<ellipse cx="25" cy="90" rx="12" ry="10" fill="url(#handGradient)" filter="url(#handShadow)"/>
|
|
||||||
|
|
||||||
<!-- Fingers - spread out in wave -->
|
|
||||||
<ellipse cx="75" cy="50" rx="10" ry="20" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(30 75 50)"/>
|
|
||||||
<ellipse cx="58" cy="42" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(10 58 42)"/>
|
|
||||||
<ellipse cx="40" cy="40" rx="9" ry="22" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-10 40 40)"/>
|
|
||||||
<ellipse cx="24" cy="48" rx="8" ry="18" fill="url(#handGradient)" filter="url(#handShadow)" transform="rotate(-25 24 48)"/>
|
|
||||||
|
|
||||||
<!-- Palm highlight -->
|
|
||||||
<ellipse cx="55" cy="70" rx="12" ry="10" fill="#FFCC80" opacity="0.5"/>
|
|
||||||
|
|
||||||
<!-- Motion lines for waving -->
|
|
||||||
<path d="M 100 30 Q 105 35 100 40" stroke="#4cc9f0" stroke-width="3" fill="none" stroke-linecap="round" opacity="0.6"/>
|
|
||||||
<path d="M 108 45 Q 113 50 108 55" stroke="#4cc9f0" stroke-width="2.5" fill="none" stroke-linecap="round" opacity="0.5"/>
|
|
||||||
<path d="M 95 55 Q 100 60 95 65" stroke="#4cc9f0" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.4"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,17 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<!-- Matching the face colors -->
|
|
||||||
<linearGradient id="neckGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#CC9900"/>
|
|
||||||
<stop offset="30%" stop-color="#FFCC00"/>
|
|
||||||
<stop offset="70%" stop-color="#FFCC00"/>
|
|
||||||
<stop offset="100%" stop-color="#CC9900"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Neck - cylindrical shape connecting to torso below -->
|
|
||||||
<rect x="42" y="0" width="36" height="120" fill="url(#neckGradient)"/>
|
|
||||||
|
|
||||||
<!-- Subtle center highlight -->
|
|
||||||
<rect x="52" y="0" width="16" height="120" fill="#FFE566" opacity="0.3"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 687 B |
|
|
@ -1,24 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<!-- Decorative sparkles/stars above the head -->
|
|
||||||
|
|
||||||
<!-- Main star -->
|
|
||||||
<polygon points="60,10 63,25 78,28 65,35 68,50 60,40 52,50 55,35 42,28 57,25"
|
|
||||||
fill="#FFD700" opacity="0.9"/>
|
|
||||||
|
|
||||||
<!-- Smaller stars -->
|
|
||||||
<polygon points="25,45 27,52 34,53 28,57 30,64 25,59 20,64 22,57 16,53 23,52"
|
|
||||||
fill="#4cc9f0" opacity="0.7"/>
|
|
||||||
|
|
||||||
<polygon points="95,40 97,47 104,48 98,52 100,59 95,54 90,59 92,52 86,48 93,47"
|
|
||||||
fill="#4cc9f0" opacity="0.7"/>
|
|
||||||
|
|
||||||
<!-- Tiny sparkle dots -->
|
|
||||||
<circle cx="40" cy="25" r="3" fill="#FFFFFF" opacity="0.8"/>
|
|
||||||
<circle cx="80" cy="20" r="2.5" fill="#FFFFFF" opacity="0.7"/>
|
|
||||||
<circle cx="15" cy="70" r="2" fill="#FFD700" opacity="0.6"/>
|
|
||||||
<circle cx="105" cy="75" r="2" fill="#FFD700" opacity="0.6"/>
|
|
||||||
|
|
||||||
<!-- Swirl decorations -->
|
|
||||||
<path d="M 30 85 Q 35 80 40 85 Q 45 90 50 85" stroke="#FF69B4" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.5"/>
|
|
||||||
<path d="M 70 90 Q 75 85 80 90 Q 85 95 90 90" stroke="#FF69B4" stroke-width="2" fill="none" stroke-linecap="round" opacity="0.5"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,34 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<!-- Matching face yellow tones -->
|
|
||||||
<radialGradient id="torsoGradient" cx="35%" cy="25%" r="70%">
|
|
||||||
<stop offset="0%" stop-color="#FFE566"/>
|
|
||||||
<stop offset="50%" stop-color="#FFCC00"/>
|
|
||||||
<stop offset="100%" stop-color="#CC9900"/>
|
|
||||||
</radialGradient>
|
|
||||||
|
|
||||||
<linearGradient id="neckGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" stop-color="#CC9900"/>
|
|
||||||
<stop offset="30%" stop-color="#FFCC00"/>
|
|
||||||
<stop offset="70%" stop-color="#FFCC00"/>
|
|
||||||
<stop offset="100%" stop-color="#CC9900"/>
|
|
||||||
</linearGradient>
|
|
||||||
|
|
||||||
<filter id="bodyShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.25"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Neck stub connecting from above -->
|
|
||||||
<rect x="42" y="0" width="36" height="20" fill="url(#neckGradient)"/>
|
|
||||||
<rect x="52" y="0" width="16" height="20" fill="#FFE566" opacity="0.3"/>
|
|
||||||
|
|
||||||
<!-- Main torso - round friendly blob -->
|
|
||||||
<ellipse cx="60" cy="65" rx="55" ry="50" fill="url(#torsoGradient)" filter="url(#bodyShadow)" stroke="#CC9900" stroke-width="1.5"/>
|
|
||||||
|
|
||||||
<!-- Belly highlight -->
|
|
||||||
<ellipse cx="45" cy="50" rx="22" ry="18" fill="#FFFFFF" opacity="0.25"/>
|
|
||||||
|
|
||||||
<!-- Belly button -->
|
|
||||||
<ellipse cx="60" cy="70" rx="5" ry="6" fill="#B8860B" opacity="0.6"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB |
|
|
@ -1,238 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
# Create a stock avatar from uploaded props and set it as server default.
|
|
||||||
#
|
|
||||||
# Usage: ./stock/avatar/create-stock-avatar.sh [--force|-f] [HOST]
|
|
||||||
#
|
|
||||||
# Prerequisites:
|
|
||||||
# 1. Props must be uploaded first: ./stock/avatar/upload-stockavatars.sh
|
|
||||||
# 2. Dev server must be running: ./run-dev.sh -f
|
|
||||||
#
|
|
||||||
# This script:
|
|
||||||
# 1. Queries existing props by slug to get UUIDs
|
|
||||||
# 2. Creates a server avatar with all emotion slots populated
|
|
||||||
# 3. Sets the avatar as the server default for all gender/age combinations
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
FORCE=""
|
|
||||||
HOST="http://localhost:3001"
|
|
||||||
DB="chattyness"
|
|
||||||
|
|
||||||
for arg in "$@"; do
|
|
||||||
case "$arg" in
|
|
||||||
--force|-f)
|
|
||||||
FORCE="?force=true"
|
|
||||||
;;
|
|
||||||
http://*)
|
|
||||||
HOST="$arg"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "=========================================="
|
|
||||||
echo "Creating Stock Avatar"
|
|
||||||
echo "=========================================="
|
|
||||||
echo "Host: $HOST"
|
|
||||||
echo "Database: $DB"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if server is running
|
|
||||||
echo "Checking server health..."
|
|
||||||
health_response=$(curl -s -o /dev/null -w "%{http_code}" "$HOST/api/admin/health" 2>/dev/null || echo "000")
|
|
||||||
if [ "$health_response" != "200" ]; then
|
|
||||||
echo "ERROR: Server is not responding at $HOST (HTTP $health_response)"
|
|
||||||
echo "Make sure the server is running: ./run-dev.sh -f"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "Server is healthy!"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Query prop UUIDs by slug
|
|
||||||
echo "Querying prop UUIDs..."
|
|
||||||
|
|
||||||
get_prop_id() {
|
|
||||||
local slug="$1"
|
|
||||||
psql -d "$DB" -t -A -c "SELECT id FROM server.props WHERE slug = '$slug'" 2>/dev/null | tr -d '[:space:]'
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get face prop (skin layer)
|
|
||||||
FACE_ID=$(get_prop_id "face")
|
|
||||||
if [ -z "$FACE_ID" ]; then
|
|
||||||
echo "ERROR: Face prop not found. Run upload-stockavatars.sh first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo " face: $FACE_ID"
|
|
||||||
|
|
||||||
# Get emotion props
|
|
||||||
NEUTRAL_ID=$(get_prop_id "neutral")
|
|
||||||
SMILE_ID=$(get_prop_id "smile") # This is "happy" emotion
|
|
||||||
SAD_ID=$(get_prop_id "sad")
|
|
||||||
ANGRY_ID=$(get_prop_id "angry")
|
|
||||||
SURPRISED_ID=$(get_prop_id "surprised")
|
|
||||||
THINKING_ID=$(get_prop_id "thinking")
|
|
||||||
LAUGHING_ID=$(get_prop_id "laughing")
|
|
||||||
CRYING_ID=$(get_prop_id "crying")
|
|
||||||
LOVE_ID=$(get_prop_id "love")
|
|
||||||
CONFUSED_ID=$(get_prop_id "confused")
|
|
||||||
SLEEPING_ID=$(get_prop_id "sleeping")
|
|
||||||
WINK_ID=$(get_prop_id "wink")
|
|
||||||
|
|
||||||
# Validate all emotion props exist
|
|
||||||
missing=""
|
|
||||||
[ -z "$NEUTRAL_ID" ] && missing="$missing neutral"
|
|
||||||
[ -z "$SMILE_ID" ] && missing="$missing smile"
|
|
||||||
[ -z "$SAD_ID" ] && missing="$missing sad"
|
|
||||||
[ -z "$ANGRY_ID" ] && missing="$missing angry"
|
|
||||||
[ -z "$SURPRISED_ID" ] && missing="$missing surprised"
|
|
||||||
[ -z "$THINKING_ID" ] && missing="$missing thinking"
|
|
||||||
[ -z "$LAUGHING_ID" ] && missing="$missing laughing"
|
|
||||||
[ -z "$CRYING_ID" ] && missing="$missing crying"
|
|
||||||
[ -z "$LOVE_ID" ] && missing="$missing love"
|
|
||||||
[ -z "$CONFUSED_ID" ] && missing="$missing confused"
|
|
||||||
[ -z "$SLEEPING_ID" ] && missing="$missing sleeping"
|
|
||||||
[ -z "$WINK_ID" ] && missing="$missing wink"
|
|
||||||
|
|
||||||
if [ -n "$missing" ]; then
|
|
||||||
echo "ERROR: Missing emotion props:$missing"
|
|
||||||
echo "Run upload-stockavatars.sh first."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo " neutral: $NEUTRAL_ID"
|
|
||||||
echo " smile (happy): $SMILE_ID"
|
|
||||||
echo " sad: $SAD_ID"
|
|
||||||
echo " angry: $ANGRY_ID"
|
|
||||||
echo " surprised: $SURPRISED_ID"
|
|
||||||
echo " thinking: $THINKING_ID"
|
|
||||||
echo " laughing: $LAUGHING_ID"
|
|
||||||
echo " crying: $CRYING_ID"
|
|
||||||
echo " love: $LOVE_ID"
|
|
||||||
echo " confused: $CONFUSED_ID"
|
|
||||||
echo " sleeping: $SLEEPING_ID"
|
|
||||||
echo " wink: $WINK_ID"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if avatar already exists
|
|
||||||
existing_avatar=$(psql -d "$DB" -t -A -c "SELECT id FROM server.avatars WHERE slug = 'stock-avatar'" 2>/dev/null | tr -d '[:space:]')
|
|
||||||
|
|
||||||
if [ -n "$existing_avatar" ] && [ -z "$FORCE" ]; then
|
|
||||||
echo "Stock avatar already exists with ID: $existing_avatar"
|
|
||||||
echo "Use --force to recreate it."
|
|
||||||
AVATAR_ID="$existing_avatar"
|
|
||||||
else
|
|
||||||
# Create the avatar via API
|
|
||||||
echo "Creating stock avatar via API..."
|
|
||||||
|
|
||||||
# Build the JSON payload
|
|
||||||
avatar_json=$(cat <<EOF
|
|
||||||
{
|
|
||||||
"name": "Stock Avatar",
|
|
||||||
"slug": "stock-avatar",
|
|
||||||
"description": "Default stock avatar with all emotion faces",
|
|
||||||
"is_public": true,
|
|
||||||
"l_skin_4": "$FACE_ID",
|
|
||||||
"e_neutral_4": "$NEUTRAL_ID",
|
|
||||||
"e_happy_4": "$SMILE_ID",
|
|
||||||
"e_sad_4": "$SAD_ID",
|
|
||||||
"e_angry_4": "$ANGRY_ID",
|
|
||||||
"e_surprised_4": "$SURPRISED_ID",
|
|
||||||
"e_thinking_4": "$THINKING_ID",
|
|
||||||
"e_laughing_4": "$LAUGHING_ID",
|
|
||||||
"e_crying_4": "$CRYING_ID",
|
|
||||||
"e_love_4": "$LOVE_ID",
|
|
||||||
"e_confused_4": "$CONFUSED_ID",
|
|
||||||
"e_sleeping_4": "$SLEEPING_ID",
|
|
||||||
"e_wink_4": "$WINK_ID"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete existing if force mode
|
|
||||||
if [ -n "$existing_avatar" ]; then
|
|
||||||
echo " Deleting existing avatar..."
|
|
||||||
curl -s -X DELETE "$HOST/api/admin/avatars/$existing_avatar" > /dev/null
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create the avatar
|
|
||||||
response=$(curl -s -w "\n%{http_code}" -X POST "$HOST/api/admin/avatars" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$avatar_json")
|
|
||||||
|
|
||||||
http_code=$(echo "$response" | tail -n1)
|
|
||||||
body=$(echo "$response" | sed '$d')
|
|
||||||
|
|
||||||
if [ "$http_code" = "200" ] || [ "$http_code" = "201" ]; then
|
|
||||||
AVATAR_ID=$(echo "$body" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
|
||||||
echo " ✓ Created avatar: $AVATAR_ID"
|
|
||||||
else
|
|
||||||
echo " ✗ Failed to create avatar (HTTP $http_code): $body"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Set as server default for all gender/age combinations
|
|
||||||
echo "Setting stock avatar as server defaults..."
|
|
||||||
|
|
||||||
psql -d "$DB" -c "
|
|
||||||
UPDATE server.config SET
|
|
||||||
default_avatar_neutral_child = '$AVATAR_ID',
|
|
||||||
default_avatar_neutral_adult = '$AVATAR_ID',
|
|
||||||
default_avatar_male_child = '$AVATAR_ID',
|
|
||||||
default_avatar_male_adult = '$AVATAR_ID',
|
|
||||||
default_avatar_female_child = '$AVATAR_ID',
|
|
||||||
default_avatar_female_adult = '$AVATAR_ID',
|
|
||||||
updated_at = now()
|
|
||||||
WHERE id = '00000000-0000-0000-0000-000000000001'
|
|
||||||
" > /dev/null
|
|
||||||
|
|
||||||
echo " ✓ Set all 6 default avatar columns"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Verify
|
|
||||||
echo "=========================================="
|
|
||||||
echo "Verification"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
||||||
# Check avatar slots
|
|
||||||
echo "Avatar emotion slots populated:"
|
|
||||||
psql -d "$DB" -t -c "
|
|
||||||
SELECT
|
|
||||||
CASE WHEN l_skin_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' body (l_skin_4)',
|
|
||||||
CASE WHEN e_neutral_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' neutral',
|
|
||||||
CASE WHEN e_happy_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' happy',
|
|
||||||
CASE WHEN e_sad_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' sad',
|
|
||||||
CASE WHEN e_angry_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' angry',
|
|
||||||
CASE WHEN e_surprised_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' surprised',
|
|
||||||
CASE WHEN e_thinking_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' thinking',
|
|
||||||
CASE WHEN e_laughing_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' laughing',
|
|
||||||
CASE WHEN e_crying_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' crying',
|
|
||||||
CASE WHEN e_love_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' love',
|
|
||||||
CASE WHEN e_confused_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' confused',
|
|
||||||
CASE WHEN e_sleeping_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' sleeping',
|
|
||||||
CASE WHEN e_wink_4 IS NOT NULL THEN '✓' ELSE '✗' END || ' wink'
|
|
||||||
FROM server.avatars WHERE slug = 'stock-avatar'
|
|
||||||
" | tr '|' '\n' | grep -v '^$' | sed 's/^ */ /'
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check server defaults
|
|
||||||
echo "Server config defaults:"
|
|
||||||
psql -d "$DB" -t -c "
|
|
||||||
SELECT
|
|
||||||
CASE WHEN default_avatar_neutral_adult IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_neutral_adult',
|
|
||||||
CASE WHEN default_avatar_neutral_child IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_neutral_child',
|
|
||||||
CASE WHEN default_avatar_male_adult IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_male_adult',
|
|
||||||
CASE WHEN default_avatar_male_child IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_male_child',
|
|
||||||
CASE WHEN default_avatar_female_adult IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_female_adult',
|
|
||||||
CASE WHEN default_avatar_female_child IS NOT NULL THEN '✓' ELSE '✗' END || ' default_avatar_female_child'
|
|
||||||
FROM server.config WHERE id = '00000000-0000-0000-0000-000000000001'
|
|
||||||
" | tr '|' '\n' | grep -v '^$' | sed 's/^ */ /'
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=========================================="
|
|
||||||
echo "Stock avatar setup complete!"
|
|
||||||
echo "Avatar ID: $AVATAR_ID"
|
|
||||||
echo "=========================================="
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<g transform="scale(2.5)">
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--face-primary: #FFCC00;
|
|
||||||
--face-highlight: #FFE566;
|
|
||||||
--face-shadow: #CC9900;
|
|
||||||
}
|
|
||||||
.face-primary { stop-color: var(--face-primary); }
|
|
||||||
.face-highlight { stop-color: var(--face-highlight); }
|
|
||||||
.face-shadow { stop-color: var(--face-shadow); }
|
|
||||||
.face-stroke { stroke: var(--face-shadow); }
|
|
||||||
.bevel-fill { fill: var(--face-primary); }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<defs>
|
|
||||||
<!-- Radial gradient for 3D sphere effect -->
|
|
||||||
<radialGradient id="faceGradient" cx="35%" cy="35%" r="65%">
|
|
||||||
<stop offset="0%" stop-color="#FFFFFF"/>
|
|
||||||
<stop offset="50%" class="face-highlight"/>
|
|
||||||
<stop offset="100%" class="face-primary"/>
|
|
||||||
</radialGradient>
|
|
||||||
|
|
||||||
<!-- Darker gradient for bottom edge (bevel effect) -->
|
|
||||||
<radialGradient id="shadowGradient" cx="50%" cy="0%" r="100%">
|
|
||||||
<stop offset="60%" class="face-primary"/>
|
|
||||||
<stop offset="100%" class="face-shadow"/>
|
|
||||||
</radialGradient>
|
|
||||||
|
|
||||||
<!-- Drop shadow filter -->
|
|
||||||
<filter id="dropShadow" x="-20%" y="-20%" width="140%" height="140%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Main face with gradient and shadow -->
|
|
||||||
<circle cx="24" cy="24" r="20" fill="url(#faceGradient)" class="face-stroke" stroke-width="1.5" filter="url(#dropShadow)"/>
|
|
||||||
|
|
||||||
<!-- Subtle bottom bevel overlay -->
|
|
||||||
<ellipse cx="24" cy="32" rx="18" ry="12" fill="url(#shadowGradient)" opacity="0.3"/>
|
|
||||||
|
|
||||||
<!-- Specular highlight (top-left light reflection) -->
|
|
||||||
<ellipse cx="16" cy="14" rx="6" ry="4" fill="#FFFFFF" opacity="0.6"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,282 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Avatar Renderer</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
|
||||||
background: #1a1a2e;
|
|
||||||
color: #eee;
|
|
||||||
margin: 0;
|
|
||||||
padding: 2rem;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 2rem;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group label {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type="color"] {
|
|
||||||
width: 60px;
|
|
||||||
height: 40px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-preview {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-grid-3x3 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, 120px);
|
|
||||||
grid-template-rows: repeat(3, 120px);
|
|
||||||
gap: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-grid-3x3 .cell {
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.avatar-grid-3x3 .cell > img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-container {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-container img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-grid {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 1.5rem;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-card {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: #16213e;
|
|
||||||
border-radius: 12px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: transform 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-card.selected {
|
|
||||||
outline: 3px solid #4cc9f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-card .avatar-small {
|
|
||||||
position: relative;
|
|
||||||
width: 120px;
|
|
||||||
height: 120px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-card .avatar-small img {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-card span {
|
|
||||||
font-size: 0.875rem;
|
|
||||||
text-transform: capitalize;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Avatar Renderer</h1>
|
|
||||||
|
|
||||||
<div class="controls">
|
|
||||||
<div class="control-group">
|
|
||||||
<label for="primaryColor">Face Color</label>
|
|
||||||
<input type="color" id="primaryColor" value="#FFCC00">
|
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<label for="highlightColor">Highlight</label>
|
|
||||||
<input type="color" id="highlightColor" value="#FFE566">
|
|
||||||
</div>
|
|
||||||
<div class="control-group">
|
|
||||||
<label for="shadowColor">Shadow</label>
|
|
||||||
<input type="color" id="shadowColor" value="#CC9900">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="avatar-preview">
|
|
||||||
<div class="avatar-grid-3x3">
|
|
||||||
<div class="cell"></div>
|
|
||||||
<div class="cell"></div>
|
|
||||||
<div class="cell"></div>
|
|
||||||
<div class="cell"></div>
|
|
||||||
<div class="cell center">
|
|
||||||
<div class="avatar-container" id="mainAvatar">
|
|
||||||
<img src="face.svg" alt="Face base" class="face-layer">
|
|
||||||
<img src="smile.svg" alt="Expression" class="expression-layer">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="cell"></div>
|
|
||||||
<div class="cell"></div>
|
|
||||||
<div class="cell">
|
|
||||||
<img src="body-torso.svg" alt="Torso">
|
|
||||||
</div>
|
|
||||||
<div class="cell"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 style="text-align: center; margin-bottom: 1rem;">Expressions</h2>
|
|
||||||
|
|
||||||
<div class="avatar-grid" id="avatarGrid"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const expressions = [
|
|
||||||
'smile',
|
|
||||||
'neutral',
|
|
||||||
'angry',
|
|
||||||
'sad',
|
|
||||||
'laughing',
|
|
||||||
'surprised',
|
|
||||||
'confused',
|
|
||||||
'love',
|
|
||||||
'wink',
|
|
||||||
'thinking',
|
|
||||||
'sleeping',
|
|
||||||
'crying'
|
|
||||||
];
|
|
||||||
|
|
||||||
const avatarGrid = document.getElementById('avatarGrid');
|
|
||||||
const mainAvatar = document.getElementById('mainAvatar');
|
|
||||||
const primaryColorInput = document.getElementById('primaryColor');
|
|
||||||
const highlightColorInput = document.getElementById('highlightColor');
|
|
||||||
const shadowColorInput = document.getElementById('shadowColor');
|
|
||||||
|
|
||||||
let currentExpression = 'smile';
|
|
||||||
|
|
||||||
// Create avatar cards
|
|
||||||
expressions.forEach(expression => {
|
|
||||||
const card = document.createElement('div');
|
|
||||||
card.className = 'avatar-card' + (expression === 'smile' ? ' selected' : '');
|
|
||||||
card.dataset.expression = expression;
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="avatar-small">
|
|
||||||
<img src="face.svg" alt="Face base" class="face-layer">
|
|
||||||
<img src="${expression}.svg" alt="${expression}" class="expression-layer">
|
|
||||||
</div>
|
|
||||||
<span>${expression}</span>
|
|
||||||
`;
|
|
||||||
card.addEventListener('click', () => selectExpression(expression));
|
|
||||||
avatarGrid.appendChild(card);
|
|
||||||
});
|
|
||||||
|
|
||||||
function selectExpression(expression) {
|
|
||||||
currentExpression = expression;
|
|
||||||
|
|
||||||
// Update main preview
|
|
||||||
mainAvatar.querySelector('.expression-layer').src = `${expression}.svg`;
|
|
||||||
|
|
||||||
// Update selected state
|
|
||||||
document.querySelectorAll('.avatar-card').forEach(card => {
|
|
||||||
card.classList.toggle('selected', card.dataset.expression === expression);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Color manipulation
|
|
||||||
async function updateColors() {
|
|
||||||
const primary = primaryColorInput.value;
|
|
||||||
const highlight = highlightColorInput.value;
|
|
||||||
const shadow = shadowColorInput.value;
|
|
||||||
|
|
||||||
// Fetch and modify the face SVG
|
|
||||||
const response = await fetch('face.svg');
|
|
||||||
const svgText = await response.text();
|
|
||||||
|
|
||||||
// Replace the CSS variable defaults with our colors
|
|
||||||
const modifiedSvg = svgText
|
|
||||||
.replace(/--face-primary:\s*#[A-Fa-f0-9]+/g, `--face-primary: ${primary}`)
|
|
||||||
.replace(/--face-highlight:\s*#[A-Fa-f0-9]+/g, `--face-highlight: ${highlight}`)
|
|
||||||
.replace(/--face-shadow:\s*#[A-Fa-f0-9]+/g, `--face-shadow: ${shadow}`)
|
|
||||||
// Also update the hardcoded gradient colors in defs
|
|
||||||
.replace(/stop-color="#FFFFFF"/g, 'stop-color="#FFFFFF"') // Keep white
|
|
||||||
.replace(/<stop offset="50%" class="face-highlight"\/>/g, `<stop offset="50%" stop-color="${highlight}"/>`)
|
|
||||||
.replace(/<stop offset="100%" class="face-primary"\/>/g, `<stop offset="100%" stop-color="${primary}"/>`)
|
|
||||||
.replace(/<stop offset="60%" class="face-primary"\/>/g, `<stop offset="60%" stop-color="${primary}"/>`)
|
|
||||||
.replace(/<stop offset="100%" class="face-shadow"\/>/g, `<stop offset="100%" stop-color="${shadow}"/>`);
|
|
||||||
|
|
||||||
// Create blob URL for modified SVG
|
|
||||||
const blob = new Blob([modifiedSvg], { type: 'image/svg+xml' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
|
|
||||||
// Update all face layers
|
|
||||||
document.querySelectorAll('.face-layer').forEach(img => {
|
|
||||||
// Revoke old blob URL if exists
|
|
||||||
if (img.dataset.blobUrl) {
|
|
||||||
URL.revokeObjectURL(img.dataset.blobUrl);
|
|
||||||
}
|
|
||||||
img.src = url;
|
|
||||||
img.dataset.blobUrl = url;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
primaryColorInput.addEventListener('input', updateColors);
|
|
||||||
highlightColorInput.addEventListener('input', updateColors);
|
|
||||||
shadowColorInput.addEventListener('input', updateColors);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -429,16 +429,6 @@
|
||||||
<h3>Good Pol</h3>
|
<h3>Good Pol</h3>
|
||||||
<div class="prop-items" id="goodpol-props" role="group" aria-label="Good Pol props"></div>
|
<div class="prop-items" id="goodpol-props" role="group" aria-label="Good Pol props"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="prop-category">
|
|
||||||
<h3>Screens</h3>
|
|
||||||
<div class="prop-items" id="screen-props" role="group" aria-label="Screen props"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="prop-category">
|
|
||||||
<h3>Keyboards</h3>
|
|
||||||
<div class="prop-items" id="keyboard-props" role="group" aria-label="Keyboard props"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|
@ -558,9 +548,7 @@
|
||||||
soda: ['cola', 'lemonlime', 'orange', 'grape', 'rootbeer'],
|
soda: ['cola', 'lemonlime', 'orange', 'grape', 'rootbeer'],
|
||||||
tea: ['iced', 'pot', 'cup', 'cup-empty', 'bag'],
|
tea: ['iced', 'pot', 'cup', 'cup-empty', 'bag'],
|
||||||
misc: ['iou', 'signed-dollar', 'thankyou', 'yousuck'],
|
misc: ['iou', 'signed-dollar', 'thankyou', 'yousuck'],
|
||||||
goodpol: ['cccp', 'china', 'palestine'],
|
goodpol: ['cccp', 'china', 'palestine']
|
||||||
screen: ['projector', 'projector-with-stand'],
|
|
||||||
keyboard: ['media']
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Flags
|
// Flags
|
||||||
|
|
@ -650,8 +638,6 @@
|
||||||
const teaContainer = document.getElementById('tea-props');
|
const teaContainer = document.getElementById('tea-props');
|
||||||
const miscContainer = document.getElementById('misc-props');
|
const miscContainer = document.getElementById('misc-props');
|
||||||
const goodpolContainer = document.getElementById('goodpol-props');
|
const goodpolContainer = document.getElementById('goodpol-props');
|
||||||
const screenContainer = document.getElementById('screen-props');
|
|
||||||
const keyboardContainer = document.getElementById('keyboard-props');
|
|
||||||
|
|
||||||
for (const name of props.hookah) {
|
for (const name of props.hookah) {
|
||||||
await loadPropPreview('hookah', name, hookahContainer);
|
await loadPropPreview('hookah', name, hookahContainer);
|
||||||
|
|
@ -671,12 +657,6 @@
|
||||||
for (const name of props.goodpol) {
|
for (const name of props.goodpol) {
|
||||||
await loadPropPreview('goodpol', name, goodpolContainer);
|
await loadPropPreview('goodpol', name, goodpolContainer);
|
||||||
}
|
}
|
||||||
for (const name of props.screen) {
|
|
||||||
await loadPropPreview('screen', name, screenContainer);
|
|
||||||
}
|
|
||||||
for (const name of props.keyboard) {
|
|
||||||
await loadPropPreview('keyboard', name, keyboardContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select first prop by default
|
// Select first prop by default
|
||||||
const firstCard = document.querySelector('#props-tab .prop-card');
|
const firstCard = document.querySelector('#props-tab .prop-card');
|
||||||
|
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="keyboardBody" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color="#3a3a3a"/>
|
|
||||||
<stop offset="100%" stop-color="#2a2a2a"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="keyTop" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color="#4a4a4a"/>
|
|
||||||
<stop offset="100%" stop-color="#3a3a3a"/>
|
|
||||||
</linearGradient>
|
|
||||||
<filter id="keyShadow" x="-10%" y="-10%" width="120%" height="130%">
|
|
||||||
<feDropShadow dx="0" dy="1" stdDeviation="0.5" flood-color="#000000" flood-opacity="0.4"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Keyboard body -->
|
|
||||||
<rect x="8" y="40" width="104" height="50" rx="4" fill="url(#keyboardBody)"/>
|
|
||||||
<rect x="8" y="40" width="104" height="50" rx="4" fill="none" stroke="#222" stroke-width="1"/>
|
|
||||||
|
|
||||||
<!-- Top row: Play, Pause, Stop -->
|
|
||||||
<!-- Play key -->
|
|
||||||
<rect x="14" y="46" width="22" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
|
|
||||||
<polygon points="22,50 22,58 29,54" fill="#4CAF50"/>
|
|
||||||
|
|
||||||
<!-- Pause key -->
|
|
||||||
<rect x="40" y="46" width="22" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
|
|
||||||
<rect x="47" y="50" width="3" height="8" fill="#FFC107"/>
|
|
||||||
<rect x="52" y="50" width="3" height="8" fill="#FFC107"/>
|
|
||||||
|
|
||||||
<!-- Stop key -->
|
|
||||||
<rect x="66" y="46" width="22" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
|
|
||||||
<rect x="72" y="50" width="10" height="8" fill="#F44336"/>
|
|
||||||
|
|
||||||
<!-- Mute key -->
|
|
||||||
<rect x="92" y="46" width="14" height="16" rx="2" fill="url(#keyTop)" filter="url(#keyShadow)"/>
|
|
||||||
<!-- Speaker icon -->
|
|
||||||
<polygon points="95,52 97,52 100,49 100,59 97,56 95,56" fill="#fff"/>
|
|
||||||
<!-- X for mute -->
|
|
||||||
<line x1="101" y1="51" x2="104" y2="57" stroke="#F44336" stroke-width="1.5" stroke-linecap="round"/>
|
|
||||||
<line x1="104" y1="51" x2="101" y2="57" stroke="#F44336" stroke-width="1.5" stroke-linecap="round"/>
|
|
||||||
|
|
||||||
<!-- URL bar -->
|
|
||||||
<rect x="14" y="66" width="92" height="18" rx="2" fill="#fff" filter="url(#keyShadow)"/>
|
|
||||||
<rect x="14" y="66" width="92" height="18" rx="2" fill="none" stroke="#888" stroke-width="0.5"/>
|
|
||||||
<text x="18" y="78" font-family="monospace" font-size="7" fill="#4CAF50">https://</text>
|
|
||||||
<line x1="52" y1="69" x2="52" y2="81" stroke="#333" stroke-width="0.5"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
|
@ -1,44 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="screenSurface" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color="#FFFFFF"/>
|
|
||||||
<stop offset="100%" stop-color="#F0F0F0"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="caseGrad" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color="#4A4A4A"/>
|
|
||||||
<stop offset="100%" stop-color="#2A2A2A"/>
|
|
||||||
</linearGradient>
|
|
||||||
<filter id="screenShadow" x="-5%" y="-5%" width="110%" height="110%">
|
|
||||||
<feDropShadow dx="1" dy="2" stdDeviation="2" flood-color="#000000" flood-opacity="0.3"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Mounting bracket / case at top -->
|
|
||||||
<rect x="10" y="8" width="100" height="8" rx="2" fill="url(#caseGrad)"/>
|
|
||||||
|
|
||||||
<!-- Screen surface - 16:9 aspect ratio (96x54) -->
|
|
||||||
<rect x="12" y="18" width="96" height="54" fill="url(#screenSurface)" filter="url(#screenShadow)"/>
|
|
||||||
|
|
||||||
<!-- Screen border/frame -->
|
|
||||||
<rect x="12" y="18" width="96" height="54" fill="none" stroke="#333" stroke-width="1.5"/>
|
|
||||||
|
|
||||||
<!-- Bottom weight bar -->
|
|
||||||
<rect x="12" y="70" width="96" height="4" rx="1" fill="#3A3A3A"/>
|
|
||||||
|
|
||||||
<!-- Pull tab -->
|
|
||||||
<rect x="54" y="74" width="12" height="6" rx="1" fill="#555"/>
|
|
||||||
<circle cx="60" cy="80" r="3" fill="#666"/>
|
|
||||||
<circle cx="60" cy="80" r="1.5" fill="#444"/>
|
|
||||||
|
|
||||||
<!-- Tripod stand -->
|
|
||||||
<rect x="58" y="84" width="4" height="20" fill="#333"/>
|
|
||||||
<!-- Tripod legs -->
|
|
||||||
<line x1="60" y1="104" x2="30" y2="115" stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
|
||||||
<line x1="60" y1="104" x2="90" y2="115" stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
|
||||||
<line x1="60" y1="104" x2="60" y2="116" stroke="#333" stroke-width="3" stroke-linecap="round"/>
|
|
||||||
|
|
||||||
<!-- Rubber feet -->
|
|
||||||
<circle cx="30" cy="115" r="2" fill="#222"/>
|
|
||||||
<circle cx="90" cy="115" r="2" fill="#222"/>
|
|
||||||
<circle cx="60" cy="116" r="2" fill="#222"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.9 KiB |
|
|
@ -1,24 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" width="120" height="120">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="ustScreen" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stop-color="#FAFAFA"/>
|
|
||||||
<stop offset="50%" stop-color="#FFFFFF"/>
|
|
||||||
<stop offset="100%" stop-color="#F5F5F5"/>
|
|
||||||
</linearGradient>
|
|
||||||
<filter id="ustShadow" x="-5%" y="-5%" width="110%" height="120%">
|
|
||||||
<feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#000000" flood-opacity="0.25"/>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<!-- Thin black frame - 16:9 ratio (106x60) centered -->
|
|
||||||
<rect x="7" y="30" width="106" height="60" rx="1" fill="#1a1a1a" filter="url(#ustShadow)"/>
|
|
||||||
|
|
||||||
<!-- Screen surface - 16:9 with thin bezel -->
|
|
||||||
<rect x="9" y="32" width="102" height="56" fill="url(#ustScreen)"/>
|
|
||||||
|
|
||||||
<!-- Subtle inner shadow on screen edges -->
|
|
||||||
<rect x="9" y="32" width="102" height="56" fill="none" stroke="#E0E0E0" stroke-width="0.5"/>
|
|
||||||
|
|
||||||
<!-- Frame edge highlight (top) -->
|
|
||||||
<line x1="8" y1="30" x2="112" y2="30" stroke="#333" stroke-width="0.5"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1 KiB |
|
|
@ -88,12 +88,6 @@ get_tags() {
|
||||||
misc)
|
misc)
|
||||||
echo '["misc", "droppable"]'
|
echo '["misc", "droppable"]'
|
||||||
;;
|
;;
|
||||||
screen)
|
|
||||||
echo '["screen", "projector", "droppable"]'
|
|
||||||
;;
|
|
||||||
keyboard)
|
|
||||||
echo '["keyboard", "media", "droppable"]'
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
echo '["prop", "droppable"]'
|
echo '["prop", "droppable"]'
|
||||||
;;
|
;;
|
||||||
|
|
|
||||||