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