app.rs
⎇
Raw
1use leptos::prelude::*;
2use wasm_bindgen_futures::spawn_local;
3
4use crate::api::{self, Me};
5use crate::i18n::{self, Locale};
6use crate::router::{Location, parse_location};
7
8#[derive(Clone, Copy, PartialEq, Debug)]
9pub enum AuthPhase {
10 Loading,
11 Setup,
12 Login,
13 Authed,
14}
15
16#[component]
17pub fn App() -> impl IntoView {
18 let (phase, set_phase) = signal(AuthPhase::Loading);
19 let (me, set_me) = signal(Option::<Me>::None);
20 let (loc, set_loc) = signal(Location::root());
21
22 // Tri-state theme (auto/light/dark): restore the persisted choice and
23 // apply it before first paint of the app UI.
24 let initial_theme = crate::theme::Theme::load();
25 initial_theme.apply();
26 let (theme, set_theme) = signal(initial_theme);
27
28 // UI language: start from the browser's language; the signed-in user's
29 // profile setting (fetched below) overrides it via the effect.
30 i18n::init(Locale::from_browser().unwrap_or(Locale::En));
31 Effect::new(move |_| {
32 let lang = me.get().and_then(|m| m.user).and_then(|u| u.language);
33 let locale = match lang.as_deref().and_then(Locale::from_code) {
34 Some(l) => l,
35 // No stored preference (or an unsupported one): follow the browser.
36 None => Locale::from_browser().unwrap_or(Locale::En),
37 };
38 // Untracked read: comparing via `current()` would register a
39 // dependency on the signal we write below, retriggering the effect.
40 if i18n::current_untracked() != locale {
41 i18n::set_locale(locale);
42 }
43 });
44
45 // The URL hash is the source of truth for the location.
46 {
47 set_loc.set(parse_location());
48 crate::util::owned_window_listener(leptos::ev::hashchange, move |_| {
49 set_loc.set(parse_location());
50 });
51 }
52
53 // Initial /me probe: first boot, signed in, or needs login.
54 spawn_local(async move {
55 match api::me().await {
56 Ok(m) => {
57 let first_boot = m.first_boot;
58 set_me.set(Some(m));
59 set_phase.set(if first_boot {
60 AuthPhase::Setup
61 } else {
62 AuthPhase::Authed
63 });
64 }
65 Err(_) => set_phase.set(AuthPhase::Login),
66 }
67 });
68
69 view! {
70 {move || {
71 // Public share links (`#/share/{token}/...`) bypass the auth flow.
72 let loc_now = loc.get();
73 if let Some(token) = loc_now.share_token.clone() {
74 return view! {
75 <crate::views::share_page::ShareView token=token loc=loc/>
76 }
77 .into_view()
78 .into_any();
79 }
80 match phase.get() {
81 AuthPhase::Loading => view! {
82 <div class="center-screen">
83 <p class="muted">{i18n::tr(i18n::k::LOADING)}</p>
84 </div>
85 }
86 .into_view()
87 .into_any(),
88 AuthPhase::Setup => view! {
89 <crate::views::setup::SetupView set_me=set_me set_phase=set_phase/>
90 }
91 .into_view()
92 .into_any(),
93 AuthPhase::Login => view! {
94 <crate::views::login::LoginView set_me=set_me set_phase=set_phase/>
95 }
96 .into_view()
97 .into_any(),
98 AuthPhase::Authed => view! {
99 <crate::views::shell::ShellView
100 me=me
101 set_me=set_me
102 set_phase=set_phase
103 loc=loc
104 theme=theme
105 set_theme=set_theme
106 />
107 }
108 .into_view()
109 .into_any(),
110 }
111 }}
112 }
113}
114