Add QR Code
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Aode (lion) 2022-02-18 21:08:56 -05:00
parent d3f9baaf4d
commit db83d0775f
5 changed files with 154 additions and 88 deletions

7
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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;

View file

@ -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>,

View file

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