use crate::db::{self, AppDatabase}; use serde::{Deserialize, Serialize}; use specta::Type; use tauri::{AppHandle, State}; use tauri_specta::Event; const SCENE_CONFIGURATION_ID: i64 = 1; const MIN_PUPPET_SCALE: f64 = 0.5; const MAX_PUPPET_SCALE: f64 = 2.0; const MIN_PUPPET_OPACITY: f64 = 0.1; const MAX_PUPPET_OPACITY: f64 = 1.0; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Type, sqlx::FromRow)] #[serde(rename_all = "camelCase")] pub struct SceneConfiguration { pub puppet_scale: f64, pub puppet_opacity: f64, } #[derive(Debug, Clone, Serialize, Deserialize, Type, Event)] #[serde(rename_all = "camelCase")] pub struct SceneConfigurationChanged { pub configuration: SceneConfiguration, } async fn get(database: &AppDatabase) -> Result { sqlx::query_as::<_, SceneConfiguration>( "SELECT puppet_scale, puppet_opacity FROM scene_configuration WHERE id = ?1", ) .bind(SCENE_CONFIGURATION_ID) .fetch_one(database.pool()) .await } async fn update( database: &AppDatabase, configuration: SceneConfiguration, ) -> Result { if !configuration.puppet_scale.is_finite() || !(MIN_PUPPET_SCALE..=MAX_PUPPET_SCALE).contains(&configuration.puppet_scale) { return Err(format!( "Puppet scale must be between {MIN_PUPPET_SCALE} and {MAX_PUPPET_SCALE}." )); } if !configuration.puppet_opacity.is_finite() || !(MIN_PUPPET_OPACITY..=MAX_PUPPET_OPACITY).contains(&configuration.puppet_opacity) { return Err(format!( "Puppet opacity must be between {MIN_PUPPET_OPACITY} and {MAX_PUPPET_OPACITY}." )); } sqlx::query( "UPDATE scene_configuration SET puppet_scale = ?1, puppet_opacity = ?2 WHERE id = ?3", ) .bind(configuration.puppet_scale) .bind(configuration.puppet_opacity) .bind(SCENE_CONFIGURATION_ID) .execute(database.pool()) .await .map_err(db::command_error)?; get(database).await.map_err(db::command_error) } fn emit_changed(handle: &AppHandle, configuration: SceneConfiguration) -> Result<(), String> { SceneConfigurationChanged { configuration } .emit(handle) .map_err(db::command_error) } #[tauri::command] #[specta::specta] pub async fn get_scene_configuration( handle: AppHandle, database: State<'_, AppDatabase>, ) -> Result { let configuration = get(&database).await.map_err(db::command_error)?; emit_changed(&handle, configuration.clone())?; Ok(configuration) } #[tauri::command] #[specta::specta] pub async fn update_scene_configuration( handle: AppHandle, database: State<'_, AppDatabase>, configuration: SceneConfiguration, ) -> Result { let configuration = update(&database, configuration).await?; emit_changed(&handle, configuration.clone())?; Ok(configuration) } #[cfg(test)] mod tests { use sqlx::sqlite::SqlitePoolOptions; use super::*; async fn database() -> AppDatabase { let pool = SqlitePoolOptions::new() .max_connections(1) .connect("sqlite::memory:") .await .expect("connect to in-memory SQLite"); sqlx::migrate!("./migrations") .run(&pool) .await .expect("run database migrations"); AppDatabase::new(pool) } #[tokio::test] async fn scene_configuration_defaults_preserve_the_current_appearance() { let database = database().await; assert_eq!( get(&database).await.unwrap(), SceneConfiguration { puppet_scale: 1.0, puppet_opacity: 1.0, } ); } #[tokio::test] async fn scene_configuration_is_persisted_and_range_checked() { let database = database().await; let configuration = update( &database, SceneConfiguration { puppet_scale: 1.5, puppet_opacity: 0.4, }, ) .await .expect("update scene configuration"); assert_eq!(configuration.puppet_scale, 1.5); assert_eq!(configuration.puppet_opacity, 0.4); assert_eq!(get(&database).await.unwrap(), configuration); let error = update( &database, SceneConfiguration { puppet_scale: 2.1, puppet_opacity: 0.4, }, ) .await .expect_err("reject an out-of-range puppet scale"); assert_eq!(error, "Puppet scale must be between 0.5 and 2."); assert_eq!(get(&database).await.unwrap().puppet_scale, 1.5); let error = update( &database, SceneConfiguration { puppet_scale: 1.5, puppet_opacity: 0.05, }, ) .await .expect_err("reject an out-of-range puppet opacity"); assert_eq!(error, "Puppet opacity must be between 0.1 and 1."); assert_eq!(get(&database).await.unwrap().puppet_opacity, 0.4); } }