1use crate::{class::Class, icon::SvgIcon};
2use dioxus::prelude::*;
3use dioxus_free_icons::icons::{bs_icons::*, fa_solid_icons::FaUpload};
4use std::{fs, path::PathBuf};
5use zino_core::SharedString;
6use zino_storage::NamedFile;
7
8pub fn FileUpload(props: FileUploadProps) -> Element {
10 let mut file_names = use_signal(Vec::new);
11 let has_name = props.children.is_some() || !file_names().is_empty();
12 rsx! {
13 div {
14 class: "{props.class}",
15 class: if !props.color.is_empty() { "is-{props.color}" },
16 class: if !props.size.is_empty() { "is-{props.size}" },
17 class: if props.fullwidth { "is-fullwidth" },
18 class: if has_name { "has-name" },
19 label {
20 class: props.label_class.clone(),
21 input {
22 class: props.input_class,
23 r#type: "file",
24 onchange: move |event| async move {
25 if let Some(handler) = props.on_change.as_ref() {
26 let mut files = Vec::new();
27 file_names.write().clear();
28 for file in event.files() {
29 if let Ok(bytes) = file.read_bytes().await {
30 let file_path = PathBuf::from(file.name());
31 if let Some(file_name) = file_path
32 .file_name()
33 .map(|f| f.to_string_lossy())
34 {
35 let mut file = NamedFile::new(file_name);
36 if let Some(file_name) = file.file_name() {
37 file_names.write().push(file_name.to_owned());
38 }
39 file.set_bytes(bytes);
40 files.push(file);
41 }
42 }
43 }
44 handler.call(files);
45 }
46 },
47 ..props.attributes,
48 }
49 div {
50 class: "file-cta",
51 span {
52 class: "file-icon",
53 if let Some(icon) = props.icon {
54 { icon }
55 } else {
56 SvgIcon {
57 shape: FaUpload,
58 width: 16,
59 }
60 }
61 }
62 span {
63 class: props.label_class,
64 { props.label.into_owned() }
65 }
66 }
67 if let Some(children) = props.children {
68 span {
69 class: "file-name",
70 { children }
71 }
72 } else if !file_names().is_empty() {
73 span {
74 class: "file-name",
75 { file_names().join(", ") }
76 }
77 }
78 }
79 }
80 }
81}
82
83#[derive(Clone, PartialEq, Props)]
85pub struct FileUploadProps {
86 #[props(into, default = "file")]
88 pub class: Class,
89 #[props(into, default = "file-label")]
91 pub label_class: Class,
92 #[props(into, default = "file-input")]
94 pub input_class: Class,
95 #[props(into, default)]
97 pub color: SharedString,
98 #[props(into, default)]
100 pub size: SharedString,
101 #[props(default)]
103 pub fullwidth: bool,
104 #[props(into)]
106 pub label: SharedString,
107 pub icon: Option<Element>,
109 pub on_change: Option<EventHandler<Vec<NamedFile>>>,
111 #[props(extends = input)]
113 attributes: Vec<Attribute>,
114 #[props(into)]
116 children: Option<Element>,
117}
118
119pub fn FileTree(props: FileTreeProps) -> Element {
121 let tree_id = props.tree_id;
122 let icon_size = props.icon_size;
123 let on_click = props.on_click;
124 let Some(current_dir) = props.current_dir.as_ref() else {
125 return rsx! {};
126 };
127 let Some(current_dir_name) = current_dir.file_name().and_then(|s| s.to_str()) else {
128 return rsx! {};
129 };
130
131 let mut opened = use_signal(|| props.opened);
132 let mut folders = Vec::new();
133 let mut files = Vec::new();
134 if opened() {
135 let entries = fs::read_dir(current_dir)?;
136 for entry in entries.flatten() {
137 if let Ok(metadata) = entry.metadata() {
138 let path = entry.path();
139 if metadata.is_dir() {
140 folders.push(path);
141 } else if metadata.is_file()
142 && let Some(name) = path
143 .file_name()
144 .and_then(|name| name.to_str())
145 .map(|s| s.to_owned())
146 {
147 files.push((name, path));
148 }
149 }
150 }
151 }
152 rsx! {
153 div {
154 key: "{tree_id}-{current_dir_name}",
155 class: props.class,
156 cursor: "pointer",
157 div {
158 onclick: move |_event| {
159 opened.set(!opened());
160 },
161 if opened() {
162 SvgIcon {
163 shape: BsChevronDown,
164 width: icon_size,
165 }
166 } else {
167 SvgIcon {
168 shape: BsChevronRight,
169 width: icon_size,
170 }
171 }
172 span { "{current_dir_name}" }
173 }
174 for path in folders {
175 FileTree {
176 class: props.class.clone(),
177 file_class: props.file_class.clone(),
178 current_dir: Some(path),
179 tree_id: tree_id.clone(),
180 icon_size: icon_size,
181 opened: false,
182 on_click: on_click,
183 }
184 }
185 for (name, path) in files {
186 div {
187 class: "{props.file_class}",
188 onclick: move |_event| {
189 if let Some(handler) = on_click.as_ref() {
190 handler.call(path.clone());
191 }
192 },
193 span { "{name}" }
194 }
195 }
196 }
197 }
198}
199
200#[derive(Clone, PartialEq, Props)]
202pub struct FileTreeProps {
203 #[props(into, default = "file-tree")]
205 pub class: Class,
206 #[props(into, default = "file-node")]
208 pub file_class: Class,
209 pub current_dir: Option<PathBuf>,
211 pub tree_id: String,
213 #[props(default = 16)]
215 pub icon_size: u32,
216 #[props(default)]
218 pub opened: bool,
219 pub on_click: Option<EventHandler<PathBuf>>,
221}