//! The file-view workspace: everything the signed-in shell and the public /// share page share around the full-page file view. /// /// Both pages open files in the same topbar-headed view with the same /// close/save/dirty plumbing, the same selection + dialog state, and the /// same Escape semantics. [`FileWorkspace`] owns all of that; the pages /// keep only what differs (sidebar + section routing vs. share resolution /// + states) and pull the exposed handles into their browser. use std::sync::Arc; use leptos::ev::keydown; use leptos::html::Header; use leptos::prelude::*; use web_sys::KeyboardEvent; use crate::api::{Entry, Me}; use crate::components::logo::Logo; use crate::components::toast::{ToastMsg, ToastView}; use crate::router::Location; use crate::util::owned_window_listener; use crate::views::browser::SelectionActions; use crate::views::dialogs::{Dialog, DialogView}; use crate::views::file_view::{FileView, FileViewActions, FileViewContent, FileViewTitle}; /// The shared state and behavior around the full-page file view. /// /// Create one per page in the page component body, after `provide_toast`: /// the signals, the navigation effect and the Escape listener are owned by /// the page, so everything is cleaned up when the page unmounts. pub struct FileWorkspace { pub file_view: ReadSignal>, /// Open a file in the full-page view (hand to the browser / search). pub open_file: WriteSignal>, pub selected: ReadSignal>, pub set_selected: WriteSignal>, pub dialog: ReadSignal>, pub set_dialog: WriteSignal>, /// The browser stores its fetch there so the editor can re-list after a /// save. pub set_browser_refresh: WriteSignal>>, pub topbar_ref: NodeRef
, me: ReadSignal>, loc: ReadSignal, toast: ToastMsg, owner: Owner, save_cb: ReadSignal>>, set_save_cb: WriteSignal>>, close_cb: ReadSignal>>, set_close_cb: WriteSignal>>, dirty: ReadSignal, set_dirty: WriteSignal, saving: ReadSignal, set_saving: WriteSignal, browser_refresh: ReadSignal>>, close_file: Callback<()>, /// What the file view becomes on close (see the parameter of the same /// name in [`FileWorkspace::new`]). close_target: Arc Option + Send + Sync>, /// Whether the topbar shows the selection actions while no file is /// open (i.e. the page's browser is the visible content). show_actions: Arc bool + Send + Sync>, } impl FileWorkspace { /// `close_target` is what the file view becomes on close: `None` = /// back to the listing (signed-in app); `Some` = the same file is /// shown again (a file share has nothing to close back to). With a /// `Some` target the close button and Escape are disabled. /// /// `show_actions` decides whether the topbar shows the selection /// actions while no file is open (i.e. the page's browser is the /// visible content). pub fn new( me: ReadSignal>, loc: ReadSignal, close_target: impl Fn() -> Option + 'static + Send + Sync, show_actions: impl Fn() -> bool + 'static + Send + Sync, ) -> Self { let toast = use_context::().expect("toast context"); let owner = Owner::current().unwrap(); let close_target = Arc::new(close_target); let show_actions = Arc::new(show_actions); let (file_view, set_file_view) = signal(Option::::None); // The open editor stores its save callback here for the top-bar button. let (save_cb, set_save_cb) = signal(Option::>::None); // The open editor's guarded close request (unsaved-changes prompt). let (close_cb, set_close_cb) = signal(Option::>::None); let (dirty, set_dirty) = signal(false); let (saving, set_saving) = signal(false); // The browser's fetch, exposed so the editor can re-list after a save. let (browser_refresh, set_browser_refresh) = signal(Option::>::None); // The selected entries in the file browser; their action buttons // live in the top bar's top right. let (selected, set_selected) = signal(Vec::::new()); // Dialogs are rendered here, above everything. let (dialog, set_dialog) = signal(Option::::None); let topbar_ref = NodeRef::
::new(); // Navigating (new folder, root, section) invalidates the selection. Effect::new(move |_| { let _ = loc.get(); set_selected.set(Vec::new()); }); let close_file = { let ct = close_target.clone(); Callback::new(move |_| { set_save_cb.set(None); set_close_cb.set(None); set_dirty.set(false); set_saving.set(false); set_file_view.set(ct()); }) }; // Escape closes previews and the "no preview" view — except when // there is nothing to close back to (file share). The editor handles // Escape itself (it asks first when there are unsaved changes). { let fv = file_view; let cf = close_file; let ct = close_target.clone(); owned_window_listener(keydown, move |ev: KeyboardEvent| { if ev.key() != "Escape" { return; } if ct().is_some() { return; } if fv .get() .is_some_and(|v| matches!(v, FileView::Preview(..) | FileView::Unsupported(_))) { cf.run(()); } }); } Self { file_view, open_file: set_file_view, selected, set_selected, dialog, set_dialog, set_browser_refresh, topbar_ref, me, loc, toast, owner, save_cb, set_save_cb, close_cb, set_close_cb, dirty, set_dirty, saving, set_saving, browser_refresh, close_file, close_target, show_actions, } } /// Topbar left: the brand. In file mode the brand shrinks to its icon /// (a link to `/`) and the file title takes its place. pub fn topbar_left(&self) -> impl IntoView + 'static + use<> { let file_view = self.file_view; let dirty = self.dirty; view! { {move || match file_view.get() { Some(ref v) => view! { } .into_view() .into_any(), None => view! { "filebrowser-ng" } .into_view() .into_any(), }} } } /// Topbar right: the open file's actions (download / save / close) or, /// while no file is open, the selected entry's actions. pub fn topbar_right(&self) -> impl IntoView + 'static + use<> { let file_view = self.file_view; let show_actions = self.show_actions.clone(); let close_target = self.close_target.clone(); let save_cb = self.save_cb; let dirty = self.dirty; let saving = self.saving; let close_cb = self.close_cb; let close_file = self.close_file; let me = self.me; let loc = self.loc; let selected = self.selected; let open_file = self.open_file; let browser_refresh = self.browser_refresh; let toast = self.toast; let set_dialog = self.set_dialog; let owner = self.owner.clone(); let topbar_ref = self.topbar_ref; view! { {move || { if let Some(v) = file_view.get() { view! { } .into_view() .into_any() } else if show_actions() { let refresh = browser_refresh .get() .unwrap_or_else(|| Callback::new(move |_| {})); view! { } .into_view() .into_any() } else { view! {}.into_any() } }} } } /// The full-page content (preview / editor / "no preview"), rendered /// while a file is open. pub fn file_content(&self) -> impl IntoView + 'static + use<> { let file_view = self.file_view; let set_save_cb = self.set_save_cb; let set_close_cb = self.set_close_cb; let dirty = self.dirty; let set_dirty = self.set_dirty; let set_saving = self.set_saving; let close_file = self.close_file; let toast = self.toast; let browser_refresh = self.browser_refresh; view! { {move || match file_view.get() { Some(v) => { let refresh = browser_refresh .get() .unwrap_or_else(|| Callback::new(move |_| {})); view! { } .into_view() .into_any() } None => view! {}.into_any(), }} } } /// The page-level overlays: toasts + dialogs, above everything. pub fn overlays(&self) -> impl IntoView + 'static + use<> { let toast = self.toast; let dialog = self.dialog; let set_dialog = self.set_dialog; view! { } } }