mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-15 17:12:59 -04:00
add fullscreen prompt and STUPID apple pwa stuff.
This commit is contained in:
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
@@ -3,10 +3,16 @@
|
||||
<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>
|
||||
<script type="module" crossorigin src="/assets/index-CoT66lYm.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DQGzoV_h.css">
|
||||
<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>
|
||||
<script type="module" crossorigin src="/assets/index-jOpL-cjE.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-CJ5HFm2O.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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'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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user