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
13 changed files with 91 additions and 91 deletions
Showing only changes of commit f0ce3a9fab - Show all commits

7
Cargo.lock generated
View File

@ -522,7 +522,7 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
name = "changeset" name = "changeset"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"uuid 0.8.2", "uuid 1.11.0",
] ]
[[package]] [[package]]
@ -4637,9 +4637,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]] [[package]]
name = "typeshare" name = "typeshare"
version = "1.0.3" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04f17399b76c2e743d58eac0635d7686e9c00f48cd4776f00695d9882a7d3187" checksum = "19be0f411120091e76e13e5a0186d8e2bcc3e7e244afdb70152197f1a8486ceb"
dependencies = [ dependencies = [
"chrono", "chrono",
"serde 1.0.210", "serde 1.0.210",
@ -4865,6 +4865,7 @@ dependencies = [
"serde_json", "serde_json",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"typeshare",
"uuid 1.11.0", "uuid 1.11.0",
"warp", "warp",
] ]

View File

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1681202837, "lastModified": 1710146030,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401", "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -35,11 +35,11 @@
}, },
"nixpkgs_2": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1681303793, "lastModified": 1714906307,
"narHash": "sha256-JEdQHsYuCfRL2PICHlOiH/2ue3DwoxUX7DJ6zZxZXFk=", "narHash": "sha256-UlRZtrCnhPFSJlDQE7M0eyhgvuuHBTe1eJ9N9AQlJQ0=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "fe2ecaf706a5907b5e54d979fbde4924d84b65fc", "rev": "25865a40d14b3f9cf19f19b924e2ab4069b09588",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -76,11 +76,11 @@
"nixpkgs": "nixpkgs_2" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1698205128, "lastModified": 1731966246,
"narHash": "sha256-jP+81TkldLtda8bzmsBWahETGsyFkoDOCT244YkA+S4=", "narHash": "sha256-e/V7Ffm5wPd9DVzCThnPZ7lFxd43bb64tSk8/oGP4Ag=",
"owner": "1Password", "owner": "1Password",
"repo": "typeshare", "repo": "typeshare",
"rev": "c3ee2ad8f27774c45db7af4f2ba746c4ae11de21", "rev": "e0e5f27ee34d7d4da76a9dc96a11552e98be56da",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -17,3 +17,4 @@ mime = "0.3.17"
uuid = { version = "1.11.0", features = ["v4"] } 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"

View File

@ -8,7 +8,7 @@ use std::{
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};
use uuid::Uuid; use uuid::Uuid;
use crate::types::{AppError, Message, PlayArea}; use crate::types::{AppError, Message, Tabletop, RGB};
#[derive(Debug)] #[derive(Debug)]
struct WebsocketClient { struct WebsocketClient {
@ -18,9 +18,7 @@ struct WebsocketClient {
#[derive(Debug)] #[derive(Debug)]
pub struct AppState { pub struct AppState {
pub image_base: PathBuf, pub image_base: PathBuf,
pub tabletop: Tabletop,
pub playfield_background: String,
pub clients: HashMap<String, WebsocketClient>, pub clients: HashMap<String, WebsocketClient>,
} }
@ -31,7 +29,10 @@ impl Core {
pub fn new() -> Self { pub fn new() -> Self {
Self(Arc::new(RwLock::new(AppState { Self(Arc::new(RwLock::new(AppState {
image_base: PathBuf::from("/home/savanni/Pictures"), image_base: PathBuf::from("/home/savanni/Pictures"),
playfield_background: "moon.jpg".to_owned(), tabletop: Tabletop {
background_color: RGB{ red: 15, green: 0, blue: 0 },
background_image: None,
},
clients: HashMap::new(), clients: HashMap::new(),
}))) })))
} }
@ -98,12 +99,13 @@ impl Core {
.collect() .collect()
} }
pub fn set_playfield_background(&self, path: String) -> Result<(), AppError> { pub fn set_background_image(&self, path: String) -> Result<(), AppError> {
{ let tabletop = {
let mut state = self.0.write().unwrap(); let mut state = self.0.write().unwrap();
state.playfield_background = path.clone(); state.tabletop.background_image = Some(path.clone());
} state.tabletop.clone()
self.publish(Message::PlayArea(PlayArea{ background_image: path })); };
self.publish(Message::UpdateTabletop(tabletop));
Ok(()) Ok(())
} }

View File

@ -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, types::PlayArea}; use crate::{core::Core};
/* /*
pub async fn handle_auth( pub async fn handle_auth(
@ -31,18 +31,6 @@ pub async fn handle_auth(
} }
*/ */
pub async fn handle_playing_field() -> impl Reply {
Response::builder()
.header("application-type", "application/json")
.body(
serde_json::to_string(&PlayArea {
background_image: "tower-in-mist.jpg".to_owned(),
})
.unwrap(),
)
.unwrap()
}
pub async fn handle_file(core: Core, file_name: String) -> impl Reply { pub async fn handle_file(core: Core, file_name: String) -> impl Reply {
let mimetype = mime_guess::from_path(&file_name).first().unwrap(); let mimetype = mime_guess::from_path(&file_name).first().unwrap();
let bytes = core.get_file(file_name); let bytes = core.get_file(file_name);
@ -102,11 +90,11 @@ pub async fn handle_connect_websocket(
let mut receiver = core.connect_client(client_id.clone()); let mut receiver = core.connect_client(client_id.clone());
tokio::task::spawn(async move { tokio::task::spawn(async move {
let background_image = core.0.read().unwrap().playfield_background.clone(); let tabletop = core.0.read().unwrap().tabletop.clone();
let _ = ws_sender let _ = ws_sender
.send(Message::text( .send(Message::text(serde_json::to_string(
serde_json::to_string(&crate::types::Message::PlayArea(PlayArea{ background_image })).unwrap(), &crate::types::Message::UpdateTabletop(tabletop),
)) ).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);

View File

@ -1,6 +1,6 @@
use authdb::AuthError; use authdb::AuthError;
use handlers::{ use handlers::{
handle_available_images, handle_connect_websocket, handle_file, handle_playing_field, handle_available_images, handle_connect_websocket, handle_file,
handle_register_client, handle_unregister_client, RegisterRequest, handle_register_client, handle_unregister_client, RegisterRequest,
}; };
use std::{ use std::{
@ -98,8 +98,6 @@ async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible
pub async fn main() { pub async fn main() {
let core = core::Core::new(); let core = core::Core::new();
let route_playing_field = warp::path!("api" / "v1" / "field").then(|| handle_playing_field());
let route_image = warp::path!("api" / "v1" / "image" / String) let route_image = warp::path!("api" / "v1" / "image" / String)
.and(warp::get()) .and(warp::get())
.then({ .then({
@ -134,13 +132,14 @@ 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_playfield_bg = warp::path!("api" / "v1" / "playfield" / "bg") 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({ .map({
let core = core.clone(); let core = core.clone();
move |body| { move |body| {
core.set_playfield_background(body); println!("background_image: {}", body);
core.set_background_image(body);
warp::reply() warp::reply()
} }
}); });
@ -148,10 +147,9 @@ pub async fn main() {
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_playing_field)
.or(route_image) .or(route_image)
.or(route_available_images) .or(route_available_images)
.or(route_set_playfield_bg) .or(route_set_bg_image)
.recover(handle_rejection); .recover(handle_rejection);
let server = warp::serve(filter); let server = warp::serve(filter);

View File

@ -1,10 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use typeshare::typeshare;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PlayArea {
pub background_image: String,
}
#[derive(Debug)] #[derive(Debug)]
pub enum AppError { pub enum AppError {
@ -12,9 +7,27 @@ pub enum AppError {
} }
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type")] #[serde(rename_all = "camelCase")]
#[typeshare]
pub struct RGB {
pub red: u32,
pub green: u32,
pub blue: u32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
#[typeshare]
pub struct Tabletop {
pub background_color: RGB,
pub background_image: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(tag = "type", content = "content")]
#[typeshare]
pub enum Message { pub enum Message {
PlayArea(PlayArea), UpdateTabletop(Tabletop),
} }

View File

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import './App.css'; import './App.css';
import { PlayingFieldComponent} from './components/PlayingField/PlayingField'; import { TabletopElement } from './components/Tabletop/Tabletop';
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 { GmPlayingFieldComponent } from './components/GmPlayingField/GmPlayingField';
@ -26,7 +26,7 @@ const App = ({ client }: AppProps) => {
}, },
{ {
path: "/", path: "/",
element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <PlayingFieldComponent client={client} /> </WebsocketProvider> : <div> </div> element: websocketUrl ? <WebsocketProvider websocketUrl={websocketUrl}> <TabletopElement client={client} /> </WebsocketProvider> : <div> </div>
} }
]); ]);
return ( return (

View File

@ -1,23 +0,0 @@
import React, { useContext } from 'react';
import './PlayingField.css';
import { WebsocketContext } from '../WebsocketProvider';
import { Client } from '../../client';
interface PlayingFieldComponentProps {
client: Client;
}
export const PlayingFieldComponent = ({ client }: PlayingFieldComponentProps) => {
const { backgroundImage } = useContext(WebsocketContext);
console.log("backgroundImage", backgroundImage);
if (backgroundImage) {
return (<div className="playing-field">
<div className="playing-field__background"> {backgroundImage && <img src={client.imageUrl(backgroundImage).toString()} alt="playing field" />} </div>
</div>)
} else {
return (<div className="playing-field">
<div> </div>
</div>
);
}
}

View File

@ -0,0 +1,23 @@
import React, { useContext } from 'react';
import './Tabletop.css';
import { WebsocketContext } from '../WebsocketProvider';
import { Client } from '../../client';
interface TabletopElementProps {
client: Client;
}
export const TabletopElement = ({ client }: TabletopElementProps) => {
const { tabletop } = useContext(WebsocketContext);
console.log("backgroundImage", tabletop.backgroundImage);
if (tabletop.backgroundImage) {
return (<div className="playing-field">
<div className="playing-field__background"> {tabletop.backgroundImage && <img src={client.imageUrl(tabletop.backgroundImage).toString()} alt="playing field" />} </div>
</div>)
} else {
return (<div className="playing-field">
<div> </div>
</div>
);
}
}

View File

@ -1,17 +1,12 @@
import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react"; import React, { createContext, PropsWithChildren, useEffect, useReducer } from "react";
import useWebSocket from "react-use-websocket"; import useWebSocket from "react-use-websocket";
import { Message, Tabletop } from "visions";
type Message = |
{
type: "PlayArea";
background_image: string;
}
type TabletopState = { type TabletopState = {
backgroundImage: string | undefined; tabletop: Tabletop;
} }
const initialState = () => ({ backgroundImage: undefined }); const initialState = (): TabletopState => ({ tabletop: { backgroundColor: { red: 0, green: 0, blue: 0 }, backgroundImage: undefined } });
export const WebsocketContext = createContext<TabletopState>(initialState()); export const WebsocketContext = createContext<TabletopState>(initialState());
@ -37,11 +32,12 @@ export const WebsocketProvider = ({ websocketUrl, children }: PropsWithChildren<
} }
const handleMessage = (state: TabletopState, message: Message): TabletopState => { const handleMessage = (state: TabletopState, message: Message): TabletopState => {
console.log(message);
switch (message.type) { switch (message.type) {
case "PlayArea": { case "UpdateTabletop": {
return { return {
...state, ...state,
backgroundImage: message.background_image, tabletop: message.content,
} }
} }
} }

View File

@ -21,6 +21,7 @@
"jsx": "react-jsx" "jsx": "react-jsx"
}, },
"include": [ "include": [
"src" "src",
"gen"
] ]
} }