Watch party: Prepared presentations queue for the host (add, name, reorder, remove, present when it is time)
This commit is contained in:
@@ -4066,6 +4066,15 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t
|
||||
.party-panel.min .pt-body { display: none; }
|
||||
.pt-present { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.pt-present:empty { display: none; }
|
||||
.pt-pq { display: grid; gap: 6px; padding: 8px; border: 1px solid var(--line); border-radius: 12px; }
|
||||
.pt-pq-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
||||
.pt-pq-row { display: flex; align-items: center; gap: 6px; padding: 6px; border-radius: 10px; background: var(--bg-1); }
|
||||
.pt-pq-row.live { box-shadow: inset 0 0 0 1px var(--accent, #f06b53); }
|
||||
.pt-pq-name { flex: 1; min-width: 0; display: grid; overflow: hidden; }
|
||||
.pt-pq-name, .pt-pq-name small { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.pt-pq-name small { color: var(--text-dim); font-size: 11px; }
|
||||
.pt-pq-row .np-btn { flex: none; min-height: 36px; padding-inline: 10px; }
|
||||
.pt-pq-empty { margin: 0; font-size: 12px; color: var(--text-2); }
|
||||
.pt-invite { display: flex; gap: 6px; }
|
||||
.pt-invite input { flex: 1; min-width: 0; }
|
||||
.pt-uninvite { margin-left: 4px; min-width: 28px; min-height: 28px; border: 0; background: none; color: var(--text-2); font-size: 16px; cursor: pointer; }
|
||||
@@ -4120,7 +4129,7 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t
|
||||
.pt-status { font-family: var(--mono); font-size: 10px; color: var(--text-dim); font-weight: 400; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pt-body { display: flex; flex-direction: column; gap: 10px; min-height: 0; flex: 1; }
|
||||
.pt-row { display: flex; gap: 8px; align-items: center; font-size: 12px; color: var(--text-dim); }
|
||||
.pt-row input, .pt-msg, .pt-name {
|
||||
.pt-row input, .pt-msg, .pt-name, .pt-invite-name {
|
||||
flex: 1; min-width: 0; background: var(--bg-1); color: var(--text); border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm); padding: 8px 10px; font-size: 14px;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
ws: null, role: null, code: '', secret: '', name: '', members: [], chat: [], state: null, offset: 0,
|
||||
dj: { enabled: false, entries: [] }, djPending: false,
|
||||
allowControl: false, quietUntil: 0, detached: false, retry: null, pushTimer: null, driftTimer: null, lastPush: '',
|
||||
loadingId: '', restoring: false, inviteCode: '', hiddenAt: 0, present: null, presentHidden: false, invites: [], inviteError: '', viewMode: {}, reader: { url: '', html: '', error: '', loading: false },
|
||||
loadingId: '', restoring: false, inviteCode: '', hiddenAt: 0, present: null, presentHidden: false, invites: [], inviteError: '', pq: [], viewMode: {}, reader: { url: '', html: '', error: '', loading: false },
|
||||
voice: { on: false, stream: null, muted: false, pcs: new Map(), audios: new Map() },
|
||||
minimized: false,
|
||||
};
|
||||
@@ -70,7 +70,7 @@
|
||||
st.dj = m.dj || { enabled: false, entries: [] }; st.djPending = false;
|
||||
st.allowControl = !!m.allowControl;
|
||||
st.present = m.present || null; renderPresent();
|
||||
st.invites = m.invites || [];
|
||||
st.invites = m.invites || []; st.pq = m.pq || [];
|
||||
setStatus(isHost() ? 'You’re hosting' : 'Synced with the host');
|
||||
if (m.lost && isHost()) toast(`🎉 The old party was lost (server restart). New code ${m.code} — share the link again`, { duration: 8000 });
|
||||
render();
|
||||
@@ -110,6 +110,8 @@
|
||||
st.allowControl = !!m.allowControl;
|
||||
render();
|
||||
if (!isHost()) sysMsg(st.allowControl ? 'The host lets everyone control playback' : 'Only the host controls playback now');
|
||||
} else if (m.type === 'pq') {
|
||||
st.pq = m.items || []; render();
|
||||
} else if (m.type === 'invites') {
|
||||
st.invites = m.invites || []; st.inviteError = m.error || '';
|
||||
if (m.error) toast('⚠ ' + m.error);
|
||||
@@ -400,6 +402,14 @@
|
||||
<div class="pt-present">${st.present
|
||||
? `${st.presentHidden ? '<button class="np-btn" type="button" data-act="present-show">🖥 Show the presentation</button>' : ''}${canPresent() ? '<button class="np-btn" type="button" data-act="present">Change link</button><button class="np-btn" type="button" data-act="present-stop">Stop presenting</button>' : ''}`
|
||||
: canPresent() ? '<button class="np-btn" type="button" data-act="present">🖥 Present a document or website</button>' : ''}</div>
|
||||
${isHost() ? `<div class="pt-pq">
|
||||
<div class="pt-pq-head"><b>Prepared</b><button class="np-btn" type="button" data-act="pq-new">+ Prepare</button></div>
|
||||
${st.pq.length ? st.pq.map((q, i) => `<div class="pt-pq-row${st.present && st.present.url === q.url ? ' live' : ''}">
|
||||
<span class="pt-pq-name">${escapeHtml(q.title || embedOf(q.url, 1).label || q.url)}<small>${q.mode === 'reader' ? 'Reader' : escapeHtml(embedOf(q.url, 1).label || '')}</small></span>
|
||||
<button class="np-btn" type="button" data-act="pq-go" data-id="${escapeHtml(q.id)}">${st.present && st.present.url === q.url ? 'Showing' : 'Present'}</button>
|
||||
<button class="np-btn" type="button" data-act="pq-up" data-id="${escapeHtml(q.id)}" aria-label="Move up"${i ? '' : ' disabled'}>↑</button>
|
||||
<button class="np-btn" type="button" data-act="pq-del" data-id="${escapeHtml(q.id)}" aria-label="Remove">×</button></div>`).join('')
|
||||
: '<p class="pt-pq-empty">Line up slides, documents and pages now; present each one when its time comes.</p>'}</div>` : ''}
|
||||
${isHost() ? `<form class="pt-invite"><input class="pt-invite-name" maxlength="40" placeholder="Invite a profile by name" autocapitalize="none" autocomplete="off" spellcheck="false" /><button class="np-btn" type="submit">Invite</button></form>
|
||||
${st.invites.length ? `<div class="pt-members pt-invited">${st.invites.map((n) => `<span class="pt-member">✉ ${escapeHtml(n)} <button type="button" class="pt-uninvite" data-act="uninvite" data-profile="${escapeHtml(n)}" aria-label="Cancel the invite for ${escapeHtml(n)}">×</button></span>`).join('')}</div>` : ''}` : ''}
|
||||
${isHost() ? `<label class="pt-toggle"><input type="checkbox" data-act="allow" ${st.allowControl ? 'checked' : ''} /> Guests can control playback</label>` : ''}
|
||||
@@ -470,6 +480,10 @@
|
||||
else if (act === 'land') { Notes.stageLyrics.landscape?.(!Notes.stageLyrics.landscapeOn?.()); renderLyricsToggle(); }
|
||||
else if (act === 'leave') leave(true);
|
||||
else if (act === 'present') presentPrompt();
|
||||
else if (act === 'pq-new') presentPrompt({ queue: true });
|
||||
else if (act === 'pq-go') { const q = st.pq.find((x) => x.id === a.dataset.id); if (q) send({ type: 'present', present: { url: q.url, mode: q.mode, page: 1 } }); }
|
||||
else if (act === 'pq-up') send({ type: 'pq', action: 'move', id: a.dataset.id, by: -1 });
|
||||
else if (act === 'pq-del') send({ type: 'pq', action: 'remove', id: a.dataset.id });
|
||||
else if (act === 'uninvite') send({ type: 'uninvite', profile: a.dataset.profile });
|
||||
else if (act === 'present-stop') send({ type: 'present', present: null });
|
||||
else if (act === 'present-show') { st.presentHidden = false; renderPresent(); render(); }
|
||||
@@ -633,22 +647,29 @@
|
||||
const frame = el.querySelector('.pp-frame');
|
||||
if (!reading && frame.getAttribute('src') !== e.src) frame.setAttribute('src', e.src);
|
||||
}
|
||||
function presentPrompt() {
|
||||
function presentPrompt({ queue = false } = {}) {
|
||||
const body = document.createElement('div');
|
||||
body.innerHTML = `<p class="modal-text">Everyone in the party sees it full screen. Google Slides and PDFs follow your page — use ‹ › or the arrow keys.</p>
|
||||
<input id="ppUrl" type="url" inputmode="url" autocomplete="off" spellcheck="false" placeholder="https://… a website, Google Slides/Docs or a PDF" value="${escapeHtml(st.present?.url || '')}" />
|
||||
<input id="ppUrl" type="url" inputmode="url" autocomplete="off" spellcheck="false" placeholder="https://… a website, Google Slides/Docs or a PDF" value="${queue ? '' : escapeHtml(st.present?.url || '')}" />
|
||||
${isHost() ? '<input id="ppTitle" maxlength="120" autocomplete="off" placeholder="Name it (optional), e.g. Sermon slides" style="margin-top:8px" />' : ''}
|
||||
<label class="pt-toggle" style="margin-top:8px"><input type="checkbox" id="ppReader" ${st.present && st.present.mode === 'embed' ? '' : 'checked'} /> Reader view for websites — clean text everyone can enlarge</label>
|
||||
<p class="modal-text" style="font-size:12px">Google files must be shared “Anyone with the link”. Sites like Facebook or banks refuse embedding; guests can still tap Open.</p>`;
|
||||
const go = () => {
|
||||
const go = (toQueue = false) => {
|
||||
let url = ($('ppUrl').value || '').trim();
|
||||
if (url && !/^https?:\/\//i.test(url)) url = 'https://' + url;
|
||||
try { new URL(url); } catch { toast('Paste a full link, e.g. https://…'); return; }
|
||||
const special = embedOf(url, 1); // Google files and PDFs always embed
|
||||
const mode = $('ppReader').checked && !special.paged && !/Google/.test(special.label || '') ? 'reader' : 'embed';
|
||||
closeModal(); send({ type: 'present', present: { url, page: 1, mode } });
|
||||
closeModal();
|
||||
if (toQueue) { send({ type: 'pq', action: 'add', url, mode, title: ($('ppTitle')?.value || '').trim() }); toast('Added to Prepared ✓'); }
|
||||
else send({ type: 'present', present: { url, page: 1, mode } });
|
||||
};
|
||||
showModal('🖥 Present to the party', body, [{ label: 'Cancel', onClick: closeModal }, { label: 'Present', primary: true, onClick: go }]);
|
||||
setTimeout(() => { const i = $('ppUrl'); if (i) { i.focus(); i.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); go(); } }); } }, 50);
|
||||
showModal(queue ? '🖥 Prepare a presentation' : '🖥 Present to the party', body, [
|
||||
{ label: 'Cancel', onClick: closeModal },
|
||||
...(isHost() ? [{ label: 'Add to Prepared', primary: queue, onClick: () => go(true) }] : []),
|
||||
...(queue ? [] : [{ label: 'Present now', primary: true, onClick: () => go(false) }]),
|
||||
]);
|
||||
setTimeout(() => { const i = $('ppUrl'); if (i) { i.focus(); i.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); go(queue); } }); } }, 50);
|
||||
}
|
||||
|
||||
function boot() {
|
||||
|
||||
Reference in New Issue
Block a user