nuts / files / src / compat.ts
compat.ts
| 1 | export type CompatResult = |
| 2 | | { ok: true } |
| 3 | | { ok: false; reason: 'no-touch' | 'no-motion' } |
| 4 | |
| 5 | /** True if the Vibration API is available (Chrome Android). */ |
| 6 | export function hasVibration(): boolean { |
| 7 | return 'vibrate' in navigator && typeof navigator.vibrate === 'function' |
| 8 | } |
| 9 | |
| 10 | /** |
| 11 | * Check whether this platform can run the full experience. |
| 12 | * Returns immediately (no async, no permissions needed). |
| 13 | */ |
| 14 | export function checkCompat(): CompatResult { |
| 15 | // Must be a touch device |
| 16 | if (!('ontouchstart' in window) && !navigator.maxTouchPoints) { |
| 17 | return { ok: false, reason: 'no-touch' } |
| 18 | } |
| 19 | |
| 20 | // Must have DeviceMotion (accelerometer / tilt gravity) |
| 21 | if (typeof DeviceMotionEvent === 'undefined') { |
| 22 | return { ok: false, reason: 'no-motion' } |
| 23 | } |
| 24 | |
| 25 | return { ok: true } |
| 26 | } |
| 27 | |
| 28 | const MESSAGES: Record<string, { headline: string; body: string }> = { |
| 29 | 'no-touch': { |
| 30 | headline: 'Phone only', |
| 31 | body: 'This app needs a touchscreen to work.\nOpen it on your phone in Chrome.', |
| 32 | }, |
| 33 | 'no-motion': { |
| 34 | headline: 'No accelerometer', |
| 35 | body: 'This app needs a motion sensor to work.\nOpen it on your phone in Chrome.', |
| 36 | }, |
| 37 | } |
| 38 | |
| 39 | /** Replace the page body with a full-screen "not supported" wall. */ |
| 40 | export function showUnsupportedWall(reason: string): void { |
| 41 | const { headline, body } = MESSAGES[reason] ?? { |
| 42 | headline: 'Not supported', |
| 43 | body: 'Open this on your Android phone in Chrome.', |
| 44 | } |
| 45 | |
| 46 | document.body.innerHTML = ` |
| 47 | <div style=" |
| 48 | position:fixed;inset:0; |
| 49 | background:#111; |
| 50 | display:flex;flex-direction:column;align-items:center;justify-content:center; |
| 51 | padding:32px;gap:20px; |
| 52 | font-family:system-ui,-apple-system,sans-serif; |
| 53 | text-align:center; |
| 54 | "> |
| 55 | <div style="font-size:72px;line-height:1;filter:grayscale(1) opacity(0.4)">🔩</div> |
| 56 | <h1 style="color:#fff;font-size:28px;font-weight:700;margin:0">${headline}</h1> |
| 57 | <p style="color:#666;font-size:16px;line-height:1.6;margin:0;white-space:pre-line">${body}</p> |
| 58 | <a href="https://apps.fuzzyworld.net/nuts/" |
| 59 | style="margin-top:8px;color:#c8a84b;font-size:13px;opacity:0.6;text-decoration:none"> |
| 60 | apps.fuzzyworld.net/nuts |
| 61 | </a> |
| 62 | </div>` |
| 63 | } |