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