Refactor: extract the shared file-view workspace from shell + share page

The signed-in shell and the public share page duplicated the whole
file-view plumbing (~110 lines each): the signal cluster, the
navigation-clears-selection effect, close_file, the Escape handler,
the topbar brand/title/actions, the full-page content, and the
toast/dialog overlays. Every file-view feature had to be implemented
twice, and the two copies had already forked (the share's is_file
guards).

FileWorkspace now owns all of that, parameterized by two closures:
- close_target: what the file view becomes on close. The app
  returns None (back to the listing); a file share returns the
  shared file — which also unifies the forked behavior: with a Some
  target the close button and Escape are disabled, because there is
  nowhere to close back to.
- show_actions: when the topbar shows the selection actions (app:
  files section; share page: once active).

Shell and share keep only what differs: sidebar + section routing
vs. share resolution + states.
AuthorKonata <konata@posteo.jp>
Date
Commitbcac51a69f89e3f6549b5e26fb7d9a701c4fe7d9
Parent61a2d89
5 files changed, 384 insertions(+), 309 deletions(-)
▾Mweb/src/views/file_view.rs
@@ -126,7 +126,9 @@ pub fn FileViewActions(
/// The editor registers its close request here (it asks about unsaved
/// changes first); other views close directly.
close_cb: ReadSignal<Option<Callback<()>>>,
/// False for file shares, where there is nowhere to close back to.
/// 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,
▾Mweb/src/views/mod.rs
@@ -8,3 +8,4 @@ pub mod setup;
pub mod share_page;
pub mod shares;
pub mod shell;
pub mod workspace;
▾Mweb/src/views/share_page.rs
@@ -1,21 +1,18 @@
//! Public share page (milestone 6): renders a shared item without login.
//! Public share page: 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::api::{self, Me, Mode, RootInfo, ShareInfo, UserInfo};
use crate::components::toast::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,
};
use crate::views::browser::Browser;
use crate::views::file_view::{FileView, UnsupportedTarget};
use crate::views::workspace::FileWorkspace;
#[derive(Clone, PartialEq)]
enum SharePageState {
@@ -58,7 +55,7 @@ fn file_view_for_share(i: &ShareInfo) -> Option<FileView> {
#[component]
pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
let toast = provide_toast();
let _ = provide_toast();
let (me, set_me) = signal(Option::<Me>::None);
let (info, set_info) = signal(Option::<ShareInfo>::None);
let (state, set_state) = signal(SharePageState::Loading);
@@ -108,45 +105,34 @@ pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
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::<FileView>::None);
// The open editor stores its save callback here for the top-bar button.
let (save_cb, set_save_cb) = signal(Option::<Callback<()>>::None);
// The open editor's guarded close request (unsaved-changes prompt).
let (close_cb, set_close_cb) = signal(Option::<Callback<()>>::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::<Callback<()>>::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::<Entry>::None);
// Dialogs are rendered here, above everything.
let (dialog, set_dialog) = signal(Option::<Dialog>::None);
// Long-lived owner for callbacks created inside short-lived scopes.
let shell_owner = Owner::current().unwrap();
let topbar_ref = NodeRef::<leptos::html::Header>::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)));
});
// The shared file-view plumbing (topbar, close/save/dirty, selection,
// dialogs, Escape). Closing a folder share returns to the listing;
// closing a file share re-opens the file itself (there is nothing to
// close back to, so the close button and Escape stay disabled as
// well).
let ws = FileWorkspace::new(
me,
loc,
move || info.get().and_then(|i| file_view_for_share(&i)),
move || state.get() == SharePageState::Active,
);
let file_view = ws.file_view;
let open_file = ws.open_file;
let selected = ws.selected;
let set_selected = ws.set_selected;
let dialog = ws.dialog;
let set_dialog = ws.set_dialog;
let set_browser_refresh = ws.set_browser_refresh;
let topbar_ref = ws.topbar_ref;
let topbar_left = ws.topbar_left();
let topbar_right = ws.topbar_right();
let file_content = ws.file_content();
let overlays = ws.overlays();
// A file share opens straight into its viewer.
{
let info2 = info;
let set_fv = set_file_view;
let set_fv = open_file;
Effect::new(move |_| {
if let Some(i) = info2.get()
&& i.is_file
@@ -156,32 +142,6 @@ pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
});
}
// 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! {
<div
class=move || {
@@ -193,71 +153,11 @@ pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
}
>
<header class="topbar" node_ref=topbar_ref>
{move || match file_view.get() {
Some(ref v) => view! {
// Wrapped like the signed-in app's topbar-left so the
// logo + title stay together at the left edge.
<div class="topbar-left">
// Icon-only brand in file mode, like the
// signed-in app: back to `/`.
<a class="brand brand-icon" href="#/">
<Logo/>
</a>
<FileViewTitle view=v.clone() dirty=dirty/>
</div>
}
.into_view()
.into_any(),
None => view! {
<a class="brand" href="#/"><Logo/><span class="brand-name">"filebrowser-ng"</span></a>
}
.into_view()
.into_any(),
}}
<div class="topbar-left">
{topbar_left}
</div>
<div class="topbar-right">
{move || {
if let Some(v) = file_view.get() {
let show_close = info
.get()
.map(|i| !i.is_file)
.unwrap_or(true);
view! {
<FileViewActions
view=v
saving=saving
dirty=dirty
save_cb=save_cb
close_cb=close_cb
show_close=show_close
on_close=close_file
toast=toast
/>
}
.into_view()
.into_any()
} else if state.get() == SharePageState::Active {
let refresh = browser_refresh
.get()
.unwrap_or_else(|| Callback::new(move |_| {}));
view! {
<SelectionActions
me=me
loc=loc
selected=selected
open_file=set_file_view
refresh=refresh
toast=toast
set_dialog=set_dialog
owner=shell_owner.clone()
topbar_ref=topbar_ref
/>
}
.into_view()
.into_any()
} else {
view! {}.into_any()
}
}}
{topbar_right}
</div>
</header>
<main class="content" class:file-open=move || file_view.get().is_some()>
@@ -313,7 +213,7 @@ pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
me=me
set_me=set_me
loc=loc
open_file=set_file_view
open_file=open_file
set_browser_refresh=set_browser_refresh
selected=selected
set_selected=set_selected
@@ -327,32 +227,9 @@ pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
}
}
}}
{move || match file_view.get() {
Some(v) => {
let refresh = browser_refresh
.get()
.unwrap_or_else(|| Callback::new(move |_| {}));
view! {
<FileViewContent
view=v
register_save=set_save_cb
register_close=set_close_cb
dirty=dirty
set_dirty=set_dirty
set_saving=set_saving
close=close_file
toast=toast
refresh=refresh
/>
}
.into_view()
.into_any()
}
None => view! {}.into_any(),
}}
{file_content}
</main>
<ToastView toast=toast/>
<DialogView dialog=dialog set_dialog=set_dialog/>
{overlays}
</div>
}
}
▾Mweb/src/views/shell.rs
@@ -4,18 +4,16 @@ use crate::i18n;
use leptos::prelude::*;
use wasm_bindgen_futures::spawn_local;
use crate::api::{self, Entry, Me};
use crate::api::{self, Me};
use crate::components::icon::Icon;
use crate::components::logo::Logo;
use crate::components::toast::{ToastMsg, ToastView, provide_toast, show_error};
use crate::components::toast::{ToastMsg, 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::browser::{Browser, effective_root};
use crate::views::search::SearchView;
use crate::views::shares::SharesView;
use crate::views::workspace::FileWorkspace;
/// Whether the signed-in user has admin rights.
fn is_admin_of(me: &Option<Me>) -> bool {
@@ -48,63 +46,27 @@ pub fn ShellView(
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::<FileView>::None);
// The open editor stores its save callback here for the top-bar button.
let (save_cb, set_save_cb) = signal(Option::<Callback<()>>::None);
// The open editor's guarded close request (unsaved-changes prompt).
let (close_cb, set_close_cb) = signal(Option::<Callback<()>>::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::<Callback<()>>::None);
let topbar_ref = NodeRef::<leptos::html::Header>::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::<Entry>::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::<Dialog>::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(());
}
},
);
}
// The shared file-view plumbing (topbar, close/save/dirty, selection,
// dialogs, Escape). Closing returns to the browser listing; the
// selection actions show wherever the browser is the visible section.
let ws = FileWorkspace::new(
me,
loc,
|| None,
move || loc.get().section == Section::Files,
);
let file_view = ws.file_view;
let open_file = ws.open_file;
let selected = ws.selected;
let set_selected = ws.set_selected;
let dialog = ws.dialog;
let set_dialog = ws.set_dialog;
let set_browser_refresh = ws.set_browser_refresh;
let topbar_ref = ws.topbar_ref;
let topbar_left = ws.topbar_left();
let topbar_right = ws.topbar_right();
let file_content = ws.file_content();
let overlays = ws.overlays();
// 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
@@ -168,63 +130,11 @@ pub fn ShellView(
>
<Icon name=IconName::Menu class="ic-btn".to_string()/>
</button>
{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.
<a class="brand brand-icon" href="#/">
<Logo/>
</a>
<FileViewTitle view=v.clone() dirty=dirty/>
}
.into_view()
.into_any(),
None => view! {
<a class="brand" href="#/"><Logo/><span class="brand-name">"filebrowser-ng"</span></a>
}
.into_view()
.into_any(),
}}
{topbar_left}
</div>
<div class="topbar-right">
{topbar_right}
</div>
{move || {
if let Some(v) = file_view.get() {
view! {
<FileViewActions
view=v
saving=saving
dirty=dirty
save_cb=save_cb
close_cb=close_cb
show_close=true
on_close=close_file
toast=toast
/>
}
.into_view()
.into_any()
} else if loc.get().section == Section::Files {
let refresh = browser_refresh
.get()
.unwrap_or_else(|| Callback::new(move |_| {}));
view! {
<SelectionActions
me=me
loc=loc
selected=selected
open_file=set_file_view
refresh=refresh
toast=toast
set_dialog=set_dialog
owner=shell_owner.clone()
topbar_ref=topbar_ref
/>
}
.into_view()
.into_any()
} else {
view! {}.into_any()
}
}}
</header>
{move || {
if nav_open.get() {
@@ -411,7 +321,7 @@ pub fn ShellView(
me=me
set_me=set_me
loc=loc
open_file=set_file_view
open_file=open_file
set_browser_refresh=set_browser_refresh
selected=selected
set_selected=set_selected
@@ -425,7 +335,7 @@ pub fn ShellView(
Section::Search => view! {
<SearchView
me=me
open_file=set_file_view
open_file=open_file
file_view=file_view
/>
}
@@ -449,7 +359,7 @@ pub fn ShellView(
me=me
set_me=set_me
loc=loc
open_file=set_file_view
open_file=open_file
set_browser_refresh=set_browser_refresh
selected=selected
set_selected=set_selected
@@ -462,33 +372,10 @@ pub fn ShellView(
.into_any(),
}
}}
{move || match file_view.get() {
Some(v) => {
let refresh = browser_refresh
.get()
.unwrap_or_else(|| Callback::new(move |_| {}));
view! {
<FileViewContent
view=v
register_save=set_save_cb
register_close=set_close_cb
dirty=dirty
set_dirty=set_dirty
set_saving=set_saving
close=close_file
toast=toast
refresh=refresh
/>
}
.into_view()
.into_any()
}
None => view! {}.into_any(),
}}
{file_content}
</main>
</div>
<ToastView toast=toast/>
<DialogView dialog=dialog set_dialog=set_dialog/>
{overlays}
</div>
}
}
▾Aweb/src/views/workspace.rs
@@ -0,0 +1,308 @@
//! The file-view workspace: everything the signed-in shell and the public
/// share page share around the full-page file view.
///
/// Both pages open files in the same topbar-headed view with the same
/// close/save/dirty plumbing, the same selection + dialog state, and the
/// same Escape semantics. [`FileWorkspace`] owns all of that; the pages
/// keep only what differs (sidebar + section routing vs. share resolution
/// + states) and pull the exposed handles into their browser.
use std::sync::Arc;
use leptos::ev::keydown;
use leptos::html::Header;
use leptos::prelude::*;
use web_sys::KeyboardEvent;
use crate::api::{Entry, Me};
use crate::components::logo::Logo;
use crate::components::toast::{ToastMsg, ToastView};
use crate::router::Location;
use crate::util::owned_window_listener;
use crate::views::browser::SelectionActions;
use crate::views::dialogs::{Dialog, DialogView};
use crate::views::file_view::{FileView, FileViewActions, FileViewContent, FileViewTitle};
/// The shared state and behavior around the full-page file view.
///
/// Create one per page in the page component body, after `provide_toast`:
/// the signals, the navigation effect and the Escape listener are owned by
/// the page, so everything is cleaned up when the page unmounts.
pub struct FileWorkspace {
pub file_view: ReadSignal<Option<FileView>>,
/// Open a file in the full-page view (hand to the browser / search).
pub open_file: WriteSignal<Option<FileView>>,
pub selected: ReadSignal<Option<Entry>>,
pub set_selected: WriteSignal<Option<Entry>>,
pub dialog: ReadSignal<Option<Dialog>>,
pub set_dialog: WriteSignal<Option<Dialog>>,
/// The browser stores its fetch there so the editor can re-list after a
/// save.
pub set_browser_refresh: WriteSignal<Option<Callback<()>>>,
pub topbar_ref: NodeRef<Header>,
me: ReadSignal<Option<Me>>,
loc: ReadSignal<Location>,
toast: ToastMsg,
owner: Owner,
save_cb: ReadSignal<Option<Callback<()>>>,
set_save_cb: WriteSignal<Option<Callback<()>>>,
close_cb: ReadSignal<Option<Callback<()>>>,
set_close_cb: WriteSignal<Option<Callback<()>>>,
dirty: ReadSignal<bool>,
set_dirty: WriteSignal<bool>,
saving: ReadSignal<bool>,
set_saving: WriteSignal<bool>,
browser_refresh: ReadSignal<Option<Callback<()>>>,
close_file: Callback<()>,
/// What the file view becomes on close (see the parameter of the same
/// name in [`FileWorkspace::new`]).
close_target: Arc<dyn Fn() -> Option<FileView> + Send + Sync>,
/// Whether the topbar shows the selection actions while no file is
/// open (i.e. the page's browser is the visible content).
show_actions: Arc<dyn Fn() -> bool + Send + Sync>,
}
impl FileWorkspace {
/// `close_target` is what the file view becomes on close: `None` =
/// back to the listing (signed-in app); `Some` = the same file is
/// shown again (a file share has nothing to close back to). With a
/// `Some` target the close button and Escape are disabled.
///
/// `show_actions` decides whether the topbar shows the selection
/// actions while no file is open (i.e. the page's browser is the
/// visible content).
pub fn new(
me: ReadSignal<Option<Me>>,
loc: ReadSignal<Location>,
close_target: impl Fn() -> Option<FileView> + 'static + Send + Sync,
show_actions: impl Fn() -> bool + 'static + Send + Sync,
) -> Self {
let toast = use_context::<ToastMsg>().expect("toast context");
let owner = Owner::current().unwrap();
let close_target = Arc::new(close_target);
let show_actions = Arc::new(show_actions);
let (file_view, set_file_view) = signal(Option::<FileView>::None);
// The open editor stores its save callback here for the top-bar button.
let (save_cb, set_save_cb) = signal(Option::<Callback<()>>::None);
// The open editor's guarded close request (unsaved-changes prompt).
let (close_cb, set_close_cb) = signal(Option::<Callback<()>>::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::<Callback<()>>::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::<Entry>::None);
// Dialogs are rendered here, above everything.
let (dialog, set_dialog) = signal(Option::<Dialog>::None);
let topbar_ref = NodeRef::<Header>::new();
// Navigating (new folder, root, section) invalidates the selection.
Effect::new(move |_| {
let _ = loc.get();
set_selected.set(None);
});
let close_file = {
let ct = close_target.clone();
Callback::new(move |_| {
set_save_cb.set(None);
set_close_cb.set(None);
set_dirty.set(false);
set_saving.set(false);
set_file_view.set(ct());
})
};
// Escape closes previews and the "no preview" view — except when
// there is nothing to close back to (file share). The editor handles
// Escape itself (it asks first when there are unsaved changes).
{
let fv = file_view;
let cf = close_file;
let ct = close_target.clone();
owned_window_listener(keydown, move |ev: KeyboardEvent| {
if ev.key() != "Escape" {
return;
}
if ct().is_some() {
return;
}
if fv
.get()
.is_some_and(|v| matches!(v, FileView::Preview(..) | FileView::Unsupported(_)))
{
cf.run(());
}
});
}
Self {
file_view,
open_file: set_file_view,
selected,
set_selected,
dialog,
set_dialog,
set_browser_refresh,
topbar_ref,
me,
loc,
toast,
owner,
save_cb,
set_save_cb,
close_cb,
set_close_cb,
dirty,
set_dirty,
saving,
set_saving,
browser_refresh,
close_file,
close_target,
show_actions,
}
}
/// Topbar left: the brand. In file mode the brand shrinks to its icon
/// (a link to `/`) and the file title takes its place.
pub fn topbar_left(&self) -> impl IntoView + 'static + use<> {
let file_view = self.file_view;
let dirty = self.dirty;
view! {
{move || match file_view.get() {
Some(ref v) => view! {
<a class="brand brand-icon" href="#/">
<Logo />
</a>
<FileViewTitle view=v.clone() dirty=dirty />
}
.into_view()
.into_any(),
None => view! {
<a class="brand" href="#/">
<Logo />
<span class="brand-name">"filebrowser-ng"</span>
</a>
}
.into_view()
.into_any(),
}}
}
}
/// Topbar right: the open file's actions (download / save / close) or,
/// while no file is open, the selected entry's actions.
pub fn topbar_right(&self) -> impl IntoView + 'static + use<> {
let file_view = self.file_view;
let show_actions = self.show_actions.clone();
let close_target = self.close_target.clone();
let save_cb = self.save_cb;
let dirty = self.dirty;
let saving = self.saving;
let close_cb = self.close_cb;
let close_file = self.close_file;
let me = self.me;
let loc = self.loc;
let selected = self.selected;
let open_file = self.open_file;
let browser_refresh = self.browser_refresh;
let toast = self.toast;
let set_dialog = self.set_dialog;
let owner = self.owner.clone();
let topbar_ref = self.topbar_ref;
view! {
{move || {
if let Some(v) = file_view.get() {
view! {
<FileViewActions
view=v
saving=saving
dirty=dirty
save_cb=save_cb
close_cb=close_cb
show_close=close_target().is_none()
on_close=close_file
toast=toast
/>
}
.into_view()
.into_any()
} else if show_actions() {
let refresh = browser_refresh
.get()
.unwrap_or_else(|| Callback::new(move |_| {}));
view! {
<SelectionActions
me=me
loc=loc
selected=selected
open_file=open_file
refresh=refresh
toast=toast
set_dialog=set_dialog
owner=owner.clone()
topbar_ref=topbar_ref
/>
}
.into_view()
.into_any()
} else {
view! {}.into_any()
}
}}
}
}
/// The full-page content (preview / editor / "no preview"), rendered
/// while a file is open.
pub fn file_content(&self) -> impl IntoView + 'static + use<> {
let file_view = self.file_view;
let set_save_cb = self.set_save_cb;
let set_close_cb = self.set_close_cb;
let dirty = self.dirty;
let set_dirty = self.set_dirty;
let set_saving = self.set_saving;
let close_file = self.close_file;
let toast = self.toast;
let browser_refresh = self.browser_refresh;
view! {
{move || match file_view.get() {
Some(v) => {
let refresh = browser_refresh
.get()
.unwrap_or_else(|| Callback::new(move |_| {}));
view! {
<FileViewContent
view=v
register_save=set_save_cb
register_close=set_close_cb
dirty=dirty
set_dirty=set_dirty
set_saving=set_saving
close=close_file
toast=toast
refresh=refresh
/>
}
.into_view()
.into_any()
}
None => view! {}.into_any(),
}}
}
}
/// The page-level overlays: toasts + dialogs, above everything.
pub fn overlays(&self) -> impl IntoView + 'static + use<> {
let toast = self.toast;
let dialog = self.dialog;
let set_dialog = self.set_dialog;
view! {
<ToastView toast=toast />
<DialogView dialog=dialog set_dialog=set_dialog />
}
}
}