//! The signed-in shell: left sidebar (folders + manage tabs) and the topbar. use crate::i18n; use leptos::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api::{self, Entry, Me}; use crate::components::icon::Icon; use crate::components::logo::Logo; use crate::components::toast::{ToastMsg, ToastView, provide_toast, show_error}; use crate::icons::IconName; use crate::router::{Location, Section, navigate}; use crate::views::admin::{SettingsView, UsersView}; use crate::views::browser::{Browser, SelectionActions, effective_root}; use crate::views::dialogs::{Dialog, DialogView}; use crate::views::file_view::{FileView, FileViewActions, FileViewContent, FileViewTitle}; use crate::views::search::SearchView; use crate::views::shares::SharesView; /// Whether the signed-in user has admin rights. fn is_admin_of(me: &Option) -> bool { me.as_ref() .and_then(|m| m.user.as_ref()) .is_some_and(|u| u.is_admin) } /// Re-fetch this session's `/me`. For things that can change the session /// outside of the current view: the sidebar's refresh button, or saving a /// user that is the current user. pub fn refresh_me(set_me: WriteSignal>, toast: ToastMsg) { spawn_local(async move { match api::me().await { Ok(m) => set_me.set(Some(m)), Err(e) => show_error(toast, e.to_string()), } }); } #[component] pub fn ShellView( me: ReadSignal>, set_me: WriteSignal>, set_phase: WriteSignal, loc: ReadSignal, theme: ReadSignal, set_theme: WriteSignal, ) -> impl IntoView { let toast = provide_toast(); // True while the off-canvas sidebar is open (narrow screens only). let (nav_open, set_nav_open) = signal(false); // A file opened in the full-page view (preview / editor / "no preview"); // the top bar doubles as its header. 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); // The editor's state, shared with the top bar (dirty dot, Save button). 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); let topbar_ref = NodeRef::::new(); // The single selected entry in the file browser; its action buttons live // in the top bar's top right. `None` shows the empty-area actions. let (selected, set_selected) = signal(Option::::None); // Dialogs are rendered here, above everything (used by both the browser // and the top bar's selection actions). let (dialog, set_dialog) = signal(Option::::None); // Long-lived owner for callbacks created inside short-lived scopes // (top-bar selection actions). let shell_owner = Owner::current().unwrap(); // Navigating (new folder, root, section) invalidates the selection. Effect::new(move |_| { let _ = loc.get(); set_selected.set(None); }); let close_file = Callback::new(move |_| { set_file_view.set(None); set_save_cb.set(None); set_close_cb.set(None); set_dirty.set(false); set_saving.set(false); }); // Escape closes previews and the "no preview" view. The editor handles // Escape itself (it asks first when there are unsaved changes). { let fv = file_view; let cf = close_file; crate::util::owned_window_listener( leptos::ev::keydown, move |ev: web_sys::KeyboardEvent| { if ev.key() != "Escape" { return; } if fv .get() .is_some_and(|v| matches!(v, FileView::Preview(..) | FileView::Unsupported(_))) { cf.run(()); } }, ); } // A non-admin cannot open the admin sections. If the URL points at one // (deep link, hand-typed, or left over from a demotion), redirect to the // files view so the URL, the content fallback, and the sidebar highlight // all agree. { let me2 = me; let loc2 = loc; Effect::new(move |_| { let Some(m) = me2.get() else { return; }; let admin = m.user.as_ref().is_some_and(|u| u.is_admin); if admin { return; } let section = loc2.get().section; if section == Section::Users { navigate(&Location::root()); } }); } let on_logout = move |_| { spawn_local(async move { let _ = api::logout().await; set_me.set(None); set_phase.set(crate::app::AuthPhase::Login); }); }; // Navigate to a manage section (each has its own URL) and, on narrow // screens, close the off-canvas sidebar. let open_section = move |section: Section| { set_nav_open.set(false); navigate(&Location { root_id: None, path: Vec::new(), share_token: None, section, }); }; view! {
{move || match file_view.get() { Some(ref v) => view! { // The brand stays in file mode as an icon-only // link to the root folder, left of the file title. } .into_view() .into_any(), None => view! { "filebrowser-ng" } .into_view() .into_any(), }}
{move || { if let Some(v) = file_view.get() { view! { } .into_view() .into_any() } else if loc.get().section == Section::Files { let refresh = browser_refresh .get() .unwrap_or_else(|| Callback::new(move |_| {})); view! { } .into_view() .into_any() } else { view! {}.into_any() } }}
{move || { if nav_open.get() { view! { } .into_view() .into_any() } else { view! {}.into_any() } }}
{move || { if file_view.get().is_none() { view! { } .into_view() .into_any() } else { view! {}.into_any() } }}
{move || { let admin = is_admin_of(&me.get()); match loc.get().section { Section::Files => view! { } .into_view() .into_any(), Section::Search => view! { } .into_view() .into_any(), Section::Shares => view! { } .into_view() .into_any(), Section::Users if admin => { view! { }.into_view().into_any() } Section::Settings => view! { } .into_view() .into_any(), // Not an admin anymore (e.g. permissions changed): // fall back to the files view. _ => view! { } .into_view() .into_any(), } }} {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(), }}
} }