feat: uploading images
This commit is contained in:
parent
51372e4886
commit
5fd2b1206a
@ -30,7 +30,6 @@ pub fn hydrate() {
|
||||
.without_time()
|
||||
.with_ansi(false)
|
||||
.init();
|
||||
tracing::warn!("test");
|
||||
|
||||
leptos::mount::hydrate_body(App);
|
||||
}
|
||||
|
@ -4,12 +4,30 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct Wall {
|
||||
pub rows: u64,
|
||||
pub cols: u64,
|
||||
pub holds: BTreeMap<HoldPosition, Hold>,
|
||||
}
|
||||
impl Wall {
|
||||
pub fn new(rows: u64, cols: u64) -> Self {
|
||||
let mut holds = BTreeMap::new();
|
||||
for row in 0..rows {
|
||||
for col in 0..cols {
|
||||
let position = HoldPosition { row, col };
|
||||
let hold = Hold { position, image: None };
|
||||
holds.insert(position, hold);
|
||||
}
|
||||
}
|
||||
Self { rows, cols, holds }
|
||||
}
|
||||
}
|
||||
impl Default for Wall {
|
||||
fn default() -> Self {
|
||||
Self::new(12, 12)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Copy)]
|
||||
pub struct HoldPosition {
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::codec::ron::RonCodec;
|
||||
use crate::components::header::Header;
|
||||
use crate::models;
|
||||
use crate::models::HoldPosition;
|
||||
@ -8,6 +9,7 @@ use leptos::prelude::*;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use server_fn::codec::Cbor;
|
||||
use std::ops::Deref;
|
||||
use wasm_bindgen::JsCast;
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::FileList;
|
||||
@ -24,7 +26,7 @@ pub fn EditWall() -> impl leptos::IntoView {
|
||||
<div class="container mx-auto">
|
||||
<Header />
|
||||
<Await future=load let:data>
|
||||
<Ready data=data.to_owned() />
|
||||
<Ready data=data.deref().to_owned() />
|
||||
</Await>
|
||||
</div>
|
||||
</div>
|
||||
@ -34,16 +36,10 @@ pub fn EditWall() -> impl leptos::IntoView {
|
||||
#[component]
|
||||
fn Ready(data: InitialData) -> impl leptos::IntoView {
|
||||
tracing::debug!("ready");
|
||||
let mut hold_positions = vec![];
|
||||
for row in 0..(data.wall.rows) {
|
||||
for col in 0..(data.wall.cols) {
|
||||
hold_positions.push(HoldPosition { row, col });
|
||||
}
|
||||
}
|
||||
|
||||
let mut holds = vec![];
|
||||
for &hold_position in &hold_positions {
|
||||
holds.push(view! { <Hold hold_position /> });
|
||||
for hold in data.wall.holds.values().cloned() {
|
||||
holds.push(view! { <Hold hold /> });
|
||||
}
|
||||
|
||||
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-2", data.wall.rows, data.wall.cols);
|
||||
@ -52,7 +48,8 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Hold(hold_position: HoldPosition) -> impl leptos::IntoView {
|
||||
fn Hold(hold: models::Hold) -> impl leptos::IntoView {
|
||||
let hold_position = hold.position;
|
||||
let file_input_ref = NodeRef::<Input>::new();
|
||||
|
||||
let open_camera = move |_| {
|
||||
@ -63,6 +60,11 @@ fn Hold(hold_position: HoldPosition) -> impl leptos::IntoView {
|
||||
|
||||
let upload = Action::from(ServerAction::<SetImage>::new());
|
||||
|
||||
let hold = Signal::derive(move || {
|
||||
let refreshed = upload.value().get().map(Result::unwrap);
|
||||
refreshed.unwrap_or(hold.clone())
|
||||
});
|
||||
|
||||
// Callback to handle file selection
|
||||
let on_file_input = move |event: Event| {
|
||||
let files: FileList = event.target().unwrap().unchecked_ref::<web_sys::HtmlInputElement>().files().unwrap();
|
||||
@ -93,9 +95,18 @@ fn Hold(hold_position: HoldPosition) -> impl leptos::IntoView {
|
||||
on_load.forget();
|
||||
};
|
||||
|
||||
let img = move || {
|
||||
hold.read().image.as_ref().map(|img| {
|
||||
let src = format!("/files/holds/{}", img.filename);
|
||||
view! {
|
||||
<img class="object-cover w-full h-full" src=src />
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
view! {
|
||||
<button on:click=open_camera>
|
||||
<div class="bg-indigo-100 aspect-square rounded"></div>
|
||||
<div class="bg-indigo-100 aspect-square rounded">{ img }</div>
|
||||
</button>
|
||||
|
||||
<input
|
||||
@ -121,26 +132,26 @@ pub struct Image {
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn load_initial_data() -> Result<InitialData, ServerFnError> {
|
||||
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
||||
use crate::server::state::State;
|
||||
|
||||
let state = expect_context::<State>();
|
||||
|
||||
let wall = state.persistent.with(|s| s.wall.clone()).await;
|
||||
Ok(InitialData { wall })
|
||||
Ok(RonCodec::new(InitialData { wall }))
|
||||
}
|
||||
|
||||
#[server(name = SetImage, input = Cbor)]
|
||||
#[tracing::instrument(skip(image))]
|
||||
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<(), ServerFnError> {
|
||||
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
|
||||
tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len());
|
||||
|
||||
use crate::server::state::State;
|
||||
|
||||
// TODO: Fix file extension presumption, and possibly use uuid
|
||||
let filename = format!("row{}_col{}.jpg", hold_position.row, hold_position.col);
|
||||
tokio::fs::create_dir_all("datastore").await?;
|
||||
tokio::fs::write(format!("datastore/{filename}"), image.file_contents).await?;
|
||||
tokio::fs::create_dir_all("datastore/public/holds").await?;
|
||||
tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?;
|
||||
|
||||
let state = expect_context::<State>();
|
||||
state
|
||||
@ -152,5 +163,8 @@ async fn set_image(hold_position: HoldPosition, image: Image) -> Result<(), Serv
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
// Return updated hold
|
||||
let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await;
|
||||
|
||||
Ok(hold)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
use crate::codec::ron::RonCodec;
|
||||
use crate::components::header::Header;
|
||||
use crate::models;
|
||||
use crate::models::HoldPosition;
|
||||
use crate::models::HoldRole;
|
||||
use leptos::prelude::*;
|
||||
use leptos::reactive::graph::ReactiveNode;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::ops::Deref;
|
||||
|
||||
#[component]
|
||||
pub fn Wall() -> impl leptos::IntoView {
|
||||
@ -20,7 +20,7 @@ pub fn Wall() -> impl leptos::IntoView {
|
||||
<div class="container mx-auto">
|
||||
<Header />
|
||||
<Await future=load let:data>
|
||||
<Ready data=data.to_owned() />
|
||||
<Ready data=data.deref().to_owned() />
|
||||
</Await>
|
||||
</div>
|
||||
</div>
|
||||
@ -29,12 +29,7 @@ pub fn Wall() -> impl leptos::IntoView {
|
||||
|
||||
#[component]
|
||||
fn Ready(data: InitialData) -> impl leptos::IntoView {
|
||||
let mut hold_positions = vec![];
|
||||
for row in 0..(data.wall.rows) {
|
||||
for col in 0..(data.wall.cols) {
|
||||
hold_positions.push(HoldPosition { row, col });
|
||||
}
|
||||
}
|
||||
tracing::debug!("ready");
|
||||
|
||||
let (current_problem, current_problem_writer) = signal(None::<models::Problem>);
|
||||
let problem_fetcher = LocalResource::new(move || async move {
|
||||
@ -44,11 +39,11 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
|
||||
});
|
||||
|
||||
let mut cells = vec![];
|
||||
for &hold_position in &hold_positions {
|
||||
for (&hold_position, hold) in &data.wall.holds {
|
||||
let role = move || current_problem.get().and_then(|problem| problem.holds.get(&hold_position).copied());
|
||||
let role = Signal::derive(role);
|
||||
|
||||
let cell = view! { <Hold role /> };
|
||||
let cell = view! { <Hold role hold=hold.clone()/> };
|
||||
cells.push(cell);
|
||||
}
|
||||
|
||||
@ -61,7 +56,7 @@ fn Ready(data: InitialData) -> impl leptos::IntoView {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Hold(#[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
|
||||
fn Hold(hold: models::Hold, #[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
|
||||
let class = move || {
|
||||
let role_classes = match role.get() {
|
||||
Some(HoldRole::Start) => Some("outline outline-offset-2 outline-green-500"),
|
||||
@ -78,7 +73,14 @@ fn Hold(#[prop(into)] role: Signal<Option<HoldRole>>) -> impl leptos::IntoView {
|
||||
s
|
||||
};
|
||||
|
||||
view! { <div class=class></div> }
|
||||
let img = hold.image.map(|img| {
|
||||
let src = format!("/files/holds/{}", img.filename);
|
||||
view! {
|
||||
<img class="hover:object-scale-down object-cover w-full h-full" src=src />
|
||||
}
|
||||
});
|
||||
|
||||
view! { <div class=class>{ img }</div> }
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
@ -87,13 +89,13 @@ pub struct InitialData {
|
||||
}
|
||||
|
||||
#[server]
|
||||
async fn load_initial_data() -> Result<InitialData, ServerFnError> {
|
||||
async fn load_initial_data() -> Result<RonCodec<InitialData>, ServerFnError> {
|
||||
use crate::server::state::State;
|
||||
|
||||
let state = expect_context::<State>();
|
||||
|
||||
let wall = state.persistent.with(|s| s.wall.clone()).await;
|
||||
Ok(InitialData { wall })
|
||||
Ok(RonCodec::new(InitialData { wall }))
|
||||
}
|
||||
|
||||
#[server]
|
||||
|
@ -9,6 +9,7 @@ use state::PersistentState;
|
||||
use state::State;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use tower_http::services::ServeDir;
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use type_toppings::ResultExt;
|
||||
|
||||
@ -135,9 +136,9 @@ async fn serve() -> Result<(), Error> {
|
||||
let leptos_options = leptos_options.clone();
|
||||
move || shell(leptos_options.clone())
|
||||
})
|
||||
.nest_service("/files", file_service("datastore/public"))
|
||||
.fallback(leptos_axum::file_and_error_handler(shell))
|
||||
.with_state(leptos_options);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
tracing::info!("Listening on http://{addr}");
|
||||
axum::serve(listener, app.into_make_service()).await?;
|
||||
@ -145,6 +146,12 @@ async fn serve() -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(path))]
|
||||
fn file_service(path: impl AsRef<Path>) -> ServeDir {
|
||||
tracing::debug!("Creating file service for {}", path.as_ref().display());
|
||||
ServeDir::new(path)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
async fn load_state() -> Result<State, Error> {
|
||||
tracing::info!("Loading state");
|
||||
|
@ -98,7 +98,7 @@ impl<T> Persistent<T> {
|
||||
T: Serialize,
|
||||
{
|
||||
tracing::debug!("Persisting state");
|
||||
let serialized = ron::to_string(state).map_err(|source| Error::Serialize { source })?;
|
||||
let serialized = ron::ser::to_string_pretty(state, ron::ser::PrettyConfig::default()).map_err(|source| Error::Serialize { source })?;
|
||||
tokio::fs::write(file_path, serialized).await.map_err(|source| Error::Write {
|
||||
file_path: file_path.to_owned(),
|
||||
source,
|
||||
|
Loading…
x
Reference in New Issue
Block a user