@@ -583,6 +583,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
const [ currentHp , setCurrentHp ] = useState ( participant . currentHp ) ;
const [ maxHp , setMaxHp ] = useState ( participant . maxHp ) ;
const [ hpFormula , setHpFormula ] = useState ( participant . hpFormula || '' ) ;
const [ ac , setAc ] = useState ( participant . ac != null ? participant . ac : '' ) ;
const [ asNpc , setAsNpc ] = useState ( participant . type === 'npc' ) ;
const handleSubmit = ( e ) => {
@@ -602,6 +603,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
currentHp : finalCurrentHp ,
maxHp : finalMaxHp ,
hpFormula : ( participant . type === 'monster' || participant . type === 'npc' ) ? ( hpFormula . trim ( ) || null ) : null ,
ac : ac !== '' ? parseInt ( ac , 10 ) : null ,
type : participant . type === 'monster' || participant . type === 'npc' ? ( asNpc ? 'npc' : 'monster' ) : participant . type ,
} ) ;
} ;
@@ -618,15 +620,27 @@ function EditParticipantModal({ participant, onClose, onSave }) {
className = "mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div >
< label htmlFor = "edit-initiative" className = "block text-sm font-medium text-stone-300" > Initiative < / l a b e l >
< input
type = "number"
id = "edit-initiative "
value = { initiative }
onChange = { ( e ) => setInitiative ( e . target . value ) }
className = "mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< div className = "flex gap-4" >
< div className = "flex-1" >
< label htmlFor = "edit-initiative" className = "block text-sm font-medium text-stone-300" > Initiative < / l a b e l >
< input
type = "number "
id = "edit-initiative"
value = { initiative }
onChange = { ( e ) => setInitiative ( e . target . value ) }
className = "mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "flex-1" >
< label className = "block text-sm font-medium text-stone-300" > AC < / l a b e l >
< input
type = "number"
value = { ac }
onChange = { ( e ) => setAc ( e . target . value ) }
placeholder = "optional"
className = "mt-1 block w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< / d i v >
< div className = "flex gap-4" >
< div className = "flex-1" >
@@ -656,7 +670,7 @@ function EditParticipantModal({ participant, onClose, onSave }) {
type = "text"
value = { hpFormula }
onChange = { ( e ) => setHpFormula ( e . target . value ) }
placeholder = "2d6+9"
placeholder = "e.g. 2d6+9"
className = "flex-1 px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< button
@@ -717,6 +731,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
const [ characterName , setCharacterName ] = useState ( '' ) ;
const [ defaultMaxHp , setDefaultMaxHp ] = useState ( DEFAULT _MAX _HP ) ;
const [ defaultInitMod , setDefaultInitMod ] = useState ( DEFAULT _INIT _MOD ) ;
const [ defaultAc , setDefaultAc ] = useState ( '' ) ;
const [ editingCharacter , setEditingCharacter ] = useState ( null ) ;
const [ showDeleteConfirm , setShowDeleteConfirm ] = useState ( false ) ;
const [ itemToDelete , setItemToDelete ] = useState ( null ) ;
@@ -762,7 +777,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
id : generateId ( ) ,
name : characterName . trim ( ) ,
defaultMaxHp : hp ,
defaultInitMod : initMod
defaultInitMod : initMod ,
defaultAc : defaultAc !== '' ? parseInt ( defaultAc , 10 ) : null
} ;
try {
@@ -772,12 +788,13 @@ function CharacterManager({ campaignId, campaignCharacters }) {
setCharacterName ( '' ) ;
setDefaultMaxHp ( DEFAULT _MAX _HP ) ;
setDefaultInitMod ( DEFAULT _INIT _MOD ) ;
setDefaultAc ( '' ) ;
} catch ( err ) {
console . error ( "Error adding character:" , err ) ;
showToast ( "Failed to add character. Please try again." ) ; }
} ;
const handleUpdateCharacter = async ( characterId , newName , newDefaultMaxHp , newDefaultInitMod ) => {
const handleUpdateCharacter = async ( characterId , newName , newDefaultMaxHp , newDefaultInitMod , newDefaultAc ) => {
if ( ! db || ! newName . trim ( ) || ! campaignId ) return ;
const hp = parseInt ( newDefaultMaxHp , 10 ) ;
@@ -794,7 +811,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
const updatedCharacters = campaignCharacters . map ( c =>
c . id === characterId
? { ... c , name : newName . trim ( ) , defaultMaxHp : hp , defaultInitMod : initMod }
? { ... c , name : newName . trim ( ) , defaultMaxHp : hp , defaultInitMod : initMod , defaultAc : newDefaultAc !== undefined && newDefaultAc !== '' ? parseInt ( newDefaultAc , 10 ) : null }
: c
) ;
@@ -843,8 +860,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
{ isOpen && (
< >
< form onSubmit = { ( e ) => { e . preventDefault ( ) ; handleAddCharacter ( ) ; } } className = "grid grid-cols-1 sm:grid-cols-3 gap-2 mb-4 items-end" >
< div className = "sm:col-span-1 " >
< form onSubmit = { ( e ) => { e . preventDefault ( ) ; handleAddCharacter ( ) ; } } className = "grid grid-cols-2 sm:grid-cols-12 gap-2 mb-4 items-end" >
< div className = "col-span-2 sm:col-span-6 " >
< label htmlFor = "characterName" className = "block text-xs font-medium text-stone-400" >
Name
< / l a b e l >
@@ -857,19 +874,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
className = "w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "w-full sm:w-auto " >
< label htmlFor = "defaultMaxHp" className = "block text-xs font-medium text-stone-400" >
Default HP
< / l a b e l >
< input
type = "number"
id = "defaultMaxHp"
value = { defaultMaxHp }
onChange = { ( e ) => setDefaultMaxHp ( e . target . value ) }
className = "w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "w-full sm:w-auto" >
< div className = "sm:col-span-2 " >
< label htmlFor = "defaultInitMod" className = "block text-xs font-medium text-stone-400" >
Init Mod
< / l a b e l >
@@ -881,9 +886,33 @@ function CharacterManager({ campaignId, campaignCharacters }) {
className = "w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "sm:col-span-2" >
< label htmlFor = "defaultAc" className = "block text-xs font-medium text-stone-400" >
AC
< / l a b e l >
< input
type = "number"
id = "defaultAc"
value = { defaultAc }
onChange = { ( e ) => setDefaultAc ( e . target . value ) }
className = "w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "sm:col-span-2" >
< label htmlFor = "defaultMaxHp" className = "block text-xs font-medium text-stone-400" >
HP
< / l a b e l >
< input
type = "number"
id = "defaultMaxHp"
value = { defaultMaxHp }
onChange = { ( e ) => setDefaultMaxHp ( e . target . value ) }
className = "w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< button
type = "submit"
className = "sm:col-span-3 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center justify-center"
className = "col-span-2 sm:col-span-12 sm:w-auto sm:justify-self-end px-4 py-2 text-sm font-medium text-white bg-amber-700 hover:bg-amber-800 rounded-md transition-colors flex items-center justify-center"
>
< PlusCircle size = { 18 } className = "mr-1" / > Add Character
< / b u t t o n >
@@ -914,31 +943,51 @@ function CharacterManager({ campaignId, campaignCharacters }) {
character . id ,
editingCharacter . name ,
editingCharacter . defaultMaxHp ,
editingCharacter . defaultInitMod
editingCharacter . defaultInitMod ,
editingCharacter . defaultAc
) ;
} }
className = "flex-grow flex flex-wrap gap-2 items-center"
>
< input
type = "text"
value = { editingCharacter . name }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , name : e . target . value } ) }
className = "flex-grow min-w-[100px] px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
/ >
< input
type = "number"
value = { editingCharacter . defaultMaxHp }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , defaultMaxHp : e . target . value } ) }
className = "w-20 px-2 py-1 bg-stone-700 border border -stone-6 00 rounded-md text-white"
title = "Default Max HP"
/ >
< input
type = "number"
value = { editingCharacter . defaultInitMod }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , defaultInitMod : e . target . value } ) }
className = "w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title = "Default Init Mod"
/ >
< div className = "flex flex-col flex-grow min-w-[100px]" >
< label className = "text-[10px] text-stone-400 mb-0.5" > Name < / l a b e l >
< input
type = "text"
value = { editingCharacter . name }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , name : e . target . value } ) }
className = "w-full px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
/ >
< / d i v >
< div className = "flex flex-col" >
< label className = "text-[10px] text -stone-4 00 mb-0.5" > HP < / l a b e l >
< input
type = "number"
value = { editingCharacter . defaultMaxHp }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , defaultMaxHp : e . target . value } ) }
className = "w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title = "Default Max HP"
/ >
< / d i v >
< div className = "flex flex-col" >
< label className = "text-[10px] text-stone-400 mb-0.5" > Init < / l a b e l >
< input
type = "number"
value = { editingCharacter . defaultInitMod }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , defaultInitMod : e . target . value } ) }
className = "w-20 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title = "Default Init Mod"
/ >
< / d i v >
< div className = "flex flex-col" >
< label className = "text-[10px] text-stone-400 mb-0.5" > AC < / l a b e l >
< input
type = "number"
value = { editingCharacter . defaultAc ? ? '' }
onChange = { ( e ) => setEditingCharacter ( { ... editingCharacter , defaultAc : e . target . value } ) }
className = "w-16 px-2 py-1 bg-stone-700 border border-stone-600 rounded-md text-white"
title = "AC"
/ >
< / d i v >
< button type = "submit" className = "p-1 text-green-400 hover:text-green-300" >
< Save size = { 18 } / >
< / b u t t o n >
@@ -955,7 +1004,7 @@ function CharacterManager({ campaignId, campaignCharacters }) {
< span className = "text-stone-100" >
{ character . name } { ' ' }
< span className = "text-xs text-stone-400" >
( HP : { character . defaultMaxHp || 'N/A' } , Init Mod : { formatInitMod ( character . defaultInitMod ) } )
( HP : { character . defaultMaxHp || 'N/A' } , Init Mod : { formatInitMod ( character . defaultInitMod ) } { character . defaultAc != null ? ` , AC: ${ character . defaultAc } ` : '' } )
< / s p a n >
< / s p a n >
< div className = "flex space-x-2" >
@@ -964,7 +1013,8 @@ function CharacterManager({ campaignId, campaignCharacters }) {
id : character . id ,
name : character . name ,
defaultMaxHp : character . defaultMaxHp || DEFAULT _MAX _HP ,
defaultInitMod : character . defaultInitMod || DEFAULT _INIT _MOD
defaultInitMod : character . defaultInitMod || DEFAULT _INIT _MOD ,
defaultAc : character . defaultAc ? ? ''
} ) }
className = "p-1 rounded transition-colors text-yellow-400 hover:text-yellow-300 bg-stone-700 hover:bg-stone-600"
aria - label = "Edit character"
@@ -1013,6 +1063,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
const [ maxHp , setMaxHp ] = useState ( '' ) ;
const [ hpFormula , setHpFormula ] = useState ( '' ) ;
const [ manualInitiative , setManualInitiative ] = useState ( '' ) ;
const [ monsterAc , setMonsterAc ] = useState ( '' ) ;
const [ asNpc , setAsNpc ] = useState ( false ) ;
const [ editingParticipant , setEditingParticipant ] = useState ( null ) ;
const [ hpChangeValues , setHpChangeValues ] = useState ( { } ) ;
@@ -1095,6 +1146,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
maxHp : currentMaxHp ,
currentHp : currentMaxHp ,
hpFormula : participantType === 'monster' ? ( hpFormula . trim ( ) || null ) : null ,
ac : participantType === 'monster' && monsterAc !== '' ? parseInt ( monsterAc , 10 ) : ( participantType === 'character' ? ( campaignCharacters . find ( c => c . id === selectedCharacterId ) ? . defaultAc ? ? null ) : null ) ,
conditions : [ ] ,
isActive : true ,
status : 'conscious' ,
@@ -1120,6 +1172,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
setHpFormula ( '' ) ;
setSelectedCharacterId ( '' ) ;
setMonsterInitMod ( MONSTER _DEFAULT _INIT _MOD ) ;
setMonsterAc ( '' ) ;
setAsNpc ( false ) ;
setManualInitiative ( '' ) ;
} catch ( err ) {
@@ -1411,7 +1464,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
className = "mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "md:col-span-3 " >
< div className = "md:col-span-2 " >
< label htmlFor = "monsterInitMod" className = "block text-sm font-medium text-stone-300" >
Init Mod
< / l a b e l >
@@ -1423,7 +1476,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
className = "mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "md:col-span-3 " >
< div className = "md:col-span-2 " >
< label htmlFor = "manualInitiative" className = "block text-sm font-medium text-stone-300" >
Initiative < span className = "text-xs text-stone-400" > ( blank = roll ) < / s p a n >
< / l a b e l >
@@ -1436,7 +1489,17 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
className = "mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "md:col-span-3 " >
< div className = "md:col-span-2 " >
< label htmlFor = "monsterAc" className = "block text-sm font-medium text-stone-300" > AC < / l a b e l >
< input
type = "number"
id = "monsterAc"
value = { monsterAc }
onChange = { ( e ) => setMonsterAc ( e . target . value ) }
className = "mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "md:col-span-2" >
< label htmlFor = "monsterMaxHp" className = "block text-sm font-medium text-stone-300" >
Max HP
< / l a b e l >
@@ -1448,16 +1511,16 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
className = "mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
< div className = "md:col-span-3 " >
< div className = "md:col-span-4 " >
< label htmlFor = "monsterHpFormula" className = "block text-sm font-medium text-stone-300" >
HP Formula ( e . g . 2 d6 + 9 )
HP Formula
< / l a b e l >
< input
type = "text"
id = "monsterHpFormula"
value = { hpFormula }
onChange = { ( e ) => setHpFormula ( e . target . value ) }
placeholder = "2d6+9"
placeholder = "e.g. 2d6+9"
className = "mt-1 w-full px-3 py-2 bg-stone-800 border border-stone-700 rounded-md shadow-sm focus:outline-none focus:ring-amber-600 focus:border-amber-600 sm:text-sm text-white"
/ >
< / d i v >
@@ -1579,6 +1642,12 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
< p className = "font-semibold text-lg text-white" >
{ isZeroHp && < span className = "mr-2" > ☠ ️ < / s p a n > }
{ p . name } < span className = "text-xs" > ( { participantDisplayType } ) < / s p a n >
{ p . ac != null && (
< span className = "ml-2 inline-flex items-baseline gap-1 px-2 py-0.5 bg-sky-900 border border-sky-500 rounded" >
< span className = "text-[10px] text-sky-300 font-semibold tracking-wide" > AC < / s p a n >
< span className = "text-sky-100 text-lg font-bold leading-none" > { p . ac } < / s p a n >
< / s p a n >
) }
{ isCurrentTurn && ! encounter . isPaused && (
< span className = "ml-2 px-2 py-0.5 bg-yellow-400 text-black text-xs font-bold rounded-full inline-flex items-center" >
< Zap size = { 12 } className = "mr-1" / > CURRENT
@@ -1604,7 +1673,7 @@ function ParticipantManager({ encounter, encounterPath, campaignCharacters, camp
onFocus = { ( e ) => e . target . select ( ) }
onBlur = { ( e ) => { if ( e . target . value !== String ( p . initiative ) ) handleInlineInitiative ( p . id , e . target . value ) ; } }
onKeyDown = { ( e ) => { if ( e . key === 'Enter' ) e . target . blur ( ) ; } }
className = "w-10 px-1 py-0.5 bg-stone-800 border border-stone-700 rounded-md shadow-sm text-white text-sm focus:outline-none focus:ring-1 focus:ring-amber-600 focus:border-amber-600 disabled:opacity-50 disabled:cursor-not-allowed"
className = "w-8 px-1 py-0.5 bg-stone-800 border border-stone-700 rounded-md shadow-sm text-white text-sm text-center focus:outline-none focus:ring-1 focus:ring-amber-600 focus:border-amber-600 disabled:opacity-50 disabled:cursor-not-allowed"
aria - label = { ` Initiative for ${ p . name } ` }
/ >
< / s p a n >
@@ -2420,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 ) ;
@@ -2666,6 +2782,22 @@ function AdminView({ userId }) {
>
< PlusCircle size = { 20 } className = "mr-2" / > Create Campaign
< / b u t t o n >
< 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 } / > }
< / b u t t o n >
< 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 } / > }
< / b u t t o n >
< / d i v >
< / d i v >
{ ! campaignsCollapsed && (
@@ -2894,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 ) ;
@@ -2960,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 ;
} ;
@@ -3052,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 } / > }
< / b u t t o n >
< 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 } / > }
< / b u t t o n >
< / d i v >
< EyeOff size = { 64 } className = "mb-4 text-stone-500" / >
< h2 className = "text-3xl font-semibold font-cinzel tracking-wide" > Game Session Paused < / h 2 >
< p className = "text-xl mt-2" > The Dungeon Master has not activated an encounter for display . < / p >
@@ -3084,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' } ` }
>