Set up a tabletop view for both the GM and the player #260
|
@ -4858,6 +4858,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"authdb",
|
||||
"http 1.1.0",
|
||||
"mime 0.3.17",
|
||||
"mime_guess 2.0.5",
|
||||
"serde 1.0.210",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
|
@ -12,3 +12,5 @@ serde_json = { version = "*" }
|
|||
serde = { version = "1" }
|
||||
tokio = { version = "1", features = [ "full" ] }
|
||||
warp = { version = "0.3" }
|
||||
mime_guess = "2.0.5"
|
||||
mime = "0.3.17"
|
||||
|
|
|
@ -1,21 +1,58 @@
|
|||
use std::sync::{Arc, RwLock};
|
||||
use std::{
|
||||
io::Read,
|
||||
path::PathBuf,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AppError {
|
||||
JsonError(serde_json::Error)
|
||||
JsonError(serde_json::Error),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {
|
||||
pub image_base: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AppState {}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Core(Arc<RwLock<AppState>>);
|
||||
pub struct Core(pub Arc<RwLock<AppState>>);
|
||||
|
||||
impl Core {
|
||||
pub fn new() -> Self {
|
||||
Self(Arc::new(RwLock::new(AppState {})))
|
||||
Self(Arc::new(RwLock::new(AppState {
|
||||
image_base: PathBuf::from("/home/savanni/Pictures"),
|
||||
})))
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
println!("path: {:?}", full_path);
|
||||
|
||||
let mut content: Vec<u8> = Vec::new();
|
||||
let mut file = std::fs::File::open(&full_path).unwrap();
|
||||
file.read_to_end(&mut content).unwrap();
|
||||
content
|
||||
}
|
||||
|
||||
pub fn available_images(&self) -> Vec<String> {
|
||||
std::fs::read_dir(&self.0.read().unwrap().image_base)
|
||||
.unwrap()
|
||||
.filter_map(|entry| match entry {
|
||||
Ok(entry_) => match mime_guess::from_path(entry_.path()).first() {
|
||||
Some(mime) if mime.type_() == mime::IMAGE => Some(
|
||||
entry_
|
||||
.path()
|
||||
.file_name()
|
||||
.and_then(|filename| filename.to_str())
|
||||
.and_then(|filename| Some(filename.to_owned()))
|
||||
.unwrap(),
|
||||
),
|
||||
_ => None,
|
||||
},
|
||||
Err(_) => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ use authdb::{AuthDB, AuthToken};
|
|||
use http::{response::Response, status::StatusCode, Error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::Core;
|
||||
|
||||
pub async fn handle_auth(
|
||||
auth_ctx: &AuthDB,
|
||||
auth_token: AuthToken,
|
||||
|
@ -37,17 +39,6 @@ pub fn handle_playing_field() -> PlayArea {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn handle_file(file_name: String) -> Vec<u8> {
|
||||
let mut full_path = PathBuf::new();
|
||||
full_path.push("/home");
|
||||
full_path.push("savanni");
|
||||
full_path.push("Pictures");
|
||||
full_path.push(&file_name);
|
||||
|
||||
println!("path: {:?}", full_path);
|
||||
|
||||
let mut content: Vec<u8> = Vec::new();
|
||||
let mut file = std::fs::File::open(&full_path).unwrap();
|
||||
file.read_to_end(&mut content).unwrap();
|
||||
content
|
||||
pub fn handle_file(core: Core, file_name: String) -> Vec<u8> {
|
||||
core.get_file(file_name)
|
||||
}
|
||||
|
|
|
@ -93,22 +93,39 @@ async fn handle_rejection(err: warp::Rejection) -> Result<impl Reply, Infallible
|
|||
|
||||
#[tokio::main]
|
||||
pub async fn main() {
|
||||
let core = core::Core::new();
|
||||
|
||||
let route_playing_field =
|
||||
warp::path!("api" / "v1" / "field").map(move || warp::reply::json(&handle_playing_field()));
|
||||
|
||||
let route_static_file = warp::path!("api" / "v1" / "file" / String)
|
||||
let route_image = warp::path!("api" / "v1" / "image" / String)
|
||||
.and(warp::get())
|
||||
.map(move |file_name| {
|
||||
let bytes = handle_file(file_name);
|
||||
.map({
|
||||
let core = core.clone();
|
||||
move |file_name| {
|
||||
let core = core.clone();
|
||||
let mimetype = mime_guess::from_path(&file_name).first().unwrap();
|
||||
let bytes = handle_file(core, file_name);
|
||||
Response::builder()
|
||||
.header("application-type", "image/jpeg")
|
||||
.header("application-type", mimetype.to_string())
|
||||
.body(bytes)
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let route_available_images = warp::path!("api" / "v1" / "image").and(warp::get()).map({
|
||||
let core = core.clone();
|
||||
move || {
|
||||
let core = core.clone();
|
||||
Response::builder()
|
||||
.header("Access-Control-Allow-Origin", "*")
|
||||
.body(serde_json::to_string(&core.available_images()).unwrap())
|
||||
}
|
||||
});
|
||||
|
||||
let core = core::Core::new();
|
||||
let filter = route_playing_field
|
||||
.or(route_static_file)
|
||||
.or(route_image)
|
||||
.or(route_available_images)
|
||||
.recover(handle_rejection);
|
||||
|
||||
let server = warp::serve(filter);
|
||||
|
|
|
@ -11,11 +11,17 @@ export class Client {
|
|||
|
||||
imageUrl(imageId: string) {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/file/${imageId}`;
|
||||
url.pathname = `/api/v1/image/${imageId}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
async playingField(): Promise<PlayingField> {
|
||||
return { backgroundImage: "su-pearl.png" };
|
||||
return { backgroundImage: "trans-ferris.jpg" };
|
||||
}
|
||||
|
||||
async availableImages(): Promise<string[]> {
|
||||
const url = new URL(this.base);
|
||||
url.pathname = `/api/v1/image`;
|
||||
return fetch(url).then((response) => response.json());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Client, PlayingField } from '../../client';
|
||||
import { ThumbnailComponent } from '../Thumbnail';
|
||||
import './PlayingField.css';
|
||||
|
||||
interface PlayingFieldProps {
|
||||
|
@ -12,9 +13,16 @@ export const PlayingFieldComponent = ({ client }: PlayingFieldProps) => {
|
|||
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> Left Panel </div>
|
||||
<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> Right Panel </div>
|
||||
</div>)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
.thumbnail > img {
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { Client } from '../client';
|
||||
import './Thumbnail.css';
|
||||
|
||||
interface ThumbnailProps {
|
||||
client: Client
|
||||
imageId: string
|
||||
}
|
||||
|
||||
export const ThumbnailComponent = ({ client, imageId }: ThumbnailProps) =>
|
||||
(<div className="thumbnail"> <img src={client.imageUrl(imageId).toString()} /> </div>)
|
Loading…
Reference in New Issue