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