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;
13use crate::components::toast::{ToastMsg, show};
14use crate::editor::Editor;
15use crate::icons::{IconName, icon_for};
16use crate::preview::Preview;
17
18/// A file that is open in the full-page view.
19#[derive(Clone)]
20pub struct FileView {
21 pub root_id: i64,
22 /// Path relative to the root (e.g. "docs/a.txt").
23 pub path: String,
24 /// Display name (used for the title + language detection).
25 pub name: String,
26 /// Sniffed kind: picks the viewer and the icon.
27 pub kind: FileKind,
28 /// True when the file's folder is read-only. Only the editor acts on it.
29 pub readonly: bool,
30}
31
32/// Top-bar replacement for the brand: file icon + name (with a dirty dot)
33/// and, for read-only text files, a badge.
34#[component]
35pub fn FileViewTitle(view: FileView, dirty: ReadSignal<bool>) -> impl IntoView {
36 let icon = icon_for(view.kind, &view.name);
37 let readonly = view.kind == FileKind::Text && view.readonly;
38 let name = view.name;
39 view! {
40 <div class="file-title">
41 <Icon name=icon class="file-title-icon".to_string()/>
42 <span class="file-title-name">
43 {move || {
44 let dot = if dirty.get() { " •" } else { "" };
45 format!("{name}{dot}")
46 }}
47 </span>
48 {readonly.then(|| view! { <span class="badge">{i18n::t(i18n::k::READ_ONLY_BADGE)}</span> })}
49 </div>
50 }
51}
52
53/// Top-bar actions: Save (only present while an editable file has
54/// registered its save callback), Download and close.
55#[component]
56pub fn FileViewActions(
57 /// The open file — the download button targets it.
58 view: FileView,
59 saving: ReadSignal<bool>,
60 dirty: ReadSignal<bool>,
61 save_cb: ReadSignal<Option<Callback<()>>>,
62 /// The editor registers its close request here (it asks about unsaved
63 /// changes first); other views close directly.
64 close_cb: ReadSignal<Option<Callback<()>>>,
65 /// Whether the close button is shown at all: closing means "back to
66 /// the listing", so it is hidden when there is nothing to go back to
67 /// (file shares).
68 show_close: bool,
69 on_close: Callback<()>,
70 toast: ToastMsg,
71) -> impl IntoView {
72 // Serves the file from disk (for an open editor: the saved version,
73 // not the working copy). Built once per open file — nothing about it
74 // is reactive.
75 let download_btn = {
76 let dl = view;
77 view! {
78 <button
79 class="icon-btn file-download"
80 title={i18n::tr(i18n::k::DOWNLOAD)}
81 aria-label={i18n::tr(i18n::k::DOWNLOAD)}
82 on:click=move |_| {
83 let url = api::download_url(dl.root_id, &dl.path, None);
84 api::trigger_download(&url, &dl.name);
85 show(toast, i18n::t(i18n::k::DOWNLOAD_STARTED));
86 }
87 >
88 <Icon name=IconName::Download class="ic-btn".to_string()/>
89 </button>
90 }
91 };
92 view! {
93 <div class="file-actions">
94 {move || save_cb.get().map(|cb| view! {
95 <button
96 class="btn btn-sm btn-primary"
97 disabled=move || !dirty.get() || saving.get()
98 on:click=move |_| cb.run(())
99 >
100 {move || {
101 if saving.get() {
102 i18n::t(i18n::k::SAVING).to_string()
103 } else {
104 i18n::t(i18n::k::SAVE).to_string()
105 }
106 }}
107 </button>
108 })}
109 {download_btn}
110 <Show when=move || show_close>
111 <button
112 class="icon-btn file-close"
113 title={i18n::tr(i18n::k::CLOSE_ESC)}
114 aria-label={i18n::tr(i18n::k::CLOSE)}
115 on:click=move |_| {
116 if let Some(cb) = close_cb.get() {
117 cb.run(());
118 } else {
119 on_close.run(());
120 }
121 }
122 >
123 <Icon name=IconName::Close class="ic-btn".to_string()/>
124 </button>
125 </Show>
126 </div>
127 }
128}
129
130/// The full-page content: preview, editor, or the "no preview" fallback.
131#[component]
132pub fn FileViewContent(
133 view: FileView,
134 register_save: WriteSignal<Option<Callback<()>>>,
135 register_close: WriteSignal<Option<Callback<()>>>,
136 dirty: ReadSignal<bool>,
137 set_dirty: WriteSignal<bool>,
138 set_saving: WriteSignal<bool>,
139 close: Callback<()>,
140 toast: ToastMsg,
141 /// Re-fetch the (hidden, still mounted) browser after a save.
142 refresh: Callback<()>,
143) -> impl IntoView {
144 let content = if view.kind == FileKind::Text {
145 view! {
146 <Editor
147 target=view
148 register_save=register_save
149 register_close=register_close
150 dirty=dirty
151 set_dirty=set_dirty
152 set_saving=set_saving
153 close=close
154 toast=toast
155 refresh=refresh
156 />
157 }
158 .into_any()
159 } else {
160 view! { <Preview target=view/> }.into_any()
161 };
162 view! { <div class="file-view">{content}</div> }
163}
164