Backfill dimensions onto entry records to help browser layout

This commit is contained in:
asonix 2023-11-13 15:38:00 -06:00
parent 2fded78d18
commit 532c88da82
9 changed files with 196 additions and 18 deletions

View file

@ -219,6 +219,7 @@ ul {
img {
display: block;
width: 100%;
height: auto;
border-radius: 3px;
}
}

View file

@ -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<Details, UploadError> {
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}"));

View file

@ -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<Dimensions>,
},
}
#[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<String>,
@ -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<Dimensions> {
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<CollectionPath>,
token: Option<ValidToken>,
state: web::Data<State>,
connection: web::Data<Connection>,
req: HttpRequest,
) -> Result<HttpResponse, StateError> {
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<State>,
connection: web::Data<Connection>,
collection_id: Uuid,
entry_id: Uuid,
filename: String,
) -> Result<Entry, Error> {
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<State>,
connection: web::Data<Connection>,
collection_id: Uuid,
mut entries: Vec<(Uuid, Entry)>,
) -> Result<Vec<(Uuid, Entry)>, 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::<Vec<_>>();
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<CollectionPath>,
connection: web::Data<Connection>,
state: web::Data<State>,
) -> Result<HttpResponse, Error> {
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<CollectionPath>,
token: ValidToken,
connection: web::Data<Connection>,
state: web::Data<State>,
req: HttpRequest,
) -> Result<HttpResponse, Error> {
@ -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();

View file

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

View file

@ -15,7 +15,7 @@
<div class="content-group">
<div class="edit-row">
<div class="edit-item">
@:image_html(entry, state)
@:image_html(id, entry, state)
</div>
<div class="edit-item">
<p class="delete-confirmation">Are you sure you want to delete this image?</p>

View file

@ -48,7 +48,7 @@ text_input_html, statics::file_upload_js};
<article>
<div class="edit-row">
<div class="edit-item">
@:image_html(entry, state)
@:image_html(*id, entry, state)
</div>
<div class="edit-item">
<form method="POST" action="@state.update_entry_path(collection_id, *id, token)">

View file

@ -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)
<div class="image-meta">
@if let Some(title) = entry.title.as_ref() {
<div class="image-title">@title</div>

View file

@ -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() {
<div class="image-box">
<picture>
<source type="image/webp" srcset="@state.srcset(filename, Extension::Webp)" />
<source type="image/avif" srcset="@state.srcset(filename, Extension::Avif)" />
<source type="image/jpeg" srcset="@state.srcset(filename, Extension::Jpg)" />
<img src="@state.image_path(filename)" @if let Some(title)=entry.title.as_ref() { title="@title" } @if let
Some(description)=entry.description.as_ref() { alt="@description" } />
</picture>
<a href="#@id">
<picture>
<source type="image/webp" srcset="@state.srcset(filename, Extension::Webp)" />
<source type="image/avif" srcset="@state.srcset(filename, Extension::Avif)" />
<source type="image/jpeg" srcset="@state.srcset(filename, Extension::Jpg)" />
<img
loading="lazy"
src="@state.image_path(filename)"
@if let Some(title)=entry.title.as_ref() { title="@title" }
@if let Some(description)=entry.description.as_ref() { alt="@description" }
@if let Some(dimensions) = entry.dimensions() { width="@dimensions.width" height="@dimensions.height" }
/>
</picture>
</a>
</div>
} else {
<span>Pending</span>

View file

@ -28,10 +28,10 @@
</div>
</article>
<ul>
@for (_, entry) in entries {
<li class="content-group even">
@for (id, entry) in entries {
<li class="content-group even" id="@id">
<article>
@:image_html(entry, state)
@:image_html(*id, entry, state)
</article>
</li>
}