diff options
| author | jelemux <jeremias.weber@protonmail.com> | 2020-11-19 07:37:20 +0100 | 
|---|---|---|
| committer | jelemux <jeremias.weber@protonmail.com> | 2020-11-19 07:37:20 +0100 | 
| commit | 49588f22f7d20193f899226107c9e323a82c6951 (patch) | |
| tree | 7f7bb739336f87aa2c950038f7d5a7e154f09dbd /src | |
| parent | 104f70b0968d7138d6cf944da98d95a405b1a049 (diff) | |
| download | wasm-card-49588f22f7d20193f899226107c9e323a82c6951.tar.gz wasm-card-49588f22f7d20193f899226107c9e323a82c6951.tar.bz2 | |
added telephone, but causes problems
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 370 | ||||
| -rw-r--r-- | src/util.rs | 39 | ||||
| -rw-r--r-- | src/view/address.rs (renamed from src/address.rs) | 130 | ||||
| -rw-r--r-- | src/view/birthday.rs | 11 | ||||
| -rw-r--r-- | src/view/mod.rs | 386 | ||||
| -rw-r--r-- | src/view/name.rs (renamed from src/name.rs) | 94 | ||||
| -rw-r--r-- | src/view/photo.rs | 10 | ||||
| -rw-r--r-- | src/view/telephone.rs | 183 | 
8 files changed, 724 insertions, 499 deletions
| @@ -1,382 +1,22 @@  #![recursion_limit="1024"]  extern crate wee_alloc;  extern crate console_error_panic_hook; -use std::collections::HashSet; -use name::{NameView,Name}; -use address::{AddressView,Address,AddressType}; -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 yew::prelude::App;  use std::panic; - -mod name; -mod address; +use view::MainView;  // Use `wee_alloc` as the global allocator.  #[global_allocator]  static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; +mod view; +mod util; +  fn init() {      panic::set_hook(Box::new(console_error_panic_hook::hook));  } -#[derive(Clone)] -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) -    } -} - -#[derive(Clone, Copy)] -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", -        } -    } -} - -#[derive(Clone, Copy)] -pub enum DownloadOption { -    PDF, -    VCard, -    QrCode, -} - -pub struct MainView { -    link: ComponentLink<Self>, -    error: Vec<String>, -    name: Name, -    work_address: Address, -    home_address: Address, -    download: Option<Download>, -    selected_option: DownloadOption, -} - -pub enum Msg { -    UpdateName(Name), -    UpdateHomeAddress(Address), -    UpdateWorkAddress(Address), -    Generate(DownloadOption), -    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(),  -            work_address: Address::new_with_type(AddressType::Work),  -            home_address: Address::new_with_type(AddressType::Home),  -            download: None,  -            selected_option: DownloadOption::VCard  -        } -    } - -    fn update(&mut self, msg: Self::Message) -> ShouldRender { -        self.error.clear(); -        match msg { -            Msg::UpdateName(value) => { -                self.name = value; -                self.link.send_message(Msg::Generate(self.selected_option)); -            }, -            Msg::UpdateHomeAddress(value) => { -                self.home_address = value; -                self.link.send_message(Msg::Generate(self.selected_option)); -            }, -            Msg::UpdateWorkAddress(value) => { -                self.work_address = value; -                self.link.send_message(Msg::Generate(self.selected_option)); -            }, -            Msg::Generate(option) => { -                self.selected_option = option; - -                let vcard_content = match self.generate_vcard() { -                    Ok(vcard) => Some(vcard.to_string()), -                    Err(VCardError::FormatError(err)) => { -                        self.error.push(err.to_string()); -                        None -                    } -                    Err(VCardError::EmptyFormatName) => { -                        self.error.push(String::from("At least one of the name fields should be filled out.")); -                        None -                    } -                }; - -                match option { -                    DownloadOption::VCard => { -                        if vcard_content.is_some() { -                            self.download = Some( -                                Download {  -                                    file_name: format!("{}.vcs", self.name.formatted_name()),  -                                    content: vcard_content.unwrap().to_string(),  -                                    mime_type: MimeType::VCard, -                                } -                            ) -                        } -                    } -                    DownloadOption::QrCode => { -                        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!")), -                            }; -                        } -                    } -                    DownloadOption::PDF => { -                        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::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_options = self.link.callback(|e: ChangeData| -            match e { -                ChangeData::Select(v) => match v.value().as_str() { -                    "vcard" => Msg::Generate(DownloadOption::VCard), -                    "pdf" => Msg::Generate(DownloadOption::PDF), -                    "qrcode" => Msg::Generate(DownloadOption::QrCode), -                    _ => 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=self.link.callback(|n: Name| Msg::UpdateName(n)) /> - -                            <AddressView address_type=AddressType::Home oninput=self.link.callback(|a: Address| Msg::UpdateHomeAddress(a)) /> - -                            <AddressView address_type=AddressType::Work oninput=self.link.callback(|a: Address| Msg::UpdateWorkAddress(a)) /> - -                            <div class="block level-left"> -                                <div class="select level-item"> -                                    <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 level-item" > -                    { "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!{ -                        <pre> -                            <code> { download.content.clone() } </code> -                        </pre> -                    } -                } -                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) => { -                let mut vcard = vcard; - -                let names = { -                    let mut names = HashSet::new(); -                    names.insert(self.name.to_vcard_name()); - -                    names -                }; - -                let addresses = { -                    let mut addresses = HashSet::new(); -                    addresses.insert(self.home_address.to_vcard_address()); -                    addresses.insert(self.work_address.to_vcard_address()); - -                    addresses -                }; - -                vcard.names = Some(vcard::Set::from_hash_set(names).unwrap()); -                vcard.addresses = Some(vcard::Set::from_hash_set(addresses).unwrap()); - -                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(); diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..3d8f231 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,39 @@ +use yew::prelude::*; + +pub fn text_field_input(label: &str, id: &str, placeholder: Option<&str>, oninput: Callback<InputData>) -> Html { +    html!{ +        <div class="field column  +                    is-one-fifth-widescreen +                    is-one-quarter-desktop +                    is-one-third-tablet +                    is-half-mobile" > +            <label class="label">{ label }</label> +            <div class="control"> +                <input  id=id +                        type="text"  +                        placeholder=placeholder.unwrap_or("") +                        oninput=oninput +                /> +            </div> +        </div> +    } +} + +pub fn checkbox_field_input(label: &str, id: &str, checked: bool, onclick: Callback<MouseEvent>) -> Html { +    html!{ +        <div class="field column  +                    is-one-fifth-widescreen  +                    is-one-quarter-desktop  +                    is-one-third-tablet  +                    is-half-mobile" > +            <label class="checkbox"> +                <input  id=id +                        type="checkbox"  +                        checked=checked +                        onclick=onclick +                /> +                { label } +            </label> +        </div> +    } +}
\ No newline at end of file diff --git a/src/address.rs b/src/view/address.rs index d048dc0..a30ba85 100644 --- a/src/address.rs +++ b/src/view/address.rs @@ -4,6 +4,8 @@ use vcard::parameters;  use vcard::values::{self, text};  use std::collections::HashSet; +use crate::util; +  #[derive(Clone)]  pub struct Address {      pub post_office_box: String, @@ -29,7 +31,7 @@ impl Address {              address_type          }      } -    pub fn to_vcard_address(&self) -> properties::Address { +    pub fn to_vcard_value(&self) -> properties::Address {          let address_value = values::address_value::AddressValue::from_components(              match self.post_office_box.is_empty() {                  true => None, @@ -154,83 +156,55 @@ impl Component for AddressView {                  <h3 class="subtitle">{ format!("{} Address", self.value.address_type.to_str()) }</h3>                  <div class="columns is-mobile is-multiline"> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Post Office Box" }</label> -                        <div class="control"> -                            <input  id="post_office_box"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdatePostOfficeBox(e.value)) -                            /> -                        </div> -                    </div> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Extension" }</label> -                        <div class="control"> -                            <input  id="extension"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) -                            /> -                        </div> -                    </div> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Street" }</label> -                        <div class="control"> -                            <input  id="street"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdateStreet(e.value)) -                            /> -                        </div> -                    </div> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Locality" }</label> -                        <div class="control"> -                            <input  id="locality"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdateLocality(e.value)) -                            /> -                        </div> -                    </div> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Region" }</label> -                        <div class="control"> -                            <input  id="region"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdateRegion(e.value)) -                            /> -                        </div> -                    </div> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Postal Code" }</label> -                        <div class="control"> -                            <input  id="code"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdateCode(e.value)) -                            /> -                        </div> -                    </div> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <label class="label">{ "Country" }</label> -                        <div class="control"> -                            <input  id="country"  -                                    type="text"  -                                    placeholder=""  -                                    oninput=self.link.callback(|e: InputData| Msg::UpdateCountry(e.value)) -                            /> -                        </div> -                    </div> + +                    { util::text_field_input( +                        "Post Office Box",  +                        "post_office_box",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdatePostOfficeBox(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Extension",  +                        "extension",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Street",  +                        "street",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateStreet(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Locality",  +                        "locality",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateLocality(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Region",  +                        "region",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateRegion(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Postal Code",  +                        "code",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateCode(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Country",  +                        "country",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateCountry(e.value)) +                    ) }                  </div>              </div> diff --git a/src/view/birthday.rs b/src/view/birthday.rs new file mode 100644 index 0000000..d4b9356 --- /dev/null +++ b/src/view/birthday.rs @@ -0,0 +1,11 @@ +use yew::prelude::*; +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; + +#[derive(Clone)] +pub struct Birthday { +    pub year: u16, +    pub month: u8, +    pub day: u8, +}
\ No newline at end of file diff --git a/src/view/mod.rs b/src/view/mod.rs new file mode 100644 index 0000000..d34eb39 --- /dev/null +++ b/src/view/mod.rs @@ -0,0 +1,386 @@ +use crate::view::telephone::{TelephoneView,Telephone}; +use std::collections::HashSet; +use name::{NameView,Name}; +use address::{AddressView,Address,AddressType}; +use genpdf::Element as _; +use genpdf::{elements, style, fonts}; +use qrcodegen::QrCode; +use qrcodegen::QrCodeEcc; +use yew::prelude::*; +use vcard::{VCard, VCardError}; + + +mod name; +mod photo; +mod birthday; +mod address; +mod telephone; + +#[derive(Clone)] +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) +    } +} + +#[derive(Clone, Copy)] +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", +        } +    } +} + +#[derive(Clone, Copy)] +pub enum DownloadOption { +    PDF, +    VCard, +    QrCode, +} + +pub struct MainView { +    link: ComponentLink<Self>, +    error: Vec<String>, +    name: Name, +    work_address: Address, +    home_address: Address, +    telephone: Telephone, +    download: Option<Download>, +    selected_option: DownloadOption, +} + +pub enum Msg { +    UpdateName(Name), +    UpdateHomeAddress(Address), +    UpdateWorkAddress(Address), +    UpdateTelephone(Telephone), +    Generate(DownloadOption), +    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(),  +            work_address: Address::new_with_type(AddressType::Work),  +            home_address: Address::new_with_type(AddressType::Home),  +            telephone: Telephone::new(), +            download: None,  +            selected_option: DownloadOption::VCard  +        } +    } + +    fn update(&mut self, msg: Self::Message) -> ShouldRender { +        self.error.clear(); +        match msg { +            Msg::UpdateName(value) => { +                self.name = value; +                self.link.send_message(Msg::Generate(self.selected_option)); +            }, +            Msg::UpdateHomeAddress(value) => { +                self.home_address = value; +                self.link.send_message(Msg::Generate(self.selected_option)); +            }, +            Msg::UpdateWorkAddress(value) => { +                self.work_address = value; +                self.link.send_message(Msg::Generate(self.selected_option)); +            }, +            Msg::UpdateTelephone(value) => { +                self.telephone = value; +                self.link.send_message(Msg::Generate(self.selected_option)); +            }, +            Msg::Generate(option) => { +                self.selected_option = option; + +                let vcard_content = match self.generate_vcard() { +                    Ok(vcard) => Some(vcard.to_string()), +                    Err(VCardError::FormatError(err)) => { +                        self.error.push(err.to_string()); +                        None +                    } +                    Err(VCardError::EmptyFormatName) => { +                        self.error.push(String::from("At least one of the name fields should be filled out.")); +                        None +                    } +                }; + +                match option { +                    DownloadOption::VCard => { +                        if vcard_content.is_some() { +                            self.download = Some( +                                Download {  +                                    file_name: format!("{}.vcs", self.name.formatted_name()),  +                                    content: vcard_content.unwrap().to_string(),  +                                    mime_type: MimeType::VCard, +                                } +                            ) +                        } +                    } +                    DownloadOption::QrCode => { +                        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!")), +                            }; +                        } +                    } +                    DownloadOption::PDF => { +                        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::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_options = self.link.callback(|e: ChangeData| +            match e { +                ChangeData::Select(v) => match v.value().as_str() { +                    "vcard" => Msg::Generate(DownloadOption::VCard), +                    "pdf" => Msg::Generate(DownloadOption::PDF), +                    "qrcode" => Msg::Generate(DownloadOption::QrCode), +                    _ => 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=self.link.callback(|n: Name| Msg::UpdateName(n)) /> + +                            <AddressView address_type=AddressType::Home oninput=self.link.callback(|a: Address| Msg::UpdateHomeAddress(a)) /> + +                            <AddressView address_type=AddressType::Work oninput=self.link.callback(|a: Address| Msg::UpdateWorkAddress(a)) /> + +                            <TelephoneView oninput=self.link.callback(|t: Telephone| Msg::UpdateTelephone(t)) /> + +                            <div class="block level-left"> +                                <div class="select level-item"> +                                    <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 level-item" > +                    { "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!{ +                        <pre> +                            <code> { download.content.clone() } </code> +                        </pre> +                    } +                } +                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) => { +                let mut vcard = vcard; + +                let names = { +                    let mut names = HashSet::new(); +                    names.insert(self.name.to_vcard_value()); + +                    names +                }; + +                let addresses = { +                    let mut addresses = HashSet::new(); +                    addresses.insert(self.home_address.to_vcard_value()); +                    addresses.insert(self.work_address.to_vcard_value()); + +                    addresses +                }; + +                let telephones = { +                    let mut telephones = HashSet::new(); +                    telephones.insert(self.telephone.to_vcard_value()); +                     +                    telephones +                }; + +                vcard.names = Some(vcard::Set::from_hash_set(names).unwrap()); +                vcard.addresses = Some(vcard::Set::from_hash_set(addresses).unwrap()); +                vcard.telephones = Some(vcard::Set::from_hash_set(telephones).unwrap()); + +                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(()), +        } +    } +}
\ No newline at end of file diff --git a/src/name.rs b/src/view/name.rs index 4b51def..872676c 100644 --- a/src/name.rs +++ b/src/view/name.rs @@ -2,6 +2,8 @@ use yew::prelude::*;  use vcard::properties;  use vcard::values::{self, text}; +use crate::util; +  #[derive(Clone)]  pub struct Name {      pub prefix: String, @@ -46,7 +48,7 @@ impl Name {          formatted_name      } -    pub fn to_vcard_name(&self) -> properties::Name { +    pub fn to_vcard_value(&self) -> properties::Name {          let name_value = values::name_value::NameValue::from_components(              match self.last_name.is_empty() {                  true => None, @@ -126,61 +128,41 @@ impl Component for NameView {                  <h3 class="subtitle">{ "Name" }</h3>                  <div class="columns is-mobile is-multiline"> -                 -                    <div class="field column is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <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 is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <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 is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <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 is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <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 is-one-fifth-widescreen is-one-quarter-desktop is-one-third-tablet is-half-mobile"> -                        <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> + +                    { util::text_field_input( +                        "Prefix",  +                        "prefix",  +                        Some("Sir"),  +                        self.link.callback(|e: InputData| Msg::UpdatePrefix(e.value)) +                    ) } + +                    { util::text_field_input( +                        "First name",  +                        "first_name",  +                        Some("Arthur"),  +                        self.link.callback(|e: InputData| Msg::UpdateFirstName(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Middle name",  +                        "middle_name",  +                        Some("Charles"),  +                        self.link.callback(|e: InputData| Msg::UpdateMiddleName(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Last name",  +                        "last_name",  +                        Some("Clarke"),  +                        self.link.callback(|e: InputData| Msg::UpdateLastName(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Suffix",  +                        "suffix",  +                        Some("CBE FRAS"),  +                        self.link.callback(|e: InputData| Msg::UpdateSuffix(e.value)) +                    ) }                  </div>              </div> diff --git a/src/view/photo.rs b/src/view/photo.rs new file mode 100644 index 0000000..1c2d088 --- /dev/null +++ b/src/view/photo.rs @@ -0,0 +1,10 @@ +use yew::prelude::*; +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; + +#[derive(Clone)] +pub struct Photo { +    pub base64_image: String, +} + diff --git a/src/view/telephone.rs b/src/view/telephone.rs new file mode 100644 index 0000000..28a6f01 --- /dev/null +++ b/src/view/telephone.rs @@ -0,0 +1,183 @@ +use yew::services::ConsoleService; +use yew::prelude::*; +use vcard::properties; +use vcard::parameters; +use vcard::values::{self, text}; +use vcard::validators::ValidatedWrapper; +use std::collections::HashSet; +use crate::util; + +#[derive(Clone)] +pub struct Telephone { +    pub number: String, +    pub extension: String, +    pub work: bool, +    pub home: bool, +    pub text: bool, +    pub voice: bool, +    pub fax: bool, +    pub cell: bool, +    pub video: bool, +    pub pager: bool, +    pub text_phone: bool, +} + +impl Telephone { +    pub fn new() -> Self { +        Self { +            number: String::new(), +            extension: String::new(), +            work: false, +            home: false, +            text: false, +            voice: false, +            fax: false, +            cell: false, +            video: false, +            pager: false, +            text_phone: false, +        } +    } +    pub fn to_vcard_value(&self) -> properties::Telephone { +        let mut telephone = properties::Telephone::from_telephone_value( +            values::telephone_value::TelephoneValue::from_telephone_number_str( +                self.number.clone(),  +                match self.extension.is_empty() { +                    true => None::<&str>, +                    false => Some(&self.extension), +                }, +            ).unwrap() +        ); + +        let type_values = { +            let mut type_values = HashSet::new(); + +            if self.work { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Work); +            } +            if self.home { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Home); +            } +            if self.text { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Text); +            } +            if self.voice { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Voice); +            } +            if self.fax { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Fax); +            } +            if self.cell { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Cell); +            } +            if self.video { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Video); +            } +            if self.pager { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::Pager); +            } +            if self.text_phone { +                type_values.insert(values::type_value::TypeValueWithTelephoneType::TextPhone); +            } + +            vcard::Set::from_hash_set(type_values).unwrap() +        }; + +        if let properties::Telephone::TelephoneValue { ref mut typ, .. } = telephone { +            *typ = Some(parameters::typ::TypeWithTelType::from_type_values(type_values)); +        } + +        telephone +    } +} + +pub struct TelephoneView { +    link: ComponentLink<Self>, +    value: Telephone, +    oninput: Callback<Telephone>, +} + +pub enum Msg { +    UpdateNumber(String), +    UpdateExtension(String), +    ToggleWork, +    ToggleHome, +    ToggleText, +    ToggleVoice, +    ToggleFax, +    ToggleCell, +    ToggleVideo, +    TogglePager, +    ToggleTextPhone, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct Props { +    pub oninput: Callback<Telephone>, +    //pub errors: Vec<String>, +} + +impl Component for TelephoneView { +    type Message = Msg; +    type Properties = Props; +    fn create(props: <Self as yew::Component>::Properties, link: yew::html::Scope<Self>) -> Self { +        Self { +            link, +            value: Telephone::new(), +            oninput: props.oninput, +        } +    } +    fn update(&mut self, msg: <Self as yew::Component>::Message) -> bool { +        match msg { +            Msg::UpdateNumber(n) => self.value.number = n, +            Msg::UpdateExtension(e) => self.value.extension = e, +            Msg::ToggleWork => self.value.work = !self.value.work, +            Msg::ToggleHome => self.value.home = !self.value.home, +            Msg::ToggleText => self.value.text = !self.value.text, +            Msg::ToggleVoice => self.value.voice = !self.value.voice, +            Msg::ToggleFax => self.value.fax = !self.value.fax, +            Msg::ToggleCell => self.value.cell = !self.value.cell, +            Msg::ToggleVideo => self.value.video = !self.value.video, +            Msg::TogglePager => self.value.pager = !self.value.pager, +            Msg::ToggleTextPhone => self.value.text_phone = !self.value.text_phone, +        }; +        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!{ +            <div class="box"> +                <h3 class="subtitle">{ "Telephone" }</h3> + +                <div class="columns is-mobile is-multiline"> +                 +                    { util::text_field_input( +                        "Number",  +                        "number",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateNumber(e.value)) +                    ) } + +                    { util::text_field_input( +                        "Extension",  +                        "extension",  +                        None,  +                        self.link.callback(|e: InputData| Msg::UpdateExtension(e.value)) +                    ) } + +                    { util::checkbox_field_input( +                        "Work",  +                        "work", +                        self.value.work, +                        self.link.callback(|_| Msg::ToggleWork) +                    ) } + +                </div> +            </div> +        } +    } +}
\ No newline at end of file | 
