Generic (non-5e) ruleset mode + UI polish + combat switch fix #7
@@ -307,6 +307,26 @@ ttrpg-initiative-tracker/
|
|||||||
* `docs/GLOSSARY.md` — domain terms (turn vs. round, etc.)
|
* `docs/GLOSSARY.md` — domain terms (turn vs. round, etc.)
|
||||||
* `TODO.md` — known bugs and backlog
|
* `TODO.md` — known bugs and backlog
|
||||||
|
|
||||||
|
## Prevent Sleep (Wake Lock)
|
||||||
|
|
||||||
|
DM and Player views have a Coffee/Moon toggle to keep screen awake during combat.
|
||||||
|
|
||||||
|
**Requires secure context** (HTTPS or localhost). The Screen Wake Lock API refuses
|
||||||
|
on plain HTTP.
|
||||||
|
|
||||||
|
- **Prod (HTTPS):** works automatically on all devices.
|
||||||
|
- **Local dev (localhost):** works automatically — localhost is a trusted origin.
|
||||||
|
- **LAN IP (phones, plain HTTP):** wake lock fails silently. Two options:
|
||||||
|
1. **Android Chrome flag (dev testing):** `chrome://flags/#unsafely-treat-insecure-origin-as-secure` → add full URL with port, e.g. `http://10.0.0.5:3999`. Enable flag, relaunch.
|
||||||
|
2. **mkcert local CA:** generate trusted cert for LAN IP. Heavier setup.
|
||||||
|
|
||||||
|
**iOS Safari:** Wake Lock API broken in standalone PWA mode (iOS 16.4–18.4, WebKit bug 254545). Fixed in 18.4+. Works in browser tab (not installed PWA).
|
||||||
|
|
||||||
|
**Troubleshooting:** if toggle shows active but screen still sleeps, check:
|
||||||
|
- Battery saver / power save mode (browser refuses)
|
||||||
|
- Chrome devtools console for `WakeLockError`
|
||||||
|
- Secure context: `window.isSecureContext` must be `true`
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
If you want to contribute, send me a message here: [https://discourse.draft13.com/c/ttrpg-initiative-tracker/16](https://discourse.draft13.com/c/ttrpg-initiative-tracker/16), and I can add to this Gitea instance and you can feel free to fork the repository and submit pull requests. For major changes, please pose a topic to the Discourse instance above linked above first to discuss what you would like to change.
|
If you want to contribute, send me a message here: [https://discourse.draft13.com/c/ttrpg-initiative-tracker/16](https://discourse.draft13.com/c/ttrpg-initiative-tracker/16), and I can add to this Gitea instance and you can feel free to fork the repository and submit pull requests. For major changes, please pose a topic to the Discourse instance above linked above first to discuss what you would like to change.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md.
|
|||||||
## Open
|
## Open
|
||||||
|
|
||||||
|
|
||||||
fullscreen and dont lock on main app dm view and the no-game-player view
|
x fullscreen and dont lock on main app dm view and the no-game-player view ...and doesnt actually prevent lock on android
|
||||||
|
|
||||||
also better vert tab layout - labelt friendly
|
also better vert tab layout - labelt friendly
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,14 @@ git config core.hooksPath .githooks # enable pre-push test gate
|
|||||||
|
|
||||||
Idempotent: if port busy, leaves existing proc as-is.
|
Idempotent: if port busy, leaves existing proc as-is.
|
||||||
|
|
||||||
|
**LAN access (phones):** frontend binds 0.0.0.0, LAN IP auto-detected.
|
||||||
|
Other devices hit `http://<lan-ip>:3999`.
|
||||||
|
|
||||||
|
**Wake Lock (Prevent Sleep) on LAN IP:** requires secure context.
|
||||||
|
Plain HTTP on LAN IP = wake lock silently fails. Android Chrome flag workaround:
|
||||||
|
`chrome://flags/#unsafely-treat-insecure-origin-as-secure` → add
|
||||||
|
`http://<lan-ip>:3999` (with port), enable, relaunch. See README for full details.
|
||||||
|
|
||||||
Smoke check:
|
Smoke check:
|
||||||
```bash
|
```bash
|
||||||
curl http://127.0.0.1:4001/health # -> {"ok":true}
|
curl http://127.0.0.1:4001/health # -> {"ok":true}
|
||||||
|
|||||||
+13
-3
@@ -27,11 +27,15 @@ fi
|
|||||||
|
|
||||||
# frontend: server storage, :3999
|
# frontend: server storage, :3999
|
||||||
if ! lsof -ti :3999 >/dev/null 2>&1; then
|
if ! lsof -ti :3999 >/dev/null 2>&1; then
|
||||||
echo "starting frontend :3999..."
|
# detect LAN ip so other devices (phones) can reach backend
|
||||||
|
LAN_IP=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || echo 127.0.0.1)
|
||||||
|
echo "starting frontend :3999 (lan ip: $LAN_IP)..."
|
||||||
NODE_ENV=development REACT_APP_DEV_TOOLS=1 \
|
NODE_ENV=development REACT_APP_DEV_TOOLS=1 \
|
||||||
REACT_APP_STORAGE=server \
|
REACT_APP_STORAGE=server \
|
||||||
REACT_APP_BACKEND_URL=http://127.0.0.1:4001 \
|
REACT_APP_BACKEND_URL=http://${LAN_IP}:4001 \
|
||||||
REACT_APP_BACKEND_REALTIME_URL=ws://127.0.0.1:4001/ws \
|
REACT_APP_BACKEND_REALTIME_URL=ws://${LAN_IP}:4001/ws \
|
||||||
|
DANGEROUSLY_DISABLE_HOST_CHECK=true \
|
||||||
|
HOST=0.0.0.0 \
|
||||||
BROWSER=none PORT=3999 \
|
BROWSER=none PORT=3999 \
|
||||||
nohup npm start > tmp/fe.log 2>&1 &
|
nohup npm start > tmp/fe.log 2>&1 &
|
||||||
echo $! > tmp/fe.pid
|
echo $! > tmp/fe.pid
|
||||||
@@ -51,5 +55,11 @@ done
|
|||||||
echo ""
|
echo ""
|
||||||
echo "backend : http://127.0.0.1:4001 (curl http://127.0.0.1:4001/health)"
|
echo "backend : http://127.0.0.1:4001 (curl http://127.0.0.1:4001/health)"
|
||||||
echo "frontend : http://127.0.0.1:3999 (admin / player /display)"
|
echo "frontend : http://127.0.0.1:3999 (admin / player /display)"
|
||||||
|
echo "lan url : http://${LAN_IP}:3999 (phones/other devices on network)"
|
||||||
echo "logs : tmp/server.log tmp/fe.log"
|
echo "logs : tmp/server.log tmp/fe.log"
|
||||||
echo "stop : ./scripts/dev-stop.sh"
|
echo "stop : ./scripts/dev-stop.sh"
|
||||||
|
echo ""
|
||||||
|
echo "NOTE: Prevent Sleep (wake lock) requires secure context (HTTPS/localhost)."
|
||||||
|
echo " LAN IP (http) won't work unless Android Chrome flag set:"
|
||||||
|
echo " chrome://flags/#unsafely-treat-insecure-origin-as-secure"
|
||||||
|
echo " Add: http://${LAN_IP}:3999 (include port)"
|
||||||
|
|||||||
+88
-2
@@ -2489,6 +2489,53 @@ function AdminView({ userId }) {
|
|||||||
);
|
);
|
||||||
const { data: initialActiveInfo } = useFirestoreDocument(getPath.activeDisplay());
|
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 [campaignsWithDetails, setCampaignsWithDetails] = useState([]);
|
||||||
const [draggedCampaignId, setDraggedCampaignId] = useState(null);
|
const [draggedCampaignId, setDraggedCampaignId] = useState(null);
|
||||||
const [selectedCampaignId, setSelectedCampaignId] = useState(null);
|
const [selectedCampaignId, setSelectedCampaignId] = useState(null);
|
||||||
@@ -2735,6 +2782,22 @@ function AdminView({ userId }) {
|
|||||||
>
|
>
|
||||||
<PlusCircle size={20} className="mr-2" /> Create Campaign
|
<PlusCircle size={20} className="mr-2" /> Create Campaign
|
||||||
</button>
|
</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>
|
</div>
|
||||||
|
|
||||||
{!campaignsCollapsed && (
|
{!campaignsCollapsed && (
|
||||||
@@ -2963,7 +3026,10 @@ function DisplayView() {
|
|||||||
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
|
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
|
||||||
const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
|
const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
|
||||||
const [isFullscreen, setIsFullscreen] = 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 [displayParticipants, setDisplayParticipants] = useState([]);
|
||||||
const wakeLockRef = useRef(null);
|
const wakeLockRef = useRef(null);
|
||||||
const currentParticipantRef = useRef(null);
|
const currentParticipantRef = useRef(null);
|
||||||
@@ -3029,8 +3095,12 @@ function DisplayView() {
|
|||||||
// Re-acquire after tab becomes visible again (browser auto-releases on hide)
|
// Re-acquire after tab becomes visible again (browser auto-releases on hide)
|
||||||
const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); };
|
const onVisChange = () => { if (document.visibilityState === 'visible') acquire(); };
|
||||||
document.addEventListener('visibilitychange', onVisChange);
|
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 () => {
|
return () => {
|
||||||
document.removeEventListener('visibilitychange', onVisChange);
|
document.removeEventListener('visibilitychange', onVisChange);
|
||||||
|
document.removeEventListener('fullscreenchange', onFsChange);
|
||||||
wakeLockRef.current?.release();
|
wakeLockRef.current?.release();
|
||||||
wakeLockRef.current = null;
|
wakeLockRef.current = null;
|
||||||
};
|
};
|
||||||
@@ -3121,6 +3191,22 @@ function DisplayView() {
|
|||||||
if (!isPlayerDisplayActive || !activeEncounterData) {
|
if (!isPlayerDisplayActive || !activeEncounterData) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black text-stone-400 flex flex-col items-center justify-center p-4 text-center">
|
<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" />
|
<EyeOff size={64} className="mb-4 text-stone-500" />
|
||||||
<h2 className="text-3xl font-semibold font-cinzel tracking-wide">Game Session Paused</h2>
|
<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>
|
<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">
|
<div className="fixed top-3 right-3 z-50 flex gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setWakeLockEnabled(v => !v)}
|
onClick={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })}
|
||||||
title={wakeLockEnabled ? 'Allow sleep' : 'Prevent sleep'}
|
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'}`}
|
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