share_page.rs
⎇
Raw
1//! Public share page (milestone 6): renders a shared item without login.
2
3use leptos::prelude::*;
4use wasm_bindgen_futures::spawn_local;
5
6use crate::api::{self, Me, Mode, RootInfo, ShareInfo, UserInfo};
7use crate::components::icon::Icon;
8use crate::components::logo::Logo;
9use crate::components::toast::{ToastView, provide_toast};
10use crate::icons::IconName;
11use crate::router::Location;
12use crate::views::browser::Browser;
13
14#[derive(Clone, PartialEq)]
15enum SharePageState {
16 Loading,
17 NotFound,
18 Expired,
19 Active,
20}
21
22#[component]
23pub fn ShareView(token: String, loc: ReadSignal<Location>) -> impl IntoView {
24 let toast = provide_toast();
25 let (me, set_me) = signal(Option::<Me>::None);
26 let (info, set_info) = signal(Option::<ShareInfo>::None);
27 let (state, set_state) = signal(SharePageState::Loading);
28
29 // Resolve the share once per token and scope all file API calls to it.
30 {
31 let tok = token.clone();
32 let set_me2 = set_me;
33 let set_info2 = set_info;
34 let set_state2 = set_state;
35 spawn_local(async move {
36 api::set_share_token(Some(&tok));
37 match api::resolve_share(&tok).await {
38 Ok(i) => {
39 let mode = if i.writable { Mode::Rw } else { Mode::Ro };
40 let me_val = Me {
41 first_boot: false,
42 user: Some(UserInfo {
43 id: i.id,
44 name: "shared".to_string(),
45 is_admin: false,
46 }),
47 roots: vec![RootInfo {
48 id: i.root_id,
49 name: i.name.clone(),
50 path: "/".to_string(),
51 mode,
52 }],
53 allow_writable_shares: false,
54 };
55 set_me2.set(Some(me_val));
56 set_info2.set(Some(i));
57 set_state2.set(SharePageState::Active);
58 }
59 Err(e) => {
60 let expired = e.status() == Some(410);
61 set_state2.set(if expired {
62 SharePageState::Expired
63 } else {
64 SharePageState::NotFound
65 });
66 }
67 }
68 });
69 on_cleanup(move || api::set_share_token(None));
70 }
71
72 view! {
73 <div class="layout share-layout">
74 <header class="topbar">
75 <div class="brand">
76 <Logo/>
77 <span class="brand-name">"filebrowser-ng"</span>
78 </div>
79 <div class="topbar-right">
80 {move || {
81 let name = info.get().map(|i| i.name.clone());
82 match name {
83 Some(n) => view! {
84 <span class="share-badge">
85 <Icon name=IconName::Share class="ic-badge".to_string()/>
86 {format!("Shared: {n}")}
87 </span>
88 }
89 .into_view()
90 .into_any(),
91 None => {
92 view! {}.into_any()
93 },
94 }
95 }}
96 </div>
97 </header>
98 <main class="content">
99 {move || match state.get() {
100 SharePageState::Loading => view! {
101 <div class="center-screen">
102 <p class="muted">"Loading share…"</p>
103 </div>
104 }
105 .into_view()
106 .into_any(),
107 SharePageState::NotFound => view! {
108 <div class="center-screen">
109 <h2>"Share not found"</h2>
110 <p class="muted">"This link is invalid or the share was removed."</p>
111 </div>
112 }
113 .into_view()
114 .into_any(),
115 SharePageState::Expired => view! {
116 <div class="center-screen">
117 <h2>"Share expired"</h2>
118 <p class="muted">"This share link is no longer active."</p>
119 </div>
120 }
121 .into_view()
122 .into_any(),
123 SharePageState::Active => {
124 let Some(i) = info.get() else {
125 return {
126 view! {}.into_any()
127 };
128 };
129 if i.is_file {
130 let name = i.name.clone();
131 let rid = i.root_id;
132 view! {
133 <div class="share-file-page">
134 <Icon name=IconName::File class="share-file-glyph".to_string()/>
135 <h2>{name.clone()}</h2>
136 <div class="share-file-actions">
137 <button
138 class="btn btn-primary"
139 on:click=move |_| {
140 let url = api::download_url(rid, "", None);
141 api::trigger_download(&url, &name);
142 }
143 >
144 "Download"
145 </button>
146 </div>
147 </div>
148 }
149 .into_view()
150 .into_any()
151 } else {
152 if me.get().is_none() {
153 return view! {
154 <div class="center-screen">
155 <p class="muted">"Loading…"</p>
156 </div>
157 }
158 .into_view()
159 .into_any();
160 }
161 view! {
162 <Browser me=me set_me=set_me loc=loc/>
163 }
164 .into_view()
165 .into_any()
166 }
167 }
168 }}
169 </main>
170 <footer class="share-footer muted">"Powered by filebrowser-ng"</footer>
171 <ToastView toast=toast/>
172 </div>
173 }
174}
175