//! Modal dialogs: text prompts (new folder / rename), confirmations (delete), //! overwrite conflict warnings, and the move/copy destination picker. use leptos::prelude::*; use wasm_bindgen_futures::spawn_local; use web_sys::MouseEvent; use crate::api::{self, Entry, RootInfo}; use crate::components::icon::{Icon, IconName}; use crate::components::toast::{ToastMsg, show}; /// Which mutating operation the destination picker is for. #[derive(Clone, Copy, PartialEq)] pub enum Op { Move, Copy, } impl Op { pub fn verb(self) -> &'static str { match self { Op::Move => "Move", Op::Copy => "Copy", } } } /// The single active dialog. The browser renders whichever is `Some`. #[derive(Clone)] pub enum Dialog { /// Text input (new folder, rename). Prompt { title: String, label: String, initial: String, submit: String, on_submit: Callback, }, /// Simple confirmation (delete). Confirm { title: String, message: String, submit: String, danger: bool, on_submit: Callback<()>, }, /// Overwrite warning listing the conflicting files. Conflict { title: String, files: Vec, on_submit: Callback<()>, }, /// Move/copy destination picker. Picker { op: Op, entry: Entry, roots: Vec, source_root: i64, source_dir: String, on_pick: Callback<(i64, String)>, }, /// Folder download — format choice (zip | tar | tar.gz | tar.zst). DownloadFormat { name: String, root_id: i64, path: String, }, /// Read-only item info. Info { entry: Entry, root_name: String, rel_path: String, }, /// Create a share link for an item. Share { name: String, root_id: i64, path: String, allow_writable: bool, }, } /// Split a "/"-separated directory path into its non-empty segments. fn dir_segs(dir: &str) -> Vec { dir.split('/') .filter(|s| !s.is_empty()) .map(|s| s.to_string()) .collect() } #[component] pub fn DialogView( dialog: ReadSignal>, set_dialog: WriteSignal>, ) -> impl IntoView { // Escape closes the active dialog. { let d = dialog; let set_d = set_dialog; crate::util::owned_window_listener( leptos::ev::keydown, move |ev: web_sys::KeyboardEvent| { if ev.key() == "Escape" && d.get().is_some() { set_d.set(None); } }, ); } view! { {move || { let Some(d) = dialog.get() else { return { view! {}.into_any() }; }; let close = Callback::new(move |_| set_dialog.set(None)); match &d { Dialog::Prompt { title, label, initial, submit, on_submit, } => { let (title, label, initial, submit, on_submit, close) = ( title.clone(), label.clone(), initial.clone(), submit.clone(), *on_submit, close, ); view! { } .into_view() .into_any() } Dialog::Confirm { title, message, submit, danger, on_submit, } => { let (title, message, submit, danger, on_submit, close) = ( title.clone(), message.clone(), submit.clone(), *danger, *on_submit, close, ); view! { } .into_view() .into_any() } Dialog::Conflict { title, files, on_submit, } => { let (title, files, on_submit, close) = ( title.clone(), files.clone(), *on_submit, close, ); view! { } .into_view() .into_any() } Dialog::Picker { op, entry, roots, source_root, source_dir, on_pick, } => { let (op, entry, roots, source_root, source_dir, on_pick, close) = ( *op, entry.clone(), roots.clone(), *source_root, source_dir.clone(), *on_pick, close, ); view! { } .into_view() .into_any() } Dialog::DownloadFormat { name, root_id, path, } => { let (name, root_id, path) = ( name.clone(), *root_id, path.clone(), ); view! { } .into_view() .into_any() } Dialog::Info { entry, root_name, rel_path, } => { let (entry, root_name, rel_path) = ( entry.clone(), root_name.clone(), rel_path.clone(), ); view! { } .into_view() .into_any() } Dialog::Share { name, root_id, path, allow_writable, } => { let (name, root_id, path, allow_writable) = ( name.clone(), *root_id, path.clone(), *allow_writable, ); view! { } .into_view() .into_any() } } }} } } // --------------------------------------------------------------------------- // Prompt (text input) // --------------------------------------------------------------------------- #[component] fn PromptDialog( title: String, label: String, initial: String, submit: String, on_submit: Callback, on_close: Callback<()>, ) -> impl IntoView { let input_id = "fbng-prompt-input"; let id = input_id.to_string(); let id2 = id.clone(); let submit_cb = on_submit; view! { } } // --------------------------------------------------------------------------- // Confirm // --------------------------------------------------------------------------- #[component] fn ConfirmDialog( title: String, message: String, submit: String, danger: bool, on_submit: Callback<()>, on_close: Callback<()>, ) -> impl IntoView { view! { } } // --------------------------------------------------------------------------- // Conflict (overwrite warning) // --------------------------------------------------------------------------- #[component] fn ConflictDialog( title: String, files: Vec, on_submit: Callback<()>, on_close: Callback<()>, ) -> impl IntoView { let shown: Vec = files.iter().take(20).cloned().collect(); let extra = files.len().saturating_sub(shown.len()); view! { } } // --------------------------------------------------------------------------- // Picker (move / copy destination) // --------------------------------------------------------------------------- #[component] fn PickerDialog( op: Op, entry: Entry, roots: Vec, source_root: i64, source_dir: String, on_pick: Callback<(i64, String)>, on_close: Callback<()>, ) -> impl IntoView { let (sel_root, set_sel_root) = signal(source_root); let (sel_dir, set_sel_dir) = signal(source_dir.clone()); let (entries, set_entries) = signal(Vec::::new()); // Load the subfolders of the selected directory whenever it changes. { let root_sig = sel_root; let dir_sig = sel_dir; let set_ent = set_entries; Effect::new(move |_| { let rid = root_sig.get(); let dir = dir_sig.get(); set_ent.set(Vec::new()); spawn_local(async move { match api::list_files(rid, &dir).await { Ok(r) => set_ent.set(r.entries.into_iter().filter(|e| e.is_dir).collect()), Err(_) => set_ent.set(Vec::new()), } }); }); } let sel_root2 = sel_root; let sel_dir2 = sel_dir; let set_dir = set_sel_dir; let roots_by_id = roots.clone(); view! { } } // --------------------------------------------------------------------------- // Download format (folder archive choice) // --------------------------------------------------------------------------- #[component] fn DownloadFormatDialog( name: String, root_id: i64, path: String, on_close: Callback<()>, ) -> impl IntoView { let toast = use_context::().expect("toast context"); let formats: [(&str, &str, &str); 4] = [ ("zip", "Zip", ".zip"), ("tar", "Tar", ".tar"), ("tar.gz", "Tar + gzip", ".tar.gz"), ("tar.zst", "Tar + zstd", ".tar.zst"), ]; view! { } } // --------------------------------------------------------------------------- // Item info // --------------------------------------------------------------------------- #[component] fn InfoDialog( entry: Entry, root_name: String, rel_path: String, on_close: Callback<()>, ) -> impl IntoView { // `rel_path` is the item's full path relative to the root (e.g. "docs/a.txt"). let is_dir = entry.is_dir; let size = if is_dir { "—".to_string() } else { crate::util::format_size(entry.size) }; let modified = crate::util::format_date(&entry.mtime); let parent = rel_path.rsplit_once('/').map(|(p, _)| p).unwrap_or(""); let location = if parent.is_empty() { format!("{root_name}/") } else { format!("{root_name}/{parent}") }; view! { } }