Set up a tabletop view for both the GM and the player #260
|
@ -4858,6 +4858,8 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"authdb",
|
"authdb",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
|
"mime 0.3.17",
|
||||||
|
"mime_guess 2.0.5",
|
||||||
"serde 1.0.210",
|
"serde 1.0.210",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
|
@ -12,3 +12,5 @@ serde_json = { version = "*" }
|
||||||
serde = { version = "1" }
|
serde = { version = "1" }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
warp = { version = "0.3" }
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum AppError {
|
pub enum AppError {
|
||||||
JsonError(serde_json::Error)
|
JsonError(serde_json::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub image_base: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct AppState {}
|
pub struct Core(pub Arc<RwLock<AppState>>);
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Core(Arc<RwLock<AppState>>);
|
|
||||||
|
|
||||||
impl Core {
|
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"),
|
||||||
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 http::{response::Response, status::StatusCode, Error};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::core::Core;
|
||||||
|
|
||||||
pub async fn handle_auth(
|
pub async fn handle_auth(
|
||||||
auth_ctx: &AuthDB,
|
auth_ctx: &AuthDB,
|
||||||
auth_token: AuthToken,
|
auth_token: AuthToken,
|
||||||
|
@ -37,17 +39,6 @@ pub fn handle_playing_field() -> PlayArea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_file(file_name: String) -> Vec<u8> {
|
pub fn handle_file(core: Core, file_name: String) -> Vec<u8> {
|
||||||
let mut full_path = PathBuf::new();
|
core.get_file(file_name)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,22 +93,39 @@ 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 route_playing_field =
|
let route_playing_field =
|
||||||
warp::path!("api" / "v1" / "field").map(move || warp::reply::json(&handle_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())
|
.and(warp::get())
|
||||||
.map(move |file_name| {
|
.map({
|
||||||
let bytes = handle_file(file_name);
|
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()
|
Response::builder()
|
||||||
.header("application-type", "image/jpeg")
|
.header("application-type", mimetype.to_string())
|
||||||
.body(bytes)
|
.body(bytes)
|
||||||
.unwrap()
|
.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
|
let filter = route_playing_field
|
||||||
.or(route_static_file)
|
.or(route_image)
|
||||||
|
.or(route_available_images)
|
||||||
.recover(handle_rejection);
|
.recover(handle_rejection);
|
||||||
|
|
||||||
let server = warp::serve(filter);
|
let server = warp::serve(filter);
|
||||||
|
|
|
@ -11,11 +11,17 @@ export class Client {
|
||||||
|
|
||||||
imageUrl(imageId: string) {
|
imageUrl(imageId: string) {
|
||||||
const url = new URL(this.base);
|
const url = new URL(this.base);
|
||||||
url.pathname = `/api/v1/file/${imageId}`;
|
url.pathname = `/api/v1/image/${imageId}`;
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
async playingField(): Promise<PlayingField> {
|
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 React, { useEffect, useState } from 'react';
|
||||||
import { Client, PlayingField } from '../../client';
|
import { Client, PlayingField } from '../../client';
|
||||||
|
import { ThumbnailComponent } from '../Thumbnail';
|
||||||
import './PlayingField.css';
|
import './PlayingField.css';
|
||||||
|
|
||||||
interface PlayingFieldProps {
|
interface PlayingFieldProps {
|
||||||
|
@ -12,9 +13,16 @@ export const PlayingFieldComponent = ({ client }: PlayingFieldProps) => {
|
||||||
client.playingField().then((field) => setField(field));
|
client.playingField().then((field) => setField(field));
|
||||||
}, [client]);
|
}, [client]);
|
||||||
|
|
||||||
|
const [images, setImages] = useState<string[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
client.availableImages().then((images) => setImages(images));
|
||||||
|
}, [client]);
|
||||||
|
|
||||||
const backgroundUrl = field && client.imageUrl(field.backgroundImage);
|
const backgroundUrl = field && client.imageUrl(field.backgroundImage);
|
||||||
return (<div className="playing-field">
|
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 className="playing-field__background"> {backgroundUrl && <img src={backgroundUrl.toString()} alt="playing field" />} </div>
|
||||||
<div> Right Panel </div>
|
<div> Right Panel </div>
|
||||||
</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