mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
add fullscreen prompt and STUPID apple pwa stuff.
This commit is contained in:
@@ -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