renderer: the celebration itself
The Wednesday→Tuesday birthday week (cached, and re-rendered when it rolls over so an app left open for days lights up on its own), the canvas confetti loop, the gold glow, and the Birthday Mode controls — on/off, intensity, and a force-on picker for testing that lives in memory and clears on restart. The confetti deliberately ignores OS reduced-motion: it is a celebration the operator opts into, and freezing it leaves confetti stuck mid-fall.
This commit is contained in:
@@ -3,7 +3,7 @@ if (window.__bandLineupRendererLoaded) {
|
|||||||
} else {
|
} else {
|
||||||
window.__bandLineupRendererLoaded = true;
|
window.__bandLineupRendererLoaded = true;
|
||||||
/**
|
/**
|
||||||
* @typedef {{ id: string, fileName: string, originalName: string, lastLabel: string, lastName: string, source?: string, pcoPersonId?: string, pcoPlanPersonId?: string }} LibraryPerson
|
* @typedef {{ id: string, fileName: string, originalName: string, lastLabel: string, lastName: string, source?: string, pcoPersonId?: string, pcoPlanPersonId?: string, birthday?: string }} LibraryPerson
|
||||||
* @typedef {{ personId: string, label: string }} StageSlot
|
* @typedef {{ personId: string, label: string }} StageSlot
|
||||||
* @typedef {{ teamName: string, positionName: string, count: number }} PcoDisplayPosition
|
* @typedef {{ teamName: string, positionName: string, count: number }} PcoDisplayPosition
|
||||||
*/
|
*/
|
||||||
@@ -27,6 +27,10 @@ let pcoDisplayPositions = [];
|
|||||||
let pcoPositionOptions = [];
|
let pcoPositionOptions = [];
|
||||||
let slotLabelMemory = [];
|
let slotLabelMemory = [];
|
||||||
let slotNameMemory = [];
|
let slotNameMemory = [];
|
||||||
|
/** Birthday Mode display settings (persisted). */
|
||||||
|
let birthdayMode = { enabled: true, intensity: 'subtle' };
|
||||||
|
/** Person id forced into Birthday Mode for testing — in-memory only, never saved, clears on restart. */
|
||||||
|
let birthdayTestPersonId = '';
|
||||||
/** @type {StageSlot[]} */
|
/** @type {StageSlot[]} */
|
||||||
let stageSlots = createEmptySlots();
|
let stageSlots = createEmptySlots();
|
||||||
const imageUrlCache = new Map();
|
const imageUrlCache = new Map();
|
||||||
@@ -65,7 +69,13 @@ const btnApplyLayout = document.getElementById('btn-apply-layout');
|
|||||||
const btnToggleParallax = document.getElementById('btn-toggle-parallax');
|
const btnToggleParallax = document.getElementById('btn-toggle-parallax');
|
||||||
const btnPcoConnect = document.getElementById('btn-pco-connect');
|
const btnPcoConnect = document.getElementById('btn-pco-connect');
|
||||||
const btnPcoSync = document.getElementById('btn-pco-sync');
|
const btnPcoSync = document.getElementById('btn-pco-sync');
|
||||||
|
const btnPcoTestBirthdays = document.getElementById('btn-pco-test-birthdays');
|
||||||
const btnPcoDisconnect = document.getElementById('btn-pco-disconnect');
|
const btnPcoDisconnect = document.getElementById('btn-pco-disconnect');
|
||||||
|
const pcoBirthdayResult = document.getElementById('pco-birthday-result');
|
||||||
|
const bdayEnabled = document.getElementById('bday-enabled');
|
||||||
|
const bdayIntensity = document.getElementById('bday-intensity');
|
||||||
|
const bdayControls = document.getElementById('bday-controls');
|
||||||
|
const bdayTestSelect = document.getElementById('bday-test');
|
||||||
const btnPcoAddPosition = document.getElementById('btn-pco-add-position');
|
const btnPcoAddPosition = document.getElementById('btn-pco-add-position');
|
||||||
const inputLayoutCols = document.getElementById('layout-cols');
|
const inputLayoutCols = document.getElementById('layout-cols');
|
||||||
const inputLayoutRows = document.getElementById('layout-rows');
|
const inputLayoutRows = document.getElementById('layout-rows');
|
||||||
@@ -244,6 +254,7 @@ function setPcoStatus(message, isError = false) {
|
|||||||
function setPcoControlsEnabled(enabled) {
|
function setPcoControlsEnabled(enabled) {
|
||||||
btnPcoConnect.disabled = !enabled;
|
btnPcoConnect.disabled = !enabled;
|
||||||
btnPcoSync.disabled = !enabled;
|
btnPcoSync.disabled = !enabled;
|
||||||
|
btnPcoTestBirthdays.disabled = !enabled;
|
||||||
btnPcoDisconnect.disabled = !enabled;
|
btnPcoDisconnect.disabled = !enabled;
|
||||||
btnPcoAddPosition.disabled = !enabled;
|
btnPcoAddPosition.disabled = !enabled;
|
||||||
pcoServiceTypeSelect.disabled = !enabled;
|
pcoServiceTypeSelect.disabled = !enabled;
|
||||||
@@ -427,6 +438,9 @@ function applyLoadedState(data) {
|
|||||||
source: person?.source === 'pco' ? 'pco' : 'local',
|
source: person?.source === 'pco' ? 'pco' : 'local',
|
||||||
pcoPersonId: typeof person?.pcoPersonId === 'string' ? person.pcoPersonId : '',
|
pcoPersonId: typeof person?.pcoPersonId === 'string' ? person.pcoPersonId : '',
|
||||||
pcoPlanPersonId: typeof person?.pcoPlanPersonId === 'string' ? person.pcoPlanPersonId : '',
|
pcoPlanPersonId: typeof person?.pcoPlanPersonId === 'string' ? person.pcoPlanPersonId : '',
|
||||||
|
// Carried through so a routine save doesn't wipe birthdays a PCO sync stored (main.js
|
||||||
|
// re-validates the "MM-DD" shape on write); Birthday Mode reads this.
|
||||||
|
birthday: typeof person?.birthday === 'string' ? person.birthday : '',
|
||||||
}))
|
}))
|
||||||
: [];
|
: [];
|
||||||
const savedColumns = Number.parseInt(String(data?.layout?.columns ?? DEFAULT_COLUMNS), 10);
|
const savedColumns = Number.parseInt(String(data?.layout?.columns ?? DEFAULT_COLUMNS), 10);
|
||||||
@@ -451,6 +465,11 @@ function applyLoadedState(data) {
|
|||||||
pcoServiceTypeSelect.value = pcoServiceTypeId;
|
pcoServiceTypeSelect.value = pcoServiceTypeId;
|
||||||
renderPcoDisplayPositions();
|
renderPcoDisplayPositions();
|
||||||
stageSlots = normalizeLineup(data.lineup, getStageSlotCount());
|
stageSlots = normalizeLineup(data.lineup, getStageSlotCount());
|
||||||
|
birthdayMode = {
|
||||||
|
enabled: data?.birthdayMode?.enabled === undefined ? true : Boolean(data.birthdayMode.enabled),
|
||||||
|
intensity: data?.birthdayMode?.intensity === 'full' ? 'full' : 'subtle',
|
||||||
|
};
|
||||||
|
syncBirthdayControls();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSlotSizeReadout() {
|
function updateSlotSizeReadout() {
|
||||||
@@ -528,6 +547,7 @@ function scheduleSave() {
|
|||||||
slotLabels: slotLabelMemory,
|
slotLabels: slotLabelMemory,
|
||||||
slotNames: slotNameMemory,
|
slotNames: slotNameMemory,
|
||||||
lineup: stageSlots,
|
lineup: stageSlots,
|
||||||
|
birthdayMode,
|
||||||
});
|
});
|
||||||
}, 250);
|
}, 250);
|
||||||
}
|
}
|
||||||
@@ -950,6 +970,132 @@ function parallaxMotionVars(seedText) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------------- Birthday Mode ---------------- */
|
||||||
|
|
||||||
|
const BIRTHDAY_COLORS = ['#f5c451', '#ff6b81', '#4fd1c5', '#a78bfa', '#ffffff'];
|
||||||
|
/** Live confetti canvases; a single rAF loop drives them and prunes any whose tile was rebuilt. */
|
||||||
|
const birthdayCanvases = new Set();
|
||||||
|
let birthdayRaf = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The 7 "MM-DD" dates of the current celebration week, which runs Wednesday→Tuesday. A birthday
|
||||||
|
* anywhere in this window celebrates all week (so it's already up for the weekend services).
|
||||||
|
* Cached per week; birthdayTick() re-renders the stage when the window rolls over to a new Wednesday.
|
||||||
|
*/
|
||||||
|
let birthdayWeekCache = { key: '', set: null };
|
||||||
|
function birthdayWeekSet() {
|
||||||
|
const now = new Date();
|
||||||
|
const daysSinceWed = (now.getDay() - 3 + 7) % 7; // getDay: Sun=0…Wed=3…Sat=6
|
||||||
|
const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - daysSinceWed);
|
||||||
|
const key = `${start.getFullYear()}-${start.getMonth()}-${start.getDate()}`;
|
||||||
|
if (birthdayWeekCache.key === key && birthdayWeekCache.set) return birthdayWeekCache.set;
|
||||||
|
const set = new Set();
|
||||||
|
for (let i = 0; i < 7; i += 1) {
|
||||||
|
const d = new Date(start.getFullYear(), start.getMonth(), start.getDate() + i);
|
||||||
|
set.add(`${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`);
|
||||||
|
}
|
||||||
|
birthdayWeekCache = { key, set };
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Is this person celebrating this week — their birthday falls in the current Wed→Tue window, or they're the test override. */
|
||||||
|
function isBirthdayActive(person) {
|
||||||
|
if (!birthdayMode.enabled || !person) return false;
|
||||||
|
if (birthdayTestPersonId && person.id === birthdayTestPersonId) return true;
|
||||||
|
return typeof person.birthday === 'string' && person.birthday.length === 5 && birthdayWeekSet().has(person.birthday);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Re-render the stage when the celebration week rolls over (the app can stay open for days). */
|
||||||
|
function birthdayTick() {
|
||||||
|
const before = birthdayWeekCache.key;
|
||||||
|
birthdayWeekSet();
|
||||||
|
if (before && birthdayWeekCache.key !== before) void renderStage();
|
||||||
|
}
|
||||||
|
setInterval(birthdayTick, 15 * 60 * 1000);
|
||||||
|
|
||||||
|
function birthdayMakeParticle(w, h, intensity, seedY) {
|
||||||
|
const ribbon = Math.random() < 0.55;
|
||||||
|
return {
|
||||||
|
x: Math.random() * w,
|
||||||
|
y: seedY != null ? seedY : Math.random() * h,
|
||||||
|
w: ribbon ? 4 + Math.random() * 4 : 5 + Math.random() * 5,
|
||||||
|
h: ribbon ? 9 + Math.random() * 8 : 0,
|
||||||
|
ribbon,
|
||||||
|
color: BIRTHDAY_COLORS[(Math.random() * BIRTHDAY_COLORS.length) | 0],
|
||||||
|
rot: Math.random() * Math.PI * 2,
|
||||||
|
vr: -0.14 + Math.random() * 0.28,
|
||||||
|
sway: 0.6 + Math.random() * 1.1,
|
||||||
|
phase: Math.random() * Math.PI * 2,
|
||||||
|
vy: (intensity === 'subtle' ? 0.6 : 1.0) * (1.1 + Math.random() * 1.5),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function birthdaySeedCanvas(c, w, h) {
|
||||||
|
const base = c._intensity === 'subtle' ? 26 : 64;
|
||||||
|
const count = Math.max(10, Math.round((base * (w * h)) / (300 * 500)));
|
||||||
|
c._parts = Array.from({ length: count }, () => birthdayMakeParticle(w, h, c._intensity));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The confetti ALWAYS animates — this is a celebration effect, so it intentionally ignores the
|
||||||
|
// OS "reduced motion" setting (which was freezing the confetti mid-fall on the backstage machine).
|
||||||
|
function birthdayStepCanvas(c) {
|
||||||
|
const w = c.clientWidth;
|
||||||
|
const h = c.clientHeight;
|
||||||
|
if (!w || !h) return; // not laid out yet — try again next frame
|
||||||
|
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||||
|
if (c._w !== w || c._h !== h) {
|
||||||
|
c.width = w * dpr;
|
||||||
|
c.height = h * dpr;
|
||||||
|
c._ctx = c.getContext('2d');
|
||||||
|
c._ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||||
|
c._w = w;
|
||||||
|
c._h = h;
|
||||||
|
birthdaySeedCanvas(c, w, h);
|
||||||
|
}
|
||||||
|
const ctx = c._ctx;
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
for (const p of c._parts) {
|
||||||
|
p.phase += 0.03;
|
||||||
|
p.y += p.vy;
|
||||||
|
p.x += Math.sin(p.phase) * p.sway;
|
||||||
|
p.rot += p.vr;
|
||||||
|
if (p.y > h + 16) Object.assign(p, birthdayMakeParticle(w, h, c._intensity, -16));
|
||||||
|
ctx.save();
|
||||||
|
ctx.translate(p.x, p.y);
|
||||||
|
ctx.rotate(p.rot);
|
||||||
|
ctx.fillStyle = p.color;
|
||||||
|
if (p.ribbon) ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
|
||||||
|
else { ctx.beginPath(); ctx.arc(0, 0, p.w / 2, 0, Math.PI * 2); ctx.fill(); }
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function birthdayLoop() {
|
||||||
|
for (const c of [...birthdayCanvases]) {
|
||||||
|
if (!c.isConnected) { birthdayCanvases.delete(c); continue; }
|
||||||
|
birthdayStepCanvas(c);
|
||||||
|
}
|
||||||
|
birthdayRaf = birthdayCanvases.size ? window.requestAnimationFrame(birthdayLoop) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function birthdayRegisterCanvas(canvas, intensity) {
|
||||||
|
canvas._intensity = intensity;
|
||||||
|
birthdayCanvases.add(canvas);
|
||||||
|
if (!birthdayRaf) birthdayRaf = window.requestAnimationFrame(birthdayLoop);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rain confetti (plus a soft gold glow) onto a tile whose person is celebrating. */
|
||||||
|
function applyBirthdayEffects(tile, photoArea) {
|
||||||
|
const intensity = birthdayMode.intensity === 'full' ? 'full' : 'subtle';
|
||||||
|
tile.classList.add('birthday', `birthday-${intensity}`);
|
||||||
|
|
||||||
|
const confetti = document.createElement('canvas');
|
||||||
|
confetti.className = 'birthday-confetti';
|
||||||
|
confetti.setAttribute('aria-hidden', 'true');
|
||||||
|
photoArea.appendChild(confetti);
|
||||||
|
birthdayRegisterCanvas(confetti, intensity);
|
||||||
|
}
|
||||||
|
|
||||||
function createPhotoTile(person, href, slotIndex, parallaxAssets) {
|
function createPhotoTile(person, href, slotIndex, parallaxAssets) {
|
||||||
void parallaxAssets;
|
void parallaxAssets;
|
||||||
const tile = document.createElement('article');
|
const tile = document.createElement('article');
|
||||||
@@ -1002,6 +1148,7 @@ function createPhotoTile(person, href, slotIndex, parallaxAssets) {
|
|||||||
|
|
||||||
photoArea.appendChild(nameBadge);
|
photoArea.appendChild(nameBadge);
|
||||||
photoArea.appendChild(removeBtn);
|
photoArea.appendChild(removeBtn);
|
||||||
|
if (isBirthdayActive(person)) applyBirthdayEffects(tile, photoArea);
|
||||||
tile.appendChild(photoArea);
|
tile.appendChild(photoArea);
|
||||||
return tile;
|
return tile;
|
||||||
}
|
}
|
||||||
@@ -1156,6 +1303,7 @@ async function renderLibrary() {
|
|||||||
async function renderAll() {
|
async function renderAll() {
|
||||||
await renderStage();
|
await renderStage();
|
||||||
await renderLibrary();
|
await renderLibrary();
|
||||||
|
renderBirthdayTestOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function importNewPeople(added) {
|
async function importNewPeople(added) {
|
||||||
@@ -1170,6 +1318,7 @@ async function importNewPeople(added) {
|
|||||||
source: 'local',
|
source: 'local',
|
||||||
pcoPersonId: '',
|
pcoPersonId: '',
|
||||||
pcoPlanPersonId: '',
|
pcoPlanPersonId: '',
|
||||||
|
birthday: '',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await renderLibrary();
|
await renderLibrary();
|
||||||
@@ -1232,6 +1381,97 @@ async function syncUpcomingPlanFromPco() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const BIRTHDAY_MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
function formatBirthdayMmdd(mmdd) {
|
||||||
|
const parts = String(mmdd || '').split('-');
|
||||||
|
const mm = Number(parts[0]);
|
||||||
|
const dd = Number(parts[1]);
|
||||||
|
if (!(mm >= 1 && mm <= 12) || !(dd >= 1 && dd <= 31)) return '';
|
||||||
|
return `${BIRTHDAY_MONTHS[mm - 1]} ${dd}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPcoBirthdayResult(message, tone) {
|
||||||
|
if (!pcoBirthdayResult) return;
|
||||||
|
pcoBirthdayResult.textContent = message || '';
|
||||||
|
pcoBirthdayResult.style.display = message ? '' : 'none';
|
||||||
|
pcoBirthdayResult.style.color = tone === 'error' ? '#ffb1b1' : tone === 'good' ? '#8fe0b0' : '#9ea7bc';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testPcoBirthdays() {
|
||||||
|
setPcoControlsEnabled(false);
|
||||||
|
setPcoBirthdayResult('Checking Planning Center for birthday access…', 'info');
|
||||||
|
try {
|
||||||
|
const r = await window.lineup.pcoTestBirthdays();
|
||||||
|
if (!r || r.connected === false) {
|
||||||
|
setPcoBirthdayResult('Connect Planning Center first, then test birthday access.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.canRead) {
|
||||||
|
const why = r.reason === 'network'
|
||||||
|
? `Couldn't reach Planning Center (${r.detail || 'network error'}).`
|
||||||
|
: `This token can't read People birthdays${r.status ? ` (HTTP ${r.status})` : ''}${r.detail ? ` — ${r.detail}` : ''}.`;
|
||||||
|
setPcoBirthdayResult(`✕ ${why} Birthday Mode needs People access on the token.`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Access works. Report coverage across synced band members.
|
||||||
|
if (r.total === 0) {
|
||||||
|
setPcoBirthdayResult('✓ People access works. Sync an upcoming plan, then test again to see how many musicians have a birthday on file.', 'good');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bits = [`✓ People access works — ${r.withBirthday} of ${r.total} synced ${r.total === 1 ? 'person has' : 'people have'} a birthday on file.`];
|
||||||
|
if (r.nextUp) {
|
||||||
|
const when = r.nextUp.inDays === 0 ? 'today 🎂' : r.nextUp.inDays === 1 ? 'tomorrow' : `in ${r.nextUp.inDays} days`;
|
||||||
|
bits.push(`Next up: ${r.nextUp.name} (${formatBirthdayMmdd(r.nextUp.mmdd)}, ${when}).`);
|
||||||
|
}
|
||||||
|
if (r.notFound) bits.push(`${r.notFound} not matched in People.`);
|
||||||
|
setPcoBirthdayResult(bits.join(' '), r.withBirthday > 0 ? 'good' : 'info');
|
||||||
|
} catch (error) {
|
||||||
|
setPcoBirthdayResult(error instanceof Error ? error.message : 'Birthday test failed.', 'error');
|
||||||
|
} finally {
|
||||||
|
setPcoControlsEnabled(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------- Birthday Mode controls ---------------- */
|
||||||
|
|
||||||
|
/** Reflect current birthdayMode settings onto the controls (called after load). */
|
||||||
|
function syncBirthdayControls() {
|
||||||
|
if (bdayEnabled) bdayEnabled.checked = birthdayMode.enabled;
|
||||||
|
if (bdayIntensity) {
|
||||||
|
for (const b of bdayIntensity.querySelectorAll('button')) {
|
||||||
|
b.setAttribute('aria-pressed', b.dataset.int === birthdayMode.intensity ? 'true' : 'false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bdayControls) bdayControls.style.opacity = birthdayMode.enabled ? '' : '0.45';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rebuild the "Force on (test)" dropdown from whoever is currently on the stage. */
|
||||||
|
function renderBirthdayTestOptions() {
|
||||||
|
if (!bdayTestSelect) return;
|
||||||
|
const previous = birthdayTestPersonId;
|
||||||
|
bdayTestSelect.innerHTML = '<option value="">Off</option>';
|
||||||
|
const seen = new Set();
|
||||||
|
stageSlots.forEach((slot, index) => {
|
||||||
|
if (!slot.personId || seen.has(slot.personId)) return;
|
||||||
|
const person = getPerson(slot.personId);
|
||||||
|
if (!person) return;
|
||||||
|
seen.add(slot.personId);
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = person.id;
|
||||||
|
const name = String(getStageName(index, person) || person.lastName || 'Unknown').trim();
|
||||||
|
const label = String(slotLabelMemory[index] || '').trim();
|
||||||
|
option.textContent = label ? `${name} — ${label}` : name;
|
||||||
|
bdayTestSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
// Keep the current override selected if that person is still on stage; otherwise drop it.
|
||||||
|
if (previous && seen.has(previous)) {
|
||||||
|
bdayTestSelect.value = previous;
|
||||||
|
} else if (previous) {
|
||||||
|
birthdayTestPersonId = '';
|
||||||
|
bdayTestSelect.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- Setup panel tabs ---------------- */
|
/* ---------------- Setup panel tabs ---------------- */
|
||||||
|
|
||||||
/** Set while a tab switch blurs the port field, so the blur handler doesn't commit. */
|
/** Set while a tab switch blurs the port field, so the blur handler doesn't commit. */
|
||||||
@@ -1257,6 +1497,7 @@ function activatePanelTab(name) {
|
|||||||
}
|
}
|
||||||
// The readout measures a stage slot, so it can only be filled in once its pane is laid out.
|
// The readout measures a stage slot, so it can only be filled in once its pane is laid out.
|
||||||
if (name === 'pictures') window.requestAnimationFrame(updateSlotSizeReadout);
|
if (name === 'pictures') window.requestAnimationFrame(updateSlotSizeReadout);
|
||||||
|
if (name === 'pco') renderBirthdayTestOptions();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- Studio displays (hosting the Now/Next page on the LAN) ---------------- */
|
/* ---------------- Studio displays (hosting the Now/Next page on the LAN) ---------------- */
|
||||||
@@ -1330,11 +1571,11 @@ function renderDisplay() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!state?.enabled) {
|
if (!state?.enabled) {
|
||||||
setDisplayStatus('Off \u2014 screens around the building can\u2019t see this yet.');
|
setDisplayStatus('Off — screens around the building can’t see this yet.');
|
||||||
} else if (state.lastError) {
|
} else if (state.lastError) {
|
||||||
setDisplayStatus(state.lastError, true);
|
setDisplayStatus(state.lastError, true);
|
||||||
} else if (!state.listening) {
|
} else if (!state.listening) {
|
||||||
setDisplayStatus('Starting\u2026');
|
setDisplayStatus('Starting…');
|
||||||
} else if (state.mdnsConflict) {
|
} else if (state.mdnsConflict) {
|
||||||
setDisplayStatus(
|
setDisplayStatus(
|
||||||
`Another computer is already publishing that name (${state.mdnsConflict}). Use one of the numeric addresses below.`,
|
`Another computer is already publishing that name (${state.mdnsConflict}). Use one of the numeric addresses below.`,
|
||||||
@@ -1346,19 +1587,19 @@ function renderDisplay() {
|
|||||||
// when two machines are open is just wrong.
|
// when two machines are open is just wrong.
|
||||||
const screens = state.devices || state.viewers;
|
const screens = state.devices || state.viewers;
|
||||||
const extra = state.viewers > screens ? ` (${state.viewers} connections)` : '';
|
const extra = state.viewers > screens ? ` (${state.viewers} connections)` : '';
|
||||||
setDisplayStatus(`On \u2014 ${screens} screen${screens === 1 ? '' : 's'} watching${extra}.`);
|
setDisplayStatus(`On — ${screens} screen${screens === 1 ? '' : 's'} watching${extra}.`);
|
||||||
} else if (state.everHadViewer) {
|
} else if (state.everHadViewer) {
|
||||||
setDisplayStatus('On \u2014 no screens open right now.');
|
setDisplayStatus('On — no screens open right now.');
|
||||||
} else if (displayOnSince && Date.now() - displayOnSince > DISPLAY_FIREWALL_GRACE_MS) {
|
} else if (displayOnSince && Date.now() - displayOnSince > DISPLAY_FIREWALL_GRACE_MS) {
|
||||||
// On Windows the firewall prompt fires when the server starts listening, not when a
|
// On Windows the firewall prompt fires when the server starts listening, not when a
|
||||||
// studio first connects — so localhost looks fine here while every other screen times
|
// studio first connects — so localhost looks fine here while every other screen times
|
||||||
// out. A viewer count stuck at zero is the only hint we get; say it plainly.
|
// out. A viewer count stuck at zero is the only hint we get; say it plainly.
|
||||||
setDisplayStatus(
|
setDisplayStatus(
|
||||||
`On, but no screen has connected yet. If a studio can\u2019t load the page, Windows Firewall on this computer is probably blocking Band Lineup \u2014 it needs to allow TCP port ${state.port} for Private networks.`,
|
`On, but no screen has connected yet. If a studio can’t load the page, Windows Firewall on this computer is probably blocking Band Lineup — it needs to allow TCP port ${state.port} for Private networks.`,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setDisplayStatus('On \u2014 open one of these on any screen in the building.');
|
setDisplayStatus('On — open one of these on any screen in the building.');
|
||||||
}
|
}
|
||||||
|
|
||||||
renderDisplayUrls();
|
renderDisplayUrls();
|
||||||
@@ -1385,7 +1626,7 @@ function scheduleDisplayRefresh() {
|
|||||||
async function toggleDisplayHosting() {
|
async function toggleDisplayHosting() {
|
||||||
const next = !displayState?.enabled;
|
const next = !displayState?.enabled;
|
||||||
btnDisplayToggle.disabled = true;
|
btnDisplayToggle.disabled = true;
|
||||||
setDisplayStatus(next ? 'Starting\u2026' : 'Stopping\u2026');
|
setDisplayStatus(next ? 'Starting…' : 'Stopping…');
|
||||||
try {
|
try {
|
||||||
displayState = await window.lineup.display.setEnabled(next);
|
displayState = await window.lineup.display.setEnabled(next);
|
||||||
displayOnSince = next ? Date.now() : 0;
|
displayOnSince = next ? Date.now() : 0;
|
||||||
@@ -1733,6 +1974,36 @@ btnPcoSync.addEventListener('click', () => {
|
|||||||
void syncUpcomingPlanFromPco();
|
void syncUpcomingPlanFromPco();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnPcoTestBirthdays.addEventListener('click', () => {
|
||||||
|
revealChrome();
|
||||||
|
void testPcoBirthdays();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bdayEnabled) {
|
||||||
|
bdayEnabled.addEventListener('change', () => {
|
||||||
|
birthdayMode.enabled = bdayEnabled.checked;
|
||||||
|
syncBirthdayControls();
|
||||||
|
scheduleSave();
|
||||||
|
void renderStage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (bdayIntensity) {
|
||||||
|
bdayIntensity.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('button[data-int]');
|
||||||
|
if (!btn) return;
|
||||||
|
birthdayMode.intensity = btn.dataset.int === 'full' ? 'full' : 'subtle';
|
||||||
|
syncBirthdayControls();
|
||||||
|
scheduleSave();
|
||||||
|
void renderStage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (bdayTestSelect) {
|
||||||
|
bdayTestSelect.addEventListener('change', () => {
|
||||||
|
birthdayTestPersonId = bdayTestSelect.value || '';
|
||||||
|
void renderStage();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
btnPcoDisconnect.addEventListener('click', async () => {
|
btnPcoDisconnect.addEventListener('click', async () => {
|
||||||
revealChrome();
|
revealChrome();
|
||||||
setPcoControlsEnabled(false);
|
setPcoControlsEnabled(false);
|
||||||
@@ -1744,6 +2015,7 @@ btnPcoDisconnect.addEventListener('click', async () => {
|
|||||||
renderPcoPositionOptions();
|
renderPcoPositionOptions();
|
||||||
scheduleSave();
|
scheduleSave();
|
||||||
setPcoStatus('Planning Center disconnected.');
|
setPcoStatus('Planning Center disconnected.');
|
||||||
|
setPcoBirthdayResult('');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setPcoStatus(error instanceof Error ? error.message : 'Could not disconnect Planning Center.', true);
|
setPcoStatus(error instanceof Error ? error.message : 'Could not disconnect Planning Center.', true);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user