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//!
7//! The URL hash is the single source of truth; the `Location` signal in the
8//! app mirrors it.
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Location {
12 /// None = the top-level "folders" view (root picker).
13 pub root_id: Option<i64>,
14 /// Path segments relative to the current root (or share target).
15 pub path: Vec<String>,
16 /// Present when the location is a public share (`#/s/{token}/...`).
17 pub share_token: Option<String>,
18}
19
20impl Location {
21 pub const fn root() -> Self {
22 Self {
23 root_id: None,
24 path: Vec::new(),
25 share_token: None,
26 }
27 }
28}
29
30/// Percent-encode one path segment for the URL hash. Without this a name
31/// containing `#`, `%` or `/` would break the round trip.
32fn encode_segment(seg: &str) -> String {
33 js_sys::encode_uri_component(seg).into()
34}
35
36fn decode_segment(seg: &str) -> String {
37 // A hand-typed hash can hold invalid escapes; keep it verbatim then.
38 js_sys::decode_uri_component(seg)
39 .map(String::from)
40 .unwrap_or_else(|_| seg.to_string())
41}
42
43pub fn parse_location() -> Location {
44 let hash = web_sys::window()
45 .and_then(|w| w.location().hash().ok())
46 .unwrap_or_default();
47 let h = hash.trim_start_matches('#');
48 let h = h.trim_start_matches('/');
49 let mut parts = h.split('/').filter(|s| !s.is_empty());
50 match parts.next() {
51 Some("r") => match parts.next().and_then(|s| s.parse().ok()) {
52 Some(root_id) => Location {
53 root_id: Some(root_id),
54 path: parts.map(decode_segment).collect(),
55 share_token: None,
56 },
57 None => Location::root(),
58 },
59 Some("s") => match parts.next() {
60 Some(token) => Location {
61 root_id: None,
62 path: parts.map(decode_segment).collect(),
63 share_token: Some(token.to_string()),
64 },
65 None => Location::root(),
66 },
67 _ => Location::root(),
68 }
69}
70
71pub fn location_to_hash(loc: &Location) -> String {
72 if let Some(token) = &loc.share_token {
73 let mut s = format!("#/s/{token}");
74 for seg in &loc.path {
75 s.push('/');
76 s.push_str(&encode_segment(seg));
77 }
78 return s;
79 }
80 match loc.root_id {
81 None => "#/".to_string(),
82 Some(id) => {
83 let mut s = format!("#/r/{id}");
84 for seg in &loc.path {
85 s.push('/');
86 s.push_str(&encode_segment(seg));
87 }
88 s
89 }
90 }
91}
92
93/// Change the app location (updates the hash; the app listens for hashchange).
94pub fn navigate(loc: &Location) {
95 if let Some(w) = web_sys::window() {
96 let _ = w.location().set_hash(&location_to_hash(loc));
97 }
98}
99
100/// Build the share link for a token, relative to the current page origin.
101pub fn share_url(token: &str) -> String {
102 let base = web_sys::window()
103 .and_then(|w| w.location().href().ok())
104 .unwrap_or_default();
105 // Cut off any existing hash, then append the share hash.
106 let cut = base.find('#').unwrap_or(base.len());
107 format!("{}#/s/{token}", &base[..cut])
108}
109