Wake lock persistence, button reposition, docs, dev LAN support
Wake lock (Prevent Sleep) toggles now persist across reloads via localStorage in both AdminView and DisplayView. Buttons repositioned inline in AdminView campaigns header bar (was floating overlay causing overlap on tablets). DisplayView buttons persist localStorage too. Wake lock acquire failure now shows toast with fix hint (HTTPS or Chrome flag). Fullscreenchange listener re-acquires wake lock (Android discards on screen off). dev-start.sh: auto-detects LAN IP (en0/en1), frontend binds 0.0.0.0, backend URL inlined as LAN IP so phones reach backend. DANGEROUSLY_DISABLE_HOST_CHECK for LAN access. Outputs LAN URL + wake lock flag instructions. Docs: README 'Prevent Sleep (Wake Lock)' section covering secure context requirement, Android Chrome flag workaround for LAN testing, iOS Safari standalone PWA bug. DEVELOPMENT.md LAN access + wake lock note.
This commit is contained in:
+88
-2
@@ -2489,6 +2489,53 @@ function AdminView({ userId }) {
|
||||
);
|
||||
const { data: initialActiveInfo } = useFirestoreDocument(getPath.activeDisplay());
|
||||
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [wakeLockEnabled, setWakeLockEnabled] = useState(() => {
|
||||
try { return localStorage.getItem('ttrpg.wakeLock') === 'true'; }
|
||||
catch { return false; }
|
||||
});
|
||||
const wakeLockRef = useRef(null);
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!document.fullscreenElement) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onFsChange = () => setIsFullscreen(!!document.fullscreenElement);
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!wakeLockEnabled) {
|
||||
wakeLockRef.current?.release();
|
||||
wakeLockRef.current = null;
|
||||
return;
|
||||
}
|
||||
const acquire = async () => {
|
||||
try { wakeLockRef.current = await navigator.wakeLock.request('screen'); }
|
||||
catch (e) {
|
||||
console.error('Wake lock failed:', e);
|
||||
showToast('Prevent Sleep failed. Requires HTTPS or Chrome flag: chrome://flags/#unsafely-treat-insecure-origin-as-secure');
|
||||
}
|
||||
};
|
||||
acquire();
|
||||
const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); };
|
||||
const onFsChange = () => { if (document.fullscreenElement) acquire(); };
|
||||
document.addEventListener('visibilitychange', onVisChange);
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVisChange);
|
||||
document.removeEventListener('fullscreenchange', onFsChange);
|
||||
wakeLockRef.current?.release();
|
||||
wakeLockRef.current = null;
|
||||
};
|
||||
}, [wakeLockEnabled, showToast]);
|
||||
|
||||
const [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
|
||||
const [draggedCampaignId, setDraggedCampaignId] = useState(null);
|
||||
const [selectedCampaignId, setSelectedCampaignId] = useState(null);
|
||||
@@ -2735,6 +2782,22 @@ function AdminView({ userId }) {
|
||||
>
|
||||
<PlusCircle size={20} className="mr-2" /> Create Campaign
|
||||
</button>
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
|
||||
title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
|
||||
className={`p-2 rounded-lg transition-all ${wakeLockEnabled ? 'bg-amber-600 hover:bg-amber-700 text-white' : 'bg-stone-800 hover:bg-stone-700 text-stone-300 hover:text-white'}`}
|
||||
>
|
||||
{wakeLockEnabled ? <Coffee size={20} /> : <Moon size={20} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
||||
className="bg-stone-800 hover:bg-stone-700 text-stone-300 hover:text-white p-2 rounded-lg transition-all"
|
||||
>
|
||||
{isFullscreen ? <Minimize2 size={20} /> : <Maximize2 size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!campaignsCollapsed && (
|
||||
@@ -2963,7 +3026,10 @@ function DisplayView() {
|
||||
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
|
||||
const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [wakeLockEnabled, setWakeLockEnabled] = useState(false);
|
||||
const [wakeLockEnabled, setWakeLockEnabled] = useState(() => {
|
||||
try { return localStorage.getItem('ttrpg.wakeLock') === 'true'; }
|
||||
catch { return false; }
|
||||
});
|
||||
const [displayParticipants, setDisplayParticipants] = useState([]);
|
||||
const wakeLockRef = useRef(null);
|
||||
const currentParticipantRef = useRef(null);
|
||||
@@ -3029,8 +3095,12 @@ function DisplayView() {
|
||||
// Re-acquire after tab becomes visible again (browser auto-releases on hide)
|
||||
const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); };
|
||||
document.addEventListener('visibilitychange', onVisChange);
|
||||
// Re-acquire on fullscreen change (Android discards wakeLock on screen off)
|
||||
const onFsChange = () => { if (document.fullscreenElement) acquire(); };
|
||||
document.addEventListener('fullscreenchange', onFsChange);
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVisChange);
|
||||
document.removeEventListener('fullscreenchange', onFsChange);
|
||||
wakeLockRef.current?.release();
|
||||
wakeLockRef.current = null;
|
||||
};
|
||||
@@ -3121,6 +3191,22 @@ function DisplayView() {
|
||||
if (!isPlayerDisplayActive || !activeEncounterData) {
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-stone-400 flex flex-col items-center justify-center p-4 text-center">
|
||||
<div className="fixed top-3 right-3 z-50 flex gap-2">
|
||||
<button
|
||||
onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
|
||||
title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
|
||||
className={`p-2 rounded-lg transition-all ${wakeLockEnabled ? 'bg-amber-600 hover:bg-amber-700 text-white' : 'bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white'}`}
|
||||
>
|
||||
{wakeLockEnabled ? <Coffee size={20} /> : <Moon size={20} />}
|
||||
</button>
|
||||
<button
|
||||
onClick={toggleFullscreen}
|
||||
title={isFullscreen ? 'Exit fullscreen' : 'Enter fullscreen'}
|
||||
className="bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white p-2 rounded-lg transition-all"
|
||||
>
|
||||
{isFullscreen ? <Minimize2 size={20} /> : <Maximize2 size={20} />}
|
||||
</button>
|
||||
</div>
|
||||
<EyeOff size={64} className="mb-4 text-stone-500" />
|
||||
<h2 className="text-3xl font-semibold font-cinzel tracking-wide">Game Session Paused</h2>
|
||||
<p className="text-xl mt-2">The Dungeon Master has not activated an encounter for display.</p>
|
||||
@@ -3153,7 +3239,7 @@ function DisplayView() {
|
||||
>
|
||||
<div className="fixed top-3 right-3 z-50 flex gap-2">
|
||||
<button
|
||||
onClick={() => setWakeLockEnabled(v => !v)}
|
||||
onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
|
||||
title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
|
||||
className={`p-2 rounded-lg transition-all ${wakeLockEnabled ? 'bg-amber-600 hover:bg-amber-700 text-white' : 'bg-stone-800 bg-opacity-80 hover:bg-opacity-100 text-stone-300 hover:text-white'}`}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user