//! Public share page (milestone 6): renders a shared item without login. use crate::i18n; use api_types::FileKind; use leptos::prelude::*; use wasm_bindgen_futures::spawn_local; use crate::api::{self, Entry, Me, Mode, RootInfo, ShareInfo, UserInfo}; use crate::components::logo::Logo; use crate::components::toast::{ToastView, provide_toast}; use crate::editor::EditTarget; use crate::preview::{PreviewTarget, preview_kind}; use crate::router::Location; use crate::views::browser::{Browser, SelectionActions}; use crate::views::dialogs::{Dialog, DialogView}; use crate::views::file_view::{ FileView, FileViewActions, FileViewContent, FileViewTitle, UnsupportedTarget, }; #[derive(Clone, PartialEq)] enum SharePageState { Loading, NotFound, Expired, Active, } /// The full-page view for a shared *file*: the share page *is* the file. /// The file sits at the (empty) path of its synthetic root. fn file_view_for_share(i: &ShareInfo) -> Option { let kind = i.kind?; let root_id = i.root_id; let name = i.name.clone(); let path = String::new(); match kind { FileKind::Text => Some(FileView::Editor(EditTarget { root_id, path, name, readonly: !i.writable, })), k if preview_kind(k).is_some() => Some(FileView::Preview( PreviewTarget { root_id, path, name, }, preview_kind(k).unwrap(), )), k => Some(FileView::Unsupported(UnsupportedTarget { root_id, path, name, kind: k, })), } } #[component] pub fn ShareView(token: String, loc: ReadSignal) -> impl IntoView { let toast = provide_toast(); let (me, set_me) = signal(Option::::None); let (info, set_info) = signal(Option::::None); let (state, set_state) = signal(SharePageState::Loading); // Resolve the share once per token and scope all file API calls to it. { let tok = token.clone(); let set_me2 = set_me; let set_info2 = set_info; let set_state2 = set_state; spawn_local(async move { api::set_share_token(Some(&tok)); match api::resolve_share(&tok).await { Ok(i) => { let mode = if i.writable { Mode::Rw } else { Mode::Ro }; let me_val = Me { first_boot: false, user: Some(UserInfo { id: i.id, name: "shared".to_string(), is_admin: false, single_click_open: false, language: None, }), roots: vec![RootInfo { id: i.root_id, name: i.name.clone(), path: "/".to_string(), mode, }], allow_writable_shares: false, }; set_me2.set(Some(me_val)); set_info2.set(Some(i)); set_state2.set(SharePageState::Active); } Err(e) => { let expired = e.status() == Some(410); set_state2.set(if expired { SharePageState::Expired } else { SharePageState::NotFound }); } } }); on_cleanup(move || api::set_share_token(None)); } // A file opened in the full-page view; 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); 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 single selected entry in the file browser; its action buttons live // in the top bar's top right. let (selected, set_selected) = signal(Option::::None); // Dialogs are rendered here, above everything. let (dialog, set_dialog) = signal(Option::::None); // Long-lived owner for callbacks created inside short-lived scopes. let shell_owner = Owner::current().unwrap(); let topbar_ref = NodeRef::::new(); // Navigating into a folder invalidates the selection. Effect::new(move |_| { let _ = loc.get(); set_selected.set(None); }); let close_file = Callback::new(move |_| { set_save_cb.set(None); set_close_cb.set(None); set_dirty.set(false); set_saving.set(false); // A folder share returns to the listing; a file share re-opens the // file itself (the editor remounts, so a "discard" is a real one). set_file_view.set(info.get().and_then(|i| file_view_for_share(&i))); }); // A file share opens straight into its viewer. { let info2 = info; let set_fv = set_file_view; Effect::new(move |_| { if let Some(i) = info2.get() && i.is_file { set_fv.set(file_view_for_share(&i)); } }); } // Escape closes previews and the "no preview" view of a *folder* share. // File shares have nothing to close back to; the editor handles Escape // itself (it asks first when there are unsaved changes). { let fv = file_view; let cf = close_file; let info2 = info; crate::util::owned_window_listener( leptos::ev::keydown, move |ev: web_sys::KeyboardEvent| { if ev.key() != "Escape" { return; } if info2.get().is_some_and(|i| i.is_file) { return; } if fv .get() .is_some_and(|v| matches!(v, FileView::Preview(..) | FileView::Unsupported(_))) { cf.run(()); } }, ); } view! {
{move || match file_view.get() { Some(ref v) => view! { } .into_view() .into_any(), None => view! {
"filebrowser-ng"
} .into_view() .into_any(), }}
{move || { if file_view.get().is_some() { let show_close = info .get() .map(|i| !i.is_file) .unwrap_or(true); view! { } .into_view() .into_any() } else if state.get() == SharePageState::Active { let refresh = browser_refresh .get() .unwrap_or_else(|| Callback::new(move |_| {})); view! { } .into_view() .into_any() } else { view! {}.into_any() } }}
{move || match state.get() { SharePageState::Loading => view! {

{i18n::tr(i18n::k::LOADING_SHARE)}

} .into_view() .into_any(), SharePageState::NotFound => view! {

{i18n::tr(i18n::k::SHARE_NOT_FOUND)}

{i18n::tr(i18n::k::SHARE_NOT_FOUND_MSG)}

} .into_view() .into_any(), SharePageState::Expired => view! {

{i18n::tr(i18n::k::SHARE_EXPIRED)}

{i18n::tr(i18n::k::SHARE_EXPIRED_MSG)}

} .into_view() .into_any(), SharePageState::Active => { let Some(i) = info.get() else { return { view! {}.into_any() }; }; // File shares render through the full-page file view // (opened by the effect above); only folder shares // get a browser. if i.is_file { view! {} .into_view() .into_any() } else { if me.get().is_none() { return view! {

{i18n::tr(i18n::k::LOADING)}

} .into_view() .into_any(); } 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(), }}
} }