Set up a tabletop view for both the GM and the player #260
|
@ -4866,6 +4866,7 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-stream",
|
||||
"typeshare",
|
||||
"urlencoding",
|
||||
"uuid 1.11.0",
|
||||
"warp",
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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: "/",
|
||||
|
|
|
@ -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) });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>)
|
||||
}
|
||||
|
|
@ -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>)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.gm-view {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
|
@ -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>)
|
||||
}
|
||||
|
Loading…
Reference in New Issue