use super::AnimatedProgress;
use crate::{prelude::FluentBuilder as _, *};
use std::path::PathBuf;
use std::sync::Arc;
#[derive(Clone)]
pub struct UploadFile {
pub path: PathBuf,
pub name: SharedString,
pub size: u64,
pub progress: f32,
}
impl UploadFile {
pub fn from_path(path: PathBuf) -> Self {
let name: SharedString = path
.file_name()
.map(|s| s.to_string_lossy().into_owned().into())
.unwrap_or_default();
let size = std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0);
Self {
path,
name,
size,
progress: 0.0,
}
}
pub fn size_text(&self) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
if self.size >= MB {
format!("{:.1} MB", self.size as f64 / MB as f64)
} else if self.size >= KB {
format!("{:.1} KB", self.size as f64 / KB as f64)
} else {
format!("{} B", self.size)
}
}
}
pub struct UploadState {
files: Vec<UploadFile>,
multiple: bool,
directories: bool,
prompt: Option<SharedString>,
on_change: Option<Arc<dyn Fn(Vec<PathBuf>, &mut Window, &mut App) + Send + Sync>>,
pending_select: bool,
style: StyleRefinement,
}
impl UploadState {
pub fn new(_window: &mut Window, _cx: &mut Context<Self>) -> Self {
Self {
files: Vec::new(),
multiple: true,
directories: false,
prompt: None,
on_change: None,
pending_select: false,
style: StyleRefinement::default(),
}
}
pub fn multiple(mut self, multiple: bool) -> Self {
self.multiple = multiple;
self
}
pub fn directories(mut self, directories: bool) -> Self {
self.directories = directories;
self
}
pub fn prompt(mut self, prompt: impl Into<SharedString>) -> Self {
self.prompt = Some(prompt.into());
self
}
pub fn on_change<F>(mut self, handler: F) -> Self
where
F: Fn(Vec<PathBuf>, &mut Window, &mut App) + Send + Sync + 'static,
{
self.on_change = Some(Arc::new(handler));
self
}
pub fn files(&self) -> &[UploadFile] {
&self.files
}
pub fn pick(&mut self, cx: &mut Context<Self>) {
let receiver = cx.prompt_for_paths(PathPromptOptions {
files: true,
directories: self.directories,
multiple: self.multiple,
prompt: self.prompt.clone(),
});
cx.spawn(async move |this, cx| {
if let Ok(Ok(Some(paths))) = receiver.await {
_ = this.update(cx, |state, cx| {
state.push_paths(paths);
state.pending_select = true;
cx.notify();
});
}
})
.detach();
}
pub fn add_paths(&mut self, paths: Vec<PathBuf>, cx: &mut Context<Self>) {
self.push_paths(paths);
cx.notify();
}
fn push_paths(&mut self, paths: Vec<PathBuf>) {
for path in paths {
if !self.files.iter().any(|f| f.path == path) {
self.files.push(UploadFile::from_path(path));
}
}
}
pub fn remove_file(&mut self, index: usize, cx: &mut Context<Self>) {
if index < self.files.len() {
self.files.remove(index);
cx.notify();
}
}
pub fn set_progress(&mut self, index: usize, progress: f32, cx: &mut Context<Self>) {
if let Some(file) = self.files.get_mut(index) {
file.progress = progress.clamp(0.0, 1.0);
cx.notify();
}
}
pub fn clear(&mut self, cx: &mut Context<Self>) {
self.files.clear();
cx.notify();
}
}
impl Styled for UploadState {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl Render for UploadState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
if self.pending_select {
self.pending_select = false;
if let Some(ref cb) = self.on_change {
let paths: Vec<PathBuf> = self.files.iter().map(|f| f.path.clone()).collect();
let cb = cb.clone();
cb(paths, window, cx);
}
}
let theme = cx.theme();
let border = theme.tokens.border;
let muted_foreground = theme.tokens.muted_foreground;
let accent = theme.tokens.accent.color;
let user_style = self.style.clone();
let mut root =
div().flex().flex_col().gap(px(8.0)).child(
div()
.id("upload-pick")
.flex()
.items_center()
.justify_center()
.gap(px(8.0))
.py(px(20.0))
.rounded_md()
.border(px(1.0))
.border_color(border)
.cursor_pointer()
.hover(|this| this.bg(accent.opacity(0.08)))
.child(Icon::new(IconName::File).text_color(muted_foreground))
.child(div().text_sm().text_color(muted_foreground.color).child(
if self.multiple {
"点击选择文件(可多选)"
} else {
"点击选择文件"
},
))
.on_click(cx.listener(|this, _, _, cx| {
this.pick(cx);
})),
);
for (ix, file) in self.files.iter().enumerate() {
let name = file.name.clone();
let size_text = file.size_text();
let progress = file.progress;
root = root.child(
div()
.flex()
.flex_col()
.gap(px(4.0))
.px(px(12.0))
.py(px(8.0))
.rounded_md()
.bg(accent.opacity(0.05))
.child(
div()
.flex()
.items_center()
.gap(px(8.0))
.child(div().flex_1().text_sm().child(name))
.child(
div()
.text_xs()
.text_color(muted_foreground.color)
.child(size_text),
)
.child(
Button::new(ElementId::named_usize("upload-remove", ix))
.ghost()
.small()
.icon(IconName::Close)
.on_click(cx.listener(move |this, _, _, cx| {
this.remove_file(ix, cx);
})),
),
)
.child(
AnimatedProgress::new(ElementId::named_usize("upload-progress", ix))
.value(progress),
),
);
}
root.map(|mut this| {
this.style().refine(&user_style);
this
})
}
}