Skip to main content

zino_dioxus/form/
file.rs

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
8/// A custom file upload input.
9pub 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/// The [`FileUpload`] properties struct for the configuration of the component.
84#[derive(Clone, PartialEq, Props)]
85pub struct FileUploadProps {
86    /// The class attribute for the component.
87    #[props(into, default = "file")]
88    pub class: Class,
89    /// A class to apply to the `label` element.
90    #[props(into, default = "file-label")]
91    pub label_class: Class,
92    /// A class to apply to the `label` element.
93    #[props(into, default = "file-input")]
94    pub input_class: Class,
95    /// The color of the button: `primary` | `link` | `info` | `success` | `warning` | `danger`.
96    #[props(into, default)]
97    pub color: SharedString,
98    /// The size of the button: `small` | `normal` | `medium` | `large`.
99    #[props(into, default)]
100    pub size: SharedString,
101    /// A flag to determine whether the control is fullwidth or not.
102    #[props(default)]
103    pub fullwidth: bool,
104    /// The label content.
105    #[props(into)]
106    pub label: SharedString,
107    /// An optional upload icon.
108    pub icon: Option<Element>,
109    /// An event handler to be called when the files are selected.
110    pub on_change: Option<EventHandler<Vec<NamedFile>>>,
111    /// Spreading the props of the `input` element.
112    #[props(extends = input)]
113    attributes: Vec<Attribute>,
114    /// The children to render within the component.
115    #[props(into)]
116    children: Option<Element>,
117}
118
119/// A list of files and folders in a hierarchical tree structure.
120pub 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/// The [`FileTree`] properties struct for the configuration of the component.
201#[derive(Clone, PartialEq, Props)]
202pub struct FileTreeProps {
203    /// The class attribute for the component.
204    #[props(into, default = "file-tree")]
205    pub class: Class,
206    /// The class attribute for files in the current directory.
207    #[props(into, default = "file-node")]
208    pub file_class: Class,
209    /// The current directory.
210    pub current_dir: Option<PathBuf>,
211    /// An identifer of the tree.
212    pub tree_id: String,
213    /// The size of the icon.
214    #[props(default = 16)]
215    pub icon_size: u32,
216    /// A flag to indicate whether the directory is opened or not.
217    #[props(default)]
218    pub opened: bool,
219    /// An event handler to be called when a file is clicked.
220    pub on_click: Option<EventHandler<PathBuf>>,
221}