//! 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! {
}
}
}