//! Full-page file view (opened files take over the whole content area). //! //! The top bar doubles as the file's header: the file name where the brand //! was, with save/close on the right. Used by both the signed-in shell and //! the public share page. use crate::i18n; use api_types::FileKind; use leptos::prelude::*; use crate::api; use crate::components::icon::Icon; use crate::components::toast::{ToastMsg, show}; use crate::editor::{EditTarget, Editor}; use crate::icons::{IconName, icon_for}; use crate::preview::{Preview, PreviewKind, PreviewTarget}; /// A file that is open in the full-page view. #[derive(Clone)] pub enum FileView { /// Image / PDF / video / audio. Preview(PreviewTarget, PreviewKind), /// Text file; `EditTarget.readonly` decides whether it is editable. Editor(EditTarget), /// No built-in viewer for this kind: show a message + download button. Unsupported(UnsupportedTarget), } /// A file the app cannot preview (binary, archive, ...). #[derive(Clone)] pub struct UnsupportedTarget { pub root_id: i64, /// Path relative to the root. pub path: String, /// Display name. pub name: String, /// Sniffed kind, for the icon. pub kind: FileKind, } impl FileView { pub fn name(&self) -> &str { match self { FileView::Preview(t, _) => &t.name, FileView::Editor(t) => &t.name, FileView::Unsupported(t) => &t.name, } } /// The glyph shown next to the name in the top bar. pub fn icon(&self) -> IconName { match self { FileView::Preview(_t, kind) => match kind { PreviewKind::Image => IconName::Image, PreviewKind::Pdf => IconName::Pdf, PreviewKind::Video => IconName::Video, PreviewKind::Audio => IconName::Audio, }, FileView::Editor(t) => icon_for(FileKind::Text, &t.name), FileView::Unsupported(t) => icon_for(t.kind, &t.name), } } /// Root the file lives in (for the download link). pub fn root_id(&self) -> i64 { match self { FileView::Preview(t, _) => t.root_id, FileView::Editor(t) => t.root_id, FileView::Unsupported(t) => t.root_id, } } /// Path relative to the root (for the download link). pub fn path(&self) -> &str { match self { FileView::Preview(t, _) => &t.path, FileView::Editor(t) => &t.path, FileView::Unsupported(t) => &t.path, } } /// True when the file is shown read-only (a text file in a read-only /// root). Media is always "read-only" in the sense that there is no /// save action; only the editor exposes the badge. pub fn readonly(&self) -> bool { matches!(self, FileView::Editor(t) if t.readonly) } } /// Top-bar replacement for the brand: file icon + name (with a dirty dot) /// and, for read-only text files, a badge. #[component] pub fn FileViewTitle(view: FileView, dirty: ReadSignal) -> impl IntoView { let name = view.name().to_string(); let icon = view.icon(); let readonly = view.readonly(); view! {
{move || { let dot = if dirty.get() { " •" } else { "" }; format!("{name}{dot}") }} {move || { if readonly { view! { {i18n::t(i18n::k::READ_ONLY_BADGE)} }.into_view().into_any() } else { view! {}.into_any() } }}
} } /// Top-bar actions: Save (only present while an editable file has /// registered its save callback), Download and close. #[component] pub fn FileViewActions( /// The open file — the download button targets it. view: FileView, saving: ReadSignal, dirty: ReadSignal, save_cb: ReadSignal>>, /// The editor registers its close request here (it asks about unsaved /// changes first); other views close directly. close_cb: ReadSignal>>, /// Whether the close button is shown at all: closing means "back to /// the listing", so it is hidden when there is nothing to go back to /// (file shares). show_close: bool, on_close: Callback<()>, toast: ToastMsg, ) -> impl IntoView { // Serves the file from disk (for an open editor: the saved version, // not the working copy). Built once per open file — nothing about it // is reactive. let download_btn = { let dl = view; view! { } }; view! {
{move || match save_cb.get() { Some(cb) => view! { } .into_view() .into_any(), None => view! {}.into_any(), }} {download_btn}
} } /// The full-page content: preview, editor, or the "no preview" fallback. #[component] pub fn FileViewContent( view: FileView, register_save: WriteSignal>>, register_close: WriteSignal>>, dirty: ReadSignal, set_dirty: WriteSignal, set_saving: WriteSignal, close: Callback<()>, toast: ToastMsg, /// Re-fetch the (hidden, still mounted) browser after a save. refresh: Callback<()>, ) -> impl IntoView { match view { FileView::Preview(target, kind) => view! {
} .into_view() .into_any(), FileView::Editor(target) => view! {
} .into_view() .into_any(), FileView::Unsupported(target) => view! {
} .into_view() .into_any(), } } /// Shown for files with no built-in viewer: a message and a download button. #[component] fn NoPreview(target: UnsupportedTarget) -> impl IntoView { let icon = icon_for(target.kind, &target.name); let name = target.name.clone(); let t = target; view! {

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

{i18n::t_fmt(i18n::k::NO_PREVIEW_MSG, &name)}

} }