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
+7 -1
View File
@@ -3,8 +3,14 @@
<head>
<meta charset="UTF-8" />
<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" />
<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>
<body>
<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 UserListPanel from './components/UserListPanel.jsx';
import ChatPanel from './components/ChatPanel.jsx';
import FullscreenPrompt from './components/FullscreenPrompt.jsx';
import { useFullscreenPrompt } from './hooks/useFullscreenPrompt.js';
function useLayoutMode() {
const [mode, setMode] = useState(() => {
@@ -121,6 +123,7 @@ function MobileLandscapeLayout() {
function App() {
const layout = useLayoutMode();
const isDesktop = layout === 'desktop';
const { visible: fullscreenVisible, mode: fullscreenMode, enterFullscreen, dismiss } = useFullscreenPrompt(layout);
const renderedLayout =
isDesktop
? <DesktopLayout layout={layout} />
@@ -139,6 +142,12 @@ function App() {
</main>
<AlertFeed />
<ModeGateOverlay />
<FullscreenPrompt
visible={fullscreenVisible}
mode={fullscreenMode}
onEnterFullscreen={enterFullscreen}
onDismiss={dismiss}
/>
</ControlSystemProvider>
</SettingsProvider>
</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;