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
9 changed files with 112 additions and 34 deletions
Showing only changes of commit 69ef3c3892 - Show all commits

2
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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()
}
}

View File

@ -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)
}

View File

@ -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);
Response::builder()
.header("application-type", "image/jpeg")
.body(bytes)
.unwrap()
.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", mimetype.to_string())
.body(bytes)
.unwrap()
}
});
let core = core::Core::new();
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 filter = route_playing_field
.or(route_static_file)
.or(route_image)
.or(route_available_images)
.recover(handle_rejection);
let server = warp::serve(filter);

View File

@ -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());
}
}

View File

@ -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>)

View File

@ -0,0 +1,4 @@
.thumbnail > img {
max-width: 300px;
max-height: 300px;
}

View File

@ -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>)