From 30712f0e1e9fdb4032a90e2ef2e194ea396ef1f6 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Tue, 14 Jul 2026 22:38:44 -0400 Subject: [PATCH 1/3] chore: strengthen lint coverage and update project housekeeping - Recursively lint all production JS/JSX under src/ and shared/ - Exclude tests, mocks, and node_modules from static guard - Update TODO status - Exclude TODO.md from Docker build context --- .dockerignore | 1 + TODO.md | 23 ++++++++++++----------- shared/tests/static.eslint.test.js | 27 ++++++++++++++++++--------- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/.dockerignore b/.dockerignore index 4f7bee5..7610c0f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ +TODO.md node_modules **/node_modules .git diff --git a/TODO.md b/TODO.md index 60252ca..5aa8345 100644 --- a/TODO.md +++ b/TODO.md @@ -2,44 +2,45 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md. + ## Open 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 +x also better vert tab layout - labelt friendly x needs AC for players dude x and quick entry hp -hp do not carry from encounter to ecnounter!!! +x hp do not carry from encounter to ecnounter!!! ^feature to add back to campaign character after encounter? maybe campaign toggle in charc section (choosing each end is DM overload will be missed forgotten done wront) -hp wont go over max and no temp hp support - - x refresh/reload/code update causes UI to update to top unselected cambpaign have to drill all the way back down to active encounter....every time. siemtmes? +x encounters cards need clear "in progress"... -I wonder...if a charcter-list-character should have a isNPC option vailble to them - for longer lived + + +x I wonder...if a charcter-list-character should have a isNPC option vailble to them - for longer lived npcs..... +x hp wont go over max and no temp hp support + + +## monsters per campaign and npcs +## or/and ....copy from encoubnter? -### dm list - keep active particpant in view (scroll) -not sure good way to do this - ### npm install warnings cleanup pass lots of updates -## monsters per campaign and npcs -## or/and ....copy from encoubnter? diff --git a/shared/tests/static.eslint.test.js b/shared/tests/static.eslint.test.js index 8657f95..6b32bfc 100644 --- a/shared/tests/static.eslint.test.js +++ b/shared/tests/static.eslint.test.js @@ -1,21 +1,30 @@ // STATIC GUARD: prod source must pass eslint with zero errors/warnings. -// Scans App.js + storage adapters + shared modules. Catches unused imports, +// Scans ALL .js/.jsx in src/ + shared/ (excl tests/mocks). Catches unused imports, // dead code, undefined vars before they hit the browser console. // Run via: npx eslint --format json -> parse -> fail on any problem. const { execSync } = require('child_process'); const path = require('path'); +const fs = require('fs'); const ROOT = path.resolve(__dirname, '..', '..'); +function walkJs(dir) { + let out = []; + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) { + if (entry.name === 'node_modules' || entry.name === 'tests' || entry.name === '__mocks__') continue; + out = out.concat(walkJs(full)); + } else if (/\.(js|jsx)$/.test(entry.name)) { + out.push(full); + } + } + return out; +} + const TARGETS = [ - 'src/App.js', - 'src/storage/contract.js', - 'src/storage/firebase.js', - 'src/storage/index.js', - 'src/storage/server.js', - 'shared/index.js', - 'shared/logEvent.js', - 'shared/turn.js', + ...walkJs(path.join(ROOT, 'src')), + ...walkJs(path.join(ROOT, 'shared')), ]; function runEslint(files) { From 119d252d02dadad6d0874778894bed0e2292f2b6 Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 15 Jul 2026 12:24:51 -0400 Subject: [PATCH 2/3] feat: improve tablet screen controls and participant action states - Add wake lock and browser fullscreen controls to encounter header/popout - Re-acquire wake lock after browser release (still doesnt work on tablet) - Retry fullscreen after Android unlock; keep manual control visible - Add larger labeled character Save button - Use distinct panel icon for encounter full-page expansion - Add strong active/inactive, conditions-open, and editing button states - Keep inactive participant controls visible - Add regression tests and update affected characterization tests - Update project TODO notes --- TODO.md | 6 + src/App.js | 163 +++++++++++++----- .../Participant.characterization.test.js | 4 +- src/tests/ParticipantActionStates.test.js | 42 +++++ src/tests/ReslotAllPaths.test.js | 2 +- src/tests/ScreenControls.test.js | 95 ++++++++++ 6 files changed, 270 insertions(+), 42 deletions(-) create mode 100644 src/tests/ParticipantActionStates.test.js create mode 100644 src/tests/ScreenControls.test.js diff --git a/TODO.md b/TODO.md index 5aa8345..8118807 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,12 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md. ## Open +caff doent work on fron tpage or big tablet + +max/caff need to float or at least be availabe in combagt popout + +save needs bigger save button on char editor + x fullscreen and dont lock on main app dm view and the no-game-player view ...and doesnt actually prevent lock on android diff --git a/src/App.js b/src/App.js index 2d682cf..28a87e1 100644 --- a/src/App.js +++ b/src/App.js @@ -8,7 +8,7 @@ import { UserCheck, UserX, HeartCrack, HeartPulse, Zap, EyeOff, ExternalLink, AlertTriangle, Play as PlayIcon, Pause as PauseIcon, SkipForward as SkipForwardIcon, StopCircle as StopCircleIcon, Users2, Dices, ChevronDown, ScrollText, - Maximize2, Minimize2, Moon, Coffee, Clock, ChevronRight, X, + Maximize2, Minimize2, PanelTopOpen, PanelTopClose, Moon, Coffee, Clock, ChevronRight, X, Undo2, Redo2, Crosshair } from 'lucide-react'; @@ -1051,8 +1051,12 @@ function CharacterManager({ campaignId, campaignCharacters, syncCharacters }) { /> NPC - @@ -2402,7 +2412,7 @@ function InitiativeControls({ campaignId, encounter, encounterPath }) { // ENCOUNTER MANAGER COMPONENT // ============================================================================ -function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters, encounterStartedRef, encounterActiveRef }) { +function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharacters, encounterStartedRef, encounterActiveRef, wakeLockEnabled, onToggleWakeLock, isBrowserFullscreen, onToggleBrowserFullscreen }) { const { showToast } = useUIFeedback(); const { data: encountersData, isLoading: isLoadingEncounters } = useFirestoreCollection( campaignId ? getPath.encounters(campaignId) : null @@ -2708,15 +2718,34 @@ function EncounterManager({ campaignId, initialActiveEncounterId, campaignCharac {selectedEncounter && (
-

- Managing Encounter: {selectedEncounter.name} - +

+ Managing Encounter: {selectedEncounter.name} + + + + +

{/* Combat Controls - always left */} @@ -2770,21 +2799,43 @@ function AdminView({ userId }) { catch { return false; } }); const wakeLockRef = useRef(null); + const fullscreenWantedRef = useRef(false); - const toggleFullscreen = () => { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen(); - } else { - document.exitFullscreen(); + const toggleFullscreen = async () => { + try { + if (!document.fullscreenElement) { + fullscreenWantedRef.current = true; + await document.documentElement.requestFullscreen(); + } else { + fullscreenWantedRef.current = false; + await document.exitFullscreen(); + } + } catch (e) { + showToast('Fullscreen failed. Tap fullscreen again.'); } }; useEffect(() => { - const onFsChange = () => setIsFullscreen(!!document.fullscreenElement); + const onFsChange = () => { + const active = !!document.fullscreenElement; + setIsFullscreen(active); + if (active) fullscreenWantedRef.current = true; + else if (document.visibilityState === 'visible') fullscreenWantedRef.current = false; + }; document.addEventListener('fullscreenchange', onFsChange); return () => document.removeEventListener('fullscreenchange', onFsChange); }, []); + useEffect(() => { + const restoreFullscreen = async () => { + if (document.visibilityState === 'visible' && fullscreenWantedRef.current && !document.fullscreenElement) { + try { await document.documentElement.requestFullscreen(); } catch (e) { /* browser may require tap */ } + } + }; + document.addEventListener('visibilitychange', restoreFullscreen); + return () => document.removeEventListener('visibilitychange', restoreFullscreen); + }, []); + useEffect(() => { if (!wakeLockEnabled) { wakeLockRef.current?.release(); @@ -2792,8 +2843,13 @@ function AdminView({ userId }) { return; } const acquire = async () => { - try { wakeLockRef.current = await navigator.wakeLock.request('screen'); } - catch (e) { + if (document.visibilityState !== 'visible' || wakeLockRef.current?.released === false) return; + try { + if (!navigator.wakeLock?.request) throw new Error('Wake Lock API unavailable'); + const sentinel = await navigator.wakeLock.request('screen'); + wakeLockRef.current = sentinel; + sentinel.addEventListener?.('release', () => { wakeLockRef.current = null; }); + } 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'); } @@ -3250,6 +3306,10 @@ function AdminView({ userId }) { campaignCharacters={selectedCampaign.characters || []} encounterStartedRef={encounterStartedRef} encounterActiveRef={encounterActiveRef} + wakeLockEnabled={wakeLockEnabled} + onToggleWakeLock={() => setWakeLockEnabled(v => { localStorage.setItem('ttrpg.wakeLock', String(!v)); return !v; })} + isBrowserFullscreen={isFullscreen} + onToggleBrowserFullscreen={toggleFullscreen} />
)} @@ -3350,6 +3410,7 @@ function DisplayView() { }); const [displayParticipants, setDisplayParticipants] = useState([]); const wakeLockRef = useRef(null); + const fullscreenWantedRef = useRef(false); const currentParticipantRef = useRef(null); // Player display transition state. Active participants render normally. @@ -3380,19 +3441,40 @@ function DisplayView() { }, []); useEffect(() => { - const onFsChange = () => setIsFullscreen(!!document.fullscreenElement); + const onFsChange = () => { + const active = !!document.fullscreenElement; + setIsFullscreen(active); + if (active) fullscreenWantedRef.current = true; + else if (document.visibilityState === 'visible') fullscreenWantedRef.current = false; + }; document.addEventListener('fullscreenchange', onFsChange); return () => document.removeEventListener('fullscreenchange', onFsChange); }, []); - const toggleFullscreen = () => { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen(); - } else { - document.exitFullscreen(); + const toggleFullscreen = async () => { + try { + if (!document.fullscreenElement) { + fullscreenWantedRef.current = true; + await document.documentElement.requestFullscreen(); + } else { + fullscreenWantedRef.current = false; + await document.exitFullscreen(); + } + } catch (e) { + console.error('Fullscreen failed:', e); } }; + useEffect(() => { + const restoreFullscreen = async () => { + if (document.visibilityState === 'visible' && fullscreenWantedRef.current && !document.fullscreenElement) { + try { await document.documentElement.requestFullscreen(); } catch (e) { /* browser may require tap */ } + } + }; + document.addEventListener('visibilitychange', restoreFullscreen); + return () => document.removeEventListener('visibilitychange', restoreFullscreen); + }, []); + useEffect(() => { if (!wakeLockEnabled) { wakeLockRef.current?.release(); @@ -3401,8 +3483,12 @@ function DisplayView() { } const acquire = async () => { + if (document.visibilityState !== 'visible' || wakeLockRef.current?.released === false) return; try { - wakeLockRef.current = await navigator.wakeLock.request('screen'); + if (!navigator.wakeLock?.request) throw new Error('Wake Lock API unavailable'); + const sentinel = await navigator.wakeLock.request('screen'); + wakeLockRef.current = sentinel; + sentinel.addEventListener?.('release', () => { wakeLockRef.current = null; }); } catch (e) { console.error('Wake lock failed:', e); } @@ -3410,10 +3496,9 @@ function DisplayView() { acquire(); - // 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(); }; 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 () => { diff --git a/src/tests/Participant.characterization.test.js b/src/tests/Participant.characterization.test.js index 77b6bd1..5504f6d 100644 --- a/src/tests/Participant.characterization.test.js +++ b/src/tests/Participant.characterization.test.js @@ -70,7 +70,7 @@ describe('Participant -> Firebase', () => { test('toggleActive: updateDoc flips isActive', async () => { await setupReady(); await addMonsterViaUI('Toggle', 10, 0); - fireEvent.click(screen.getByTitle('Mark Inactive')); + fireEvent.click(screen.getByRole('button', { name: 'Disable participant' })); await waitFor(() => lastEncCall()?.data?.participants?.[0]?.isActive === false); expect(lastEncCall().data.participants[0].isActive).toBe(false); }); @@ -116,7 +116,7 @@ describe('Participant -> Firebase', () => { test('toggleCondition: updateDoc adds condition to array', async () => { await setupReady(); await addMonsterViaUI('Cond', 10, 0); - fireEvent.click(screen.getByTitle('Conditions')); + fireEvent.click(screen.getByRole('button', { name: 'Open conditions' })); await waitFor(() => screen.getByRole('button', { name: /Blinded/i })); fireEvent.click(screen.getByRole('button', { name: /Blinded/i })); await waitFor(() => { diff --git a/src/tests/ParticipantActionStates.test.js b/src/tests/ParticipantActionStates.test.js new file mode 100644 index 0000000..7361841 --- /dev/null +++ b/src/tests/ParticipantActionStates.test.js @@ -0,0 +1,42 @@ +import { screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { setupReady, addMonsterViaUI } from './testHelpers'; + +describe('participant action selected states', () => { + test('active toggle clearly changes to selected inactive state', async () => { + await setupReady('ActionCamp1', 'ActionEnc1'); + await addMonsterViaUI('Toggle Orc', 15, 3); + + const disable = screen.getByRole('button', { name: 'Disable participant' }); + expect(disable).toHaveAttribute('aria-pressed', 'true'); + expect(disable).toHaveClass('bg-emerald-700'); + fireEvent.click(disable); + + const enable = await screen.findByRole('button', { name: 'Enable participant' }); + expect(enable).toHaveAttribute('aria-pressed', 'false'); + expect(enable).toHaveClass('bg-red-900', 'ring-red-500'); + }); + + test('conditions toggle highlights open state and gives close action', async () => { + await setupReady('ActionCamp2', 'ActionEnc2'); + await addMonsterViaUI('Condition Orc', 15, 3); + + const open = screen.getByRole('button', { name: 'Open conditions' }); + expect(open).toHaveAttribute('aria-expanded', 'false'); + fireEvent.click(open); + + const close = screen.getByRole('button', { name: 'Close conditions' }); + expect(close).toHaveAttribute('aria-expanded', 'true'); + expect(close).toHaveClass('bg-purple-700', 'ring-purple-300'); + }); + + test('edit toggle exposes selected state while editor is open', async () => { + await setupReady('ActionCamp3', 'ActionEnc3'); + await addMonsterViaUI('Edit Orc', 15, 3); + + fireEvent.click(screen.getByRole('button', { name: 'Edit participant' })); + const closeEditor = screen.getByRole('button', { name: 'Close participant editor' }); + expect(closeEditor).toHaveAttribute('aria-pressed', 'true'); + expect(closeEditor).toHaveClass('bg-amber-500', 'ring-amber-200'); + }); +}); diff --git a/src/tests/ReslotAllPaths.test.js b/src/tests/ReslotAllPaths.test.js index 771b66f..f7d7159 100644 --- a/src/tests/ReslotAllPaths.test.js +++ b/src/tests/ReslotAllPaths.test.js @@ -57,7 +57,7 @@ describe('reslot on all mutation paths', () => { expect(lastParticipantsUpdate().map(p => p.name)).toEqual(['Orc', 'Goblin']); // open edit modal for Goblin, bump init to 8 - const editBtns = screen.getAllByTitle('Edit'); + const editBtns = screen.getAllByRole('button', { name: 'Edit participant' }); const goblinEdit = editBtns.find(b => b.closest('li')?.textContent.includes('Goblin')); fireEvent.click(goblinEdit); diff --git a/src/tests/ScreenControls.test.js b/src/tests/ScreenControls.test.js new file mode 100644 index 0000000..4703cb2 --- /dev/null +++ b/src/tests/ScreenControls.test.js @@ -0,0 +1,95 @@ +import { screen, fireEvent, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { setupReady } from './testHelpers'; + +describe('screen controls', () => { + let originalRequestFullscreen; + let originalExitFullscreen; + let originalFullscreenElement; + let originalVisibilityState; + let originalWakeLock; + + beforeEach(() => { + localStorage.clear(); + originalRequestFullscreen = document.documentElement.requestFullscreen; + originalExitFullscreen = document.exitFullscreen; + originalFullscreenElement = Object.getOwnPropertyDescriptor(document, 'fullscreenElement'); + originalVisibilityState = Object.getOwnPropertyDescriptor(document, 'visibilityState'); + originalWakeLock = Object.getOwnPropertyDescriptor(navigator, 'wakeLock'); + }); + + afterEach(() => { + document.documentElement.requestFullscreen = originalRequestFullscreen; + document.exitFullscreen = originalExitFullscreen; + if (originalFullscreenElement) Object.defineProperty(document, 'fullscreenElement', originalFullscreenElement); + else Object.defineProperty(document, 'fullscreenElement', { configurable: true, value: null }); + if (originalVisibilityState) Object.defineProperty(document, 'visibilityState', originalVisibilityState); + if (originalWakeLock) Object.defineProperty(navigator, 'wakeLock', originalWakeLock); + else Object.defineProperty(navigator, 'wakeLock', { configurable: true, value: undefined }); + localStorage.clear(); + }); + + test('encounter header exposes prevent-sleep, browser-fullscreen, and popout controls', async () => { + await setupReady('ScreenCamp', 'ScreenEnc'); + + expect(screen.getAllByRole('button', { name: 'Prevent sleep' }).length).toBeGreaterThanOrEqual(2); + expect(screen.getAllByRole('button', { name: 'Enter browser fullscreen' })).toHaveLength(1); + expect(screen.getByRole('button', { name: 'Expand encounter to full page' })).toBeInTheDocument(); + }); + + test('re-acquires wake lock when tablet becomes visible after browser release', async () => { + let releaseListener; + const sentinel = { + released: false, + release: jest.fn().mockResolvedValue(undefined), + addEventListener: jest.fn((event, callback) => { if (event === 'release') releaseListener = callback; }), + }; + const request = jest.fn().mockResolvedValue(sentinel); + Object.defineProperty(navigator, 'wakeLock', { configurable: true, value: { request } }); + let visibility = 'visible'; + Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => visibility }); + + await setupReady('WakeCamp', 'WakeEnc'); + fireEvent.click(screen.getAllByRole('button', { name: 'Prevent sleep' })[0]); + await waitFor(() => expect(request).toHaveBeenCalledTimes(1)); + + visibility = 'hidden'; + releaseListener(); + visibility = 'visible'; + document.dispatchEvent(new Event('visibilitychange')); + + await waitFor(() => expect(request).toHaveBeenCalledTimes(2)); + }); + + test('character editor has labeled tablet-sized save button', async () => { + await setupReady('SaveCamp', 'SaveEnc'); + fireEvent.change(screen.getByLabelText('Name', { selector: '#characterName' }), { target: { value: 'Tablet Hero' } }); + fireEvent.click(screen.getByRole('button', { name: 'Add Character' })); + await screen.findByText('Tablet Hero'); + + fireEvent.click(screen.getByRole('button', { name: 'Edit character' })); + const save = screen.getByRole('button', { name: 'Save character' }); + expect(save).toHaveTextContent('Save'); + expect(save).toHaveClass('min-h-[40px]'); + }); + + test('attempts to restore fullscreen when tablet becomes visible after lock', async () => { + const requestFullscreen = jest.fn().mockResolvedValue(undefined); + document.documentElement.requestFullscreen = requestFullscreen; + document.exitFullscreen = jest.fn().mockResolvedValue(undefined); + Object.defineProperty(document, 'fullscreenElement', { configurable: true, get: () => null }); + let visibility = 'visible'; + Object.defineProperty(document, 'visibilityState', { configurable: true, get: () => visibility }); + + await setupReady('RestoreCamp', 'RestoreEnc'); + fireEvent.click(screen.getByRole('button', { name: 'Enter browser fullscreen' })); + await waitFor(() => expect(requestFullscreen).toHaveBeenCalledTimes(1)); + + visibility = 'hidden'; + document.dispatchEvent(new Event('fullscreenchange')); + visibility = 'visible'; + document.dispatchEvent(new Event('visibilitychange')); + + await waitFor(() => expect(requestFullscreen).toHaveBeenCalledTimes(2)); + }); +}); From dc0a1f163348768cef5c7f0d8ce6c5e9f81a7dde Mon Sep 17 00:00:00 2001 From: david raistrick <1108844+keen99@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:49:52 -0400 Subject: [PATCH 3/3] fix: preserve user-renamable Android shortcuts Chrome Android forces the root URL into PWA installation when required manifest icon files exist. PWA names come from the manifest and cannot be edited, which breaks workflows using multiple tracker copies. Keep required 192/512 icon assets intentionally unavailable by committing generated files with .png-not suffixes. Removing the manifest link does not restore root shortcut naming; Chrome still installs the root as a PWA. Chrome handles /display differently: even with a working manifest it offers Install app or Create shortcut, and Create shortcut permits renaming. Keep both manifests intentionally non-installable for consistent user-controlled shortcut names. Also add separate player-display favicon/logo artwork and swap /display browser favicon to player-favicon.ico. Keep generated standard/player logo sources under renamed inert filenames. --- TODO.md | 6 +++--- public/display-manifest.json | 20 ++++++++---------- public/favicon.ico | Bin 0 -> 7303 bytes public/index.html | 2 +- public/logo192.png-not | Bin 0 -> 3896 bytes public/logo512.png-not | Bin 0 -> 11917 bytes public/manifest.json | 16 +++++++------- public/player-favicon.ico | Bin 0 -> 7216 bytes public/player-logo192.png-not | Bin 0 -> 1956 bytes public/player-logo512.png-not | Bin 0 -> 5217 bytes src/App.js | 2 ++ .../DisplayView.characterization.test.js | 13 ++++++++++++ 12 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 public/favicon.ico create mode 100644 public/logo192.png-not create mode 100644 public/logo512.png-not create mode 100644 public/player-favicon.ico create mode 100644 public/player-logo192.png-not create mode 100644 public/player-logo512.png-not diff --git a/TODO.md b/TODO.md index 8118807..e67569c 100644 --- a/TODO.md +++ b/TODO.md @@ -5,11 +5,11 @@ Backlog of bugs + long-term items. Milestones live in REWORK_PLAN.md. ## Open -caff doent work on fron tpage or big tablet +x caff doent work on fron tpage or big tablet -max/caff need to float or at least be availabe in combagt popout +x max/caff need to float or at least be availabe in combagt popout -save needs bigger save button on char editor +x save needs bigger save button on char editor x fullscreen and dont lock on main app dm view and the no-game-player view ...and doesnt actually prevent lock on android diff --git a/public/display-manifest.json b/public/display-manifest.json index 369bc38..7e5ed8a 100644 --- a/public/display-manifest.json +++ b/public/display-manifest.json @@ -1,27 +1,25 @@ { + "id": "/display", "short_name": "TTRPG Display", - "name": "TTRPG Initiative Tracker — Player Display", + "name": "TTRPG Initiative Tracker - Player Display", "icons": [ { - "src": "favicon.ico", + "src": "player-favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "player-logo192.png", + "note": "this image intentionally missing from filesystem - keeping manfiest and having broken icon allows android to _shortcut_ which user can control name of.", "type": "image/png", "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" } ], "start_url": "/display", - "scope": "/", - "display": "standalone", - "orientation": "landscape", + "scope": "/display", + "note:" "none of the display stuff actually works, even when it properly installs a pwa (which user cant control name of...)", + "display": "fullscreen", + "orientation": "portrait", "theme_color": "#1A202C", "background_color": "#1A202C" } diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bff275c0037ee2ec59f14e64a3ac3b8fab81a746 GIT binary patch literal 7303 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 need to enable JavaScript to run this app.
- \ No newline at end of file + diff --git a/public/logo192.png-not b/public/logo192.png-not new file mode 100644 index 0000000000000000000000000000000000000000..5a2c40035cd00da85d8aba59ac2f3883d6e173b1 GIT binary patch literal 3896 zcmY+HXD}QJ*T;92Se@7;N}@&(qHok?SC*1 zH=+d*M7Pn!>&`RxnfLi{&Y9oLf6j+9b3XiHbf2lyQn6A2003G|4Hd-o?EI%dB-efZ z^SLenKzF670ylU)yPX^8&GejoAjD5x^GqYVL9%iZ!M|IQq>7GfKqx@IE{Ng3q!jvJ z0+U4iOZg0(|4Z-ZmTRc9aGx_FXpVk2?cRk>(6nssSN5OB_F$4}*?_lPT2`(~>O{n= zD)K-|9^DTcUAhsB{0=WH`MW{hw`kvN452;ycPE%*88FVuoV*Vhlo=Bllh*6Vz>Dcm z8tkmq;)ozHzdb~G0zw%#s@uyWOQ4beCymLUQi|t};-cGdv>9P^tMXSV*^XU}@6Ufp zC#;tIs>+{69&XQHpme=#ForRZ##2K_L(F=?^e~z(k~z_|IJ4sDop)VB=d-ad_nk{>|fk(Tf7YKXE-Moqe`4?HMZmI{6}9t>C|W^XN@9a znqM(-&OF~pe);ooQ+%Ye#XxFxF#n|*a!i9PmZ7yLqj~KuJB{jQA}l(wT$wTH70X!t zLw)8fyT_YB?8w`Z+z?gHq9`%s7>}$&)pG#D>^(yX)~J6{N<0i}6&fJbyZG=m0;KD%AWAarnE*WdK$5F~&jE1iRx-mY*mGRSqh?hyuv<*|h0cMCh2kJcSLF9~ zhP;o&{3!}km=&f%FNv=HeyV7FUr`qACNe})KaRQure4?$XC6wT6BW(eWQ%3sc{NEX z1LOTB9xUkJ%Wt}_pb;ByhO@^PDtJgEy8shS4G3AR^I;y_Q9WS>WR!l_KnsNG~@_ zAwxdX=1rj!K)r_&saT54W<)8O{x3JEi)E2)YtO8&&SqaUtvB;m5v^D~%r6|M?Upy~ zd%sXMKqATa&Prf81y`gr?b0M%P`HR%ntHv$;HTplZD)8kg`bf0N;D6Z>1ZJj2dwJ= zM}QtV0L>r?nqwLh&A}(3CILrV$;p=Safx3DCN7Zrw5weC!!g{uz$f%l&n>hb(1umD z8@X}LNcLMfNWGcE#H`#nUPoIwrDF)rBF_ot{2g^yOZzE!o;BneTl7r2#wjkj3r-5q zbkAcYs~kwY@XTZJr+I@cLPxFg5{3VXf>9fw~|w10eVVUbGa ztLMD%Ynv>AC0-w!XmWINxt+3n$+U*;lT&!v`sY!2{V+)Td#tQ^sM1di%iPg7Dezab z2%X2z*x#8MNUK*9AXjR+&$0t2B9eRD{r8f8K71o>#Z8Mx=3vB>yN91sdm$f4PJsuK zUrN+Q(YFfx{M=pEK29o~q4|Qb#=Y$r!CTDc&LVZ1SrBFXr@u~e%H+ya4DvIk1z8e5 zxq5z+Tv}%ZZT!dbkobBABC(yj{CosshJdr(pfNk;B6bbS=D&*(`v@Y9@57cgOC-?_^bwPKa$+@5{ z_N%M5W?>F_d&*UBGrEgUD_xyta|>IEJ8tB;qZSqobo7)kR}1E8H3%>-=gXVkz3gI< zd#4jIET@8Jbc5To$2&NtJ1&0{8>Ti&wuUnwH=sA$uPP|!U3qT!?tanRip{CI^Vo$$ z-r9E&*o<6L3>-HV5JrG9vlWXY)lh;;5z}yq?C#C7okYz>P3I~oZ`9$dTXf+)R~fqd zf97Z604`<_i+~XVne@!_RVB`!zyv`hw<{2R*LNQE9GurVI5) zoy5!$SK5REWbLO&T*hl42rD{M%6I}NJ&UGmG3}0>$)3u0J7x^TosTm=+RoiFhJFb+ zI(Ju4^w46J!OLcpvq=LyKt(Z0DgB=;CX6nfC+tJmteiQ1CgSj<11@O?hjNRj2HwU7 z@7?~MWOJ>x7b7dx2K?T{X?pLS_>~By3;PdHLYlh1hmOc!ClhazP z#iHAd@V}`;T6TXa`nkm75WC`2Wca4@6=gg&l6J?v#N~FI%Ns$dw__&0B^fCQEy|(o zn_Af3_p1@_O2#*N3rETOlxQ)lR}9TrA%oUquu$-Q!>vTvX4O5(hP_b${;JM}mndnaDU156PGd161x z}0rsOck4qkxo=8+)X0Q#WtWZwbnt96Eklc%IFm3dG=teYA9vKi9O?8dJOB z;E$RF1x4cM9`N?75A-+L9pmzayI!zsxa4njvLO%yN%!u{es;gYaXCJlm`L9?b#h7} zw)^-PCXjK`&H4O(yfIWJpecMiP_6*zMOudOL_IyMH#V1S`Q&rXXCod$ZS-6Fz8L)C zWEuM)YGmX78`<}>F1{PPPc%e9Acm&}u<@yrxi&eh)@N-ZKG9D>+t-LFcDdHHEo>c~ zaASX~J&t-Z|N3?TbC%IoUi{HEUP0}4#NLE#`L&IbwP^G5ik3FFo`%!LKVodVU>LXn z(8&t|OuMfLcTH5Z8jk4SmXnD9pn{bO{R7nOll(oml@ePYKGc13N7!FLuCT=?NK&z>6H5Jlchh6$YDSxyH!F!}wJV?88qS5O zJ@p?p^}XXD`>f4Wuoq?#@WLv6O-p@jV4+I36q%09RHq0H`8fv&%GtiVh1 z2@@lE4eQAp2qU@brPG7LKtg;Yt9M-{8y``HUk(WmkpF~?Le?8W72-*N=ILL$$Wk)v z4d?5l1+)apF`k9BYPW^kIqowto*o)f+g%B>y3RN|95&9c!%y~$@_fh|>DGS--=bG& zO!^wOJ1?m3RG@RQHv-u;QV9i@utpEB6YP7kPzCjs@A2euUp9t2%pcETxd@*ZMObqN zI-tDw28Yn1TT9get-q4`f0ZgYcQZIrtACbdRL%+g6=qN;ItXNSHfU`mh$P0|tmX~& zIjm~W3ns^W^TRgVVU0cf9@NV>L11Q&M{qWZv@Fv`-b^o#R`(~^A7(0kv>Njk_v5RE z)K9b;wTV5mDO?#$=fih(*@oTp3RSx7#lXsoYLZ08N4DnRXZRjuiGsY}Cd4E|#{P#$ z6y57IhQ4=LdIfn-5)i-;db!k+PLmXTf~w5-+$&YMe!T>>@b#Rer}W6909va_xI}AF zJ+N+wg~h+Qk>UfKM{@gYy)#8sBIO;^(&qPGzVy)muc3-iVKjcsedpQzgixv5+f>AYTPYkI~ zJV?O!fUak!T;XS^0=s*lTug&Q-^d{qy1L~x@1&!fRIs!9MqSPTBz-)FeZOnL8Y-hz zH;Q_}vY@Ked(*vPTdxV>7mj#x2KjI!98#T@n(2ye@mh z3y-R-)lHl*!q?7{fs@Hm6s}yFr!^QZN4cfhJ3-6L_SBd-sY8T?>P)pKCWQgY@Wdgl zq;&!+D_?YBhnop@AVNcR-p?y80lrN!=0c@E*dS$P1%fi@qr?3pr6tD)8AytVx%|vk zt38yu9?le~U7MRN^QArKy}(ldl_M86*33hxy|DR$mzhUEt2mIq#wt>}h=LZ9L4kbWK8% zGE}-(0ajTXw&j{VpEg|)QL->ht7{S@`GHtBPlFnPe-J>`+zXs`E|n>wV2KrC8r#Ez zAmY8>&kSreegh!-+A3gZrL_hZTT%oNlXl12Wl`@|h2*{dn_4&lp_}m%q@?lJGJp)J z07ei?2XzI>Ijeb90*t%gdNu4&)5F4oB=J+T(qMCn$_j8Ku~aPGQ+lpKE%N=RxKae* zV4JZ>T{Xm@4ZpsTlB$=2sxxh^wtaKZ;l=Y7f8=e*qm>MLyI2mV3_(z(FA7r`QDTLd z5{RWWh0~$%iO8 z=O8&}SYTmy?^)jGf9rm@b*pZux*txRsdKt}e%&+EGt=E~HPuyUsjgB106?qyP)QpA zpkNaUP!eCLtBUymAf&6RbWhi3d~?eFv+0Ymmfdx=nQ8BVjvojrQfSz9Qs_O(=Btjs zNvYFxA-lgHGOcP~(kcR4NN-sF)w)N<8AKYZc^~lz4O|#=mO@HTcxR?AHzy?tbZprz zciMGyZlCTpGnABw$H*KV3LnX|vPjn42hg4Ke{Y^+L4Y_Y!3P2WFz`hR0BG>V06ri> z1o-;j{P};z^}p=z|61gR=Po0RB;VL{W-+zLM87GN#O}ZAr#SWFvTw5f!r=~KkMR`% z^p{|d{>(s{ctk#^f3F{~;HWQ3>Y*hCKHU1A%7Qk8Ld9yZ5tpxvkN{$32jZ6jaR8Q{ z3)g`VKa>+cKmZ$;RbFyfJQ__?h=2NkvJ*hR6U3>JHP8SogUl~1Z-dsg{F{19i!_WI zrinFTgeF4JYcs(y-Q!K+3?Nl3UI_os3QUl)A|7=$U=@+BLnOJ z%aVj_aB84m!IK+_7WtlfZjaFoTcW&kDv<4*it;~H4381Ozf?{Nc_&3TNTC4D8S8P$ ze>KkSMvkXBOQJ$JLxcM1r9ywx0C^|q%h!p~N>YI3IH=3&oc;%t0Rn4NCjY$yzJp?7!Fs6qAzX zz|TcWp?)ukPy!-56grSFc|a5U+sGQU*YL}RaE9~G{P$r%Lj$K884Lq7{u(nA4==$ids`l1_6 zH0ZUS22RV{4K(Qfo~-{g)-(<&L>rbwAPsiHpeFqHL9|`w;F##YNrV)QQHxwYr(X;} zX9GGAJ3yC+N-FdMAcX>f7&2abjChAVZ#^YP98x4wfehq`)&FW7QjD_Oc_tFa$?`#% zhBgrlg&`x6HWN#tMwSTC#NNVzlA-8+5J?!Mm{jS|{+yT<98>_5M*?c|AI$V126426 zOnJl_z1xyo=A)m_+~px6EZlgj(G9ZRW&ZoQS4J{*4h^t^kY|O+}D&xqSpZlbaL$IS@TMkQ{hJxMoP+ z37>T#nGG)8>oct;79s`{z1;S>DwRhZU=uG?s?otg48OzTv)}fcLqwkrA z#Ns6`pnW^S{q9`r#2(!w2ObgX_;8B2a0F}d1vSvY1SsQJr^u9utw;4@zDyM!_(U5* zk|PtezZz>%Di{4~2;OB6u%#BPZE+4=9ppU}w2}fc78?Q!DPCN)xN#$0gS6Vgmusw$ zS`@HRU$rMs`#McInCyyv&+<2y#Kvb;BPjWG?&a!IM7&BCz7w!1WDLs&J~*c1-ytFGVL(wIC8(0XafUxNxcZv_#40licod|Z z26_9+rvA05$t9MxA-KFTIuRm5?D;AEe~2C<+CMA9svf*j+uH72xRR!p1Uc}%$)E#) zZK~iLXP}_h6b09G5+sZt%18oyhX)y>o4&(Vu8@u;MQ%?gT;8QjcD} z1t)IkK^C0J3!Ma^v7s5QV3HRF9>FOA+OPh96+TlL2Bwmb#|C(zF91~!AjBU}fwTj~ z(pS&2yr8{;R5@@#L?;kC{T|T?t{2XQU#|f`6m%GhK$tS@DY!wX3F91xppp|}^0Xaf3wV(Ig@Sp*SDEP#xNGp6&Rx_0S3^e1p%5N zQlK_z2Ln*hRj`0=?HjGGGgY4;6;^f(Vn?1vGf8208&Jk=089Xk91+M%3D~YEh=_vY z^6#fr3J8chHh=+mR84)KD0KkJHI8UjhEIJ1~wBWHbgmYMB5m0OYUYye-WGmba%Q$mk<0Mo)zKIjr=5Kdlrjpjp8s;cxkBm;DNikdi=;b#_;l=!JTl40MwKq@Flc&Z9U-!u8x_T~1fe(9t`mjNv9XtZN) zlxy2dT)O}30evpt$y!2`49Gw5L;o(jvbO8;*z&MKmB&yBJ!S)ORjlJ(*~MQv*r6yv z#1s;mxqDGFgd9Q7iU1B&{j(EXGm{3~pUc@}?pet0znPDe>D=5(_FcwYdl6M&dKlV@ z*DA@E+70!kq7yg9R7qRP0^h{Sk}Kcu@!%I4R+)sWkKa|QbUnYTvzBdg)N*=+UwRvO zw!hevYQrz_j|2Wd7u{bk3{4BY0)wIf7>VEd*`nS1q=_3Ea~hHGt)0h0cMd@@N6is5 z!T-?1v7Pcq`IEo+F|M1jGVKoS`dK|BFs{}sB`}7W78d!OxzHJlmwRtZPIor*%#OBb z%g?qB2`&$768e;_zv+baYXD*UKoh1G>u3!d+uNOWu}>)!(ZcaQVaJ8e5RUPvgpKqy z;Xj*R{x~Pz`kkKBy5bd^MiH(DHo8er~%8dw&xg_qDs8 z?MzXVe$C&h&D8DZYD?P`o#2vhfQGclSJGwGLMWcXf~o6&MBeT9;P%7=PsTQkC@_r? zo!xt8fnAkR^3Ugs!Y``+DDCRZ*NMj#R0ozAIsEeRliNaVVW(5d&7+fJehE)U46Ifx zydK~PT!`=QzptvrD%l?B9+CVC^i$oJI5R|xT$0x*a`(pXhh+TodNMTdD4lx-KC!qq zq7z0BcOH+4n)#B5+>r3}I(k6o$YbZ-pFqEZvSRBH z)$qis-QdqhPG&S`GNl&)_wggkh*vqv%ZIt1ij1mPMQaY;s<_DSg|U+CQSrA&7!*|a zc5ejb5hkscBwmab#pR2~=XRZa_#Exw`IE!Ut1+)J^QZ^KDqb}g$XWT~tb#EbYBir@ z@99-~mk*~)0tJHj$Ul*^MC(-w-n$77h&qcTTDWI+y~>2$uZ@krB2-NnpZoSw^)R5dOL!rX=R4?)MjKfLMBRFF$0khJhXLPJ z*Fsj&n&%A`$vw-yJ0k`fUnG>Zb(Mc5jjTS;H-8o*TO9TrC%N1$|ICh1p8y&_ekV6K zZ+fiUy9!@1t+JZw#{jTg>Nejp{UtiQ?|F1BVH57V0rRzz8LK4ph%4QkSw|jizfaBl`c{YMDQ4QnCs#5+FW&%&+irv*cG=iT4mh~BzUyMkUCp3tZ)fzFKt@&xuU!4-mA^3a=W@#yNDNo-jjEL%BcgNv zW@2eM)M5WZIX>aCS@lA4(9Y$6-B_7c4*#&Z+>=LhSCNtO5paVP9doIM#}M_*`Aqmp z!7qkE9`D@Am$*Aw7l7hJxMp7NSEDF!jnX(3+cIExe=QsF>iudTm|j;!q_9@EPsHL{ z{7)iO=tHE%-2cf6&b;O-KW)3TLz_Vd+C${66D*2D&mC(J#a3GA*|`#~M5e(Nj~PC; z;DmoXM$O0%-2Lq%Qn7MY=vT|3y6N$vW|SNOZkB0yUDuXJ3n$9~m8lxXkA{P1PI8`> zBX^bk`sfB>sgh@KOYEEZDA|`gH6O$G=9k<`$hb6J7e5L_PN9Ab za=CsyLtOyGy|zY!mjxfL`sbszqX?%P{7xgTGv>@xDvush(|r*=SzwAx3_KHSzy-OhUC0^8K2ra>UZ#yWVWN z_+f>0MWt((Y(?0AdT9rK@$Ix*$MzNfb*zQ~LH+gb-k+fa1wuqZ?LoV2nZ-_@C!PM; zWI-)0!TPa@#Yw*@s$A%JCw)!lifLaq*88BZWsS^U$k*%82{YZ{NWFBcjCOuLw{5nQ z29YfR9JqOlWF=GIG_hW&soCgZzdK}p2;cr@l5dsARX8A5Am-B+wzDwjRyT4`)%gQ$ zZIHaXM0d2GJhkp!$u%@2DI|9~zDRFwzrFX(TG`+oi@7nlf81^qPN=H%wb50+F>&#Q zRHcKni|jhv$k4FVLl2g_R`)IOO@H6ag$-yvDX~7Z>zUv7GYL|@!+wH!P%e*KuIAC@ z@gIBV>q#$o>sD5-6V0DK9`7D+1%P;5>rF78=ibY@NnciuLoHA7UmPN275@%pmM~Mu z-CQaDJ?q=Y9<}O`b%(#oLUmy<8;??VuG_4LkIu`=aO3ir{qF6YKW4jaYr?B-G~%Uy zG!h_U`*1vP&k$uVIpedw4W1cdV&_Ez{qFByRw;cVOmAPQ7B#?bz#@ZEJ|#(Yy_&=s zcZrfT(5=hgOBpG&%&dO5-*571KQ(xnrNspdy0q{_dODl!?WeyVI>KQtth*PF7s^)s zY8x52#!i2Yv1)r^>E}jP#?;}Ha>BuNlrUX*ZElR$)-S6e% z#_;OmtOgGKBM&t~6=+$K(3BqaQDN{}w*6zDByDL(468*%ANhz}1*x z6WQlV4ww}e1G|<>N_*+WY<2LW17WFvzPui_BcI7=s7J6&_&p%(eR=?6d}GyFP|tFb zoa6JK@ae%p>_~mRgS^mo@9&=@EQ#hE3eHLowYe_(uelP6ciXA=J(;L+X87PwcA=6o zZGSM})=IERgLU1^p5_ysk47%Ba0`+J_i_H5{r2o0zq*&Kog zXcGE>wWk>M6G687tY4&7JsH_whrUYVO8qBwx^_c`Jn|Z6DM!s&PKsArp0h;{WM}4D z@hl;)hr=?Rusk#}JTfHt=yq2mhn(EtzTW9n_`+?zKN4N`_-*d*{UbI%shWy4fHaFY za}Z6LfQi1yRI_^Wz(sac4Bsc15* z@k0hS#Lg*O{uUC6KTfDy=jf%~1@2gmE3vt54*gosrx1tBoNWvB>|LTp|5~5kKMneW zugcbGk!25&B`V&07j1>4214?tn!^Lw@+H4obp-~g5%Bv!#w4G{BrIeocy%m z>iV7n-KPgAsUh>C$QlkDB72>;u3O~74a>H$9B13N=m=nbeC_uvZH&%W3bJ`4k!f}0T!iwl7w%l@U*@Jdzf_V zNXo@!#G(1zWU-^|#|ikzgUZR_&y;8nHepu-WYBxStzCjk=h?GowB(AzL)T)}ZfZI@ zS|O1^w}KbkcE11%q9b#U~KmyKdPo*Jgaj#nw8 zPA9sLXJ*W1Z03(5$4aQ0xByc0$_-6pb2Ohj^`xtSg3K5vJ-MPrt8n?wn3fqSB51}6 zDa_2wJX(E0&D#1gz5EY(4d|u%N73(RWStFF=WfoS{`vhF2#ZG_lyr2K3H=rS>M{71 zw^MtM+0ntpB}PIFemA=!;>q|D!Z|-DOQdI--`V*mD-d~1)8H5U=TXgHO8+$WZBigM zZ5e>45f+YKo0a)?c`aRpN&Dqa1->zRAJ@jEhxUcJa>FC5vQKqbV+UgA56 zsmC8IJX_y&)Tz!d>dF>`a2G5(I+0uckRS=Gw~R;MaR!f&!m%7`tV4DI)u#GROH<_* ze!Z`}y79HEW#5N`(>*Uw7M4zQd_S2dXn39d5PkHsqYdLo|CaMY@TO^(ptN-Eaxy#m zW(v^In}g4h+P}ygP%%61nkqq{Wb4^KtDku|nX$Mfe2h9y{_2qD$Ah}9ztO5_HSrh$ ziU{2>tpxzBV|Y39*9_keo2W4P)p+G)FlgSj1EP?WiMHIip7 zJ0%8qje^30SrL5B{D5gEkVONA8Zxoct+@nS=2UmQuSM~ZF1&JmR&Yp`OU72g?Nj}7 zJ$qyiFexOvJfM6u`K1$&kb5>>`9X1xOq>LOps&|&rDow1Ox?yo0an}D=Kh;g-#b!& zau9kxB7+r5mM3VQCqgQaLoVXMtmveQXen_p(eN!D!8*QQV!yZ?O|4!%*B^}el2iBf zWUgbobZvF^redq!7H(_*?V>m8F{00b(==Fc5dbIzAh2~MbA5~65*V}+Dq=mq)nniLKL*Lvn91TozTFu z+~c5a!K%L#)XwmU!Y#r;PGEkV%|nB37s0MAJw5-Ebcfq|qbm_Z%%P1|+qWVwjnK1wKzVj}1@_8S4t{e*}E1$-%yngIbuHR43M<2NU7c*@T zm>v=EC4+oBWb;L0(^7t;e)tZl4+%WGP2oUK~O#0?t{(~>i#Cvl5xSLPt<&V=Hb@-8R6VM4~3Rn3#bqT4O zC-}}63r?Bs=7)j-39Cf?U3JsZ%9-vKF?igKKmK2KC)&XDh&WKZ3Qv^$V}e{V(er59 z>5mJn;Er3X-<|)Y(}T6j!^(ECMLDKuS!H6qYugzzQVFEDy^$A?E4RW`0Pp z0ZJ!)M2(E5eIye$gdn!|-&w${7Wk-J4BTjua@tIlI=J{LZ$= z6_u6U(~j?`u{H1LoVA)5s<%FLem63(0EH>|XxNf}h>5Y~1+IRwcKrQ1olO12lOYq@%|!n!Ld&o>vQ(xdnG=?qAIfi+2P z7*;5n;Vdnu)ijOetgrg`QI|eTlEm%qwO}$|sQ_2KL%Q`>CX96+49gHp5r-JrzWtJ! z1yO!&meb@PzEiYE!u~6T!-49zXlLXKxo4a(dWRbC(tQ zqUT;Wkp=q|*2-IMzF~__W(5)ce!V(`L%HL zY^+uD$90AHHBCfruJ`!y?%r{kmo|hUoUpjJ_Y^$h_^qpl5ZJ$fL1LC(SanP0(4^Q) z!$0BQM-pN$jnZ5U-1)rkir?1}HY6k!Hh_1RV^e<%9JlYBWa+aGFE+TsGn-2A{vlx? z24Sd9=~y|A;{VfO`TUWvn-UYy+b}iNOx*YMrNSfj3HHg)em#*)>8FMP zN+f9Vm4YpkPPyDHTU*}*Jm%AdxbzKqJw@vd)`&v?lOgXsf`HxxC-W)F@TNvF_VHee z(+UNrOY!bJ6K~V>YAY?BYHcSP&3fuI-x^i4`*_BjptaG=W>puU3nocP9FvnKgvwt8 z#iF)q&eBMD$53CYs#*r>y$+LI108Vo!F|}3IW)%haGM1no<8+OiajzqR%3OiHk@g% zf|3z6)YfZwuPcQ!FJ-R);ADpZB5q#%xmv~iH%z^UWLi1BD{7as^O1LueM8%IgyO^0 z#`p1;BVM0Yfkxne`r`HQrpr$up%l{qANk?nO*I|Yt#@&UU&LWIZo)FT5;`gg3&nr% zEoEMWmM13jkcNw3*0j$t?bS2vq*aUG8S@vfGwCZS&fC6gY@d40u)l_Hyu1Eh;uELw zrL}VJk1Q(nC&QtjnK6tM#`JidMfEc)de+{YQAs8G&kB9Z`&_5%!}ZmF?(+YWOF8m- zseubnnn05~%uBm}EmyQxubi#g-;mc3VZknWyZ8kv>V3lso0KNRy|259_ohO>AWT`# zw`$1YONN7(%YANKqw?@(?R9Pm$BU|PKAj(_1tUXw1UCV2^;=5X6%3u3YisYUqwuZM zDfwK2m6*Db;aYon4l4;lU0`l~hVxCS37^HGL#co>Fx;#{BOEu#A_9UQTbKN6JSUD; z^5C*Pq%QRK8$;h=GW=5x$ulU-ZQC&%XVaP319FA<7TI;@=*_sE2mL|qn53zh|G$d zgr2*4Pum0Lc{Hx3FZ4hHY>@)rJuWYx9#S)i0|(_r>~Fsl_UlfX(<9~DruSc#7GvXO z<8v+4wR1)HFDknFuYKz^59SWOKg&E)V>^GsEG*YI@BY}&K4HBw3chP{SBNxtF-y|g zfF6e_ud|tF&i6eLT>s`{9$ZPoP6^PUuh~UPzRPEqo(q4>JdTQX7(AP%-?~7Gb!C}w z@Ir0pn>NSm=*yQ6w}^vlmrJT)+cXB(Ei{rT4iiwBQ(IMKQVE=ml>uV`_4u9A08&hm_N880-Ubme!D?|vOo#;4 zV~@1U?h8F=^cWnHEbu+azZsSI@j%?>BwpTaGuz%w&t!r@@^FxRTZgSG(d|x3Wa^A> z<@+_)F7>>%ZT;9a4eAeu(ED(wkC|pdxgT&ni}v3KOgf@m8R2`af#ax<3-c-pC#@4G z%BbCWWjRdmlg!T&6Vvgu4W%^OZ$P_AD?kER4+DThD|{9>0^G7F}GG$ zY3rt}_mD85aVEdDSy?vpI>k*0%Kky)ZX%T*0`2yn_e_w`%ATxad zI4JqRj`ulQzQxr1|NU=WY->zM0wZfo54}`xE3X7+*PD0ffc-M%C7|G+imFcdM!jP}9ILC7cMFAMl~%ki8>g>D|6n8XcJhSIjN0^w7QHg+L3iyxIsBjmU{QPbtHplY2Sjr zuC`M$VZ+n)ctYF>yKLY(VP5{XlY7@BBPsnIPxl!V0=r5={stZ}eH;+ea)&)cY~}gM z3QKxlJK9p@`Rw;{!r63d-s$d^EVvo$CTP_6SIPg(U{t0a@t=Rl3+^2d%N8LE>v=kT z#T1o~#mYgFK$Oxq?=$D-e+n&}@;eV?1|&EQu?x%0FYE10kaL)}lewR87?AeL6dF#} z(A39zjF}3L6u)}0aR*Fwt|u}H)qqw!+RS&J_B%}%zhdEu>!LTTuA@szf3KVarmgy3 ziTve-7}E3D+5itd+S#2Xc)I^VIwI>2yB9q*D%@5T1CfrZzhss-y%nnd_D^USxix=E zn297qs@l}_eQ0c_r%0=~nMl4jY`KQtvP|-HDiX8{@b(@U(vx{)^1wu21r3m3FgyOlrz~h%+z+m`` z@xgXgCgmqt?0Wjfi7_#8=egib|uSi?1EUWdaeDPLZ8C-#=x zl|^+&acyy_k)v4~V6|{JlP%>`tpSX2&-gV#$<<%(@O!%n4L6qS9l@k5k4eSs#I6C|%?U)$t`S z{yE*j4~`G%l7t?}hcECM=3%UqJMN@g{nU~kceou*Xjf^>ewoAWXGxys?`Q1mor`GO zSu51=im1B$A%A~C$e3gGWWbuE;Q@?TQI}eV!7_@{0}z-xQzkYB@^^&?5CJQ;uu>%8 zsYvk$R;LeahM9m@Ew)!{8su)huK*#Ih{5FiKM26mH%4Wa=RuDS$LmJl=D zh*EY<+&ybPqKjS;AO+3B%rPA39^VN$a2X`->$ETMqu z2E+!eQ!9>A8sKdMh{Y%R*CvB0GF z;DQ(cqT?mPA%+`?9KdVhOCkhhB*zH4_1-|9V?an;2g#Qp#9-gQ=s!_`=p8>2^YlS6 zYxGwY?*V-Km{-~OhaEJQ6p3IErs2RZ$QO`p_%B-_HUzYAzneaSsL{pn|MWt%JQ1zt zRcWaE-|L&=M2!&=(7yknx0pAeXhk-fbCrqx-Md0GHgSy4S+PlCjQW{aV9S!Q3Lp5jzHX&H#$JmHb4~r zWnw&SATkllXx`zn@h}nuSkXmKWbQ9#42usGQTqxSO_N*-s`c;Kp$)N_=R=#O=0N)G zU#nVf&=Y4!7M_Oi`#JzcIF1t$#-KGgDT#OvyxM>hBkCIfCP4f8b#`iYjH|?Hp0~mcyA-EZdbJAHp)|x|yb3Q883d|&-#jt=y zdONoc@-<%CLNZ<=-1!&*utk&@_QdjkQt*rb|4@Pt1OV?h{|_pm89*V>|1TmNEHQ*G XO6r!Nu52R^1fcprU8&%{jFJXmlBcXxtAa0u@15Ih7A?k>TDC%C)2Conj{gKzFR`>+pN zb^E3Jm+G$S`udk@000hv1ONg7e+L!d3p@az{dWZd{VN9`0szNsr{srnwlgv#kUrNXIPAw~%;mUk=H#hnR}lEU&CNQ$dlbPj7AY@nFF1re zB%^{5rtI+a8nfIQlsJ#K+bj;ceUjN;#yF`io15Wta(bS4qMD#ws^jtJx-4fyw>mqU z^4ZBX5ZLH}<)VkuTY~St!mCS>3o+S&QW}LM2ushzRMkWWg197YCJvfMwYla(aOz)T zx+D-J6?p9?RSYn$0!wj+la9j%2Ym!+jD_Y#>0X1S@KV^nruH;bpH>{)b9niTWM@dQ zA$qehnYL67rTgjCI)3ntzq@sp#jiGudhon~x!SzA^*lE1Er0@En-ixu8P+&$r5s3g>IoatP25!I<`ADKbo1hoY|Du9urxV&mW+ z7P0%p$){fQ=b@HZn0X7Hp6PZ7oXBE<9U5EoRwRSl8FSU8{>GcS`c+|`m2Jt-BScNb zBl&dEsKIKd!=WicS#GXn@RXY>>+J=DKRHQIsEjJYiV`bOaK;XcMwe92?%o2iF@i#X zXZ!79(++npKg-bQoW-c9=7VeHv!0FOEnUy{1yHwG{Hjg7H|7>Ui zo~j9xa%_n9m4g%(HwZyea!LE`zG(?+5AXp74+*uAU~+iN7W@WM7z#Tr5=RW7Ql)>M zjUvGy@t_KYwFNPmtaT8a_eG*M?-8VB?Q9St^a$_6y}%!zVwI!noP__y4InSAB2^<{ z68ir@`5(+d|3K+PSXm1IAWHoUWwoZAHJKKHh}YTp-h=M?+@JUyR$~V)IW_?GWV;NgVk=Th6*=l>&i(HSYrsdf=4Na1 zk1L+jT(tF=C!*IOm;F{CsxX#;)){jO$X>YH1I?p_s3zKH{&=!)Hkq1%k=T4R#}Ll?Z}7Gj4_0C!=X zCS|Hj-RAl8RMqcE=+CJKZDvx$^^Jf-a9u}!_vg|C&FBj}(W-3Ll3i3Q zxhPUty)9%de~AgWwXyY2@qmKk#)^5A^^mhL)+tcq%p&{DcEHFsGtxVokL0SNU9*o` z50`Xd$^|Po+N?q=_a@vxb~BexLsd-Z^0cQ&aX`k9NCt>a%`U6WsNn_9`j84~%$xIah%KQcnd7=}5KxJjc| z5#fjlW%OkX!`rZ_?s#_UKug*ErTKo{9jnb9S=uJC&*)|P^P=`OA=W1?P9395zo zO{|l~G9slGCGu1AzoA% zPouxV!Zoz2T!KYbf$8^2$^w&|Nc@S*h+~@JLE|c2por4NTAgGlM25 zo^-o|pc2aAXpElP+$A0+f`qx6cDPhkg^Q=pe$ya^JG}+1j!>CVVbBXU5ySw?wVspY zl86EM@lXj>-qkbt*Ex+sA{A$nJsbU;RTfescwbgQj#^y#zL|5CQEq`yxWJyjD;zvn zHNa~=Hrv0f)Sfl$^yKu5aFfBLocZa$1RtuucB+4Z&lBjh2mnAf_&4}8X`|U|4H5p} zy4nmHGu{k(^$VXJtk5ISPvcIBpeJYy(~^man$K^pnPrU7tq!jIqF-5z2T_@qD?lG|8H@$$=)v&;N_(INSapwB?#?hcnv zm|Dq2K_g^OTvcC@6~v=nRpC4!{U|?Hg>QCXx8+yF7^M$E4=GJ0NVH}`$d#~nseC(Z z)QG4(nn~K4kDd2*o4>{Ur)oJFR1-46*GB`tlh-Pe6+~NT?jFRo)jjavxc6p4<6?(m zBbqF0$$%khuY)%$&Cm0cRHZsZmk3!)WpSORO(%7MPyG`mPigRjVyGEWjia`Y6*-{a z^ajS`ogT5_y3AJs})A5GfJpw+IC)<7*{Yr0NZMAQ_C*&vvONNZL#rkbOr%h#QL}kDr%3eltEqp z(sy+Yn%PS(j(OE*ZTaNKE&>`fwU{JT&ScLLp-l;C-XD)?;e4+uC+t*l*TH~Rr+E+M zo-(hJ)m3C(r-U{ZtcYM>OgZD3qbJ$W&N}&7GS?u13>x*#gGD{G&L*I;n)S~$o5%Y( zb|~1CQ( z)G5Xp?cd78eV1GPxvJ}F;BB^$SHbD>VKu9ug>*Pd;(&Q3xnZO2$PbM30Ox@uhahO( z>u}coz32UmN4qVSju73!3_X6#3U|((hX!AK%jp>5{f2Qy<8h0ie)H@8pS9C^In6l_ zH;=xf$D@wR-O>7=e;#&pxk>82ChP_6GECe{?e1WS&JrH!`H1E^rJW8%A2Mql;^QQM zA`XO#RPv;mD=`&#?QH`+VVk07f~Zk@J8C{xcb}WKtiQggt*To=z(J<)Ev{@ZToO){ zI`T8fMN`UYKIsf!KVGuhga}Ll`#_DKA_5jMi7*N0Y^vHDO84D@0!}}V#&Cve90*k8 zh?~s}p8W~&p3I8sz0Q!Bqa)VtCjHc}l6Pp>ZUKoOAQU{INHmR<#iIhsG`xXbN;jTO z_CGH9`EJ_js&9!xX>macz5H_?IE5&Q)&e_mp>mAMv|OHy-goQmG!C-Y9s7hGn~)U( z;68kumKfMV<7t)9;Wn*dr2ndG^YNo>S5vbozj{i@alps7LR0VFn5M6@=V)?1&z?VQ z`gLvpcJ6Muf*B;u3>Sz#Pl!4N)<5D|XwV4G9t3uSAYm}KOou!H56_j|#}<+MWt^<8 zfXiPi=1yMO&jVcA&(zVn&gdpp8bL)a1f(9?0{5c>F*+qlQmX z?E4_nDcs`4M~@em(Cd=A7~~Kr*_Yz%JeYDR!KMl*w-La0>RKc{J`M0VAA6?6K4_0z zffQZLoGeaory*DRYZT6)qry!_+rUF4P^ITUF_Jou*bZworPMnObmqX&vDikiN&$lC)S5I>W{45G zl(@?fna|C86HG$Y`o@n0SLO=b4LjRddC3hevc})Y%+>!qv8ZYK?2%6ziY#E2a1(^1 zje2_#!}ngVfZ`87rH*Y<4AVM6WAVMX5p6sCD76SfAqOvM25M&j7}U3Q!35ns$E!)@ zo@Gaxgo!dl$T1oyJ~t4K&3i0(wEJawL;5MuOS*5!HTyS;^MPd385>I#o~$u~GG($* zYVc7+tG7yxU-52-a=LummS|Mp7fL=< z23>A&6OdhnC#Lm#ql+v;IC4^h`3(4Cb9d0ee*N_H&c@8ivahXtsVvznJr+z{ZGZU6 z$HAR-da<_uB`5u-uVeowCoP*eP6Ghw$N%P}4P761%?0r5+25Ap&WbZxNn*K&HY`9^ z4ZA#;fK@^m1-Rag-pm%YKI+>gtttp%pSsK^;YMKN~qa-VwsxZ(61KzZqYf}A$n;~Y(nnYP^Xn+q`vXoA@t z0t8$KA@%c{z&B6OFaS&`Y=Pd>(x!#PS6oSiMEv5jPZod5z0bs!V3Zk_D0mugpp&Y0 z5G8;)QMq_b((NqC>lo7IHZ$i|0K?uLhHe0PybOuxA9Rc#$GQt~k#l zJi~^5_JV?kgbzoUZLbLjr1uY`_s|KP81!Zjah31+iq2GF#{wA0)}D9s;fnt8Tcpf_ zYCJCP?l9i&Zv`fMGdB3pFS9xw5$y@If zvFLvCuaF>u5yLoRaZ7INZKcXZgWmhT7+ky^wWWBfuJ>c$!d$Hpc5-!6M`b^q9ud(F zxK16x3a@Q1Q~6!Xl7ei+X2g+Nt|iJ+E*(cjf#o(S+_-Per=%f+jm_fC`!wmBof!QL z?gqDfr4@B$%f%1XU?idd@c z)2(tZ<~n=b)>}O1Rm`yJcEM8pN0EU;hx?U55mF#5Tg`p$J-B76J8Wq@fXc`v{m)!@ zzr~6;Y7fX|pNbVpCN}(z&sk5pn=G<^e;QvR+axC`Ijij#-F|-eaqR_GRf2imVJkfw zzW#lky2pUu_%05v$&p5XQ6n5K^)9(7Sp`O@4V+gu{zo;$&MG)9{F}j_x5eI2^X#j+ zVLXX)#d#Fgq>y{x1H~;CAoR&XF_84kq{#XvalbVEA2mjCgC)PEFmNnw-UtGmt%8~Ys}q8 z83#M4GoRhs%xoBBfDxGZNT@T$fHI|~zE2fS4lTJs)EXM0k2tuQyBwE!@rhurU3x3~ zO?iXrzP9jF5<{k2$~C#Rm@#CZC{=|Ncn2~NZU5pTC8wDlXT!prsWsB zsRYRE_wNyJmWC6l(x%WmXl>%Z+vGwPh3@?ND3s z9xq!|kg(w-_EpE2kVez9d%0yPC-Gu`5J$UuWx7N$(!hy~p{hnp@ukjlud->cz_%|! z$Y^mI5Q|1QA~3QN4E-6S5T_-xM8qD!(F1D5UFdKhGiq`CBCAfn!PjMRF0QBk^rC5z zp|oJwcm{dj{++$N?+Bnt2CmVjoZ0eLZmLd+J9Xdi-r18_c-ixDlTiVQIvnNn zH{ru+zy#iMXMkjLwEQ-rG z)~qsz-0tiHwiAq-6>~yPG8k}}c1YO~PF%jHKv zxoPo`yuA0no8TE!bXf-q)kMD(7o&`DCeek}I~eH4Fq|ho>Nq|*zrHlM2kEbKJ52gO zS>ij1d~a2R)w|eCgq*oM>jtl>98r$tYDF#&?0jf@aU1N{xT`%g92OnD&$oAIKIQtX z$4S-GqV2d+%S;W|rUF8uD3~A;t)@{gqiqOcXo(ild9M6}yVW+UsE>5tc|kBhtIHF+ z^2L$;i-INEr(^zLARnewly%z3k%-Ewp8B?5RkO_KVI0&Pd+lydC#^{tWxM$lRUq{M zBIovz7BWeW9r)1I4>J^+UoT6+l_+fAxhwP0Z2R_&3?k*`TD?j_@W;r+mS}zgz%j^! zv+|CBZ8P^Wi+{d3*4q80m+mf(^#0l$hdWH7@Qp6-BDZ#X{1SsN8LYXFq!F)z31e{u zJP~`Lqw?hg&{PdIdY(~Qfae`?*nhyUav+M z>|XRcn|=?Y1-!VuHQn~ROQX#6h!n8JB-fYvG;)|bH*|iSy>Nqrvv~;b z9q<$7o#U_@Dt*i@VsDg554OIE<_MHW0vLb6Jk5-A?1uAGXgZ{@k7~a|9a#n zgK}3%?UXVA-7{5FGw`@KD#L2mGo^LDt|p$8$IM>a4`us#R#H0a6S|SHY`GL&G`M}X zBky=WVr04b5P{NWGw$HH%Wjj0@Pnr+CGT$70Pz-Y zpvn3A0XVJ&zzINq>AZJBiEl>pe`lnxePtQm;f6BgiO5&WpL;QFF7s&bwuqRWR6bwr z0fuc~pqJvKqZa?(3_ekt5A*@{q?8{J2ucvGQ8y;nP0V=y=YigLSfWkvF!A6 zwDX_*^kM?yZ!bV5c0#A{V|vMe1eD{1q;#Xqn|W>bLo&-X~PlM_O$wsU9BuOCAV7sz3H)}dJg++AC}fo;x1mGJwM`9 zOI#1A!*INz`@Xw4T)%2u z|Mgl9OG3CdW^&_|G^3tu_%AiG*k8NYKQ%HP#(wnQpNysdt&#O=oz}z%0s}Z%z#PA~ z(IXl3(9x|?kjuoFElgTi%vbd9A)0@5W@?E z9?mAxg$nB+KxT7u!GF*Qp^f@N@ckzNgy-}P(P<9RtuE3Wl-anQ4%D5P{s8pcm?&SU zPX5nxpDN~+NM|JE&tNp~o{EpiWV62^h9L-~n;pzaVk5ROwj$;kSb&F-YUb|R1z{pa zG7+mMD{1OiU%4aC8ldlXoncN`KrJ(^aN9V-f}?TQ7dt3!Ey-^~mN52|z1Gi(^ zbjrxU*k&>nE!ESutm6L530&}M?p<&kvYEzo=+=1CawOpI&Hw2_qUBp!aGYYmuf}__ z`h}5YrJ^04q8wI*;>#n4UgF<9xJbpy;mO)U z)5*4N6caN0Q-x(flc8JzQxKfH=&jSEM!7iG#|fe7xttDJm6alK7-*X&%zq%@kU;V0 zfsCRb7BsctoxW{%YfPyn^M$Hp{ThmeG0Op7Cf%?%Lk$k!9GaYy0l9kgQBxW)$(X=T l+^RK99Q8B)|Dg-W{};L3%wXmhz`>_*E9N@#;{W>|{s(}agZKad literal 0 HcmV?d00001 diff --git a/public/player-logo192.png-not b/public/player-logo192.png-not new file mode 100644 index 0000000000000000000000000000000000000000..e474366b7aff1065fd4e5d215bfe1b17f8b73c52 GIT binary patch literal 1956 zcmai#dpy$%AIE>Ygw1VbW#l$3Lu8Lju4BzyMi{Y&ojkE}V&}N4W{zt^WUuHjm%^B2 zZV@U^th|MWGmF^Sezz$bY9Q%_w&d1`+j}CukWAV@9UjM@%2!JYr_EmP$hek z0(SWDM=61K`rLy}3IHhnLM9yzWKOSES`_T}Kt9Ftvpu}vI%N+Rytrz*o^W^*v8j&t z*dV|)-!;I5hcUpI7Qi9X-~-;+y}4ZNVm49%`D@Qq$Hw>J>W%qt&Xu)8?-tfL>D%+R z9gb@sL+8?^2%6~#!T=xwY(NQw_%FZ)eo{Jr{M$yKgNp}XeOV7x%Wfe`R;stZe(-jODOUKXQ`wMRkHCk7nIFYs^2qyp zv5qd!@_|U5EN{9TWvau6C)H<+Zr>0s+Xi-y>Nec`SygQ16?M~z}nj$ux_)G zqL((e-SD^?M$F8IPs}eyqiUt?_>m2iA3dias1u%~G_lLRu(cwHcz2x}VOc7jJ zE9_FAq1JXfGK*>cV-OOY@I^!ZhwRDwxW`OP?(*2%$@{@X?_iO5X-PYYl)XH(aa)Q= zb@1t@tcYo|jgecISNGw$R%Is*5kR zWJG>A$=}u}xWZqHp9}(zJ8dp)fcP@09Ypi^3>E5573Zmmm%dzev`WSevWMR_c9Jq0 zC4L{e1Y>XQkW&whtCy|EDl&W;tmXpM-5r#Gpqa;vcOvzmVrqT|^VW1KVf;4n3weBh7>(9ZYZNJ&?QPmrhd7@|3vv` z!>kr%Eq%6qZLw*R{%xHhx3yqLh0aC+`9-|zdY^95VJXE|pEMs!?{sdgkQ^z&{DT}K z*?m5d+m_o6Qx=&uI)rOu$q)zaVb5!b0=by1BC;&|fK69CfR{?`u|0}>-z0XNmCBeF zxf*xHc3@Dy7V`R4&y1ra<{mWy!?(g`#GZVhdeJupzR!fDs9-!v`ZeJg^_j?+3@R<6 z`}G%hIoxdyDUx=x))OTY-?!dB^Ss_7qIlp8Ap%^LbIjpBv>o}&&BUA{c#i2=+3BM< z);3R_s>`ZDN+2yKd`jI%+y9vZWraN__Tl`yu?Jk;Al9~Gn^-%@GP?#<5kZXFlVp}bU6M|o(;YB3 z#IFn%#tj!&YXj*0;S`_2W*?J?2ESRTrnYZ@rHc06F z3N`O#yIG=d{5GSYWju_Ab}RdgFO;p>8FJK+j3{vCZ*i`q zcPmF@m;z=QP5a50-Qqzs``yR;3w2nde*MT|+*_4_x?GjI3uvi-M_2N_)bUx*pv$q7 zGa?4E{Vi4~QRw3S*@^maCsaicLWO$k% zyU4aY_zY~K(4h6s>QH_Whw#jZz();W1hH@dQuhm>8kV%qh+s!C=Z@Mq*n=uwx}I91 z`AcpK9^*KNsZZV*P1KhnJKCTgcW56=&`kKxHEd2F?cP?j-<_LK&0bZcfCysfAA6Ac z_1|0R-%_$#`p*wvd%YTcp5E-WtUMJ3yCkSMjjPvcw2bNF=w7HlZw@;dIo*=8U{xGH z!>n!ktaUE(bnJ3VfL#-4VG>Y-t%vK# zG5x`-yzCO3)IWI0X-K54b!E(L-20b5VECT-z!GcL5!;E@S4hwo&a3xvvq~sqi=mXl z+ciT96iIXVg^dD1;>84?{?pl!(wBCWs-u}`>~~(gEmI-cU6E=Ib1l_3Se(?%0hE3m m^Z#`kfZj1&1_G!VmFbJiI=EVqt@=Cm1;}o`q*h{B)_(w0z;!_Y literal 0 HcmV?d00001 diff --git a/public/player-logo512.png-not b/public/player-logo512.png-not new file mode 100644 index 0000000000000000000000000000000000000000..d182d3587d0cbcb263cce94bfe6fe8f8952378a7 GIT binary patch literal 5217 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 literal 0 HcmV?d00001 diff --git a/src/App.js b/src/App.js index 28a87e1..e28cd5a 100644 --- a/src/App.js +++ b/src/App.js @@ -4142,6 +4142,8 @@ function App() { // swap manifest so Android home-screen installs launch /display const link = document.querySelector('link[rel="manifest"]'); if (link) link.href = `${process.env.PUBLIC_URL || ''}/display-manifest.json`; + const favicon = document.querySelector('link[rel="icon"]'); + if (favicon) favicon.href = `${process.env.PUBLIC_URL || ''}/player-favicon.ico`; } if (window.location.pathname === '/logs') { setIsLogsMode(true); diff --git a/src/tests/DisplayView.characterization.test.js b/src/tests/DisplayView.characterization.test.js index c367bca..144610c 100644 --- a/src/tests/DisplayView.characterization.test.js +++ b/src/tests/DisplayView.characterization.test.js @@ -39,6 +39,10 @@ function participant(status) { describe('DisplayView characterization', () => { beforeEach(() => { + const favicon = document.createElement('link'); + favicon.rel = 'icon'; + favicon.href = '/favicon.ico'; + document.head.appendChild(favicon); window.history.replaceState({}, '', '/display'); global.alert = jest.fn(); window.open = jest.fn(); @@ -46,9 +50,18 @@ describe('DisplayView characterization', () => { Element.prototype.scrollIntoView = jest.fn(); }); afterEach(() => { + document.querySelectorAll('link[rel="icon"]').forEach(link => link.remove()); window.history.replaceState({}, '', '/'); }); + test('display route swaps browser favicon to player icon', async () => { + seedActiveDisplay(); + render(); + await waitFor(() => { + expect(document.querySelector('link[rel="icon"]')).toHaveAttribute('href', '/player-favicon.ico'); + }); + }); + test('DisplayView subscribes via adapter.subscribeDoc (not raw SDK)', async () => { seedActiveDisplay(); render();