Set up a tabletop view for both the GM and the player #260

Merged
savanni merged 15 commits from visions-playfield into main 2024-11-20 04:06:13 +00:00
12 changed files with 93 additions and 52 deletions
Showing only changes of commit 154efcb6df - Show all commits

1
Cargo.lock generated
View File

@ -4866,6 +4866,7 @@ dependencies = [
"tokio",
"tokio-stream",
"typeshare",
"urlencoding",
"uuid 1.11.0",
"warp",
]

View File

@ -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"

View File

@ -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<u8> {
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<u8> = Vec::new();
let mut file = std::fs::File::open(&full_path).unwrap();

View File

@ -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()
}

View File

@ -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<impl Reply, Infallible> {
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<impl Reply, Infallible
#[tokio::main]
pub async fn main() {
let core = core::Core::new();
let log = warp::log("visions::api");
let route_image = warp::path!("api" / "v1" / "image" / String)
.and(warp::get())
@ -132,23 +133,33 @@ pub async fn main() {
move |ws, client_id| handle_connect_websocket(core.clone(), ws, client_id)
});
let route_set_bg_image_options = warp::path!("api" / "v1" / "tabletop" / "bg_image")
.and(warp::options())
.map({
move || {
Response::builder()
.header("Access-Control-Allow-Origin", "*")
.header("Access-Control-Allow-Methods", "PUT")
.header("Access-Control-Allow-Headers", "content-type")
.header("Content-Type", "application/json")
.body("")
.unwrap()
}
});
let route_set_bg_image = warp::path!("api" / "v1" / "tabletop" / "bg_image")
.and(warp::put())
.and(warp::body::json())
.map({
.then({
let core = core.clone();
move |body| {
println!("background_image: {}", body);
let _ = core.set_background_image(body);
warp::reply()
}
});
move |body| handle_set_background_image(core.clone(), body)
}).with(log);
let filter = route_register_client
.or(route_unregister_client)
.or(route_websocket)
.or(route_image)
.or(route_available_images)
.or(route_set_bg_image_options)
.or(route_set_bg_image)
.recover(handle_rejection);

View File

@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import './App.css';
import { Client } from './client';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { GmPlayingFieldComponent } from './components/GmPlayingField/GmPlayingField';
import { GmView } from './views/GmView/GmView';
import { WebsocketProvider } from './components/WebsocketProvider';
import { PlayerView } from './views/PlayerView/PlayerView';
@ -22,7 +22,7 @@ const App = ({ client }: AppProps) => {
createBrowserRouter([
{
path: "/gm",
element: <GmPlayingFieldComponent client={client} />
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div>
},
{
path: "/",

View File

@ -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) });
}
}

View File

@ -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<PlayingField | undefined>(undefined);
useEffect(() => {
client.playingField().then((field) => setField(field));
}, [client]);
const [images, setImages] = useState<string[]>([]);
useEffect(() => {
client.availableImages().then((images) => setImages(images));
}, [client]);
const backgroundUrl = field && client.imageUrl(field.backgroundImage);
return (<div className="playing-field">
<div>
{images.map((imageName) => <ThumbnailComponent client={client} imageId={imageName} />)}
</div>
<div className="playing-field__background"> {backgroundUrl && <img src={backgroundUrl.toString()} alt="playing field" />} </div>
</div>)
}

View File

@ -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) =>
(<div className="thumbnail"> <img src={client.imageUrl(imageId).toString()} /> </div>)
export const ThumbnailComponent = ({ id, url, onclick }: ThumbnailProps) => {
const clickHandler = () => {
if (onclick) { onclick(); }
}
return (<div id={id} className="thumbnail" onClick={clickHandler}> <img src={url.toString()} /> </div>)
}

View File

@ -0,0 +1,4 @@
.gm-view {
display: flex;
width: 100%;
}

View File

@ -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<string[]>([]);
useEffect(() => {
client.availableImages().then((images) => setImages(images));
}, [client]);
const backgroundUrl = tabletop.backgroundImage ? client.imageUrl(tabletop.backgroundImage) : undefined;
return (<div className="gm-view">
<div>
{images.map((imageName) => <ThumbnailComponent id={imageName} url={client.imageUrl(imageName)} onclick={() => { client.setBackgroundImage(imageName); }} />)}
</div>
<TabletopElement backgroundColor={tabletop.backgroundColor} backgroundUrl={backgroundUrl} />
</div>)
}