Add download qrcode to table

This commit is contained in:
Dietrich 2021-05-31 06:55:56 +02:00 committed by Franz Dietrich
parent 6b0daecd31
commit 5886272585
8 changed files with 107 additions and 15 deletions

View File

@ -1,6 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use fluent::{FluentArgs, FluentBundle, FluentResource}; use fluent::{FluentArgs, FluentBundle, FluentResource};
use seed::log;
use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; use strum_macros::{AsRefStr, Display, EnumIter, EnumString};
use unic_langid::LanguageIdentifier; use unic_langid::LanguageIdentifier;
@ -42,6 +43,7 @@ impl I18n {
/// Get a localized string. Optionally with parameters provided in `args`. /// Get a localized string. Optionally with parameters provided in `args`.
pub fn translate(&self, key: impl AsRef<str>, args: Option<&FluentArgs>) -> String { pub fn translate(&self, key: impl AsRef<str>, args: Option<&FluentArgs>) -> String {
log!(key.as_ref());
let msg = self let msg = self
.ftl_bundle .ftl_bundle
.get_message(key.as_ref()) .get_message(key.as_ref())

View File

@ -4,6 +4,7 @@ pub mod pages;
use pages::list_links; use pages::list_links;
use pages::list_users; use pages::list_users;
use seed::window;
use seed::{attrs, button, div, input, label, log, prelude::*, App, Url, C}; use seed::{attrs, button, div, input, label, log, prelude::*, App, Url, C};
use shared::apirequests::users::LoginUser; use shared::apirequests::users::LoginUser;
use shared::datatypes::{Loadable, User}; use shared::datatypes::{Loadable, User};
@ -18,14 +19,11 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
orders.subscribe(Msg::UrlChanged); orders.subscribe(Msg::UrlChanged);
orders.send_msg(Msg::GetLoggedUser); orders.send_msg(Msg::GetLoggedUser);
log!(&url);
let lang = I18n::new(Lang::EnUS); let lang = I18n::new(Lang::EnUS);
Model { Model {
index: 0, index: 0,
base_url: Url::new().add_path_part("app"), location: Location::new(url.clone()),
current_url: url.clone(),
page: Page::init(url, orders, lang.clone()), page: Page::init(url, orders, lang.clone()),
i18n: lang, i18n: lang,
user: Loadable::Data(None), user: Loadable::Data(None),
@ -41,8 +39,7 @@ fn init(url: Url, orders: &mut impl Orders<Msg>) -> Model {
#[derive(Debug)] #[derive(Debug)]
struct Model { struct Model {
index: usize, index: usize,
base_url: Url, location: Location,
current_url: Url,
page: Page, page: Page,
i18n: i18n::I18n, i18n: i18n::I18n,
user: Loadable<User>, user: Loadable<User>,
@ -56,6 +53,32 @@ struct LoginForm {
password: ElRef<web_sys::HtmlInputElement>, password: ElRef<web_sys::HtmlInputElement>,
} }
#[derive(Debug)]
struct Location {
host: String,
base_url: Url,
current_url: Url,
}
impl Location {
fn new(url: Url) -> Self {
let host = get_host();
Self {
host,
base_url: Url::new().add_path_part("app"),
current_url: url,
}
}
}
#[must_use]
pub fn get_host() -> String {
window()
.location()
.host()
.expect("Failed to extract the host of the url")
}
#[derive(Debug)] #[derive(Debug)]
enum Page { enum Page {
Home(pages::list_links::Model), Home(pages::list_links::Model),
@ -65,6 +88,7 @@ enum Page {
impl Page { impl Page {
fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Self { fn init(mut url: Url, orders: &mut impl Orders<Msg>, i18n: I18n) -> Self {
log!(&url);
url.next_path_part(); url.next_path_part();
let result = match url.next_path_part() { let result = match url.next_path_part() {
None | Some("list_links") => Self::Home(pages::list_links::init( None | Some("list_links") => Self::Home(pages::list_links::init(
@ -140,7 +164,11 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
} }
Msg::UserReceived(user) => { Msg::UserReceived(user) => {
model.user = Loadable::Data(Some(user)); model.user = Loadable::Data(Some(user));
model.page = Page::init(model.current_url.clone(), orders, model.i18n.clone()); model.page = Page::init(
model.location.current_url.clone(),
orders,
model.i18n.clone(),
);
} }
Msg::NotAuthenticated => { Msg::NotAuthenticated => {
if model.user.is_some() { if model.user.is_some() {
@ -252,8 +280,8 @@ fn view(model: &Model) -> Node<Msg> {
C!["page"], C!["page"],
match model.user { match model.user {
Loadable::Data(Some(ref user)) => div![ Loadable::Data(Some(ref user)) => div![
navigation::navigation(&model.i18n, &model.base_url, user), navigation::navigation(&model.i18n, &model.location.base_url, user),
view_content(&model.page, &model.base_url) view_content(&model.page, &model.location.base_url)
], ],
Loadable::Data(None) => view_login(&model.i18n, model), Loadable::Data(None) => view_login(&model.i18n, model),
Loadable::Loading => div![C!("lds-ellipsis"), div!(), div!(), div!(), div!()], Loadable::Loading => div![C!("lds-ellipsis"), div!(), div!(), div!(), div!()],

View File

@ -17,7 +17,7 @@ use shared::{
datatypes::FullLink, datatypes::FullLink,
}; };
use crate::{i18n::I18n, unwrap_or_return, unwrap_or_send}; use crate::{get_host, i18n::I18n, unwrap_or_return, unwrap_or_send};
/// 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 {
@ -543,6 +543,15 @@ fn view_link(l: &FullLink) -> Node<Msg> {
&l.clicks.number &l.clicks.number
] ]
}, },
{
td![
C!["table_qr"],
a![
attrs![At::Href => format!["http://localhost:8080/admin/download/png/{}", &l.link.code], At::Download => true.as_at_value()],
raw!(&generate_qr_from_code(&l.link.code))
]
]
},
{ {
td![img![ td![img![
ev(Ev::Click, |_| Msg::Edit(EditMsg::MayDeleteSelected(link5))), ev(Ev::Click, |_| Msg::Edit(EditMsg::MayDeleteSelected(link5))),
@ -602,7 +611,7 @@ 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_code(&format!("http://{}", &link.code)))] td![raw!(&generate_qr_from_code(&link.code))]
] ]
], ],
a![ a![
@ -616,8 +625,12 @@ fn edit_or_create_link<F: Fn(&str) -> String>(l: &RefCell<LinkDelta>, t: F) -> N
] ]
} }
fn generate_qr_code(link: &str) -> String { fn generate_qr_from_code(code: &str) -> String {
if let Ok(qr) = QrCode::with_error_correction_level(&link, qrcode::EcLevel::L) { generate_qr_from_link(&format!("https://{}/{}", get_host(), code))
}
fn generate_qr_from_link(url: &str) -> String {
if let Ok(qr) = QrCode::with_error_correction_level(&url, qrcode::EcLevel::L) {
let svg = qr let svg = qr
.render() .render()
.min_dimensions(100, 100) .min_dimensions(100, 100)

View File

@ -17,6 +17,7 @@ create-link = Create link
link-description = Description link-description = Description
link-target = Link target link-target = Link target
link-code = Link code link-code = Link code
qr-code = QR-code
shortlink = Shortlink shortlink = Shortlink
search-placeholder = Filter according to... search-placeholder = Filter according to...
really-delete = Do you really want to delete {$code}? really-delete = Do you really want to delete {$code}?

View File

@ -357,6 +357,7 @@ pub async fn webservice(
"/create_link/", "/create_link/",
web::post().to(views::process_create_link_json), web::post().to(views::process_create_link_json),
) )
.route("/get_qr_code/", web::post().to(views::get_qr_code_json))
.route( .route(
"/edit_link/", "/edit_link/",
web::post().to(views::process_update_link_json), web::post().to(views::process_update_link_json),

View File

@ -16,7 +16,7 @@ use qrcode::{render::svg, QrCode};
use queries::{authenticate, Role}; use queries::{authenticate, Role};
use shared::apirequests::{ use shared::apirequests::{
general::{Message, Status}, general::{Message, Status},
links::{LinkDelta, LinkRequestForm}, links::{LinkDelta, LinkRequestForm, QrCodeRequest, SvgQrCodeResponse},
users::{LoginUser, UserDelta, UserRequestForm}, users::{LoginUser, UserDelta, UserRequestForm},
}; };
use tera::{Context, Tera}; use tera::{Context, Tera};
@ -130,7 +130,7 @@ pub async fn index_json(
Err(e) => { Err(e) => {
error!("Failed to access database: {:?}", e); error!("Failed to access database: {:?}", e);
warn!("Not logged in - redirecting to login page"); warn!("Not logged in - redirecting to login page");
Ok(redirect_builder("/admin/login/")) Ok(HttpResponse::Unauthorized().body("Failed"))
} }
} }
} }
@ -188,6 +188,31 @@ pub async fn view_link_empty(
view_link(tera, config, id, web::Path::from("".to_owned())).await view_link(tera, config, id, web::Path::from("".to_owned())).await
} }
pub async fn get_qr_code_json(
config: web::Data<crate::ServerConfig>,
qr_request: web::Json<QrCodeRequest>,
id: Identity,
) -> Result<HttpResponse, ServerError> {
if let Ok(link) = queries::get_link(&id, &qr_request.link_id, &config).await {
let host = config.public_url.to_string();
let qr = QrCode::with_error_correction_level(
&format!("http://{}/{}", &host, &link.item.code),
qrcode::EcLevel::L,
)?;
let svg = qr
.render()
.min_dimensions(100, 100)
.dark_color(svg::Color("#000000"))
.light_color(svg::Color("#ffffff"))
.build();
Ok(HttpResponse::Ok().json2(&SvgQrCodeResponse { svg }))
} else {
Ok(redirect_builder("/admin/login/"))
}
}
#[instrument(skip(id, tera))] #[instrument(skip(id, tera))]
pub async fn view_link( pub async fn view_link(
tera: web::Data<Tera>, tera: web::Data<Tera>,

View File

@ -40,6 +40,7 @@ div.login div {
height: 60px; height: 60px;
display: table; display: table;
} }
div.login input { div.login input {
padding: 15px; padding: 15px;
margin-bottom: 20px; margin-bottom: 20px;
@ -63,6 +64,10 @@ td {
padding: 10px; padding: 10px;
} }
td.table_qr svg {
max-height: 40px;
}
table tr:nth-child(even) { table tr:nth-child(even) {
background-color: #eee; background-color: #eee;
} }

View File

@ -73,3 +73,20 @@ pub enum LinkOverviewColumns {
Author, Author,
Statistics, Statistics,
} }
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
pub struct QrCodeRequest {
pub link_id: String,
pub format: QrCodeFormat,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
pub struct SvgQrCodeResponse {
pub svg: String,
}
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq)]
pub enum QrCodeFormat {
Svg,
Png,
}