WIP: adding qrcodes
This commit is contained in:
parent
91543e2d74
commit
93472c061e
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -465,6 +465,7 @@ version = "0.3.1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"enum-map",
|
"enum-map",
|
||||||
"fluent 0.15.0",
|
"fluent 0.15.0",
|
||||||
|
"image",
|
||||||
"qrcode",
|
"qrcode",
|
||||||
"seed",
|
"seed",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -25,6 +25,7 @@ description = "Build client in release mode"
|
|||||||
args = ["build", "app", "--target", "web", "--out-name", "app", "--release"]
|
args = ["build", "app", "--target", "web", "--out-name", "app", "--release"]
|
||||||
|
|
||||||
[tasks.build_server]
|
[tasks.build_server]
|
||||||
|
env = { SQLX_OFFLINE = 1 }
|
||||||
description = "Build server"
|
description = "Build server"
|
||||||
command = "cargo"
|
command = "cargo"
|
||||||
args = ["build", "--package", "pslink"]
|
args = ["build", "--package", "pslink"]
|
||||||
|
@ -24,5 +24,6 @@ strum_macros = "0.20"
|
|||||||
strum = "0.20"
|
strum = "0.20"
|
||||||
enum-map = "1"
|
enum-map = "1"
|
||||||
qrcode = "0.12"
|
qrcode = "0.12"
|
||||||
|
image = "0.23"
|
||||||
|
|
||||||
shared = { path = "../shared" }
|
shared = { path = "../shared" }
|
||||||
|
@ -2,10 +2,11 @@ use std::cell::RefCell;
|
|||||||
|
|
||||||
use enum_map::EnumMap;
|
use enum_map::EnumMap;
|
||||||
use fluent::fluent_args;
|
use fluent::fluent_args;
|
||||||
|
use image::{DynamicImage, ImageOutputFormat, Luma};
|
||||||
use qrcode::{render::svg, QrCode};
|
use qrcode::{render::svg, QrCode};
|
||||||
use seed::{
|
use seed::{
|
||||||
a, attrs, button, div, h1, img, input, log, prelude::*, raw, section, span, table, td, th, tr,
|
a, attrs, button, div, h1, img, input, log, nodes, prelude::*, raw, section, span, table, td,
|
||||||
Url, C,
|
th, tr, Url, C,
|
||||||
};
|
};
|
||||||
|
|
||||||
use shared::{
|
use shared::{
|
||||||
@ -14,10 +15,10 @@ use shared::{
|
|||||||
general::{EditMode, Message, Operation, Status},
|
general::{EditMode, Message, Operation, Status},
|
||||||
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
|
links::{LinkDelta, LinkOverviewColumns, LinkRequestForm},
|
||||||
},
|
},
|
||||||
datatypes::FullLink,
|
datatypes::{FullLink, Loadable},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{get_host, i18n::I18n, unwrap_or_return, unwrap_or_send};
|
use crate::{get_host, i18n::I18n, unwrap_or_return};
|
||||||
|
|
||||||
/// Setup the page
|
/// Setup the page
|
||||||
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
||||||
@ -35,9 +36,7 @@ pub fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Model {
|
|||||||
i18n, // to translate
|
i18n, // to translate
|
||||||
formconfig: LinkRequestForm::default(), // when requesting links the form is stored here
|
formconfig: LinkRequestForm::default(), // when requesting links the form is stored here
|
||||||
inputs: EnumMap::default(), // the input fields for the searches
|
inputs: EnumMap::default(), // the input fields for the searches
|
||||||
edit_link, // if set this will trigger a link edit dialog
|
dialog: Dialog::None,
|
||||||
last_message: None, // if a message to the user is recieved
|
|
||||||
question: None, // some operations should be confirmed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,9 +46,47 @@ pub struct Model {
|
|||||||
i18n: I18n, // to translate
|
i18n: I18n, // to translate
|
||||||
formconfig: LinkRequestForm, // when requesting links the form is stored here
|
formconfig: LinkRequestForm, // when requesting links the form is stored here
|
||||||
inputs: EnumMap<LinkOverviewColumns, FilterInput>, // the input fields for the searches
|
inputs: EnumMap<LinkOverviewColumns, FilterInput>, // the input fields for the searches
|
||||||
edit_link: Option<RefCell<LinkDelta>>, // if set this will trigger a link edit dialog
|
dialog: Dialog, // User interaction - there can only ever be one dialog open.
|
||||||
last_message: Option<Status>, // if a message to the user is recieved
|
}
|
||||||
question: Option<EditMsg>, // some operations should be confirmed
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum Dialog {
|
||||||
|
EditLink {
|
||||||
|
link_delta: RefCell<LinkDelta>,
|
||||||
|
qr: Loadable<QrGuard>,
|
||||||
|
},
|
||||||
|
Message(Status),
|
||||||
|
Question(EditMsg),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct QrGuard {
|
||||||
|
svg: String,
|
||||||
|
url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl QrGuard {
|
||||||
|
fn new(link_delta: RefCell<LinkDelta>) -> Self {
|
||||||
|
log!("Generating new QrCode");
|
||||||
|
let link_delta = link_delta.borrow();
|
||||||
|
let svg = generate_qr_from_code(&link_delta.code);
|
||||||
|
use std::array;
|
||||||
|
use std::iter::FromIterator;
|
||||||
|
|
||||||
|
let mut properties = web_sys::BlobPropertyBag::new();
|
||||||
|
properties.type_("image/png");
|
||||||
|
let png_vec = generate_qr_png(&link_delta.code);
|
||||||
|
|
||||||
|
let png_jsarray: JsValue = js_sys::Uint8Array::from(&png_vec[..]).into();
|
||||||
|
// the buffer has to be an array of arrays
|
||||||
|
let png_buffer = js_sys::Array::from_iter(array::IntoIter::new([png_jsarray]));
|
||||||
|
let png_blob =
|
||||||
|
web_sys::Blob::new_with_buffer_source_sequence_and_options(&png_buffer, &properties)
|
||||||
|
.unwrap();
|
||||||
|
let url = web_sys::Url::create_object_url_with_blob(&png_blob).unwrap();
|
||||||
|
Self { svg, url }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
@ -76,9 +113,10 @@ pub enum QueryMsg {
|
|||||||
AuthorFilterChanged(String),
|
AuthorFilterChanged(String),
|
||||||
}
|
}
|
||||||
/// All the messages on link editing
|
/// All the messages on link editing
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum EditMsg {
|
pub enum EditMsg {
|
||||||
EditSelected(LinkDelta),
|
EditSelected(LinkDelta),
|
||||||
|
QrGeneration(Loadable<QrGuard>),
|
||||||
CreateNewLink,
|
CreateNewLink,
|
||||||
Created(Status),
|
Created(Status),
|
||||||
EditCodeChanged(String),
|
EditCodeChanged(String),
|
||||||
@ -94,9 +132,7 @@ pub enum EditMsg {
|
|||||||
|
|
||||||
/// hide all dialogs
|
/// hide all dialogs
|
||||||
fn clear_all(model: &mut Model) {
|
fn clear_all(model: &mut Model) {
|
||||||
model.edit_link = None;
|
model.dialog = Dialog::None;
|
||||||
model.last_message = None;
|
|
||||||
model.question = None;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// React to environment changes
|
/// React to environment changes
|
||||||
@ -107,7 +143,7 @@ pub fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
|||||||
Msg::ClearAll => clear_all(model),
|
Msg::ClearAll => clear_all(model),
|
||||||
Msg::SetMessage(msg) => {
|
Msg::SetMessage(msg) => {
|
||||||
clear_all(model);
|
clear_all(model);
|
||||||
model.last_message = Some(Status::Error(Message { message: msg }));
|
model.dialog = Dialog::Message(Status::Error(Message { message: msg }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,37 +264,80 @@ fn load_links(model: &Model, orders: &mut impl Orders<Msg>) {
|
|||||||
pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||||
match msg {
|
match msg {
|
||||||
EditMsg::EditSelected(link) => {
|
EditMsg::EditSelected(link) => {
|
||||||
clear_all(model);
|
let link_delta = RefCell::new(link);
|
||||||
model.edit_link = Some(RefCell::new(link))
|
model.dialog = Dialog::EditLink {
|
||||||
|
link_delta: link_delta.clone(),
|
||||||
|
qr: Loadable::Data(None),
|
||||||
|
};
|
||||||
|
log!("#loaded dialog");
|
||||||
|
orders.perform_cmd(async move {
|
||||||
|
let qr_code = Loadable::Data(Some(QrGuard::new(link_delta)));
|
||||||
|
Msg::Edit(EditMsg::QrGeneration(qr_code))
|
||||||
|
});
|
||||||
|
orders.force_render_now();
|
||||||
|
log!("#after async");
|
||||||
|
}
|
||||||
|
EditMsg::QrGeneration(qr_code) => {
|
||||||
|
log!("#In generate");
|
||||||
|
let new_dialog = if let Dialog::EditLink {
|
||||||
|
ref link_delta,
|
||||||
|
qr: _,
|
||||||
|
} = model.dialog
|
||||||
|
{
|
||||||
|
Some(Dialog::EditLink {
|
||||||
|
link_delta: link_delta.clone(),
|
||||||
|
qr: qr_code,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
log!("#done generating");
|
||||||
|
|
||||||
|
if let Some(dialog) = new_dialog {
|
||||||
|
model.dialog = dialog;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EditMsg::CreateNewLink => {
|
EditMsg::CreateNewLink => {
|
||||||
clear_all(model);
|
clear_all(model);
|
||||||
model.edit_link = Some(RefCell::new(LinkDelta::default()))
|
model.dialog = Dialog::EditLink {
|
||||||
|
link_delta: RefCell::new(LinkDelta::default()),
|
||||||
|
qr: Loadable::Data(None),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EditMsg::Created(success_msg) => {
|
EditMsg::Created(success_msg) => {
|
||||||
clear_all(model);
|
clear_all(model);
|
||||||
model.last_message = Some(success_msg);
|
model.dialog = Dialog::Message(success_msg);
|
||||||
orders.send_msg(Msg::Query(QueryMsg::Fetch));
|
orders.send_msg(Msg::Query(QueryMsg::Fetch));
|
||||||
}
|
}
|
||||||
EditMsg::EditCodeChanged(s) => {
|
EditMsg::EditCodeChanged(s) => {
|
||||||
if let Some(ref le) = model.edit_link {
|
if let Dialog::EditLink { ref link_delta, .. } = model.dialog {
|
||||||
le.try_borrow_mut().expect("Failed to borrow mutably").code = s;
|
link_delta
|
||||||
|
.try_borrow_mut()
|
||||||
|
.expect("Failed to borrow mutably")
|
||||||
|
.code = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditMsg::EditDescriptionChanged(s) => {
|
EditMsg::EditDescriptionChanged(s) => {
|
||||||
if let Some(ref le) = model.edit_link {
|
if let Dialog::EditLink { ref link_delta, .. } = model.dialog {
|
||||||
le.try_borrow_mut().expect("Failed to borrow mutably").title = s;
|
link_delta
|
||||||
|
.try_borrow_mut()
|
||||||
|
.expect("Failed to borrow mutably")
|
||||||
|
.title = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditMsg::EditTargetChanged(s) => {
|
EditMsg::EditTargetChanged(s) => {
|
||||||
if let Some(ref le) = model.edit_link {
|
if let Dialog::EditLink { ref link_delta, .. } = model.dialog {
|
||||||
le.try_borrow_mut()
|
link_delta
|
||||||
|
.try_borrow_mut()
|
||||||
.expect("Failed to borrow mutably")
|
.expect("Failed to borrow mutably")
|
||||||
.target = s;
|
.target = s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EditMsg::SaveLink => {
|
EditMsg::SaveLink => {
|
||||||
save_link(model, orders);
|
if let Dialog::EditLink { ref link_delta, .. } = model.dialog {
|
||||||
|
save_link(link_delta, orders);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
EditMsg::FailedToCreateLink => {
|
EditMsg::FailedToCreateLink => {
|
||||||
orders.send_msg(Msg::SetMessage("Failed to create this link!".to_string()));
|
orders.send_msg(Msg::SetMessage("Failed to create this link!".to_string()));
|
||||||
@ -267,7 +346,7 @@ pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl
|
|||||||
// capture including the message part
|
// capture including the message part
|
||||||
link @ EditMsg::MayDeleteSelected(..) => {
|
link @ EditMsg::MayDeleteSelected(..) => {
|
||||||
clear_all(model);
|
clear_all(model);
|
||||||
model.question = Some(link)
|
model.dialog = Dialog::Question(link)
|
||||||
}
|
}
|
||||||
EditMsg::DeleteSelected(link) => {
|
EditMsg::DeleteSelected(link) => {
|
||||||
orders.perform_cmd(async {
|
orders.perform_cmd(async {
|
||||||
@ -305,21 +384,15 @@ pub fn process_edit_messages(msg: EditMsg, model: &mut Model, orders: &mut impl
|
|||||||
}
|
}
|
||||||
EditMsg::DeletedLink(message) => {
|
EditMsg::DeletedLink(message) => {
|
||||||
clear_all(model);
|
clear_all(model);
|
||||||
model.last_message = Some(message);
|
model.dialog = Dialog::Message(message);
|
||||||
orders.send_msg(Msg::Query(QueryMsg::Fetch));
|
orders.send_msg(Msg::Query(QueryMsg::Fetch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a link save request to the server.
|
/// Send a link save request to the server.
|
||||||
fn save_link(model: &Model, orders: &mut impl Orders<Msg>) {
|
fn save_link(link_delta: &RefCell<LinkDelta>, orders: &mut impl Orders<Msg>) {
|
||||||
// get the link to save
|
let data = link_delta.borrow().clone();
|
||||||
let edit_link = unwrap_or_send!(
|
|
||||||
model.edit_link.as_ref(),
|
|
||||||
Msg::SetMessage("Please enter a link".to_string()),
|
|
||||||
orders
|
|
||||||
);
|
|
||||||
let data = edit_link.borrow().clone();
|
|
||||||
orders.perform_cmd(async {
|
orders.perform_cmd(async {
|
||||||
let data = data;
|
let data = data;
|
||||||
// create the request
|
// create the request
|
||||||
@ -362,41 +435,35 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
let t = move |key: &str| lang.translate(key, None);
|
let t = move |key: &str| lang.translate(key, None);
|
||||||
section![
|
section![
|
||||||
// display a message if any
|
// display a message if any
|
||||||
if let Some(message) = &model.last_message {
|
match &model.dialog {
|
||||||
div![
|
Dialog::EditLink { link_delta, qr } => nodes![edit_or_create_link(link_delta, qr, t)],
|
||||||
|
Dialog::Message(message) => nodes![div![
|
||||||
C!["message", "center"],
|
C!["message", "center"],
|
||||||
close_button(),
|
close_button(),
|
||||||
match message {
|
match message {
|
||||||
Status::Success(m) | Status::Error(m) => &m.message,
|
Status::Success(m) | Status::Error(m) => &m.message,
|
||||||
}
|
}
|
||||||
]
|
]],
|
||||||
} else {
|
Dialog::Question(question) => nodes![div![
|
||||||
section![]
|
|
||||||
},
|
|
||||||
// Display a question if any
|
|
||||||
if let Some(question) = &model.question {
|
|
||||||
div![
|
|
||||||
C!["message", "center"],
|
C!["message", "center"],
|
||||||
close_button(),
|
close_button(),
|
||||||
if let EditMsg::MayDeleteSelected(l) = question.clone() {
|
if let EditMsg::MayDeleteSelected(l) = question.clone() {
|
||||||
div![
|
nodes![div![
|
||||||
lang.translate(
|
lang.translate(
|
||||||
"really-delete",
|
"really-delete",
|
||||||
Some(&fluent_args!["code" => l.code.clone()])
|
Some(&fluent_args!["code" => l.code.clone()])
|
||||||
),
|
),
|
||||||
a![t("no"), C!["button"], ev(Ev::Click, |_| Msg::ClearAll)],
|
a![t("no"), C!["button"], ev(Ev::Click, |_| Msg::ClearAll)],
|
||||||
a![
|
a![t("yes"), C!["button"], {
|
||||||
t("yes"),
|
let l = l.clone();
|
||||||
C!["button"],
|
|
||||||
ev(Ev::Click, move |_| Msg::Edit(EditMsg::DeleteSelected(l)))
|
ev(Ev::Click, move |_| Msg::Edit(EditMsg::DeleteSelected(l)))
|
||||||
]
|
}]
|
||||||
]
|
]]
|
||||||
} else {
|
} else {
|
||||||
span!()
|
nodes!()
|
||||||
}
|
}
|
||||||
]
|
]],
|
||||||
} else {
|
Dialog::None => nodes![],
|
||||||
section![]
|
|
||||||
},
|
},
|
||||||
// display the list of links
|
// display the list of links
|
||||||
table![
|
table![
|
||||||
@ -412,12 +479,6 @@ pub fn view(model: &Model) -> Node<Msg> {
|
|||||||
ev(Ev::Click, |_| Msg::Query(QueryMsg::Fetch)),
|
ev(Ev::Click, |_| Msg::Query(QueryMsg::Fetch)),
|
||||||
"Fetch links"
|
"Fetch links"
|
||||||
],
|
],
|
||||||
// Display the edit dialog if any
|
|
||||||
if let Some(l) = &model.edit_link {
|
|
||||||
edit_or_create_link(l, t)
|
|
||||||
} else {
|
|
||||||
section!()
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -563,7 +624,11 @@ fn view_link(l: &FullLink) -> Node<Msg> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// display a link editing dialog with save and close button
|
/// display a link editing dialog with save and close button
|
||||||
fn edit_or_create_link<F: Fn(&str) -> String>(l: &RefCell<LinkDelta>, t: F) -> Node<Msg> {
|
fn edit_or_create_link<F: Fn(&str) -> String>(
|
||||||
|
l: &RefCell<LinkDelta>,
|
||||||
|
qr: &Loadable<QrGuard>,
|
||||||
|
t: F,
|
||||||
|
) -> Node<Msg> {
|
||||||
let link = l.borrow();
|
let link = l.borrow();
|
||||||
div![
|
div![
|
||||||
// close button top right
|
// close button top right
|
||||||
@ -611,7 +676,14 @@ fn edit_or_create_link<F: Fn(&str) -> String>(l: &RefCell<LinkDelta>, t: F) -> N
|
|||||||
],
|
],
|
||||||
tr![
|
tr![
|
||||||
th![t("qr-code")],
|
th![t("qr-code")],
|
||||||
td![raw!(&generate_qr_from_code(&link.code))]
|
if let Loadable::Data(Some(qr)) = qr {
|
||||||
|
td![a![
|
||||||
|
span!["Download", /* raw!(&qr.svg) */],
|
||||||
|
attrs!(At::Href => qr.url, At::Download => "qr-code.png")
|
||||||
|
]]
|
||||||
|
} else {
|
||||||
|
td!["Loading..."]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
a![
|
a![
|
||||||
@ -651,3 +723,19 @@ fn close_button() -> Node<Msg> {
|
|||||||
ev(Ev::Click, |_| Msg::ClearAll)
|
ev(Ev::Click, |_| Msg::ClearAll)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_qr_png(code: &str) -> Vec<u8> {
|
||||||
|
let qr = QrCode::with_error_correction_level(
|
||||||
|
&format!("http://{}/{}", get_host(), code),
|
||||||
|
qrcode::EcLevel::L,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let png = qr.render::<Luma<u8>>().quiet_zone(false).build();
|
||||||
|
let mut temporary_data = std::io::Cursor::new(Vec::new());
|
||||||
|
DynamicImage::ImageLuma8(png)
|
||||||
|
.write_to(&mut temporary_data, ImageOutputFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
let image_data = temporary_data.into_inner();
|
||||||
|
|
||||||
|
image_data
|
||||||
|
}
|
||||||
|
@ -89,7 +89,7 @@ impl std::fmt::Display for Secret {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub enum Loadable<T> {
|
pub enum Loadable<T> {
|
||||||
Data(Option<T>),
|
Data(Option<T>),
|
||||||
Loading,
|
Loading,
|
||||||
|
Loading…
Reference in New Issue
Block a user