Set up a tabletop view for both the GM and the player #260
|
@ -4866,6 +4866,7 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"typeshare",
|
"typeshare",
|
||||||
|
"urlencoding",
|
||||||
"uuid 1.11.0",
|
"uuid 1.11.0",
|
||||||
"warp",
|
"warp",
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,3 +18,4 @@ uuid = { version = "1.11.0", features = ["v4"] }
|
||||||
futures = "0.3.31"
|
futures = "0.3.31"
|
||||||
tokio-stream = "0.1.16"
|
tokio-stream = "0.1.16"
|
||||||
typeshare = "1.0.4"
|
typeshare = "1.0.4"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|
|
@ -6,6 +6,7 @@ use std::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
|
||||||
|
use urlencoding::decode;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::types::{AppError, Message, Tabletop, RGB};
|
use crate::types::{AppError, Message, Tabletop, RGB};
|
||||||
|
@ -68,10 +69,11 @@ impl Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_file(&self, file_name: String) -> Vec<u8> {
|
pub fn get_file(&self, file_name: String) -> Vec<u8> {
|
||||||
let mut full_path = self.0.read().unwrap().image_base.clone();
|
let file_name = decode(&file_name).expect("UTF-8");
|
||||||
full_path.push(&file_name);
|
|
||||||
|
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 content: Vec<u8> = Vec::new();
|
||||||
let mut file = std::fs::File::open(&full_path).unwrap();
|
let mut file = std::fs::File::open(&full_path).unwrap();
|
||||||
|
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use warp::{http::Response, reply::Reply, ws::Message};
|
use warp::{http::Response, reply::Reply, ws::Message};
|
||||||
|
|
||||||
use crate::{core::Core};
|
use crate::core::Core;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub async fn handle_auth(
|
pub async fn handle_auth(
|
||||||
|
@ -82,7 +82,7 @@ pub async fn handle_connect_websocket(
|
||||||
ws: warp::ws::Ws,
|
ws: warp::ws::Ws,
|
||||||
client_id: String,
|
client_id: String,
|
||||||
) -> impl Reply {
|
) -> impl Reply {
|
||||||
println!("handle_connect_websocket: {}", client_id);
|
// println!("handle_connect_websocket: {}", client_id);
|
||||||
ws.on_upgrade(move |socket| {
|
ws.on_upgrade(move |socket| {
|
||||||
let core = core.clone();
|
let core = core.clone();
|
||||||
async move {
|
async move {
|
||||||
|
@ -92,9 +92,10 @@ pub async fn handle_connect_websocket(
|
||||||
tokio::task::spawn(async move {
|
tokio::task::spawn(async move {
|
||||||
let tabletop = core.0.read().unwrap().tabletop.clone();
|
let tabletop = core.0.read().unwrap().tabletop.clone();
|
||||||
let _ = ws_sender
|
let _ = ws_sender
|
||||||
.send(Message::text(serde_json::to_string(
|
.send(Message::text(
|
||||||
&crate::types::Message::UpdateTabletop(tabletop),
|
serde_json::to_string(&crate::types::Message::UpdateTabletop(tabletop))
|
||||||
).unwrap()))
|
.unwrap(),
|
||||||
|
))
|
||||||
.await;
|
.await;
|
||||||
while let Some(msg) = receiver.recv().await {
|
while let Some(msg) = receiver.recv().await {
|
||||||
println!("Relaying message: {:?}", msg);
|
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 authdb::AuthError;
|
||||||
use handlers::{
|
use handlers::{
|
||||||
handle_available_images, handle_connect_websocket, handle_file,
|
handle_available_images, handle_connect_websocket, handle_file, handle_register_client, handle_set_background_image, handle_unregister_client, RegisterRequest
|
||||||
handle_register_client, handle_unregister_client, RegisterRequest,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
@ -81,6 +80,7 @@ fn route_echo_authenticated(
|
||||||
*/
|
*/
|
||||||
|
|
||||||
async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
|
async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible> {
|
||||||
|
println!("handle_rejection: {:?}", err);
|
||||||
if let Some(Unauthorized) = err.find() {
|
if let Some(Unauthorized) = err.find() {
|
||||||
Ok(warp::reply::with_status(
|
Ok(warp::reply::with_status(
|
||||||
"".to_owned(),
|
"".to_owned(),
|
||||||
|
@ -97,6 +97,7 @@ async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
pub async fn main() {
|
pub async fn main() {
|
||||||
let core = core::Core::new();
|
let core = core::Core::new();
|
||||||
|
let log = warp::log("visions::api");
|
||||||
|
|
||||||
let route_image = warp::path!("api" / "v1" / "image" / String)
|
let route_image = warp::path!("api" / "v1" / "image" / String)
|
||||||
.and(warp::get())
|
.and(warp::get())
|
||||||
|
@ -132,23 +133,33 @@ pub async fn main() {
|
||||||
move |ws, client_id| handle_connect_websocket(core.clone(), ws, client_id)
|
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")
|
let route_set_bg_image = warp::path!("api" / "v1" / "tabletop" / "bg_image")
|
||||||
.and(warp::put())
|
.and(warp::put())
|
||||||
.and(warp::body::json())
|
.and(warp::body::json())
|
||||||
.map({
|
.then({
|
||||||
let core = core.clone();
|
let core = core.clone();
|
||||||
move |body| {
|
move |body| handle_set_background_image(core.clone(), body)
|
||||||
println!("background_image: {}", body);
|
}).with(log);
|
||||||
let _ = core.set_background_image(body);
|
|
||||||
warp::reply()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let filter = route_register_client
|
let filter = route_register_client
|
||||||
.or(route_unregister_client)
|
.or(route_unregister_client)
|
||||||
.or(route_websocket)
|
.or(route_websocket)
|
||||||
.or(route_image)
|
.or(route_image)
|
||||||
.or(route_available_images)
|
.or(route_available_images)
|
||||||
|
.or(route_set_bg_image_options)
|
||||||
.or(route_set_bg_image)
|
.or(route_set_bg_image)
|
||||||
.recover(handle_rejection);
|
.recover(handle_rejection);
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
import { Client } from './client';
|
import { Client } from './client';
|
||||||
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
|
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 { WebsocketProvider } from './components/WebsocketProvider';
|
||||||
import { PlayerView } from './views/PlayerView/PlayerView';
|
import { PlayerView } from './views/PlayerView/PlayerView';
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ const App = ({ client }: AppProps) => {
|
||||||
createBrowserRouter([
|
createBrowserRouter([
|
||||||
{
|
{
|
||||||
path: "/gm",
|
path: "/gm",
|
||||||
element: <GmPlayingFieldComponent client={client} />
|
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <GmView client={client} /> </WebsocketProvider> : <div> </div>
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
|
|
|
@ -38,4 +38,10 @@ export class Client {
|
||||||
url.pathname = `/api/v1/image`;
|
url.pathname = `/api/v1/image`;
|
||||||
return fetch(url).then((response) => response.json());
|
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';
|
import './Thumbnail.css';
|
||||||
|
|
||||||
interface ThumbnailProps {
|
interface ThumbnailProps {
|
||||||
client: Client
|
id: string;
|
||||||
imageId: string
|
url: URL;
|
||||||
|
onclick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ThumbnailComponent = ({ client, imageId }: ThumbnailProps) =>
|
export const ThumbnailComponent = ({ id, url, onclick }: ThumbnailProps) => {
|
||||||
(<div className="thumbnail"> <img src={client.imageUrl(imageId).toString()} /> </div>)
|
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