Set up a tabletop view for both the GM and the player #260
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
18
flake.lock
18
flake.lock
|
@ -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": {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(),
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
@ -53,7 +54,7 @@ impl Core {
|
||||||
|
|
||||||
pub fn connect_client(&self, client_id: String) -> UnboundedReceiver<Message> {
|
pub fn connect_client(&self, client_id: String) -> UnboundedReceiver<Message> {
|
||||||
let mut state = self.0.write().unwrap();
|
let mut state = self.0.write().unwrap();
|
||||||
|
|
||||||
match state.clients.get_mut(&client_id) {
|
match state.clients.get_mut(&client_id) {
|
||||||
Some(client) => {
|
Some(client) => {
|
||||||
let (tx, rx) = unbounded_channel();
|
let (tx, rx) = unbounded_channel();
|
||||||
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src",
|
||||||
|
"gen"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue