diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/lib.rs | 346 | ||||
-rw-r--r-- | src/name.rs | 161 | ||||
-rw-r--r-- | src/view.rs | 287 |
3 files changed, 506 insertions, 288 deletions
@@ -1,3 +1,347 @@ #![recursion_limit="1024"] +extern crate wee_alloc; +extern crate console_error_panic_hook; +use name::{NameView,Name}; +use genpdf::Element as _; +use genpdf::{elements, style, fonts}; +use qrcodegen::QrCode; +use qrcodegen::QrCodeEcc; +use wasm_bindgen::prelude::*; +use yew::prelude::*; +use vcard::{VCard, VCardError}; +use std::panic; -mod view;
\ No newline at end of file +mod name; + +// Use `wee_alloc` as the global allocator. +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; + +fn init() { + panic::set_hook(Box::new(console_error_panic_hook::hook)); +} + +pub struct Download { + pub file_name: String, + pub content: String, + pub mime_type: MimeType, +} + +impl Download { + pub fn as_data_link(&self) -> String { + let data = base64::encode(&*self.content); + let uri_component: String = js_sys::encode_uri_component(&data).into(); + + format!("data:{};base64,{}", self.mime_type.as_text(), uri_component) + } +} + +impl Clone for Download { + fn clone(&self) -> Self { + Self { + file_name: self.file_name.clone(), + content: self.content.clone(), + mime_type: self.mime_type.clone(), + } + } +} + +pub enum MimeType { + PDF, + VCard, + SVG, +} + +impl MimeType { + pub fn as_text(&self) -> &str { + match self { + MimeType::PDF => "application/pdf", + MimeType::VCard => "text/vcard", + MimeType::SVG => "image/svg+xml", + } + } +} + +impl Clone for MimeType { + fn clone(&self) -> Self { + match self { + MimeType::PDF => MimeType::PDF, + MimeType::VCard => MimeType::VCard, + MimeType::SVG => MimeType::SVG, + } + } +} + +pub struct MainView { + link: ComponentLink<Self>, + error: Vec<String>, + name: Name, + download: Option<Download>, +} + +pub enum Msg { + UpdateName(Name), + GenerateVCard, + GeneratePdf, + GenerateQrCode, + Nope, +} + +impl Component for MainView { + type Message = Msg; + type Properties = (); + + fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { + MainView { link, error: vec![], name: Name::new(), download: None, } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + self.error.clear(); + match msg { + Msg::UpdateName(value) => self.name = value, + Msg::GenerateVCard => { + match self.generate_vcard() { + Ok(vcard) => self.download = Some( + Download { + file_name: format!("{}.vcs", self.name.formatted_name()), + content: vcard.to_string(), + mime_type: MimeType::VCard, + } + ), + Err(VCardError::FormatError(err)) => self.error.push(err.to_string()), + Err(VCardError::EmptyFormatName) => self.error.push(String::from("A VCard should have at least one formatted name.")), + }; + } + Msg::GeneratePdf => { + match self.generate_pdf() { + Ok(pdf) => self.download = Some( + Download { + file_name: format!("Visitenkarten {}.pdf", self.name.formatted_name()), + content: pdf, + mime_type: MimeType::PDF, + } + ), + Err(_) => self.error.push(String::from("Unexpected error while generating the PDF. Please contact me about it.")), + } + } + Msg::GenerateQrCode => { + let mut vcard_content = None; + match self.generate_vcard() { + Ok(vcard) => vcard_content = Some(vcard.to_string()), + Err(VCardError::FormatError(err)) => self.error.push(err.to_string()), + Err(VCardError::EmptyFormatName) => self.error.push(String::from("A VCard should have at least one formatted name.")), + }; + if vcard_content.is_some() { + match QrCode::encode_text(vcard_content.as_ref().unwrap(), QrCodeEcc::Low) { + Ok(qr) => self.download = Some( + Download { + file_name: format!("QR-Code VCard {}.svg", self.name.formatted_name()), + content: qr.to_svg_string(4), + mime_type: MimeType::SVG, + } + ), + Err(_) => self.error.push(String::from("Sorry, VCard is too long!")), + }; + } + } + Msg::Nope => return false, + }; + if self.error.len() > 0 { + self.download = None; + } + true + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + false + } + + fn view(&self) -> Html { + + let download = self.download.clone(); + + let on_name_input = self.link.batch_callback(move |n: Name| + if download.is_some() { + vec![ + Msg::UpdateName(n), + match download.as_ref().unwrap().mime_type { + MimeType::PDF => Msg::GeneratePdf, + MimeType::SVG => Msg::GenerateQrCode, + MimeType::VCard => Msg::GenerateVCard, + } + ] + } else { + vec![Msg::UpdateName(n), Msg::GenerateVCard] + } + ); + + let download_options = self.link.callback(|e: ChangeData| + match e { + ChangeData::Select(v) => match v.value().as_str() { + "vcard" => Msg::GenerateVCard, + "pdf" => Msg::GeneratePdf, + "qrcode" => Msg::GenerateQrCode, + _ => Msg::Nope, + }, + _ => Msg::Nope, + } + ); + + html!{ + <> + <main> + <section class="hero"> + <div class="hero-body"> + <div class="container is-max-widescreen"> + <h1 class="title">{ "A Generator for vCards" }</h1> + <h2 class="subtitle">{ "Supports generating vCards (.vcf), print-ready PDF business cards and QR Codes" }</h2> + </div> + </div> + </section> + + <section class="section"> + <div class="container is-max-widescreen"> + + { self.render_errors() } + + <NameView oninput=on_name_input /> + + <div class="block"> + <div class="select"> + <select id="download_options" onchange=download_options> + <option value="vcard">{ "VCard (.vcf)" }</option> + <option value="pdf">{ "Print-ready PDF" }</option> + <option value="qrcode">{ "QR Code" }</option> + </select> + </div> + + { self.render_download() } + </div> + + <div class="block"> + { self.render_preview() } + </div> + + </div> + </section> + </main> + + <footer class="footer"> + <div class="content has-text-centered"> + <p> + <strong>{ "VCard Generator" }</strong> { " by " } <a href="https://jelemux.dev">{ "Jeremias Weber" }</a>{ ". "} + { "The source code is licenced " } <a href="http://opensource.org/licenses/mit-license.php">{ "MIT" }</a>{"."} + </p> + </div> + </footer> + </> + } + } +} + +impl MainView { + fn render_errors(&self) -> Html { + html!{ + <> + { + for self.error.iter().map(|err| + html!{ + <div class="notification is-danger is-light"> + { err } + </div> + } + ) + } + </> + } + } + fn render_download(&self) -> Html { + if self.download.is_some() { + let download = self.download.as_ref().unwrap(); + + html!{ + <a href=download.as_data_link() download=download.file_name class="button is-primary" > + { "Download" } + </a> + } + } else { + html!{} + } + } + fn render_preview(&self) -> Html { + if self.download.is_some() { + let download = self.download.as_ref().unwrap(); + + match download.mime_type { + MimeType::PDF => html!{ + <iframe src=download.as_data_link() alt="PDF Preview" width="400" height="550"/> + }, + MimeType::VCard => html!{ + <code> { download.content.clone() } </code> + }, + MimeType::SVG => html!{ + <img src=download.as_data_link() alt="Image Preview" class="image is-square" width="300" height="300"/> + }, + } + } else { + html!{} + } + } + fn generate_vcard(&self) -> Result<VCard, VCardError> { + match VCard::from_formatted_name_str(&self.name.formatted_name()) { + Ok(vcard) => Ok(vcard), + Err(err) => Err(err), + } + } + fn generate_pdf(&self) -> Result<String, ()>{ + let regular_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Regular.ttf"); + let regular_font_data = fonts::FontData::new(regular_bytes.to_vec(), Some(printpdf::BuiltinFont::Helvetica)).expect("font data should be correct"); + + let bold_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Bold.ttf"); + let bold_font_data = fonts::FontData::new(bold_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBold)).expect("font data should be correct"); + + let italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Italic.ttf"); + let italic_font_data = fonts::FontData::new(italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaOblique)).expect("font data should be correct"); + + let bold_italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-BoldItalic.ttf"); + let bold_italic_font_data = fonts::FontData::new(bold_italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBoldOblique)).expect("font data should be correct"); + + let font_family = fonts::FontFamily{ + regular: regular_font_data, + bold: bold_font_data, + italic: italic_font_data, + bold_italic: bold_italic_font_data + }; + + let mut doc = genpdf::Document::new(font_family); + + doc.set_title("BCard test"); + doc.set_minimal_conformance(); + doc.set_margins(10); + doc.set_line_spacing(1.25); + + doc.push( + elements::Paragraph::new("genpdf Demo Document") + .aligned(elements::Alignment::Center) + .styled(style::Style::new().bold().with_font_size(20)), + ); + + // TODO fill doc with real data + + let mut buf: Vec<u8> = Vec::new(); + match doc.render(&mut buf) { + Ok(_) => Ok(match String::from_utf8(buf) { + Ok(s) => s, + Err(_) => return Err(()), + }), + Err(_) => Err(()), + } + } +} + + +#[wasm_bindgen(start)] +pub fn run_app() { + init(); + App::<MainView>::new().mount_to_body(); +}
\ No newline at end of file diff --git a/src/name.rs b/src/name.rs new file mode 100644 index 0000000..bf91186 --- /dev/null +++ b/src/name.rs @@ -0,0 +1,161 @@ +use yew::prelude::*; + +#[derive(Clone)] +pub struct Name { + pub prefix: String, + pub first_name: String, + pub middle_name: String, + pub last_name: String, + pub suffix: String, +} + +impl Name { + pub fn new() -> Self { + Self { + prefix: String::new(), + first_name: String::new(), + middle_name: String::new(), + last_name: String::new(), + suffix: String::new(), + } + } + pub fn formatted_name(&self) -> String { + let mut formatted_name = String::new(); + + if !self.prefix.is_empty() { + formatted_name.push_str(&self.prefix); + } + if !self.first_name.is_empty() { + formatted_name.push_str(" "); + formatted_name.push_str(&self.first_name); + } + if !self.middle_name.is_empty() { + formatted_name.push_str(" "); + formatted_name.push_str(&self.middle_name); + } + if !self.last_name.is_empty() { + formatted_name.push_str(" "); + formatted_name.push_str(&self.last_name); + } + if !self.suffix.is_empty() { + formatted_name.push_str(", "); + formatted_name.push_str(&self.suffix); + } + + formatted_name + } +} + +pub struct NameView { + link: ComponentLink<Self>, + value: Name, + oninput: Callback<Name>, + //errors: Vec<String>, +} + +pub enum Msg { + UpdatePrefix(String), + UpdateFirstName(String), + UpdateMiddleName(String), + UpdateLastName(String), + UpdateSuffix(String), +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { + pub oninput: Callback<Name>, + //pub errors: Vec<String>, +} + +impl Component for NameView { + type Message = Msg; + type Properties = Props; + fn create(props: <Self as yew::Component>::Properties, link: yew::html::Scope<Self>) -> Self { + Self { + link, + value: Name::new(), + oninput: props.oninput, + } + } + fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { + match msg { + Msg::UpdatePrefix(p) => self.value.prefix = p, + Msg::UpdateFirstName(f) => self.value.first_name = f, + Msg::UpdateMiddleName(m) => self.value.middle_name = m, + Msg::UpdateLastName(l) => self.value.last_name = l, + Msg::UpdateSuffix(s) => self.value.suffix = s, + }; + self.oninput.emit(self.value.clone()); + true + } + fn change(&mut self, props: <Self as yew::Component>::Properties) -> bool { + self.oninput = props.oninput; + true + } + fn view(&self) -> yew::virtual_dom::VNode { + html!{ + <> + <h3 class="subtitle">{ "Name" }</h3> + + <div class="columns"> + + <div class="field column"> + <label class="label">{ "Prefix" }</label> + <div class="control"> + <input id="prefix" + type="text" + placeholder="Sir" + oninput=self.link.callback(|e: InputData| Msg::UpdatePrefix(e.value)) + /> + </div> + </div> + + <div class="field column"> + <label class="label">{ "First name" }</label> + <div class="control"> + <input id="first_name" + type="text" + placeholder="Arthur" + oninput=self.link.callback(|e: InputData| Msg::UpdateFirstName(e.value)) + /> + </div> + </div> + + <div class="field column"> + <label class="label">{ "Middle name" }</label> + <div class="control"> + <input id="middle_name" + type="text" + placeholder="Charles" + oninput=self.link.callback(|e: InputData| Msg::UpdateMiddleName(e.value)) + /> + </div> + </div> + + <div class="field column"> + <label class="label">{ "Last name" }</label> + <div class="control"> + <input id="last_name" + type="text" + placeholder="Clarke" + oninput=self.link.callback(|e: InputData| Msg::UpdateLastName(e.value)) + /> + </div> + </div> + + <div class="field column"> + <label class="label">{ "Suffix" }</label> + <div class="control"> + <input id="suffix" + type="text" + placeholder="CBE FRAS" + oninput=self.link.callback(|e: InputData| Msg::UpdateSuffix(e.value)) + /> + </div> + </div> + + </div> + </> + } + } +}
\ No newline at end of file diff --git a/src/view.rs b/src/view.rs deleted file mode 100644 index 9557dc5..0000000 --- a/src/view.rs +++ /dev/null @@ -1,287 +0,0 @@ -extern crate console_error_panic_hook; -use genpdf::Element as _; -use genpdf::{elements, style, fonts}; -use qrcodegen::QrCode; -use qrcodegen::QrCodeEcc; -use wasm_bindgen::prelude::*; -use js_sys; -use yew::prelude::*; -use vcard::{VCard, VCardError}; -use std::panic; - -fn init() { - panic::set_hook(Box::new(console_error_panic_hook::hook)); -} - -struct Download { - file_name: String, - content: Box<dyn AsRef<[u8]>>, - mime_type: MimeType, -} - -impl Download { - fn as_data_link(&self) -> String { - let data = base64::encode(&*self.content); - let uri_component: String = js_sys::encode_uri_component(&data).into(); - - format!("data:{};base64,{}", self.mime_type.as_text(), uri_component) - } -} - -enum MimeType { - PDF, - VCard, - SVG, -} - -impl MimeType { - fn as_text(&self) -> &str { - match self { - MimeType::PDF => "application/pdf", - MimeType::VCard => "text/vcard", - MimeType::SVG => "image/svg+xml", - } - } -} - -pub struct Form { - link: ComponentLink<Self>, - error: Vec<String>, - formatted_name: String, - download: Option<Download>, -} - -pub enum Msg { - UpdateFormattedName(String), - GenerateVCard, - GeneratePdf, - GenerateQrCode, - Nope, -} - -impl Component for Form { - type Message = Msg; - type Properties = (); - - fn create(_props: Self::Properties, link: ComponentLink<Self>) -> Self { - Self { link, error: vec![], formatted_name: String::new(), download: None, } - } - - fn update(&mut self, msg: Self::Message) -> ShouldRender { - self.error.clear(); - match msg { - Msg::UpdateFormattedName(value) => self.formatted_name = String::from(value), - Msg::GenerateVCard => { - match self.generate_vcard() { - Ok(vcard) => self.download = Some( - Download { - file_name: format!("{}.vcs",self.formatted_name), - content: Box::new(vcard.to_string()), - mime_type: MimeType::VCard, - } - ), - Err(VCardError::FormatError(err)) => self.error.push(err.to_string()), - Err(VCardError::EmptyFormatName) => self.error.push(String::from("A VCard should have at least one formatted name.")), - }; - } - Msg::GeneratePdf => { - match self.generate_pdf() { - Ok(pdf) => self.download = Some( - Download { - file_name: format!("Visitenkarten {}.pdf", self.formatted_name), - content: Box::new(pdf), - mime_type: MimeType::PDF, - } - ), - Err(_) => self.error.push(String::from("Unexpected error while generating the PDF. Please contact me about it.")), - } - } - Msg::GenerateQrCode => { - let mut vcard_content = None; - match self.generate_vcard() { - Ok(vcard) => vcard_content = Some(vcard.to_string()), - Err(VCardError::FormatError(err)) => self.error.push(err.to_string()), - Err(VCardError::EmptyFormatName) => self.error.push(String::from("A VCard should have at least one formatted name.")), - }; - if vcard_content.is_some() { - match QrCode::encode_text(vcard_content.as_ref().unwrap(), QrCodeEcc::Low) { - Ok(qr) => self.download = Some( - Download { - file_name: format!("QR-Code VCard {}.svg", self.formatted_name), - content: Box::new(qr.to_svg_string(4)), - mime_type: MimeType::SVG, - } - ), - Err(_) => self.error.push(String::from("Sorry, VCard is too long!")), - }; - } - } - Msg::Nope => return false, - }; - if self.error.len() > 0 { - self.download = None; - } - true - } - - fn change(&mut self, _props: Self::Properties) -> ShouldRender { - false - } - - fn view(&self) -> Html { - - let formatted_name_input = self.link.batch_callback(|e: InputData| - vec![Msg::UpdateFormattedName(e.value), Msg::GenerateVCard] - ); - - let download_options = self.link.callback(|e: ChangeData| - match e { - ChangeData::Select(v) => match v.value().as_str() { - "vcard" => Msg::GenerateVCard, - "pdf" => Msg::GeneratePdf, - "qrcode" => Msg::GenerateQrCode, - _ => Msg::Nope, - }, - _ => Msg::Nope, - } - ); - - html!{ - <div class="container"> - <div class="banner row"> - <div class="span twelve"> - <h4>{ "A Generator for vCards" }</h4> - <h5>{ "Supports generating vCards (.vcf), print-ready PDF business cards and QR Codes" }</h5> - </div> - </div> - { self.render_error() } - <h4 class="explainer">{ "Name" }</h4> - <form class="row"> - <label for="formatted_name">{ "Formatted name: " }</label> - <input id="formatted_name" type="text" oninput=formatted_name_input/> - <br/> - <label for="prefix">{ "Prefix: " }</label> - <input id="prefix" type="text"/> - <br/> - <label for="first_name">{ "First name: " }</label> - <input id="first_name" type="text"/> - - <label for="middle_name">{ "Middle name: " }</label> - <input id="middle_name" type="text"/> - - <label for="last_name">{ "Last name: " }</label> - <input id="last_name" type="text"/> - <br/> - <label for="suffix">{ "Suffix: " }</label> - <input id="suffix" type="text"/> - </form> - <select id="download_options" onchange=download_options> - <option value="vcard">{ "VCard (.vcf)" }</option> - <option value="pdf">{ "Print-ready PDF" }</option> - <option value="qrcode">{ "QR Code" }</option> - </select> - - { self.render_download() } - - </div> - } - } -} - -impl Form { - fn generate_vcard(&self) -> Result<VCard, VCardError> { - match VCard::from_formatted_name_str(&self.formatted_name) { - Ok(vcard) => Ok(vcard), - Err(err) => Err(err), - } - } - fn generate_pdf(&self) -> Result<Vec<u8>, ()>{ - let regular_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Regular.ttf"); - let regular_font_data = fonts::FontData::new(regular_bytes.to_vec(), Some(printpdf::BuiltinFont::Helvetica)).expect("font data should be correct"); - - let bold_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Bold.ttf"); - let bold_font_data = fonts::FontData::new(bold_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBold)).expect("font data should be correct"); - - let italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-Italic.ttf"); - let italic_font_data = fonts::FontData::new(italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaOblique)).expect("font data should be correct"); - - let bold_italic_bytes = include_bytes!("/usr/share/fonts/liberation/LiberationSans-BoldItalic.ttf"); - let bold_italic_font_data = fonts::FontData::new(bold_italic_bytes.to_vec(), Some(printpdf::BuiltinFont::HelveticaBoldOblique)).expect("font data should be correct"); - - let font_family = fonts::FontFamily{ - regular: regular_font_data, - bold: bold_font_data, - italic: italic_font_data, - bold_italic: bold_italic_font_data - }; - - let mut doc = genpdf::Document::new(font_family); - - doc.set_title("BCard test"); - doc.set_minimal_conformance(); - doc.set_margins(10); - doc.set_line_spacing(1.25); - - doc.push( - elements::Paragraph::new("genpdf Demo Document") - .aligned(elements::Alignment::Center) - .styled(style::Style::new().bold().with_font_size(20)), - ); - - // TODO fill doc with real data - - let mut buf: Vec<u8> = Vec::new(); - match doc.render(&mut buf) { - Ok(_) => Ok(buf), - Err(_) => Err(()), - } - } - fn render_error(&self) -> Html { - html!{ - <> - { - for self.error.iter().map(|err| - html!{ - <div class="alert danger"> - <p>{ err }</p> - </div> - } - ) - } - </> - } - } - fn render_download(&self) -> Html { - if self.download.is_some() { - let download = self.download.as_ref().unwrap(); - - html!{ - <a href=download.as_data_link() download=download.file_name class="button success small" > - { "Download" } - </a> - } - } else { - html!{} - } - }/* - fn render_qrcode(&self) -> Html { - if self.qr_code.is_some() { - let data = base64::encode(self.qr_code.as_ref().unwrap()); - let uri_component: String = js_sys::encode_uri_component(&data).into(); - let src = format!("data:image/svg+xml;base64,{}", uri_component); - - html!{ - <img src=src alt="QR Code" width="200" height="200"/> - } - } else { - html!{} - } - }*/ -} - - -#[wasm_bindgen(start)] -pub fn run_app() { - init(); - App::<Form>::new().mount_to_body(); -}
\ No newline at end of file |