This commit is contained in:
parent
d3f9baaf4d
commit
db83d0775f
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -1290,6 +1290,7 @@ dependencies = [
|
|||
"minify-html",
|
||||
"opentelemetry",
|
||||
"opentelemetry-otlp",
|
||||
"qrcodegen",
|
||||
"ructe",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -1433,6 +1434,12 @@ dependencies = [
|
|||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "qrcodegen"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "135e6754eed8ca897dd70584d895e72e36860b3e163b6bcedce48571cbaef343"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.15"
|
||||
|
|
|
@ -25,6 +25,7 @@ mime = "0.3"
|
|||
minify-html = "0.8.0"
|
||||
opentelemetry = { version = "0.17", features = ["rt-tokio"] }
|
||||
opentelemetry-otlp = "0.10"
|
||||
qrcodegen = "1.7"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sled = { version = "0.34.7", features = ["zstd"] }
|
||||
|
|
|
@ -19,6 +19,16 @@ section {
|
|||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.qr {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
svg {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.content-group {
|
||||
padding: 16px 32px;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
|
|
49
src/lib.rs
49
src/lib.rs
|
@ -632,9 +632,10 @@ async fn collection(
|
|||
path: web::Path<CollectionPath>,
|
||||
token: Option<ValidToken>,
|
||||
state: web::Data<State>,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, StateError> {
|
||||
match token {
|
||||
Some(token) => edit_collection(path, token, state.clone())
|
||||
Some(token) => edit_collection(path, token, state.clone(), req)
|
||||
.await
|
||||
.stateful(&state),
|
||||
None => view_collection(path, state.clone()).await.stateful(&state),
|
||||
|
@ -668,7 +669,10 @@ async fn edit_collection(
|
|||
path: web::Path<CollectionPath>,
|
||||
token: ValidToken,
|
||||
state: web::Data<State>,
|
||||
req: HttpRequest,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let qr = qr(&req, &path, &state);
|
||||
|
||||
let collection = match state.store.collection(&path).await? {
|
||||
Some(collection) => collection,
|
||||
None => return Ok(to_404(&state)),
|
||||
|
@ -687,6 +691,7 @@ async fn edit_collection(
|
|||
&entries,
|
||||
&token,
|
||||
&state,
|
||||
&qr,
|
||||
)
|
||||
},
|
||||
HttpResponse::Ok(),
|
||||
|
@ -824,6 +829,48 @@ async fn delete_entry(
|
|||
Ok(to_edit_page(entry_path.collection, &token, &state))
|
||||
}
|
||||
|
||||
fn qr(req: &HttpRequest, path: &web::Path<CollectionPath>, state: &web::Data<State>) -> String {
|
||||
let host = req.head().headers().get("host").unwrap();
|
||||
|
||||
let url = format!(
|
||||
"https://{}{}",
|
||||
host.to_str().unwrap(),
|
||||
state.public_collection_path(path.collection)
|
||||
);
|
||||
|
||||
let code = qrcodegen::QrCode::encode_text(&url, qrcodegen::QrCodeEcc::Low).unwrap();
|
||||
|
||||
to_svg_string(&code, 4)
|
||||
}
|
||||
|
||||
fn to_svg_string(qr: &qrcodegen::QrCode, border: i32) -> String {
|
||||
assert!(border >= 0, "Border must be non-negative");
|
||||
let mut result = String::new();
|
||||
// result += "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
|
||||
// result += "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n";
|
||||
let dimension = qr
|
||||
.size()
|
||||
.checked_add(border.checked_mul(2).unwrap())
|
||||
.unwrap();
|
||||
result += &format!(
|
||||
"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {0} {0}\" stroke=\"none\">\n", dimension);
|
||||
result += "\t<rect width=\"100%\" height=\"100%\" fill=\"#FFFFFF\"/>\n";
|
||||
result += "\t<path d=\"";
|
||||
for y in 0..qr.size() {
|
||||
for x in 0..qr.size() {
|
||||
if qr.get_module(x, y) {
|
||||
if x != 0 || y != 0 {
|
||||
result += " ";
|
||||
}
|
||||
result += &format!("M{},{}h1v1h-1z", x + border, y + border);
|
||||
}
|
||||
}
|
||||
}
|
||||
result += "\" fill=\"#000000\"/>\n";
|
||||
result += "</svg>\n";
|
||||
result
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "Delete Collection")]
|
||||
async fn delete_collection(
|
||||
path: web::Path<CollectionPath>,
|
||||
|
|
|
@ -1,103 +1,104 @@
|
|||
@use crate::{ui::ButtonKind, Collection, Direction, Entry, State, ValidToken};
|
||||
@use super::{button, button_link, image, file_input, layout, return_home, text_area, text_input, statics::file_upload_js};
|
||||
@use super::{button, button_link, image, file_input, layout, return_home, text_area, text_input,
|
||||
statics::file_upload_js};
|
||||
@use uuid::Uuid;
|
||||
|
||||
@(collection: &Collection, collection_id: Uuid, entries: &[(Uuid, Entry)], token: &ValidToken, state: &State)
|
||||
@(collection: &Collection, collection_id: Uuid, entries: &[(Uuid, Entry)], token: &ValidToken, state: &State, qr: &str)
|
||||
|
||||
@:layout(state, "Edit Collection", None, {
|
||||
<script
|
||||
src="@state.statics_path(file_upload_js.name)"
|
||||
type="text/javascript"
|
||||
>
|
||||
</script>
|
||||
<script src="@state.statics_path(file_upload_js.name)" type="text/javascript">
|
||||
</script>
|
||||
}, {
|
||||
<section>
|
||||
<article class="content-group">
|
||||
<h3>Share Collection</h3>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<a
|
||||
href="@state.public_collection_path(collection_id)"
|
||||
target="_blank"
|
||||
rel="noopen noreferer"
|
||||
>
|
||||
Public Link
|
||||
</a>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<h3>Share Collection</h3>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<a href="@state.public_collection_path(collection_id)" target="_blank" rel="noopen noreferer">
|
||||
Public Link
|
||||
</a>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<div class="qr">
|
||||
@Html(qr)
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<section>
|
||||
<article class="content-group">
|
||||
<h3>Edit Collection</h3>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<p class="subtitle"><a href="@state.edit_collection_path(collection_id, token)">Do not lose this link</a></p>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<form method="POST" action="@state.update_collection_path(collection_id, token)">
|
||||
@:text_input("title", Some("Collection Title"), Some(&collection.title))
|
||||
@:text_area("description", Some("Collection Description"), Some(&collection.description))
|
||||
<div class="button-group button-space">
|
||||
@:button("Update Collection", ButtonKind::Submit)
|
||||
@:button_link("Delete Collection", &state.delete_collection_path(collection_id, token, false), ButtonKind::Outline)
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<ul>
|
||||
@for (i, (id, entry)) in entries.iter().enumerate() {
|
||||
<li class="content-group">
|
||||
<article>
|
||||
<div class="edit-row">
|
||||
<div class="edit-item">
|
||||
@:image(entry, state)
|
||||
</div>
|
||||
<div class="edit-item">
|
||||
<form method="POST" action="@state.update_entry_path(collection_id, *id, token)">
|
||||
@:text_input("title", Some("Image Title"), Some(&entry.title))
|
||||
@:text_area("description", Some("Image Description"), Some(&entry.description))
|
||||
<input type="hidden" name="filename" value="@entry.filename" />
|
||||
<input type="hidden" name="delete_token" value="@entry.delete_token" />
|
||||
<div class="button-group button-space">
|
||||
@:button("Update Image", ButtonKind::Submit)
|
||||
@:button_link("Delete Image", &state.delete_entry_path(collection_id, *id, token, false), ButtonKind::Outline)
|
||||
</div>
|
||||
<article class="content-group">
|
||||
<h3>Edit Collection</h3>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<p class="subtitle"><a href="@state.edit_collection_path(collection_id, token)">Do not lose this link</a></p>
|
||||
</article>
|
||||
<article class="content-group">
|
||||
<form method="POST" action="@state.update_collection_path(collection_id, token)">
|
||||
@:text_input("title", Some("Collection Title"), Some(&collection.title))
|
||||
@:text_area("description", Some("Collection Description"), Some(&collection.description))
|
||||
<div class="button-group button-space">
|
||||
@:button("Update Collection", ButtonKind::Submit)
|
||||
@:button_link("Delete Collection", &state.delete_collection_path(collection_id, token, false),
|
||||
ButtonKind::Outline)
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<ul>
|
||||
@for (i, (id, entry)) in entries.iter().enumerate() {
|
||||
<li class="content-group">
|
||||
<article>
|
||||
<div class="edit-row">
|
||||
<div class="edit-item">
|
||||
@:image(entry, state)
|
||||
</div>
|
||||
<div class="edit-item">
|
||||
<form method="POST" action="@state.update_entry_path(collection_id, *id, token)">
|
||||
@:text_input("title", Some("Image Title"), Some(&entry.title))
|
||||
@:text_area("description", Some("Image Description"), Some(&entry.description))
|
||||
<input type="hidden" name="filename" value="@entry.filename" />
|
||||
<input type="hidden" name="delete_token" value="@entry.delete_token" />
|
||||
<div class="button-group button-space">
|
||||
@:button("Update Image", ButtonKind::Submit)
|
||||
@:button_link("Delete Image", &state.delete_entry_path(collection_id, *id, token, false),
|
||||
ButtonKind::Outline)
|
||||
</div>
|
||||
|
||||
<div class="button-group button-space">
|
||||
@if i != 0 {
|
||||
@:button_link("Move Up", &state.move_entry_path(collection_id, *id, token, Direction::Up), ButtonKind::Outline)
|
||||
}
|
||||
<div class="button-group button-space">
|
||||
@if i != 0 {
|
||||
@:button_link("Move Up", &state.move_entry_path(collection_id, *id, token, Direction::Up),
|
||||
ButtonKind::Outline)
|
||||
}
|
||||
|
||||
@if (i + 1) != entries.len() {
|
||||
@:button_link("Move Down", &state.move_entry_path(collection_id, *id, token, Direction::Down), ButtonKind::Outline)
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
@if (i + 1) != entries.len() {
|
||||
@:button_link("Move Down", &state.move_entry_path(collection_id, *id, token, Direction::Down),
|
||||
ButtonKind::Outline)
|
||||
}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<article>
|
||||
<form
|
||||
method="POST"
|
||||
action="@state.create_entry_path(collection_id, token)"
|
||||
enctype="multipart/form-data"
|
||||
>
|
||||
<div class="content-group">
|
||||
<h3><legend>Add Image</legend></h3>
|
||||
</div>
|
||||
<div class="content-group" id="file-input-container">
|
||||
<div class="button-group">
|
||||
@:file_input("images[]", Some("Select Image"), Some(crate::accept()), false)
|
||||
</div>
|
||||
<div class="button-group button-space">
|
||||
@:button("Upload", ButtonKind::Submit)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
<article>
|
||||
<form method="POST" action="@state.create_entry_path(collection_id, token)" enctype="multipart/form-data">
|
||||
<div class="content-group">
|
||||
<h3>
|
||||
<legend>Add Image</legend>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="content-group" id="file-input-container">
|
||||
<div class="button-group">
|
||||
@:file_input("images[]", Some("Select Image"), Some(crate::accept()), false)
|
||||
</div>
|
||||
<div class="button-group button-space">
|
||||
@:button("Upload", ButtonKind::Submit)
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</article>
|
||||
</section>
|
||||
@:return_home(state)
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue