router.rs
⎇
Raw
1//! Tiny hash-based router.
2//!
3//! - `#/` — root picker / single-root browser
4//! - `#/r/{root_id}/{segment/...}` — a user's folder
5//! - `#/s/{token}/{segment/...}` — a public share (no login)
6//! - `#/shares`, `#/users`, `#/settings` — the signed-in shell's manage views
7//!
8//! The URL hash is the single source of truth; the `Location` signal in the
9//! app mirrors it.
10
11/// Which section of the signed-in shell a location points at. `Files` is the
12/// browser; the rest are the manage views, each with its own URL.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum Section {
15 Files,
16 Shares,
17 Users,
18 Settings,
19}
20
21impl Section {
22 /// The URL segment for this section, or `None` for `Files` (which has no
23 /// segment of its own — the browser is the default view).
24 const fn segment(self) -> Option<&'static str> {
25 match self {
26 Section::Files => None,
27 Section::Shares => Some("shares"),
28 Section::Users => Some("users"),
29 Section::Settings => Some("settings"),
30 }
31 }
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct Location {
36 /// None = the top-level "folders" view (root picker).
37 pub root_id: Option<i64>,
38 /// Path segments relative to the current root (or share target).
39 pub path: Vec<String>,
40 /// Present when the location is a public share (`#/s/{token}/...`).
41 pub share_token: Option<String>,
42 /// Which shell section the location points at.
43 pub section: Section,
44}
45
46impl Location {
47 pub const fn root() -> Self {
48 Self {
49 root_id: None,
50 path: Vec::new(),
51 share_token: None,
52 section: Section::Files,
53 }
54 }
55}
56
57/// Percent-encode one path segment for the URL hash. Without this a name
58/// containing `#`, `%` or `/` would break the round trip.
59fn encode_segment(seg: &str) -> String {
60 js_sys::encode_uri_component(seg).into()
61}
62
63fn decode_segment(seg: &str) -> String {
64 // A hand-typed hash can hold invalid escapes; keep it verbatim then.
65 js_sys::decode_uri_component(seg)
66 .map(String::from)
67 .unwrap_or_else(|_| seg.to_string())
68}
69
70pub fn parse_location() -> Location {
71 let hash = web_sys::window()
72 .and_then(|w| w.location().hash().ok())
73 .unwrap_or_default();
74 let h = hash.trim_start_matches('#');
75 let h = h.trim_start_matches('/');
76 let mut parts = h.split('/').filter(|s| !s.is_empty());
77 match parts.next() {
78 Some("r") => match parts.next().and_then(|s| s.parse().ok()) {
79 Some(root_id) => Location {
80 root_id: Some(root_id),
81 path: parts.map(decode_segment).collect(),
82 share_token: None,
83 section: Section::Files,
84 },
85 None => Location::root(),
86 },
87 Some("s") => match parts.next() {
88 Some(token) => Location {
89 root_id: None,
90 path: parts.map(decode_segment).collect(),
91 share_token: Some(token.to_string()),
92 section: Section::Files,
93 },
94 None => Location::root(),
95 },
96 Some("shares") => Location {
97 root_id: None,
98 path: Vec::new(),
99 share_token: None,
100 section: Section::Shares,
101 },
102 Some("users") => Location {
103 root_id: None,
104 path: Vec::new(),
105 share_token: None,
106 section: Section::Users,
107 },
108 Some("settings") => Location {
109 root_id: None,
110 path: Vec::new(),
111 share_token: None,
112 section: Section::Settings,
113 },
114 _ => Location::root(),
115 }
116}
117
118pub fn location_to_hash(loc: &Location) -> String {
119 if let Some(token) = &loc.share_token {
120 let mut s = format!("#/s/{token}");
121 for seg in &loc.path {
122 s.push('/');
123 s.push_str(&encode_segment(seg));
124 }
125 return s;
126 }
127 if let Some(segment) = loc.section.segment() {
128 return format!("#/{segment}");
129 }
130 match loc.root_id {
131 None => "#/".to_string(),
132 Some(id) => {
133 let mut s = format!("#/r/{id}");
134 for seg in &loc.path {
135 s.push('/');
136 s.push_str(&encode_segment(seg));
137 }
138 s
139 }
140 }
141}
142
143/// Change the app location (updates the hash; the app listens for hashchange).
144pub fn navigate(loc: &Location) {
145 if let Some(w) = web_sys::window() {
146 let _ = w.location().set_hash(&location_to_hash(loc));
147 }
148}
149
150/// Build the share link for a token, relative to the current page origin.
151pub fn share_url(token: &str) -> String {
152 let base = web_sys::window()
153 .and_then(|w| w.location().href().ok())
154 .unwrap_or_default();
155 // Cut off any existing hash, then append the share hash.
156 let cut = base.find('#').unwrap_or(base.len());
157 format!("{}#/s/{token}", &base[..cut])
158}
159