diff --git a/src/App.js b/src/App.js
index a51e5f6..90185ee 100644
--- a/src/App.js
+++ b/src/App.js
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { initializeApp } from 'firebase/app';
import { getAuth, signInAnonymously, onAuthStateChanged, signInWithCustomToken } from 'firebase/auth';
import { getFirestore, doc, setDoc, addDoc, getDoc, getDocs, collection, onSnapshot, updateDoc, deleteDoc, query, writeBatch } from 'firebase/firestore';
-import { ArrowLeft, PlusCircle, Users, Swords, Shield, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, ChevronDown, ChevronUp, UserCheck, UserX, HeartCrack, HeartPulse, Zap, Share2, Copy as CopyIcon, Image as ImageIcon, EyeOff } from 'lucide-react';
+import { ArrowLeft, PlusCircle, Users, Swords, Shield, Trash2, Eye, Edit3, Save, XCircle, ChevronsUpDown, ChevronDown, ChevronUp, UserCheck, UserX, HeartCrack, HeartPulse, Zap, Share2, Copy as CopyIcon, Image as ImageIcon, EyeOff, ExternalLink } from 'lucide-react'; // Added ExternalLink
// --- Firebase Configuration ---
const firebaseConfig = {
@@ -38,26 +38,26 @@ if (missingKeys.length > 0) {
const APP_ID = process.env.REACT_APP_TRACKER_APP_ID || 'ttrpg-initiative-tracker-default';
const PUBLIC_DATA_PATH = `artifacts/${APP_ID}/public/data`;
const CAMPAIGNS_COLLECTION = `${PUBLIC_DATA_PATH}/campaigns`;
-const ACTIVE_DISPLAY_DOC = `${PUBLIC_DATA_PATH}/activeDisplay/status`; // Single source for what player display shows
+const ACTIVE_DISPLAY_DOC = `${PUBLIC_DATA_PATH}/activeDisplay/status`;
// --- Helper Functions ---
const generateId = () => crypto.randomUUID();
-// getShareableLinkBase is no longer needed for individual encounter links if we have one player display URL
-// function getShareableLinkBase() {
-// return window.location.origin + window.location.pathname;
-// }
-
// --- Main App Component ---
function App() {
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);
- const [viewMode, setViewMode] = useState('admin');
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
- // directDisplayParams and hash routing removed, Player Display solely relies on ACTIVE_DISPLAY_DOC
+ const [isPlayerViewOnlyMode, setIsPlayerViewOnlyMode] = useState(false);
useEffect(() => {
+ // Check for query parameter to determine if this is a player-only display window
+ const queryParams = new URLSearchParams(window.location.search);
+ if (queryParams.get('playerView') === 'true') {
+ setIsPlayerViewOnlyMode(true);
+ }
+
if (!auth) {
setError("Firebase Auth not initialized. Check your Firebase configuration.");
setIsLoading(false);
@@ -113,46 +113,54 @@ function App() {
);
}
- const goToAdminView = () => {
- setViewMode('admin');
+ const openPlayerWindow = () => {
+ // Construct the URL for the player view.
+ // It's the current path + ?playerView=true
+ // window.location.origin gives http://localhost:3000
+ // window.location.pathname gives / (or your base path if deployed in a subfolder)
+ const playerViewUrl = window.location.origin + window.location.pathname + '?playerView=true';
+ window.open(playerViewUrl, '_blank', 'noopener,noreferrer,width=1024,height=768'); // Opens in a new tab/window
};
+
+ if (isPlayerViewOnlyMode) {
+ // Render only the DisplayView if in player-only mode
+ return (
+
+ {isAuthReady &&
}
+ {!isAuthReady && !error &&
Authenticating for Player Display...
}
+
+ );
+ }
+
+ // Default Admin View
return (
- {viewMode === 'admin' && isAuthReady && userId && }
- {viewMode === 'display' && isAuthReady && } {/* DisplayView no longer needs URL params */}
+ {isAuthReady && userId && }
{!isAuthReady && !error && Authenticating...
}
);
@@ -377,7 +385,6 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const [selectedEncounterId, setSelectedEncounterId] = useState(null);
const [showCreateEncounterModal, setShowCreateEncounterModal] = useState(false);
const [activeDisplayInfo, setActiveDisplayInfo] = useState(null);
- // copiedLinkEncounterId removed as shareable links per encounter are removed
const encountersPath = `${CAMPAIGNS_COLLECTION}/${campaignId}/encounters`;
const selectedEncounterIdRef = useRef(selectedEncounterId);
@@ -451,14 +458,12 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
const currentActiveEncounter = activeDisplayInfo?.activeEncounterId;
if (currentActiveCampaign === campaignId && currentActiveEncounter === encounterId) {
- // Currently active, so toggle off (clear the active display)
await setDoc(doc(db, ACTIVE_DISPLAY_DOC), {
activeCampaignId: null,
activeEncounterId: null,
- }, { merge: true }); // Use set with merge to ensure document exists or is overwritten
+ }, { merge: true });
console.log("Player Display for this encounter turned OFF.");
} else {
- // Not active or different encounter, so set this one as active for players
await setDoc(doc(db, ACTIVE_DISPLAY_DOC), {
activeCampaignId: campaignId,
activeEncounterId: encounterId,
@@ -470,9 +475,6 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
}
};
- // Shareable link per encounter removed
- // const handleCopyToClipboard = (encounterId) => { ... };
-
const selectedEncounter = encounters.find(e => e.id === selectedEncounterId);
return (
@@ -501,11 +503,9 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac
>
{isLiveOnPlayerDisplay ? : }
- {/* Share button for individual encounter link removed */}
- {/* Shareable link display removed */}
);
})}
@@ -831,8 +831,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
console.warn("Attempting to end encounter without confirmation");
try {
await updateDoc(doc(db, encounterPath), { isStarted: false, currentTurnParticipantId: null, round: 0, turnOrderIds: [] });
- // Optionally, also clear the active display when an encounter ends
- // await updateDoc(doc(db, ACTIVE_DISPLAY_DOC), { activeCampaignId: null, activeEncounterId: null });
+ // When an encounter ends, also deactivate it from the Player Display
+ await setDoc(doc(db, ACTIVE_DISPLAY_DOC), {
+ activeCampaignId: null,
+ activeEncounterId: null
+ }, { merge: true });
+ console.log("Encounter ended and deactivated from Player Display.");
} catch (err) { console.error("Error ending encounter:", err); }
};
@@ -855,12 +859,12 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) {
);
}
-function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
+function DisplayView() {
const [activeEncounterData, setActiveEncounterData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const [campaignBackgroundUrl, setCampaignBackgroundUrl] = useState('');
- const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false); // New state
+ const [isPlayerDisplayActive, setIsPlayerDisplayActive] = useState(false);
useEffect(() => {
if (!db) {
@@ -876,22 +880,20 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
if (docSnap.exists()) {
const { activeCampaignId, activeEncounterId } = docSnap.data();
if (activeCampaignId && activeEncounterId) {
- setIsPlayerDisplayActive(true); // An encounter is active for display
+ setIsPlayerDisplayActive(true);
- // Fetch campaign background
const campaignDocRef = doc(db, CAMPAIGNS_COLLECTION, activeCampaignId);
- if (unsubscribeCampaign) unsubscribeCampaign(); // Clean up previous listener
+ if (unsubscribeCampaign) unsubscribeCampaign();
unsubscribeCampaign = onSnapshot(campaignDocRef, (campSnap) => {
if (campSnap.exists()) {
setCampaignBackgroundUrl(campSnap.data().playerDisplayBackgroundUrl || '');
} else {
- setCampaignBackgroundUrl(''); // Campaign not found
+ setCampaignBackgroundUrl('');
}
}, (err) => console.error("Error fetching campaign background for display:", err));
- // Fetch encounter data
const encounterPath = `${CAMPAIGNS_COLLECTION}/${activeCampaignId}/encounters/${activeEncounterId}`;
- if (unsubscribeEncounter) unsubscribeEncounter(); // Clean up previous listener
+ if (unsubscribeEncounter) unsubscribeEncounter();
unsubscribeEncounter = onSnapshot(doc(db, encounterPath), (encDocSnap) => {
if (encDocSnap.exists()) {
setActiveEncounterData({ id: encDocSnap.id, ...encDocSnap.data() });
@@ -907,14 +909,12 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
setIsLoading(false);
});
} else {
- // No active encounter set by DM
setActiveEncounterData(null);
setCampaignBackgroundUrl('');
- setIsPlayerDisplayActive(false); // No encounter is active for display
+ setIsPlayerDisplayActive(false);
setIsLoading(false);
}
} else {
- // ACTIVE_DISPLAY_DOC doesn't exist
setActiveEncounterData(null);
setCampaignBackgroundUrl('');
setIsPlayerDisplayActive(false);
@@ -931,7 +931,7 @@ function DisplayView() { // Removed campaignIdFromUrl, encounterIdFromUrl props
if (unsubscribeEncounter) unsubscribeEncounter();
if (unsubscribeCampaign) unsubscribeCampaign();
};
- }, []); // Removed URL params from dependencies
+ }, []);
if (isLoading) return Loading Player Display...
;
if (error) return {error}
;