Fix prop renders

* Incorporate prop scaling
* Props now render to a canvas
This commit is contained in:
Evan Carroll 2026-01-23 16:00:47 -06:00
parent af89394df1
commit a2841c413d
21 changed files with 942 additions and 353 deletions

View file

@ -9,6 +9,8 @@ pub mod config;
#[cfg(feature = "ssr")]
pub mod dashboard;
#[cfg(feature = "ssr")]
pub mod loose_props;
#[cfg(feature = "ssr")]
pub mod props;
#[cfg(feature = "ssr")]
pub mod realms;

View file

@ -0,0 +1,75 @@
//! 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;
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?
.ok_or_else(|| AppError::NotFound("Loose prop not found or has expired".to_string()))?;
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))
}

View file

@ -5,7 +5,7 @@ use axum::{
routing::{delete, get, post, put},
};
use super::{auth, avatars, config, dashboard, props, realms, scenes, spots, staff, users};
use super::{auth, avatars, config, dashboard, loose_props, props, realms, scenes, spots, staff, users};
use crate::app::AdminAppState;
/// Create the admin API router.
@ -85,6 +85,19 @@ pub fn admin_api_router() -> Router<AdminAppState> {
"/props/{prop_id}",
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
.route(
"/avatars",

View file

@ -239,6 +239,8 @@ pub struct PropDetail {
pub default_layer: Option<String>,
/// Grid position (0-8): top row 0,1,2 / middle 3,4,5 / bottom 6,7,8
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_transferable: bool,
pub is_portable: bool,

View file

@ -95,6 +95,9 @@ fn PropDetailView(prop: PropDetail) -> impl IntoView {
None => "Not set".to_string(),
}}
</DetailItem>
<DetailItem label="Default Scale">
{format!("{}%", (prop.default_scale * 100.0) as i32)}
</DetailItem>
<DetailItem label="Status">
{if prop.is_active {
view! { <span class="status-badge status-active">"Active"</span> }.into_any()