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;
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 crate::router::install(set_loc);
47 // Read through a memo, so the views below are not rebuilt on every
48 // location change.
49 let share_token = Memo::new(move |_| loc.with(|l| l.share_token.clone()));
50
51 // Initial /me probe: first boot, signed in, or needs login.
52 spawn_local(async move {
53 match api::me().await {
54 Ok(m) => {
55 let first_boot = m.first_boot;
56 set_me.set(Some(m));
57 set_phase.set(if first_boot {
58 AuthPhase::Setup
59 } else {
60 AuthPhase::Authed
61 });
62 }
63 Err(_) => set_phase.set(AuthPhase::Login),
64 }
65 });
66
67 view! {
68 {move || {
69 // Public share links (`#/share/{token}/...`) bypass the auth flow.
70 if let Some(token) = share_token.get() {
71 return view! {
72 <crate::views::share_page::ShareView token=token loc=loc/>
73 }
74 .into_view()
75 .into_any();
76 }
77 match phase.get() {
78 AuthPhase::Loading => view! {
79 <div class="center-screen">
80 <p class="muted">{i18n::tr(i18n::k::LOADING)}</p>
81 </div>
82 }
83 .into_view()
84 .into_any(),
85 AuthPhase::Setup => view! {
86 <crate::views::setup::SetupView set_me=set_me set_phase=set_phase/>
87 }
88 .into_view()
89 .into_any(),
90 AuthPhase::Login => view! {
91 <crate::views::login::LoginView set_me=set_me set_phase=set_phase/>
92 }
93 .into_view()
94 .into_any(),
95 AuthPhase::Authed => view! {
96 <crate::views::shell::ShellView
97 me=me
98 set_me=set_me
99 set_phase=set_phase
100 loc=loc
101 theme=theme
102 set_theme=set_theme
103 />
104 }
105 .into_view()
106 .into_any(),
107 }
108 }}
109 }
110}
111