vertigo_forms/
drop_image_file.rs

1use base64::{Engine as _, engine::general_purpose::STANDARD_NO_PAD as BASE_64};
2use std::rc::Rc;
3use vertigo::{
4    Computed, Css, DomNode, DropFileEvent, DropFileItem, Value, bind, computed_tuple, css, dom,
5};
6
7/// Box that allows to accept image files on it, connected to `Value<Option<DropFileItem>>`.
8pub struct DropImageFile {
9    pub original_link: Computed<Option<Rc<String>>>,
10    pub item: Value<Option<DropFileItem>>,
11    pub params: DropImageFileParams,
12}
13
14#[derive(Clone)]
15pub struct DropImageFileParams {
16    /// Custom callback when new image dropped, leave empty to automatically set/unset `item`
17    pub callback: Option<Rc<dyn Fn(Option<DropFileItem>)>>,
18    pub revert_label: String,
19    pub cancel_label: String,
20    pub no_image_text: String,
21    pub dropzone_css: Css,
22    pub dropzone_add_css: Css,
23    pub img_css: Css,
24}
25
26impl Default for DropImageFileParams {
27    fn default() -> Self {
28        Self {
29            callback: None,
30            revert_label: "Revert".to_string(),
31            cancel_label: "Cancel".to_string(),
32            no_image_text: "No image".to_string(),
33            dropzone_css: css! {"
34                width: 400px;
35                height: 400px;
36
37                display: flex;
38                align-items: center;
39                justify-content: center;
40
41                padding: 10px;
42            "},
43            dropzone_add_css: css!(""),
44            img_css: css!(""),
45        }
46    }
47}
48
49impl DropImageFile {
50    pub fn into_component(self) -> Self {
51        self
52    }
53
54    pub fn mount(&self) -> DomNode {
55        let base64_data = self.item.to_computed().map(|item| match item {
56            Some(item) => image_as_uri(&item),
57            None => "".to_string(),
58        });
59
60        let view_deps = computed_tuple!(
61            a => self.original_link,
62            b => self.item,
63            c => base64_data
64        );
65        let item_clone = self.item.clone();
66        let params = self.params.clone();
67        let callback = self.params.callback.clone();
68        let image_view = view_deps.render_value(move |(original, item, base64_date)| match item {
69            Some(item) => {
70                let message = format_line(&item);
71                let image_css = css! {"
72                    display: flex;
73                    flex-flow: column;
74                "};
75                let restore = bind!(item_clone, callback, |_| {
76                    if let Some(callback) = &callback {
77                        callback(None);
78                    } else {
79                        item_clone.set(None);
80                    }
81                });
82                let restore_text = if original.is_some() {
83                    &params.revert_label
84                } else {
85                    &params.cancel_label
86                };
87                dom! {
88                    <div css={image_css}>
89                        <button on_click={restore}>{restore_text}</button>
90                        <img css={&params.img_css} src={base64_date} />
91                        { message }
92                    </div>
93                }
94            }
95            None => match original {
96                Some(original) => {
97                    dom! { <div><img css={&params.img_css} src={original} /></div> }
98                }
99                None => dom! { <div>{&params.no_image_text}</div> },
100            },
101        });
102
103        let item = self.item.clone();
104        let callback = self.params.callback.clone();
105        let on_dropfile = move |event: DropFileEvent| {
106            for file in event.items.into_iter() {
107                if let Some(callback) = callback.as_deref() {
108                    callback(Some(file));
109                } else {
110                    item.set(Some(file));
111                }
112            }
113        };
114
115        let dropzone_css = &self.params.dropzone_css + &self.params.dropzone_add_css;
116
117        dom! {
118            <div css={dropzone_css} on_dropfile={on_dropfile}>
119                { image_view }
120            </div>
121        }
122    }
123}
124
125fn format_line(item: &DropFileItem) -> String {
126    let file_name = &item.name;
127    let size = item.data.len();
128    format!("{file_name} ({size})")
129}
130
131pub fn name_to_mime(name: &str) -> &'static str {
132    use std::{ffi::OsStr, path::Path};
133
134    let extension = Path::new(name)
135        .extension()
136        .and_then(OsStr::to_str)
137        .unwrap_or_default();
138
139    match extension {
140        "jpg" | "jpeg" | "jpe" => "image/jpeg",
141        "png" => "image/png",
142        "svg" => "image/svg+xml",
143        "gif" => "image/gif",
144        "bmp" => "image/bmp",
145        "ico" => "image/ico",
146        _ => "application/octet-stream",
147    }
148}
149
150pub fn image_as_uri(item: &DropFileItem) -> String {
151    let mime = name_to_mime(&item.name);
152    let data = BASE_64.encode(&*item.data);
153    format!("data:{mime};base64,{data}")
154}