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::{provide_toast, ToastView};
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.unwrap_or(i.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 => view! {}.into_any(),
83 }
84 }}
85 </div>
86 </header>
87 <main class="content">
88 {move || match state.get() {
89 SharePageState::Loading => view! {
90 <div class="center-screen">
91 <p class="muted">"Loading share…"</p>
92 </div>
93 }
94 .into_view()
95 .into_any(),
96 SharePageState::NotFound => view! {
97 <div class="center-screen">
98 <h2>"Share not found"</h2>
99 <p class="muted">"This link is invalid or the share was removed."</p>
100 </div>
101 }
102 .into_view()
103 .into_any(),
104 SharePageState::Expired => view! {
105 <div class="center-screen">
106 <h2>"Share expired"</h2>
107 <p class="muted">"This share link is no longer active."</p>
108 </div>
109 }
110 .into_view()
111 .into_any(),
112 SharePageState::Active => {
113 let Some(i) = info.get() else {
114 return view! {}.into_any();
115 };
116 if i.is_file {
117 let name = i.name.clone();
118 let rid = i.root_id.unwrap_or(i.id);
119 view! {
120 <div class="share-file-page">
121 <div class="share-file-icon">"📄"</div>
122 <h2>{name.clone()}</h2>
123 <div class="share-file-actions">
124 <button
125 class="btn btn-primary"
126 on:click=move |_| {
127 let url = api::download_url(rid, "", None);
128 api::trigger_download(&url, &name);
129 }
130 >
131 "Download"
132 </button>
133 </div>
134 </div>
135 }
136 .into_view()
137 .into_any()
138 } else {
139 if me.get().is_none() {
140 return view! {
141 <div class="center-screen">
142 <p class="muted">"Loading…"</p>
143 </div>
144 }
145 .into_view()
146 .into_any();
147 }
148 view! {
149 <Browser me=me loc=loc/>
150 }
151 .into_view()
152 .into_any()
153 }
154 }
155 }}
156 </main>
157 <footer class="share-footer muted">"Powered by filebrowser-ng"</footer>
158 <ToastView toast=toast/>
159 </div>
160 }
161}
162