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  | 
