vertigo_forms/
drop_image_file.rs1use 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
7pub 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 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 ¶ms.revert_label
84 } else {
85 ¶ms.cancel_label
86 };
87 dom! {
88 <div css={image_css}>
89 <button on_click={restore}>{restore_text}</button>
90 <img css={¶ms.img_css} src={base64_date} />
91 { message }
92 </div>
93 }
94 }
95 None => match original {
96 Some(original) => {
97 dom! { <div><img css={¶ms.img_css} src={original} /></div> }
98 }
99 None => dom! { <div>{¶ms.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}