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:
david raistrick
2026-07-08 13:19:10 -04:00
parent 863e8b3719
commit 41c1e48874
5 changed files with 130 additions and 6 deletions
+88 -2
View File
@@ -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'}`}
>