From 154efcb6df2f503347c4c224dfd3b063f8785d05 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 19 Nov 2024 22:48:36 -0500 Subject: [PATCH] Set up a GM control panel that can control the currently selected background --- Cargo.lock | 1 + visions/server/Cargo.toml | 1 + visions/server/src/core.rs | 8 +++-- visions/server/src/handlers.rs | 22 ++++++++++---- visions/server/src/main.rs | 29 +++++++++++++------ visions/ui/src/App.tsx | 4 +-- visions/ui/src/client.ts | 6 ++++ .../GmPlayingField/GmPlayingField.css | 0 .../GmPlayingField/GmPlayingField.tsx | 29 ------------------- .../ui/src/components/Thumbnail/Thumbnail.tsx | 13 ++++++--- visions/ui/src/views/GmView/GmView.css | 4 +++ visions/ui/src/views/GmView/GmView.tsx | 28 ++++++++++++++++++ 12 files changed, 93 insertions(+), 52 deletions(-) delete mode 100644 visions/ui/src/components/GmPlayingField/GmPlayingField.css delete mode 100644 visions/ui/src/components/GmPlayingField/GmPlayingField.tsx create mode 100644 visions/ui/src/views/GmView/GmView.css create mode 100644 visions/ui/src/views/GmView/GmView.tsx diff --git a/Cargo.lock b/Cargo.lock index a99ead4..e982ac4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4866,6 +4866,7 @@ dependencies = [ "tokio", "tokio-stream", "typeshare", + "urlencoding", "uuid 1.11.0", "warp", ] diff --git a/visions/server/Cargo.toml b/visions/server/Cargo.toml index f00e67c..4c2c69f 100644 --- a/visions/server/Cargo.toml +++ b/visions/server/Cargo.toml @@ -18,3 +18,4 @@ uuid = { version = "1.11.0", features = ["v4"] } futures = "0.3.31" tokio-stream = "0.1.16" typeshare = "1.0.4" +urlencoding = "2.1.3" diff --git a/visions/server/src/core.rs b/visions/server/src/core.rs index 944bc82..a399112 100644 --- a/visions/server/src/core.rs +++ b/visions/server/src/core.rs @@ -6,6 +6,7 @@ use std::{ }; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use urlencoding::decode; use uuid::Uuid; use crate::types::{AppError, Message, Tabletop, RGB}; @@ -68,10 +69,11 @@ impl Core { } pub fn get_file(&self, file_name: String) -> Vec { - let mut full_path = self.0.read().unwrap().image_base.clone(); - full_path.push(&file_name); + let file_name = decode(&file_name).expect("UTF-8"); + + let mut full_path = self.0.read().unwrap().image_base.clone(); + full_path.push(file_name.to_string()); - println!("path: {:?}", full_path); let mut content: Vec = Vec::new(); let mut file = std::fs::File::open(&full_path).unwrap(); diff --git a/visions/server/src/handlers.rs b/visions/server/src/handlers.rs index 83a1386..90ab6cf 100644 --- a/visions/server/src/handlers.rs +++ b/visions/server/src/handlers.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use tokio_stream::wrappers::UnboundedReceiverStream; use warp::{http::Response, reply::Reply, ws::Message}; -use crate::{core::Core}; +use crate::core::Core; /* pub async fn handle_auth( @@ -82,7 +82,7 @@ pub async fn handle_connect_websocket( ws: warp::ws::Ws, client_id: String, ) -> impl Reply { - println!("handle_connect_websocket: {}", client_id); + // println!("handle_connect_websocket: {}", client_id); ws.on_upgrade(move |socket| { let core = core.clone(); async move { @@ -92,9 +92,10 @@ pub async fn handle_connect_websocket( tokio::task::spawn(async move { let tabletop = core.0.read().unwrap().tabletop.clone(); let _ = ws_sender - .send(Message::text(serde_json::to_string( - &crate::types::Message::UpdateTabletop(tabletop), - ).unwrap())) + .send(Message::text( + serde_json::to_string(&crate::types::Message::UpdateTabletop(tabletop)) + .unwrap(), + )) .await; while let Some(msg) = receiver.recv().await { println!("Relaying message: {:?}", msg); @@ -107,3 +108,14 @@ pub async fn handle_connect_websocket( } }) } + +pub async fn handle_set_background_image(core: Core, image_name: String) -> impl Reply { + let _ = core.set_background_image(image_name); + + Response::builder() + .header("Access-Control-Allow-Origin", "*") + .header("Access-Control-Allow-Methods", "*") + .header("Content-Type", "application/json") + .body("") + .unwrap() +} diff --git a/visions/server/src/main.rs b/visions/server/src/main.rs index b783d29..fc1e8b0 100644 --- a/visions/server/src/main.rs +++ b/visions/server/src/main.rs @@ -1,7 +1,6 @@ use authdb::AuthError; use handlers::{ - handle_available_images, handle_connect_websocket, handle_file, - handle_register_client, handle_unregister_client, RegisterRequest, + handle_available_images, handle_connect_websocket, handle_file, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest }; use std::{ convert::Infallible, @@ -81,6 +80,7 @@ fn route_echo_authenticated( */ async fn handle_rejection(err: warp::Rejection) -> Result { + println!("handle_rejection: {:?}", err); if let Some(Unauthorized) = err.find() { Ok(warp::reply::with_status( "".to_owned(), @@ -97,6 +97,7 @@ async fn handle_rejection(err: warp::Rejection) -> Result { createBrowserRouter([ { path: "/gm", - element: + element: websocketUrl ? :
}, { path: "/", diff --git a/visions/ui/src/client.ts b/visions/ui/src/client.ts index 52759a9..486eec2 100644 --- a/visions/ui/src/client.ts +++ b/visions/ui/src/client.ts @@ -38,4 +38,10 @@ export class Client { url.pathname = `/api/v1/image`; return fetch(url).then((response) => response.json()); } + + async setBackgroundImage(name: string) { + const url = new URL(this.base); + url.pathname = `/api/v1/tabletop/bg_image`; + return fetch(url, { method: 'PUT', headers: [['Content-Type', 'application/json']], body: JSON.stringify(name) }); + } } diff --git a/visions/ui/src/components/GmPlayingField/GmPlayingField.css b/visions/ui/src/components/GmPlayingField/GmPlayingField.css deleted file mode 100644 index e69de29..0000000 diff --git a/visions/ui/src/components/GmPlayingField/GmPlayingField.tsx b/visions/ui/src/components/GmPlayingField/GmPlayingField.tsx deleted file mode 100644 index 0ae8fcd..0000000 --- a/visions/ui/src/components/GmPlayingField/GmPlayingField.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Client, PlayingField } from '../../client'; -import { ThumbnailComponent } from '../Thumbnail/Thumbnail'; -import './GmPlayingField.css'; - -interface GmPlayingFieldProps { - client: Client -} - -export const GmPlayingFieldComponent = ({ client }: GmPlayingFieldProps) => { - const [field, setField] = useState(undefined); - useEffect(() => { - client.playingField().then((field) => setField(field)); - }, [client]); - - const [images, setImages] = useState([]); - useEffect(() => { - client.availableImages().then((images) => setImages(images)); - }, [client]); - - const backgroundUrl = field && client.imageUrl(field.backgroundImage); - return (
-
- {images.map((imageName) => )} -
-
{backgroundUrl && playing field}
-
) -} - diff --git a/visions/ui/src/components/Thumbnail/Thumbnail.tsx b/visions/ui/src/components/Thumbnail/Thumbnail.tsx index dfa51b0..c63f4dd 100644 --- a/visions/ui/src/components/Thumbnail/Thumbnail.tsx +++ b/visions/ui/src/components/Thumbnail/Thumbnail.tsx @@ -3,9 +3,14 @@ import { Client } from '../../client'; import './Thumbnail.css'; interface ThumbnailProps { - client: Client - imageId: string + id: string; + url: URL; + onclick?: () => void; } -export const ThumbnailComponent = ({ client, imageId }: ThumbnailProps) => - (
) +export const ThumbnailComponent = ({ id, url, onclick }: ThumbnailProps) => { + const clickHandler = () => { + if (onclick) { onclick(); } + } + return (
) +} diff --git a/visions/ui/src/views/GmView/GmView.css b/visions/ui/src/views/GmView/GmView.css new file mode 100644 index 0000000..f3b6a10 --- /dev/null +++ b/visions/ui/src/views/GmView/GmView.css @@ -0,0 +1,4 @@ +.gm-view { + display: flex; + width: 100%; +} diff --git a/visions/ui/src/views/GmView/GmView.tsx b/visions/ui/src/views/GmView/GmView.tsx new file mode 100644 index 0000000..9b8550f --- /dev/null +++ b/visions/ui/src/views/GmView/GmView.tsx @@ -0,0 +1,28 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Client, PlayingField } from '../../client'; +import { TabletopElement } from '../../components/Tabletop/Tabletop'; +import { ThumbnailComponent } from '../../components/Thumbnail/Thumbnail'; +import { WebsocketContext } from '../../components/WebsocketProvider'; +import './GmView.css'; + +interface GmViewProps { + client: Client +} + +export const GmView = ({ client }: GmViewProps) => { + const { tabletop } = useContext(WebsocketContext); + + const [images, setImages] = useState([]); + useEffect(() => { + client.availableImages().then((images) => setImages(images)); + }, [client]); + + const backgroundUrl = tabletop.backgroundImage ? client.imageUrl(tabletop.backgroundImage) : undefined; + return (
+
+ {images.map((imageName) => { client.setBackgroundImage(imageName); }} />)} +
+ +
) +} +