Backfill dimensions onto entry records to help browser layout
This commit is contained in:
parent
2fded78d18
commit
532c88da82
|
@ -219,6 +219,7 @@ ul {
|
||||||
img {
|
img {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::pict::{Extension, Images, Upload, Uploads};
|
use crate::pict::{Details, Extension, Images, Upload, Uploads};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
body::BodyStream, http::StatusCode, web, HttpRequest, HttpResponse, ResponseError,
|
body::BodyStream, http::StatusCode, web, HttpRequest, HttpResponse, ResponseError,
|
||||||
};
|
};
|
||||||
|
@ -92,6 +92,21 @@ impl Connection {
|
||||||
self.proxy(self.image_url(file), req).await
|
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(
|
pub(crate) async fn upload(
|
||||||
&self,
|
&self,
|
||||||
req: &HttpRequest,
|
req: &HttpRequest,
|
||||||
|
@ -162,6 +177,13 @@ impl Connection {
|
||||||
url.to_string()
|
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 {
|
fn delete_url(&self, file: &str, token: &str) -> String {
|
||||||
let mut url = self.upstream.clone();
|
let mut url = self.upstream.clone();
|
||||||
url.set_path(&format!("/image/delete/{token}/{file}"));
|
url.set_path(&format!("/image/delete/{token}/{file}"));
|
||||||
|
|
135
src/lib.rs
135
src/lib.rs
|
@ -479,6 +479,9 @@ enum ErrorKind {
|
||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
UploadString(String),
|
UploadString(String),
|
||||||
|
|
||||||
|
#[error("Operation canceled")]
|
||||||
|
Canceled,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
@ -496,9 +499,16 @@ pub enum EntryKind {
|
||||||
Ready {
|
Ready {
|
||||||
filename: String,
|
filename: String,
|
||||||
delete_token: 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)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct Entry {
|
pub struct Entry {
|
||||||
title: Optional<String>,
|
title: Optional<String>,
|
||||||
|
@ -591,6 +601,7 @@ impl Entry {
|
||||||
if let EntryKind::Ready {
|
if let EntryKind::Ready {
|
||||||
filename,
|
filename,
|
||||||
delete_token,
|
delete_token,
|
||||||
|
..
|
||||||
} = &self.file_info
|
} = &self.file_info
|
||||||
{
|
{
|
||||||
Some((&filename, &delete_token))
|
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> {
|
pub(crate) fn upload_id(&self) -> Option<&str> {
|
||||||
if let EntryKind::Pending { upload_id } = &self.file_info {
|
if let EntryKind::Pending { upload_id } = &self.file_info {
|
||||||
Some(&upload_id)
|
Some(&upload_id)
|
||||||
|
@ -683,6 +706,10 @@ async fn upload(
|
||||||
entry.file_info = EntryKind::Ready {
|
entry.file_info = EntryKind::Ready {
|
||||||
filename: image.file().to_owned(),
|
filename: image.file().to_owned(),
|
||||||
delete_token: image.delete_token().to_owned(),
|
delete_token: image.delete_token().to_owned(),
|
||||||
|
dimensions: Some(Dimensions {
|
||||||
|
width: image.width(),
|
||||||
|
height: image.height(),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = store::UpdateEntry {
|
let _ = store::UpdateEntry {
|
||||||
|
@ -771,30 +798,129 @@ async fn collection(
|
||||||
path: web::Path<CollectionPath>,
|
path: web::Path<CollectionPath>,
|
||||||
token: Option<ValidToken>,
|
token: Option<ValidToken>,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
|
connection: web::Data<Connection>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> Result<HttpResponse, StateError> {
|
) -> Result<HttpResponse, StateError> {
|
||||||
match token {
|
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
|
.await
|
||||||
.stateful(&state),
|
.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")]
|
#[tracing::instrument(name = "View Collection")]
|
||||||
async fn view_collection(
|
async fn view_collection(
|
||||||
path: web::Path<CollectionPath>,
|
path: web::Path<CollectionPath>,
|
||||||
|
connection: web::Data<Connection>,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let collection = match state.store.collection(&path).await? {
|
let collection = match state.store.collection(&path).await? {
|
||||||
Some(collection) => collection,
|
Some(collection) => collection,
|
||||||
None => return Ok(to_404(&state)),
|
None => return Ok(to_404(&state)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let entries = state
|
let entries = state
|
||||||
.store
|
.store
|
||||||
.entries(path.order_key(), path.entry_range())
|
.entries(path.order_key(), path.entry_range())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let entries = ensure_dimensions(state.clone(), connection, path.collection, entries).await?;
|
||||||
|
|
||||||
rendered(
|
rendered(
|
||||||
|cursor| {
|
|cursor| {
|
||||||
self::templates::view_collection_html(
|
self::templates::view_collection_html(
|
||||||
|
@ -813,6 +939,7 @@ async fn view_collection(
|
||||||
async fn edit_collection(
|
async fn edit_collection(
|
||||||
path: web::Path<CollectionPath>,
|
path: web::Path<CollectionPath>,
|
||||||
token: ValidToken,
|
token: ValidToken,
|
||||||
|
connection: web::Data<Connection>,
|
||||||
state: web::Data<State>,
|
state: web::Data<State>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
|
@ -827,6 +954,8 @@ async fn edit_collection(
|
||||||
.entries(path.order_key(), path.entry_range())
|
.entries(path.order_key(), path.entry_range())
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
let entries = ensure_dimensions(state.clone(), connection, path.collection, entries).await?;
|
||||||
|
|
||||||
rendered(
|
rendered(
|
||||||
|cursor| {
|
|cursor| {
|
||||||
self::templates::edit_collection_html(
|
self::templates::edit_collection_html(
|
||||||
|
@ -974,6 +1103,7 @@ async fn delete_entry(
|
||||||
if let EntryKind::Ready {
|
if let EntryKind::Ready {
|
||||||
filename,
|
filename,
|
||||||
delete_token,
|
delete_token,
|
||||||
|
..
|
||||||
} = &entry.file_info
|
} = &entry.file_info
|
||||||
{
|
{
|
||||||
conn.delete(filename, delete_token).await.stateful(&state)?;
|
conn.delete(filename, delete_token).await.stateful(&state)?;
|
||||||
|
@ -1075,6 +1205,7 @@ async fn delete_collection(
|
||||||
if let EntryKind::Ready {
|
if let EntryKind::Ready {
|
||||||
filename,
|
filename,
|
||||||
delete_token,
|
delete_token,
|
||||||
|
..
|
||||||
} = entry.file_info.clone()
|
} = entry.file_info.clone()
|
||||||
{
|
{
|
||||||
let conn = conn.clone();
|
let conn = conn.clone();
|
||||||
|
|
15
src/pict.rs
15
src/pict.rs
|
@ -24,6 +24,7 @@ impl std::fmt::Display for Extension {
|
||||||
pub(crate) struct Image {
|
pub(crate) struct Image {
|
||||||
file: String,
|
file: String,
|
||||||
delete_token: String,
|
delete_token: String,
|
||||||
|
details: Details,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Image {
|
impl Image {
|
||||||
|
@ -34,6 +35,20 @@ impl Image {
|
||||||
pub(crate) fn delete_token(&self) -> &str {
|
pub(crate) fn delete_token(&self) -> &str {
|
||||||
&self.delete_token
|
&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)]
|
#[derive(serde::Deserialize)]
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<div class="edit-row">
|
<div class="edit-row">
|
||||||
<div class="edit-item">
|
<div class="edit-item">
|
||||||
@:image_html(entry, state)
|
@:image_html(id, entry, state)
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-item">
|
<div class="edit-item">
|
||||||
<p class="delete-confirmation">Are you sure you want to delete this image?</p>
|
<p class="delete-confirmation">Are you sure you want to delete this image?</p>
|
||||||
|
|
|
@ -48,7 +48,7 @@ text_input_html, statics::file_upload_js};
|
||||||
<article>
|
<article>
|
||||||
<div class="edit-row">
|
<div class="edit-row">
|
||||||
<div class="edit-item">
|
<div class="edit-item">
|
||||||
@:image_html(entry, state)
|
@:image_html(*id, entry, state)
|
||||||
</div>
|
</div>
|
||||||
<div class="edit-item">
|
<div class="edit-item">
|
||||||
<form method="POST" action="@state.update_entry_path(collection_id, *id, token)">
|
<form method="POST" action="@state.update_entry_path(collection_id, *id, token)">
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
@use crate::{Entry, State};
|
@use crate::{Entry, State};
|
||||||
@use super::image_preview_html;
|
@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">
|
<div class="image-meta">
|
||||||
@if let Some(title) = entry.title.as_ref() {
|
@if let Some(title) = entry.title.as_ref() {
|
||||||
<div class="image-title">@title</div>
|
<div class="image-title">@title</div>
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
@use crate::{pict::Extension, Entry, State};
|
@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() {
|
@if let Some(filename) = entry.filename() {
|
||||||
<div class="image-box">
|
<div class="image-box">
|
||||||
<picture>
|
<a href="#@id">
|
||||||
<source type="image/webp" srcset="@state.srcset(filename, Extension::Webp)" />
|
<picture>
|
||||||
<source type="image/avif" srcset="@state.srcset(filename, Extension::Avif)" />
|
<source type="image/webp" srcset="@state.srcset(filename, Extension::Webp)" />
|
||||||
<source type="image/jpeg" srcset="@state.srcset(filename, Extension::Jpg)" />
|
<source type="image/avif" srcset="@state.srcset(filename, Extension::Avif)" />
|
||||||
<img src="@state.image_path(filename)" @if let Some(title)=entry.title.as_ref() { title="@title" } @if let
|
<source type="image/jpeg" srcset="@state.srcset(filename, Extension::Jpg)" />
|
||||||
Some(description)=entry.description.as_ref() { alt="@description" } />
|
<img
|
||||||
</picture>
|
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>
|
</div>
|
||||||
} else {
|
} else {
|
||||||
<span>Pending</span>
|
<span>Pending</span>
|
||||||
|
|
|
@ -28,10 +28,10 @@
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
<ul>
|
<ul>
|
||||||
@for (_, entry) in entries {
|
@for (id, entry) in entries {
|
||||||
<li class="content-group even">
|
<li class="content-group even" id="@id">
|
||||||
<article>
|
<article>
|
||||||
@:image_html(entry, state)
|
@:image_html(*id, entry, state)
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue