file_view.rs
⎇
Raw
1//! Full-page file view (opened files take over the whole content area).
2//!
3//! The top bar doubles as the file's header: the file name where the brand
4//! was, with save/close on the right. Used by both the signed-in shell and
5//! the public share page.
6
7use crate::i18n;
8use api_types::FileKind;
9use leptos::prelude::*;
10
11use crate::api;
12use crate::components::icon::icon_svg;
13use crate::components::toast::{ToastMsg, show};
14use crate::icons::{IconName, icon_for};
15
16/// A file that is open in the full-page view.
17#[derive(Clone)]
18pub struct FileView {
19 pub root_id: i64,
20 /// Path relative to the root (e.g. "docs/a.txt").
21 pub path: String,
22 /// Display name (used for the title + language detection).
23 pub name: String,
24 /// Sniffed kind: picks the viewer and the icon.
25 pub kind: FileKind,
26 /// True when the file's folder is read-only. Only the editor acts on it.
27 pub readonly: bool,
28}
29
30/// Top-bar replacement for the brand: file icon + name (with a dirty dot)
31/// and, for read-only text files, a badge.
32#[component]
33pub fn FileViewTitle(view: FileView, dirty: ReadSignal<bool>) -> impl IntoView {
34 let icon = icon_for(view.kind, &view.name);
35 let readonly = view.kind == FileKind::Text && view.readonly;
36 let name = view.name;
37 view! {
38 <div class="file-title">
39 {icon_svg(icon, "file-title-icon")}
40 <span class="file-title-name">
41 {move || {
42 let dot = if dirty.get() { " •" } else { "" };
43 format!("{name}{dot}")
44 }}
45 </span>
46 {readonly.then(|| view! { <span class="badge">{i18n::t(i18n::k::READ_ONLY_BADGE)}</span> })}
47 </div>
48 }
49}
50
51/// Top-bar actions: Save (only present while an editable file has
52/// registered its save callback), Download and close.
53#[component]
54pub fn FileViewActions(
55 /// The open file — the download button targets it.
56 view: FileView,
57 saving: ReadSignal<bool>,
58 dirty: ReadSignal<bool>,
59 save_cb: ReadSignal<Option<Callback<()>>>,
60 /// The editor registers its close request here (it asks about unsaved
61 /// changes first); other views close directly.
62 close_cb: ReadSignal<Option<Callback<()>>>,
63 /// Whether the close button is shown at all: closing means "back to
64 /// the listing", so it is hidden when there is nothing to go back to
65 /// (file shares).
66 show_close: bool,
67 on_close: Callback<()>,
68 toast: ToastMsg,
69) -> impl IntoView {
70 // Serves the file from disk (for an open editor: the saved version,
71 // not the working copy). Built once per open file — nothing about it
72 // is reactive.
73 let download_btn = {
74 let dl = view;
75 view! {
76 <button
77 class="icon-btn file-download"
78 title={i18n::tr(i18n::k::DOWNLOAD)}
79 aria-label={i18n::tr(i18n::k::DOWNLOAD)}
80 on:click=move |_| {
81 let url = api::download_url(dl.root_id, &dl.path, None);
82 api::trigger_download(&url, &dl.name);
83 show(toast, i18n::t(i18n::k::DOWNLOAD_STARTED));
84 }
85 >
86 {icon_svg(IconName::Download, "ic-btn")}
87 </button>
88 }
89 };
90 view! {
91 <div class="file-actions">
92 {move || save_cb.get().map(|cb| view! {
93 <button
94 class="btn btn-sm btn-primary"
95 disabled=move || !dirty.get() || saving.get()
96 on:click=move |_| cb.run(())
97 >
98 {move || {
99 if saving.get() {
100 i18n::t(i18n::k::SAVING).to_string()
101 } else {
102 i18n::t(i18n::k::SAVE).to_string()
103 }
104 }}
105 </button>
106 })}
107 {download_btn}
108 {show_close.then(|| view! {
109 <button
110 class="icon-btn file-close"
111 title={i18n::tr(i18n::k::CLOSE_ESC)}
112 aria-label={i18n::tr(i18n::k::CLOSE)}
113 on:click=move |_| {
114 if let Some(cb) = close_cb.get() {
115 cb.run(());
116 } else {
117 on_close.run(());
118 }
119 }
120 >
121 {icon_svg(IconName::Close, "ic-btn")}
122 </button>
123 })}
124 </div>
125 }
126}
127