diff --git a/scss/layout.scss b/scss/layout.scss index 0d35465..f88da98 100644 --- a/scss/layout.scss +++ b/scss/layout.scss @@ -219,6 +219,7 @@ ul { img { display: block; width: 100%; + height: auto; border-radius: 3px; } } diff --git a/src/connection.rs b/src/connection.rs index e871907..0336eb4 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,6 +1,6 @@ use std::time::Duration; -use crate::pict::{Extension, Images, Upload, Uploads}; +use crate::pict::{Details, Extension, Images, Upload, Uploads}; use actix_web::{ body::BodyStream, http::StatusCode, web, HttpRequest, HttpResponse, ResponseError, }; @@ -92,6 +92,21 @@ impl Connection { self.proxy(self.image_url(file), req).await } + pub(crate) async fn details(&self, file: &str) -> Result { + let mut response = self + .client + .get(self.details_url(file)) + .send() + .await + .map_err(|_| UploadError::Request)?; + + if !response.status().is_success() { + return Err(UploadError::Status); + } + + response.json().await.map_err(|_| UploadError::Json) + } + pub(crate) async fn upload( &self, req: &HttpRequest, @@ -162,6 +177,13 @@ impl Connection { url.to_string() } + fn details_url(&self, file: &str) -> String { + let mut url = self.upstream.clone(); + url.set_path(&format!("/image/details/original/{file}")); + + url.to_string() + } + fn delete_url(&self, file: &str, token: &str) -> String { let mut url = self.upstream.clone(); url.set_path(&format!("/image/delete/{token}/{file}")); diff --git a/src/lib.rs b/src/lib.rs index bcb487e..77bb458 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -479,6 +479,9 @@ enum ErrorKind { #[error("{0}")] UploadString(String), + + #[error("Operation canceled")] + Canceled, } #[derive(Debug, serde::Deserialize, serde::Serialize)] @@ -496,9 +499,16 @@ pub enum EntryKind { Ready { filename: String, delete_token: String, + dimensions: Option, }, } +#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)] +pub struct Dimensions { + width: u32, + height: u32, +} + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] pub struct Entry { title: Optional, @@ -591,6 +601,7 @@ impl Entry { if let EntryKind::Ready { filename, delete_token, + .. } = &self.file_info { Some((&filename, &delete_token)) @@ -599,6 +610,18 @@ impl Entry { } } + pub(crate) fn dimensions(&self) -> Option { + if let EntryKind::Ready { + dimensions: Some(dimensions), + .. + } = &self.file_info + { + Some(*dimensions) + } else { + None + } + } + pub(crate) fn upload_id(&self) -> Option<&str> { if let EntryKind::Pending { upload_id } = &self.file_info { Some(&upload_id) @@ -683,6 +706,10 @@ async fn upload( entry.file_info = EntryKind::Ready { filename: image.file().to_owned(), delete_token: image.delete_token().to_owned(), + dimensions: Some(Dimensions { + width: image.width(), + height: image.height(), + }), }; let _ = store::UpdateEntry { @@ -771,30 +798,129 @@ async fn collection( path: web::Path, token: Option, state: web::Data, + connection: web::Data, req: HttpRequest, ) -> Result { match token { - Some(token) => edit_collection(path, token, state.clone(), req) + Some(token) => edit_collection(path, token, connection, state.clone(), req) + .await + .stateful(&state), + None => view_collection(path, connection, state.clone()) .await .stateful(&state), - None => view_collection(path, state.clone()).await.stateful(&state), } } +async fn save_dimensions( + state: web::Data, + connection: web::Data, + collection_id: Uuid, + entry_id: Uuid, + filename: String, +) -> Result { + let pict::Details { width, height } = connection.details(&filename).await?; + + let entry_path = EntryPath { + collection: collection_id, + entry: entry_id, + }; + + let entry = store::GetEntry { + entry_path: &entry_path, + } + .exec(&state.store) + .await?; + + if entry.dimensions().is_none() { + let entry = Entry { + file_info: if let EntryKind::Ready { + filename, + delete_token, + .. + } = entry.file_info + { + EntryKind::Ready { + filename, + delete_token, + dimensions: Some(Dimensions { width, height }), + } + } else { + entry.file_info + }, + ..entry + }; + + store::UpdateEntry { + entry_path: &entry_path, + entry: &entry, + } + .exec(&state.store) + .await?; + + Ok(entry) + } else { + Ok(entry) + } +} + +async fn ensure_dimensions( + state: web::Data, + connection: web::Data, + collection_id: Uuid, + mut entries: Vec<(Uuid, Entry)>, +) -> Result, Error> { + let updates = entries + .iter() + .map(|(entry_id, entry)| { + if entry.dimensions().is_none() { + if let Some(filename) = entry.filename() { + let filename = filename.to_string(); + let state = state.clone(); + let connection = connection.clone(); + + Some(actix_rt::spawn(save_dimensions( + state, + connection, + collection_id, + *entry_id, + filename, + ))) + } else { + None + } + } else { + None + } + }) + .collect::>(); + + for ((_, entry), update) in entries.iter_mut().zip(updates) { + if let Some(handle) = update { + *entry = handle.await.map_err(|_| ErrorKind::Canceled)??; + } + } + + Ok(entries) +} + #[tracing::instrument(name = "View Collection")] async fn view_collection( path: web::Path, + connection: web::Data, state: web::Data, ) -> Result { let collection = match state.store.collection(&path).await? { Some(collection) => collection, None => return Ok(to_404(&state)), }; + let entries = state .store .entries(path.order_key(), path.entry_range()) .await?; + let entries = ensure_dimensions(state.clone(), connection, path.collection, entries).await?; + rendered( |cursor| { self::templates::view_collection_html( @@ -813,6 +939,7 @@ async fn view_collection( async fn edit_collection( path: web::Path, token: ValidToken, + connection: web::Data, state: web::Data, req: HttpRequest, ) -> Result { @@ -827,6 +954,8 @@ async fn edit_collection( .entries(path.order_key(), path.entry_range()) .await?; + let entries = ensure_dimensions(state.clone(), connection, path.collection, entries).await?; + rendered( |cursor| { self::templates::edit_collection_html( @@ -974,6 +1103,7 @@ async fn delete_entry( if let EntryKind::Ready { filename, delete_token, + .. } = &entry.file_info { conn.delete(filename, delete_token).await.stateful(&state)?; @@ -1075,6 +1205,7 @@ async fn delete_collection( if let EntryKind::Ready { filename, delete_token, + .. } = entry.file_info.clone() { let conn = conn.clone(); diff --git a/src/pict.rs b/src/pict.rs index b5b49b9..a9b33e7 100644 --- a/src/pict.rs +++ b/src/pict.rs @@ -24,6 +24,7 @@ impl std::fmt::Display for Extension { pub(crate) struct Image { file: String, delete_token: String, + details: Details, } impl Image { @@ -34,6 +35,20 @@ impl Image { pub(crate) fn delete_token(&self) -> &str { &self.delete_token } + + pub(crate) fn width(&self) -> u32 { + self.details.width + } + + pub(crate) fn height(&self) -> u32 { + self.details.height + } +} + +#[derive(serde::Deserialize)] +pub(crate) struct Details { + pub(super) width: u32, + pub(super) height: u32, } #[derive(serde::Deserialize)] diff --git a/templates/confirm_entry_delete.rs.html b/templates/confirm_entry_delete.rs.html index f93f546..fd4247b 100644 --- a/templates/confirm_entry_delete.rs.html +++ b/templates/confirm_entry_delete.rs.html @@ -15,7 +15,7 @@
- @:image_html(entry, state) + @:image_html(id, entry, state)

Are you sure you want to delete this image?

diff --git a/templates/edit_collection.rs.html b/templates/edit_collection.rs.html index 6f2c632..605d86d 100644 --- a/templates/edit_collection.rs.html +++ b/templates/edit_collection.rs.html @@ -48,7 +48,7 @@ text_input_html, statics::file_upload_js};
- @:image_html(entry, state) + @:image_html(*id, entry, state)
diff --git a/templates/image.rs.html b/templates/image.rs.html index 6872dc2..9456067 100644 --- a/templates/image.rs.html +++ b/templates/image.rs.html @@ -1,9 +1,10 @@ @use crate::{Entry, State}; @use super::image_preview_html; +@use uuid::Uuid; -@(entry: &Entry, state: &State) +@(id: Uuid, entry: &Entry, state: &State) -@:image_preview_html(entry, state) +@:image_preview_html(id, entry, state)
@if let Some(title) = entry.title.as_ref() {
@title
diff --git a/templates/image_preview.rs.html b/templates/image_preview.rs.html index 49a895c..e3c9692 100644 --- a/templates/image_preview.rs.html +++ b/templates/image_preview.rs.html @@ -1,16 +1,24 @@ @use crate::{pict::Extension, Entry, State}; +@use uuid::Uuid; -@(entry: &Entry, state: &State) +@(id: Uuid, entry: &Entry, state: &State) @if let Some(filename) = entry.filename() {
- - - - - @description - + + + + + + @description + +
} else { Pending diff --git a/templates/view_collection.rs.html b/templates/view_collection.rs.html index c338e89..cc84bcf 100644 --- a/templates/view_collection.rs.html +++ b/templates/view_collection.rs.html @@ -28,10 +28,10 @@
    - @for (_, entry) in entries { -
  • + @for (id, entry) in entries { +
  • - @:image_html(entry, state) + @:image_html(*id, entry, state)
  • }