This commit is contained in:
2025-02-17 18:35:41 +01:00
parent aa9ad6d9d0
commit aba6b4a329
6 changed files with 687 additions and 28 deletions

View File

@@ -22,6 +22,7 @@ derive_more = { version = "1", features = [
"from_str",
] }
http = "1"
image = { version = "0.25", optional = true }
leptos = { version = "0.7.4", features = ["tracing"] }
leptos_axum = { version = "0.7", optional = true }
leptos_meta = { version = "0.7" }
@@ -58,6 +59,7 @@ hydrate = ["leptos/hydrate", "getrandom/wasm_js", "uuid/js"]
ssr = [
"dep:axum",
"dep:redb",
"dep:image",
"dep:bincode",
"dep:tokio",
"dep:tower",

View File

@@ -1,9 +1,12 @@
//! Shared models between server and client code.
pub use v1::Hold;
pub use v1::HoldPosition;
pub use v1::HoldRole;
pub use v1::Image;
pub use v2::Hold;
pub use v2::Image;
pub use v2::ImageFilename;
pub use v2::ImageResolution;
pub use v2::ImageUid;
pub use v2::Method;
pub use v2::Problem;
pub use v2::ProblemUid;
@@ -32,7 +35,7 @@ pub mod v2 {
pub rows: u64,
pub cols: u64,
pub holds: BTreeMap<v1::HoldPosition, v1::Hold>,
pub holds: BTreeMap<v1::HoldPosition, Hold>,
pub problems: BTreeSet<ProblemUid>,
}
impl Wall {
@@ -81,6 +84,46 @@ pub mod v2 {
Footless,
FootlessPlusKickboard,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Hold {
pub position: v1::HoldPosition,
pub image: Option<Image>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Image {
pub uid: ImageUid,
pub resolutions: BTreeMap<ImageResolution, ImageFilename>,
}
impl Image {
pub(crate) fn srcset(&self) -> String {
self.resolutions
.iter()
.map(|(res, filename)| format!("/files/holds/{} {}w", filename.filename, res.width))
.collect::<Vec<String>>()
.join(", ")
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageResolution {
pub width: u64,
pub height: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ImageFilename {
pub filename: String,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)]
pub struct ImageUid(pub uuid::Uuid);
impl ImageUid {
pub fn create() -> Self {
Self(uuid::Uuid::new_v4())
}
}
}
pub mod v1 {

View File

@@ -3,6 +3,7 @@ use crate::components::header::HeaderItem;
use crate::components::header::HeaderItems;
use crate::models;
use crate::models::HoldPosition;
use crate::models::WallUid;
use leptos::Params;
use leptos::ev::Event;
use leptos::html::Input;
@@ -11,6 +12,9 @@ use leptos_router::params::Params;
use serde::Deserialize;
use serde::Serialize;
use server_fn::codec::Cbor;
use std::collections::BTreeMap;
use std::io::Cursor;
use std::path::Path;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::FileList;
@@ -78,7 +82,7 @@ fn Ready(wall: models::Wall) -> impl IntoView {
let mut holds = vec![];
for hold in wall.holds.values().cloned() {
holds.push(view! { <Hold hold /> });
holds.push(view! { <Hold wall_uid=wall.uid hold /> });
}
let grid_classes = format!("grid grid-rows-{} grid-cols-{} gap-3", wall.rows, wall.cols);
@@ -92,7 +96,7 @@ fn Ready(wall: models::Wall) -> impl IntoView {
}
#[component]
fn Hold(hold: models::Hold) -> impl IntoView {
fn Hold(wall_uid: models::WallUid, hold: models::Hold) -> impl IntoView {
let hold_position = hold.position;
let file_input_ref = NodeRef::<Input>::new();
@@ -132,7 +136,11 @@ fn Hold(hold: models::Hold) -> impl IntoView {
file_contents,
};
upload.dispatch(SetImage { hold_position, image });
upload.dispatch(SetImage {
wall_uid,
hold_position,
image,
});
}) as Box<dyn FnMut(_)>);
file_reader.set_onload(Some(on_load.as_ref().unchecked_ref()));
@@ -141,8 +149,8 @@ fn Hold(hold: models::Hold) -> impl IntoView {
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 /> }
let srcset = img.srcset();
view! { <img class="object-cover w-full h-full" srcset=srcset /> }
})
};
@@ -170,15 +178,59 @@ pub struct Image {
#[server(name = SetImage, input = Cbor)]
#[tracing::instrument(skip(image), err)]
async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
async fn set_image(wall_uid: WallUid, hold_position: HoldPosition, image: Image) -> Result<models::Hold, ServerFnError> {
tracing::info!("Setting image, {}, {} bytes", image.file_name, image.file_contents.len());
// 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/public/holds").await?;
tokio::fs::write(format!("datastore/public/holds/{filename}"), image.file_contents).await?;
let db = expect_context::<crate::server::db::Database>();
let image = tokio::task::spawn_blocking(move || -> Result<models::Image, ServerFnError> {
let img = image::ImageReader::new(Cursor::new(image.file_contents))
.with_guessed_format()?
.decode()?;
let holds_dir = Path::new("datastore/public/holds");
std::fs::create_dir_all(holds_dir)?;
let targets = [(50, 50), (150, 150), (300, 300), (400, 400)];
let uid = models::ImageUid::create();
let mut resolutions = BTreeMap::new();
for (width, height) in targets {
let resized = img.resize_to_fill(width, height, image::imageops::FilterType::Lanczos3);
let filename = format!("hold_row{}_col{}_{width}x{height}_{uid}.webp", hold_position.row, hold_position.col);
let path = holds_dir.join(&filename);
let mut file = std::fs::File::open(&path)?;
resized.write_to(&mut file, image::ImageFormat::WebP)?;
let res = models::ImageResolution {
width: width.into(),
height: height.into(),
};
resolutions.insert(res, models::ImageFilename { filename });
}
// TODO: Clean up old image?
Ok(models::Image { uid, resolutions })
})
.await??;
db.write(move |txn| {
use redb::ReadableTable;
let mut walls = txn.open_table(crate::server::db::current::TABLE_WALLS)?;
let mut wall = walls.get(wall_uid)?.expect("todo").value();
if let Some(hold) = wall.holds.get_mut(&hold_position) {
hold.image = Some(image);
}
walls.insert(wall_uid, wall);
Ok(())
})
.await?;
todo!()
// let state = expect_context::<State>();
// state
// .persistent
@@ -192,5 +244,6 @@ async fn set_image(hold_position: HoldPosition, image: Image) -> Result<models::
// // Return updated hold
// let hold = state.persistent.with(|s| s.wall.holds.get(&hold_position).cloned().unwrap()).await;
// Ok(hold)
let hold = todo!();
Ok(hold)
}

View File

@@ -164,8 +164,8 @@ fn Hold(hold: models::Hold, role: Signal<Option<HoldRole>>) -> impl IntoView {
};
let img = hold.image.map(|img| {
let src = format!("/files/holds/{}", img.filename);
view! { <img class="object-cover w-full h-full" src=src /> }
let srcset = img.srcset();
view! { <img class="object-cover w-full h-full" srcset=srcset /> }
});
tracing::trace!("view");

View File

@@ -10,14 +10,12 @@ use server_fn::ServerFnError;
output = Ron,
custom = RonEncoded
)]
// #[tracing::instrument(skip_all, err(Debug))]
#[tracing::instrument(skip_all, err(Debug))]
pub async fn get_walls() -> Result<RonEncoded<Vec<models::Wall>>, ServerFnError> {
use crate::server::db::Database;
use redb::ReadableTable;
tracing::debug!("Enter");
// dbg!(leptos::prelude::Owner::current().map(|o| o.ancestry()));
let db = expect_context::<Database>();
let walls = db