atproto blogging
1//! Platform detection for browser-specific workarounds.
2//!
3//! Based on patterns from ProseMirror's input handling, adapted for Rust/wasm.
4
5use std::sync::OnceLock;
6
7/// Cached platform detection results.
8#[derive(Debug, Clone)]
9pub struct Platform {
10 pub ios: bool,
11 pub mac: bool,
12 pub android: bool,
13 pub chrome: bool,
14 pub safari: bool,
15 pub gecko: bool,
16 pub webkit_version: Option<u32>,
17 pub chrome_version: Option<u32>,
18 pub mobile: bool,
19}
20
21impl Default for Platform {
22 fn default() -> Self {
23 Self {
24 ios: false,
25 mac: false,
26 android: false,
27 chrome: false,
28 safari: false,
29 gecko: false,
30 webkit_version: None,
31 chrome_version: None,
32 mobile: false,
33 }
34 }
35}
36
37static PLATFORM: OnceLock<Platform> = OnceLock::new();
38
39/// Get cached platform info. Detection runs once on first call.
40pub fn platform() -> &'static Platform {
41 PLATFORM.get_or_init(detect_platform)
42}
43
44fn detect_platform() -> Platform {
45 let window = match web_sys::window() {
46 Some(w) => w,
47 None => return Platform::default(),
48 };
49
50 let navigator = window.navigator();
51 let user_agent = navigator.user_agent().unwrap_or_default().to_lowercase();
52 let platform_str = navigator.platform().unwrap_or_default().to_lowercase();
53
54 // iOS detection: iPhone/iPad/iPod in UA, or Mac platform with touch.
55 let ios = user_agent.contains("iphone")
56 || user_agent.contains("ipad")
57 || user_agent.contains("ipod")
58 || (platform_str.contains("mac") && has_touch_support(&navigator));
59
60 // macOS (but not iOS).
61 let mac = platform_str.contains("mac") && !ios;
62
63 // Android.
64 let android = user_agent.contains("android");
65
66 // Chrome (but not Edge, which also contains Chrome).
67 let chrome = user_agent.contains("chrome") && !user_agent.contains("edg");
68
69 // Safari (WebKit but not Chrome).
70 let safari = user_agent.contains("safari") && !user_agent.contains("chrome");
71
72 // Firefox/Gecko.
73 let gecko = user_agent.contains("gecko/") && !user_agent.contains("like gecko");
74
75 // WebKit version extraction.
76 let webkit_version = extract_version(&user_agent, "applewebkit/");
77
78 // Chrome version extraction.
79 let chrome_version = extract_version(&user_agent, "chrome/");
80
81 // Mobile detection.
82 let mobile =
83 ios || android || user_agent.contains("mobile") || user_agent.contains("iemobile");
84
85 Platform {
86 ios,
87 mac,
88 android,
89 chrome,
90 safari,
91 gecko,
92 webkit_version,
93 chrome_version,
94 mobile,
95 }
96}
97
98fn has_touch_support(navigator: &web_sys::Navigator) -> bool {
99 navigator.max_touch_points() > 0
100}
101
102fn extract_version(ua: &str, prefix: &str) -> Option<u32> {
103 ua.find(prefix).and_then(|idx| {
104 let after = &ua[idx + prefix.len()..];
105 let version_str: String = after.chars().take_while(|c| c.is_ascii_digit()).collect();
106 version_str.parse().ok()
107 })
108}