Compare commits
6 commits
2d2ce11dc8
...
9dcb5114f3
Author | SHA1 | Date | |
---|---|---|---|
asonix | 9dcb5114f3 | ||
asonix | 78ad7e876c | ||
asonix | 2e7ea3052c | ||
asonix | e83f98dbe2 | ||
asonix | 0fbe26deab | ||
asonix | ff242af2ee |
703
Cargo.lock
generated
703
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,7 +1,7 @@
|
||||||
[package]
|
[package]
|
||||||
name = "pict-rs-aggregator"
|
name = "pict-rs-aggregator"
|
||||||
description = "A simple image aggregation service for pict-rs"
|
description = "A simple image aggregation service for pict-rs"
|
||||||
version = "0.2.0-beta.1"
|
version = "0.2.0-beta.2"
|
||||||
authors = ["asonix <asonix@asonix.dog>"]
|
authors = ["asonix <asonix@asonix.dog>"]
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
@ -46,7 +46,7 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
|
|
||||||
|
|
||||||
[dependencies.tracing-actix-web]
|
[dependencies.tracing-actix-web]
|
||||||
version = "0.6.1"
|
version = "0.7.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["emit_event_on_error", "opentelemetry_0_18"]
|
features = ["emit_event_on_error", "opentelemetry_0_18"]
|
||||||
|
|
||||||
|
@ -56,8 +56,8 @@ default-features = false
|
||||||
features = ["emit_event_on_error", "opentelemetry_0_18"]
|
features = ["emit_event_on_error", "opentelemetry_0_18"]
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ructe = "0.15.0"
|
ructe = "0.16.0"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
ructe = { version = "0.15.0", features = ["sass", "mime03"] }
|
ructe = { version = "0.16.0", features = ["sass", "mime03"] }
|
||||||
|
|
|
@ -149,22 +149,22 @@ impl Connection {
|
||||||
|
|
||||||
fn thumbnail_url(&self, size: u16, file: &str, extension: Extension) -> String {
|
fn thumbnail_url(&self, size: u16, file: &str, extension: Extension) -> String {
|
||||||
let mut url = self.upstream.clone();
|
let mut url = self.upstream.clone();
|
||||||
url.set_path(&format!("/image/process.{}", extension));
|
url.set_path(&format!("/image/process.{extension}"));
|
||||||
url.set_query(Some(&format!("src={}&resize={}", file, size)));
|
url.set_query(Some(&format!("src={file}&resize={size}")));
|
||||||
|
|
||||||
url.to_string()
|
url.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image_url(&self, file: &str) -> String {
|
fn image_url(&self, file: &str) -> String {
|
||||||
let mut url = self.upstream.clone();
|
let mut url = self.upstream.clone();
|
||||||
url.set_path(&format!("/image/original/{}", file,));
|
url.set_path(&format!("/image/original/{file}"));
|
||||||
|
|
||||||
url.to_string()
|
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}"));
|
||||||
|
|
||||||
url.to_string()
|
url.to_string()
|
||||||
}
|
}
|
||||||
|
|
76
src/lib.rs
76
src/lib.rs
|
@ -163,9 +163,9 @@ impl State {
|
||||||
} else if s.is_empty() {
|
} else if s.is_empty() {
|
||||||
self.scope.clone()
|
self.scope.clone()
|
||||||
} else if self.scope.is_empty() {
|
} else if self.scope.is_empty() {
|
||||||
format!("/{}", s)
|
format!("/{s}")
|
||||||
} else {
|
} else {
|
||||||
format!("{}/{}", self.scope, s)
|
format!("{}/{s}", self.scope)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,37 +174,31 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn edit_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
|
fn edit_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
|
||||||
self.scoped(&format!("{}?token={}", id, token.token))
|
self.scoped(&format!("{id}?token={}", token.token))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
|
fn update_collection_path(&self, id: Uuid, token: &ValidToken) -> String {
|
||||||
self.scoped(&format!("{}?token={}", id, token.token))
|
self.scoped(&format!("{id}?token={}", token.token))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_collection_path(&self, id: Uuid, token: &ValidToken, confirmed: bool) -> String {
|
fn delete_collection_path(&self, id: Uuid, token: &ValidToken, confirmed: bool) -> String {
|
||||||
if confirmed {
|
if confirmed {
|
||||||
self.scoped(&format!(
|
self.scoped(&format!("{id}/delete?token={}&confirmed=true", token.token))
|
||||||
"{}/delete?token={}&confirmed=true",
|
|
||||||
id, token.token
|
|
||||||
))
|
|
||||||
} else {
|
} else {
|
||||||
self.scoped(&format!("{}/delete?token={}", id, token.token))
|
self.scoped(&format!("{id}/delete?token={}", token.token))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn public_collection_path(&self, id: Uuid) -> String {
|
fn public_collection_path(&self, id: Uuid) -> String {
|
||||||
self.scoped(&format!("{}", id))
|
self.scoped(&format!("{id}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_entry_path(&self, collection_id: Uuid, token: &ValidToken) -> String {
|
fn create_entry_path(&self, collection_id: Uuid, token: &ValidToken) -> String {
|
||||||
self.scoped(&format!("{}/entry?token={}", collection_id, token.token))
|
self.scoped(&format!("{collection_id}/entry?token={}", token.token))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_entry_path(&self, collection_id: Uuid, id: Uuid, token: &ValidToken) -> String {
|
fn update_entry_path(&self, collection_id: Uuid, id: Uuid, token: &ValidToken) -> String {
|
||||||
self.scoped(&format!(
|
self.scoped(&format!("{collection_id}/entry/{id}?token={}", token.token))
|
||||||
"{}/entry/{}?token={}",
|
|
||||||
collection_id, id, token.token
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_entry_path(
|
fn move_entry_path(
|
||||||
|
@ -215,8 +209,8 @@ impl State {
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
) -> String {
|
) -> String {
|
||||||
self.scoped(&format!(
|
self.scoped(&format!(
|
||||||
"{}/entry/{}/move/{}?token={}",
|
"{collection_id}/entry/{id}/move/{direction}?token={}",
|
||||||
collection_id, id, direction, token.token
|
token.token
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,25 +223,24 @@ impl State {
|
||||||
) -> String {
|
) -> String {
|
||||||
if confirmed {
|
if confirmed {
|
||||||
self.scoped(&format!(
|
self.scoped(&format!(
|
||||||
"{}/entry/{}/delete?token={}&confirmed=true",
|
"{collection_id}/entry/{id}/delete?token={}&confirmed=true",
|
||||||
collection_id, id, token.token
|
token.token
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
self.scoped(&format!(
|
self.scoped(&format!(
|
||||||
"{}/entry/{}/delete?token={}",
|
"{collection_id}/entry/{id}/delete?token={}",
|
||||||
collection_id, id, token.token
|
token.token
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn statics_path(&self, file: &str) -> String {
|
fn statics_path(&self, file: &str) -> String {
|
||||||
self.scoped(&format!("static/{}", file))
|
self.scoped(&format!("static/{file}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn thumbnail_path(&self, filename: &str, size: u16, extension: pict::Extension) -> String {
|
fn thumbnail_path(&self, filename: &str, size: u16, extension: pict::Extension) -> String {
|
||||||
self.scoped(&format!(
|
self.scoped(&format!(
|
||||||
"image/thumbnail.{}?src={}&size={}",
|
"image/thumbnail.{extension}?src={filename}&size={size}",
|
||||||
extension, filename, size
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,9 +249,8 @@ impl State {
|
||||||
|
|
||||||
for size in connection::VALID_SIZES {
|
for size in connection::VALID_SIZES {
|
||||||
sizes.push(format!(
|
sizes.push(format!(
|
||||||
"{} {}w",
|
"{} {size}w",
|
||||||
self.thumbnail_path(filename, *size, extension),
|
self.thumbnail_path(filename, *size, extension),
|
||||||
size,
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +258,7 @@ impl State {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn image_path(&self, filename: &str) -> String {
|
fn image_path(&self, filename: &str) -> String {
|
||||||
self.scoped(&format!("image/full/{}", filename))
|
self.scoped(&format!("image/full/{filename}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,6 +277,7 @@ pub fn configure(cfg: &mut web::ServiceConfig, state: State, client: Client) {
|
||||||
client,
|
client,
|
||||||
)))
|
)))
|
||||||
.app_data(web::Data::new(state))
|
.app_data(web::Data::new(state))
|
||||||
|
.route("/healthz", web::get().to(healthz))
|
||||||
.service(web::resource("/static/{filename}").route(web::get().to(static_files)))
|
.service(web::resource("/static/{filename}").route(web::get().to(static_files)))
|
||||||
.service(web::resource("/404").route(web::get().to(not_found)))
|
.service(web::resource("/404").route(web::get().to(not_found)))
|
||||||
.service(
|
.service(
|
||||||
|
@ -355,6 +348,11 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn healthz(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
||||||
|
state.store.check_health().await.stateful(&state)?;
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(name = "Static files")]
|
#[tracing::instrument(name = "Static files")]
|
||||||
async fn static_files(filename: web::Path<String>, state: web::Data<State>) -> HttpResponse {
|
async fn static_files(filename: web::Path<String>, state: web::Data<State>) -> HttpResponse {
|
||||||
let filename = filename.into_inner();
|
let filename = filename.into_inner();
|
||||||
|
@ -377,7 +375,7 @@ async fn static_files(filename: web::Path<String>, state: web::Data<State>) -> H
|
||||||
#[tracing::instrument(name = "Not found")]
|
#[tracing::instrument(name = "Not found")]
|
||||||
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
async fn not_found(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
||||||
rendered(
|
rendered(
|
||||||
|cursor| self::templates::not_found(cursor, &state),
|
|cursor| self::templates::not_found_html(cursor, &state),
|
||||||
HttpResponse::NotFound(),
|
HttpResponse::NotFound(),
|
||||||
)
|
)
|
||||||
.stateful(&state)
|
.stateful(&state)
|
||||||
|
@ -409,7 +407,7 @@ impl ResponseError for StateError {
|
||||||
|
|
||||||
fn error_response(&self) -> HttpResponse {
|
fn error_response(&self) -> HttpResponse {
|
||||||
match rendered(
|
match rendered(
|
||||||
|cursor| self::templates::error(cursor, &self.error.kind.to_string(), &self.state),
|
|cursor| self::templates::error_html(cursor, &self.error.kind.to_string(), &self.state),
|
||||||
HttpResponse::build(self.status_code()),
|
HttpResponse::build(self.status_code()),
|
||||||
) {
|
) {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
|
@ -680,7 +678,7 @@ async fn upload(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!("{}", e);
|
tracing::warn!("{e}");
|
||||||
let _ = store::DeleteEntry {
|
let _ = store::DeleteEntry {
|
||||||
entry_path: &entry_path2,
|
entry_path: &entry_path2,
|
||||||
}
|
}
|
||||||
|
@ -740,7 +738,7 @@ async fn thumbnail(
|
||||||
#[tracing::instrument(name = "Index")]
|
#[tracing::instrument(name = "Index")]
|
||||||
async fn index(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
async fn index(state: web::Data<State>) -> Result<HttpResponse, StateError> {
|
||||||
rendered(
|
rendered(
|
||||||
|cursor| self::templates::index(cursor, &state),
|
|cursor| self::templates::index_html(cursor, &state),
|
||||||
HttpResponse::Ok(),
|
HttpResponse::Ok(),
|
||||||
)
|
)
|
||||||
.stateful(&state)
|
.stateful(&state)
|
||||||
|
@ -777,7 +775,13 @@ async fn view_collection(
|
||||||
|
|
||||||
rendered(
|
rendered(
|
||||||
|cursor| {
|
|cursor| {
|
||||||
self::templates::view_collection(cursor, path.collection, &collection, &entries, &state)
|
self::templates::view_collection_html(
|
||||||
|
cursor,
|
||||||
|
path.collection,
|
||||||
|
&collection,
|
||||||
|
&entries,
|
||||||
|
&state,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
HttpResponse::Ok(),
|
HttpResponse::Ok(),
|
||||||
)
|
)
|
||||||
|
@ -803,7 +807,7 @@ async fn edit_collection(
|
||||||
|
|
||||||
rendered(
|
rendered(
|
||||||
|cursor| {
|
|cursor| {
|
||||||
self::templates::edit_collection(
|
self::templates::edit_collection_html(
|
||||||
cursor,
|
cursor,
|
||||||
&collection,
|
&collection,
|
||||||
path.collection,
|
path.collection,
|
||||||
|
@ -920,7 +924,7 @@ async fn delete_entry(
|
||||||
if !query.confirmed.unwrap_or(false) {
|
if !query.confirmed.unwrap_or(false) {
|
||||||
return rendered(
|
return rendered(
|
||||||
|cursor| {
|
|cursor| {
|
||||||
self::templates::confirm_entry_delete(
|
self::templates::confirm_entry_delete_html(
|
||||||
cursor,
|
cursor,
|
||||||
entry_path.collection,
|
entry_path.collection,
|
||||||
entry_path.entry,
|
entry_path.entry,
|
||||||
|
@ -976,7 +980,7 @@ fn to_svg_string(qr: &qrcodegen::QrCode, border: i32) -> String {
|
||||||
.checked_add(border.checked_mul(2).unwrap())
|
.checked_add(border.checked_mul(2).unwrap())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
result += &format!(
|
result += &format!(
|
||||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n", dimension);
|
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dimension} {dimension}\" stroke=\"none\">\n");
|
||||||
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
|
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
|
||||||
result += "\t<path d=\"";
|
result += "\t<path d=\"";
|
||||||
for y in 0..qr.size() {
|
for y in 0..qr.size() {
|
||||||
|
@ -1012,7 +1016,7 @@ async fn delete_collection(
|
||||||
|
|
||||||
return rendered(
|
return rendered(
|
||||||
|cursor| {
|
|cursor| {
|
||||||
self::templates::confirm_delete(
|
self::templates::confirm_delete_html(
|
||||||
cursor,
|
cursor,
|
||||||
path.collection,
|
path.collection,
|
||||||
&collection,
|
&collection,
|
||||||
|
|
27
src/store.rs
27
src/store.rs
|
@ -1,12 +1,20 @@
|
||||||
use crate::{Collection, CollectionPath, Direction, Entry, EntryPath, MoveEntryPath, Token};
|
use crate::{Collection, CollectionPath, Direction, Entry, EntryPath, MoveEntryPath, Token};
|
||||||
use actix_web::web;
|
use actix_web::web;
|
||||||
use sled::{Db, Tree};
|
use sled::{Db, Tree};
|
||||||
use std::collections::BTreeMap;
|
use std::{
|
||||||
use std::ops::RangeInclusive;
|
collections::BTreeMap,
|
||||||
|
ops::RangeInclusive,
|
||||||
|
sync::{
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
Arc,
|
||||||
|
},
|
||||||
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct Store {
|
pub(crate) struct Store {
|
||||||
|
healthz: Tree,
|
||||||
|
healthz_counter: Arc<AtomicU64>,
|
||||||
tree: Tree,
|
tree: Tree,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,10 +112,25 @@ impl<'a> DeleteEntry<'a> {
|
||||||
impl Store {
|
impl Store {
|
||||||
pub(crate) fn new(db: &Db) -> Result<Self, sled::Error> {
|
pub(crate) fn new(db: &Db) -> Result<Self, sled::Error> {
|
||||||
Ok(Store {
|
Ok(Store {
|
||||||
|
healthz: db.open_tree("healthz")?,
|
||||||
|
healthz_counter: Arc::new(AtomicU64::new(0)),
|
||||||
tree: db.open_tree("collections")?,
|
tree: db.open_tree("collections")?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn check_health(&self) -> Result<(), Error> {
|
||||||
|
let next = self.healthz_counter.fetch_add(1, Ordering::Relaxed);
|
||||||
|
|
||||||
|
let healthz = self.healthz.clone();
|
||||||
|
web::block(move || healthz.insert("healthz", &next.to_be_bytes()[..])).await??;
|
||||||
|
|
||||||
|
self.healthz.flush_async().await?;
|
||||||
|
|
||||||
|
let healthz = self.healthz.clone();
|
||||||
|
web::block(move || healthz.get("healthz")).await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn create_collection(&self, config: CreateCollection<'_>) -> Result<(), Error> {
|
async fn create_collection(&self, config: CreateCollection<'_>) -> Result<(), Error> {
|
||||||
let collection_key = config.collection_path.key();
|
let collection_key = config.collection_path.key();
|
||||||
let collection_value = serde_json::to_string(&config.collection)?;
|
let collection_value = serde_json::to_string(&config.collection)?;
|
||||||
|
|
|
@ -3,22 +3,22 @@
|
||||||
@(text: &str, kind: ButtonKind)
|
@(text: &str, kind: ButtonKind)
|
||||||
|
|
||||||
@match kind {
|
@match kind {
|
||||||
ButtonKind::Submit => {
|
ButtonKind::Submit => {
|
||||||
<div class="button submit">
|
<div class="button submit">
|
||||||
<span>@text</span>
|
<span>@text</span>
|
||||||
<button class="action" type="submit">@text</button>
|
<button class="action" type="submit">@text</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
ButtonKind::Plain => {
|
ButtonKind::Plain => {
|
||||||
<div class="button plain">
|
<div class="button plain">
|
||||||
<span>@text</span>
|
<span>@text</span>
|
||||||
<button class="action">@text</button>
|
<button class="action">@text</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
ButtonKind::Outline => {
|
ButtonKind::Outline => {
|
||||||
<div class="button outline">
|
<div class="button outline">
|
||||||
<span>@text</span>
|
<span>@text</span>
|
||||||
<button class="action">@text</button>
|
<button class="action">@text</button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,23 +3,22 @@
|
||||||
@(text: &str, href: &str, kind: ButtonKind)
|
@(text: &str, href: &str, kind: ButtonKind)
|
||||||
|
|
||||||
@match kind {
|
@match kind {
|
||||||
ButtonKind::Submit => {
|
ButtonKind::Submit => {
|
||||||
<div class="button submit">
|
<div class="button submit">
|
||||||
<span>@text</span>
|
<span>@text</span>
|
||||||
<a class="action" href="@href">@text</a>
|
<a class="action" href="@href">@text</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
ButtonKind::Plain => {
|
ButtonKind::Plain => {
|
||||||
<div class="button plain">
|
<div class="button plain">
|
||||||
<span>@text</span>
|
<span>@text</span>
|
||||||
<a class="action" href="@href">@text</a>
|
<a class="action" href="@href">@text</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
ButtonKind::Outline => {
|
ButtonKind::Outline => {
|
||||||
<div class="button outline">
|
<div class="button outline">
|
||||||
<span>@text</span>
|
<span>@text</span>
|
||||||
<a class="action" href="@href">@text</a>
|
<a class="action" href="@href">@text</a>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
@use crate::{ui::ButtonKind, Collection, State, ValidToken};
|
@use crate::{ui::ButtonKind, Collection, State, ValidToken};
|
||||||
@use super::{layout, button_link, return_home};
|
@use super::{layout_html, button_link_html, return_home_html};
|
||||||
@use uuid::Uuid;
|
@use uuid::Uuid;
|
||||||
|
|
||||||
@(id: Uuid, collection: &Collection, token: &ValidToken, state: &State)
|
@(id: Uuid, collection: &Collection, token: &ValidToken, state: &State)
|
||||||
|
|
||||||
@:layout(state, &format!("Delete: {}", collection.title), Some(&format!("Are you sure you want to delete {}", collection.title)), {
|
@:layout_html(state, &format!("Delete: {}", collection.title), Some(&format!("Are you sure you want to delete {}",
|
||||||
<meta property="og:url" content="@state.delete_collection_path(id, token, false)" />
|
collection.title)), {
|
||||||
|
<meta property="og:url" content="@state.delete_collection_path(id, token, false)" />
|
||||||
}, {
|
}, {
|
||||||
<section>
|
<section>
|
||||||
<article>
|
<article>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<h3>Delete @collection.title</h3>
|
<h3>Delete @collection.title</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<p class="subtitle">Are you sure you want to delete @collection.title</p>
|
<p class="subtitle">Are you sure you want to delete @collection.title</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
@:button_link("Delete Collection", &state.delete_collection_path(id, token, true), ButtonKind::Submit)
|
@:button_link_html("Delete Collection", &state.delete_collection_path(id, token, true), ButtonKind::Submit)
|
||||||
@:button_link("Cancel", &state.edit_collection_path(id, token), ButtonKind::Outline)
|
@:button_link_html("Cancel", &state.edit_collection_path(id, token), ButtonKind::Outline)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@:return_home(state)
|
@:return_home_html(state)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
@use crate::{ui::ButtonKind, Entry, State, ValidToken};
|
@use crate::{ui::ButtonKind, Entry, State, ValidToken};
|
||||||
@use super::{layout, button_link, image, return_home};
|
@use super::{layout_html, button_link_html, image_html, return_home_html};
|
||||||
@use uuid::Uuid;
|
@use uuid::Uuid;
|
||||||
|
|
||||||
@(collection_id: Uuid, id: Uuid, entry: &Entry, token: &ValidToken, state: &State)
|
@(collection_id: Uuid, id: Uuid, entry: &Entry, token: &ValidToken, state: &State)
|
||||||
|
|
||||||
@:layout(state, "Delete Image", Some("Are you sure you want to delete this image?"), {
|
@:layout_html(state, "Delete Image", Some("Are you sure you want to delete this image?"), {
|
||||||
<meta property="og:url" content="@state.delete_entry_path(collection_id, id, token, false)" />
|
<meta property="og:url" content="@state.delete_entry_path(collection_id, id, token, false)" />
|
||||||
}, {
|
}, {
|
||||||
<section>
|
<section>
|
||||||
<article>
|
<article>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<h3>Delete Image</h3>
|
<h3>Delete Image</h3>
|
||||||
|
</div>
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="edit-row">
|
||||||
|
<div class="edit-item">
|
||||||
|
@:image_html(entry, state)
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group">
|
<div class="edit-item">
|
||||||
<div class="edit-row">
|
<p class="delete-confirmation">Are you sure you want to delete this image?</p>
|
||||||
<div class="edit-item">
|
<div class="button-group button-space">
|
||||||
@:image(entry, state)
|
@:button_link_html("Delete Image", &state.delete_entry_path(collection_id, id, token, true),
|
||||||
</div>
|
ButtonKind::Submit)
|
||||||
<div class="edit-item">
|
@:button_link_html("Cancel", &state.edit_collection_path(collection_id, token), ButtonKind::Outline)
|
||||||
<p class="delete-confirmation">Are you sure you want to delete this image?</p>
|
</div>
|
||||||
<div class="button-group button-space">
|
|
||||||
@:button_link("Delete Image", &state.delete_entry_path(collection_id, id, token, true), ButtonKind::Submit)
|
|
||||||
@:button_link("Cancel", &state.edit_collection_path(collection_id, token), ButtonKind::Outline)
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@:return_home(state)
|
@:return_home_html(state)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
@use crate::{ui::ButtonKind, Collection, Direction, Entry, State, ValidToken};
|
@use crate::{ui::ButtonKind, Collection, Direction, Entry, State, ValidToken};
|
||||||
@use super::{button, button_link, image, file_input, layout, return_home, text_area, text_input,
|
@use super::{button_html, button_link_html, image_html, file_input_html, layout_html, return_home_html, text_area_html,
|
||||||
|
text_input_html,
|
||||||
statics::file_upload_js};
|
statics::file_upload_js};
|
||||||
@use uuid::Uuid;
|
@use uuid::Uuid;
|
||||||
|
|
||||||
@(collection: &Collection, collection_id: Uuid, entries: &[(Uuid, Entry)], token: &ValidToken, state: &State, qr: &str)
|
@(collection: &Collection, collection_id: Uuid, entries: &[(Uuid, Entry)], token: &ValidToken, state: &State, qr: &str)
|
||||||
|
|
||||||
@:layout(state, "Edit Collection", None, {
|
@:layout_html(state, "Edit Collection", None, {
|
||||||
<script src="@state.statics_path(file_upload_js.name)" type="text/javascript">
|
<script src="@state.statics_path(file_upload_js.name)" type="text/javascript">
|
||||||
</script>
|
</script>
|
||||||
}, {
|
}, {
|
||||||
|
@ -33,11 +34,11 @@ statics::file_upload_js};
|
||||||
</article>
|
</article>
|
||||||
<article class="content-group">
|
<article class="content-group">
|
||||||
<form method="POST" action="@state.update_collection_path(collection_id, token)">
|
<form method="POST" action="@state.update_collection_path(collection_id, token)">
|
||||||
@:text_input("title", Some("Collection Title"), Some(&collection.title), false)
|
@:text_input_html("title", Some("Collection Title"), Some(&collection.title), false)
|
||||||
@:text_area("description", Some("Collection Description"), Some(&collection.description), false)
|
@:text_area_html("description", Some("Collection Description"), Some(&collection.description), false)
|
||||||
<div class="button-group button-space">
|
<div class="button-group button-space">
|
||||||
@:button("Update Collection", ButtonKind::Submit)
|
@:button_html("Update Collection", ButtonKind::Submit)
|
||||||
@:button_link("Delete Collection", &state.delete_collection_path(collection_id, token, false),
|
@:button_link_html("Delete Collection", &state.delete_collection_path(collection_id, token, false),
|
||||||
ButtonKind::Outline)
|
ButtonKind::Outline)
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -48,37 +49,37 @@ statics::file_upload_js};
|
||||||
<article>
|
<article>
|
||||||
<div class="edit-row">
|
<div class="edit-row">
|
||||||
<div class="edit-item">
|
<div class="edit-item">
|
||||||
@:image(entry, state)
|
@:image_html(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)">
|
||||||
@:text_input("title", Some("Image Title"), entry.title.as_deref(), entry.file_parts().is_none())
|
@:text_input_html("title", Some("Image Title"), entry.title.as_deref(), entry.file_parts().is_none())
|
||||||
@:text_area("description", Some("Image Description"), entry.description.as_deref(),
|
@:text_area_html("description", Some("Image Description"), entry.description.as_deref(),
|
||||||
entry.file_parts().is_none())
|
entry.file_parts().is_none())
|
||||||
@:text_input("link", Some("Image Link"), entry.link.as_ref().map(|l| l.as_str()),
|
@:text_input_html("link", Some("Image Link"), entry.link.as_ref().map(|l| l.as_str()),
|
||||||
entry.file_parts().is_none())
|
entry.file_parts().is_none())
|
||||||
|
|
||||||
<div class="button-group button-space">
|
<div class="button-group button-space">
|
||||||
@if let Some(upload_id) = entry.upload_id() {
|
@if let Some(upload_id) = entry.upload_id() {
|
||||||
<input type="hidden" name="upload_id" value="@upload_id" />
|
<input type="hidden" name="upload_id" value="@upload_id" />
|
||||||
@:button_link("Refresh", &state.edit_collection_path(collection_id, token), ButtonKind::Submit)
|
@:button_link_html("Refresh", &state.edit_collection_path(collection_id, token), ButtonKind::Submit)
|
||||||
}
|
}
|
||||||
@if let Some((filename, delete_token)) = entry.file_parts() {
|
@if let Some((filename, delete_token)) = entry.file_parts() {
|
||||||
<input type="hidden" name="filename" value="@filename" />
|
<input type="hidden" name="filename" value="@filename" />
|
||||||
<input type="hidden" name="delete_token" value="@delete_token" />
|
<input type="hidden" name="delete_token" value="@delete_token" />
|
||||||
@:button("Update Image", ButtonKind::Submit)
|
@:button_html("Update Image", ButtonKind::Submit)
|
||||||
@:button_link("Delete Image", &state.delete_entry_path(collection_id, *id, token, false),
|
@:button_link_html("Delete Image", &state.delete_entry_path(collection_id, *id, token, false),
|
||||||
ButtonKind::Outline)
|
ButtonKind::Outline)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group button-space">
|
<div class="button-group button-space">
|
||||||
@if i != 0 {
|
@if i != 0 {
|
||||||
@:button_link("Move Up", &state.move_entry_path(collection_id, *id, token, Direction::Up),
|
@:button_link_html("Move Up", &state.move_entry_path(collection_id, *id, token, Direction::Up),
|
||||||
ButtonKind::Outline)
|
ButtonKind::Outline)
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (i + 1) != entries.len() {
|
@if (i + 1) != entries.len() {
|
||||||
@:button_link("Move Down", &state.move_entry_path(collection_id, *id, token, Direction::Down),
|
@:button_link_html("Move Down", &state.move_entry_path(collection_id, *id, token, Direction::Down),
|
||||||
ButtonKind::Outline)
|
ButtonKind::Outline)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@ -100,14 +101,14 @@ statics::file_upload_js};
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group" id="file-input-container">
|
<div class="content-group" id="file-input-container">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
@:file_input("images[]", Some("Select Image"), Some(crate::accept()), false)
|
@:file_input_html("images[]", Some("Select Image"), Some(crate::accept()), false)
|
||||||
</div>
|
</div>
|
||||||
<div class="button-group button-space">
|
<div class="button-group button-space">
|
||||||
@:button("Upload", ButtonKind::Submit)
|
@:button_html("Upload", ButtonKind::Submit)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@:return_home(state)
|
@:return_home_html(state)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
@use crate::State;
|
@use crate::State;
|
||||||
@use super::{layout, return_home};
|
@use super::{layout_html, return_home_html};
|
||||||
|
|
||||||
@(error: &str, state: &State)
|
@(error: &str, state: &State)
|
||||||
|
|
||||||
@:layout(state, "Error", Some(error), {}, {
|
@:layout_html(state, "Error", Some(error), {}, {
|
||||||
<section>
|
<section>
|
||||||
<article>
|
<article>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<h3>Error</h3>
|
<h3>Error</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<p class="subtitle">@error</p>
|
<p class="subtitle">@error</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
@:return_home(state)
|
@:return_home_html(state)
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
@use crate::{Entry, State};
|
@use crate::{Entry, State};
|
||||||
@use super::image_preview;
|
@use super::image_preview_html;
|
||||||
|
|
||||||
@(entry: &Entry, state: &State)
|
@(entry: &Entry, state: &State)
|
||||||
|
|
||||||
@:image_preview(entry, state)
|
@:image_preview_html(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,9 +1,9 @@
|
||||||
@use crate::{ui::ButtonKind, State};
|
@use crate::{ui::ButtonKind, State};
|
||||||
@use super::{layout, text_area, text_input, button};
|
@use super::{layout_html, text_area_html, text_input_html, button_html};
|
||||||
|
|
||||||
@(state: &State)
|
@(state: &State)
|
||||||
|
|
||||||
@:layout(state, "Collection", None, {}, {
|
@:layout_html(state, "Collection", None, {}, {
|
||||||
<section>
|
<section>
|
||||||
<article>
|
<article>
|
||||||
<form method="POST" action="@state.create_collection_path()">
|
<form method="POST" action="@state.create_collection_path()">
|
||||||
|
@ -13,12 +13,12 @@
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
@:text_input("title", Some("Title"), None, false)
|
@:text_input_html("title", Some("Title"), None, false)
|
||||||
@:text_area("description", Some("Description"), None, false)
|
@:text_area_html("description", Some("Description"), None, false)
|
||||||
</div>
|
</div>
|
||||||
<div class="content-group">
|
<div class="content-group">
|
||||||
<div class="button-group">
|
<div class="button-group">
|
||||||
@:button("Create Collection", ButtonKind::Submit)
|
@:button_html("Create Collection", ButtonKind::Submit)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
@use crate::State;
|
@use crate::State;
|
||||||
@use super::layout;
|
@use super::layout_html;
|
||||||
|
|
||||||
@(state: &State)
|
@(state: &State)
|
||||||
|
|
||||||
@:layout(state, "Not Found", None, {}, {
|
@:layout_html(state, "Not Found", None, {}, {
|
||||||
<section>
|
<section>
|
||||||
<article class="content-group">
|
<article class="content-group">
|
||||||
<h3>Not Found</h3>
|
<h3>Not Found</h3>
|
||||||
</article>
|
</article>
|
||||||
<article class="content-group">
|
<article class="content-group">
|
||||||
<p><a href="@state.create_collection_path()">Return Home</a></p>
|
<p><a href="@state.create_collection_path()">Return Home</a></p>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
@use crate::{Collection, Entry, State};
|
@use crate::{Collection, Entry, State};
|
||||||
@use super::{layout, image, return_home};
|
@use super::{layout_html, image_html, return_home_html};
|
||||||
@use uuid::Uuid;
|
@use uuid::Uuid;
|
||||||
|
|
||||||
@(id: Uuid, collection: &Collection, entries: &[(Uuid, Entry)], state: &State)
|
@(id: Uuid, collection: &Collection, entries: &[(Uuid, Entry)], state: &State)
|
||||||
|
|
||||||
@:layout(state, &collection.title, Some(&collection.description), {
|
@:layout_html(state, &collection.title, Some(&collection.description), {
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content="@state.public_collection_path(id)" />
|
<meta property="twitter:url" content="@state.public_collection_path(id)" />
|
||||||
<meta property="og:url" content="@state.public_collection_path(id)" />
|
<meta property="og:url" content="@state.public_collection_path(id)" />
|
||||||
|
@ -31,11 +31,11 @@
|
||||||
@for (_, entry) in entries {
|
@for (_, entry) in entries {
|
||||||
<li class="content-group even">
|
<li class="content-group even">
|
||||||
<article>
|
<article>
|
||||||
@:image(entry, state)
|
@:image_html(entry, state)
|
||||||
</article>
|
</article>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
@:return_home(state)
|
@:return_home_html(state)
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue