add fullscreen prompt and STUPID apple pwa stuff.

This commit is contained in:
legop3
2025-11-22 19:38:42 -05:00
parent 930f411549
commit fd4f96899a
10 changed files with 257 additions and 12 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+9 -3
View File
@@ -3,10 +3,16 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="apple-touch-icon" href="/vite.svg" />
<link rel="manifest" href="/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title> <meta name="theme-color" content="#020617" />
<script type="module" crossorigin src="/assets/index-CoT66lYm.js"></script> <meta name="apple-mobile-web-app-capable" content="yes" />
<link rel="stylesheet" crossorigin href="/assets/index-DQGzoV_h.css"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MultiRoomba Rover" />
<title>MultiRoomba Rover</title>
<script type="module" crossorigin src="/assets/index-jOpL-cjE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CJ5HFm2O.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+18
View File
@@ -0,0 +1,18 @@
{
"name": "MultiRoomba Rover",
"short_name": "Rover",
"description": "Remote driving interface for the MultiRoomba Rover fleet.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#020617",
"icons": [
{
"src": "/vite.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
+7 -1
View File
@@ -3,8 +3,14 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" /> <link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="apple-touch-icon" href="/vite.svg" />
<link rel="manifest" href="/manifest.json" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>webui</title> <meta name="theme-color" content="#020617" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="MultiRoomba Rover" />
<title>MultiRoomba Rover</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+18
View File
@@ -0,0 +1,18 @@
{
"name": "MultiRoomba Rover",
"short_name": "Rover",
"description": "Remote driving interface for the MultiRoomba Rover fleet.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#020617",
"icons": [
{
"src": "/vite.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
+9
View File
@@ -19,6 +19,8 @@ import SessionSnapshot from './components/SessionSnapshot.jsx';
import HomeAssistantControls from './components/HomeAssistantControls.jsx'; import HomeAssistantControls from './components/HomeAssistantControls.jsx';
import UserListPanel from './components/UserListPanel.jsx'; import UserListPanel from './components/UserListPanel.jsx';
import ChatPanel from './components/ChatPanel.jsx'; import ChatPanel from './components/ChatPanel.jsx';
import FullscreenPrompt from './components/FullscreenPrompt.jsx';
import { useFullscreenPrompt } from './hooks/useFullscreenPrompt.js';
function useLayoutMode() { function useLayoutMode() {
const [mode, setMode] = useState(() => { const [mode, setMode] = useState(() => {
@@ -121,6 +123,7 @@ function MobileLandscapeLayout() {
function App() { function App() {
const layout = useLayoutMode(); const layout = useLayoutMode();
const isDesktop = layout === 'desktop'; const isDesktop = layout === 'desktop';
const { visible: fullscreenVisible, mode: fullscreenMode, enterFullscreen, dismiss } = useFullscreenPrompt(layout);
const renderedLayout = const renderedLayout =
isDesktop isDesktop
? <DesktopLayout layout={layout} /> ? <DesktopLayout layout={layout} />
@@ -139,6 +142,12 @@ function App() {
</main> </main>
<AlertFeed /> <AlertFeed />
<ModeGateOverlay /> <ModeGateOverlay />
<FullscreenPrompt
visible={fullscreenVisible}
mode={fullscreenMode}
onEnterFullscreen={enterFullscreen}
onDismiss={dismiss}
/>
</ControlSystemProvider> </ControlSystemProvider>
</SettingsProvider> </SettingsProvider>
</div> </div>
+39
View File
@@ -0,0 +1,39 @@
export default function FullscreenPrompt({ visible, mode, onEnterFullscreen, onDismiss }) {
if (!visible) return null;
const isIOSMode = mode === 'pwa-hint';
return (
<div className="fixed inset-0 z-40 flex items-end justify-center p-2 pointer-events-none sm:items-center">
<div className="pointer-events-auto w-full max-w-sm rounded-lg border border-cyan-500/40 bg-zinc-950/95 shadow-xl">
<div className="space-y-2 p-4 text-sm text-slate-100">
<h2 className="text-base font-semibold text-white">Better in fullscreen</h2>
{isIOSMode ? (
<p className="text-slate-300">
For fullscreen on iOS, open Safari&apos;s share menu and pick <strong>Add to Home Screen</strong>. Launching from
the home screen removes the browser chrome.
</p>
) : (
<p className="text-slate-300">
Enable fullscreen to free up more space for the video feed and controls. You can exit fullscreen at any time
via the system back or home gesture.
</p>
)}
<div className="flex justify-end gap-2 pt-1 text-sm">
<button type="button" className="rounded border border-slate-600 px-3 py-1 text-slate-200" onClick={onDismiss}>
{isIOSMode ? 'Got it' : 'Not now'}
</button>
{!isIOSMode && (
<button
type="button"
className="rounded bg-cyan-500 px-3 py-1 font-semibold text-black hover:bg-cyan-400"
onClick={onEnterFullscreen}
>
Enter fullscreen
</button>
)}
</div>
</div>
</div>
</div>
);
}
+149
View File
@@ -0,0 +1,149 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
const SESSION_KEY = 'fullscreenPromptDismissed';
const MOBILE_LAYOUTS = new Set(['mobile-portrait', 'mobile-landscape']);
const getStorage = () => {
if (typeof window === 'undefined') return null;
try {
return window.sessionStorage;
} catch {
return null;
}
};
const isDismissed = () => {
const storage = getStorage();
if (!storage) return false;
try {
return storage.getItem(SESSION_KEY) === '1';
} catch {
return false;
}
};
const markDismissed = () => {
const storage = getStorage();
if (!storage) return;
try {
storage.setItem(SESSION_KEY, '1');
} catch {
/* ignore */
}
};
const detectIOS = () => {
if (typeof navigator === 'undefined') return false;
const ua = navigator.userAgent || navigator.platform || '';
return /iphone|ipad|ipod/i.test(ua);
};
const supportsFullscreen = () => {
if (typeof document === 'undefined') return false;
const doc = document;
return Boolean(
doc.fullscreenEnabled ||
doc.webkitFullscreenEnabled ||
doc.mozFullScreenEnabled ||
doc.msFullscreenEnabled,
);
};
export function useFullscreenPrompt(layout) {
const isMobileLayout = MOBILE_LAYOUTS.has(layout);
const [visible, setVisible] = useState(false);
const [mode, setMode] = useState('native');
const isIOS = useMemo(() => detectIOS(), []);
const reevaluate = useCallback(() => {
if (!isMobileLayout) {
setVisible(false);
return;
}
if (isDismissed()) {
setVisible(false);
return;
}
if (isIOS) {
setMode('pwa-hint');
setVisible(true);
return;
}
if (!supportsFullscreen()) {
setVisible(false);
return;
}
if (typeof document !== 'undefined' && document.fullscreenElement) {
setVisible(false);
return;
}
setMode('native');
setVisible(true);
}, [isIOS, isMobileLayout]);
useEffect(() => {
reevaluate();
}, [reevaluate]);
useEffect(() => {
if (typeof document === 'undefined') return undefined;
const handler = () => {
if (document.fullscreenElement) {
setVisible(false);
return;
}
reevaluate();
};
document.addEventListener('fullscreenchange', handler);
document.addEventListener('webkitfullscreenchange', handler);
return () => {
document.removeEventListener('fullscreenchange', handler);
document.removeEventListener('webkitfullscreenchange', handler);
};
}, [reevaluate]);
const dismiss = useCallback(() => {
markDismissed();
setVisible(false);
}, []);
const enterFullscreen = useCallback(async () => {
if (!isMobileLayout || isIOS) return false;
if (typeof document === 'undefined') return false;
const element = document.documentElement;
if (!element) return false;
const request =
element.requestFullscreen ||
element.webkitRequestFullscreen ||
element.mozRequestFullScreen ||
element.msRequestFullscreen;
if (!request) return false;
try {
const result = request.call(element);
if (result && typeof result.then === 'function') {
await result;
}
markDismissed();
setVisible(false);
return true;
} catch (error) {
console.warn('Failed to enter fullscreen', error);
return false;
}
}, [isIOS, isMobileLayout]);
useEffect(() => {
if (!isMobileLayout) {
setVisible(false);
}
}, [isMobileLayout]);
return {
visible,
mode,
enterFullscreen,
dismiss,
};
}
export default useFullscreenPrompt;