Add persistent live controls and interaction notes
This commit is contained in:
1 parent
b1474bc33d
commit
fa17187af4
16 files changed
+882
-62
No files matched your search
+152
-11
@@ -13,6 +13,7 @@
|
||||
let mobileRailOpen = $state(false);
|
||||
let isMobile = $state(false);
|
||||
let showTimes = $state(true);
|
||||
let timerDisplay = $state<'elapsed' | 'remaining'>('elapsed');
|
||||
let menuOpen = $state(false);
|
||||
let expandedContext = $state<'section' | 'next' | null>(null);
|
||||
let popupMessage = $state('');
|
||||
@@ -20,6 +21,7 @@
|
||||
let loadedAt = $state(Date.now());
|
||||
let currentId = $state(untrack(() => data.live.current?.id ?? null));
|
||||
let loadedRevision = $state(untrack(() => data.live.liveRevision));
|
||||
let calculatedAtUtc = $state(untrack(() => data.live.calculatedAtUtc));
|
||||
let viewedUsageId = $state(untrack(() => data.live.current?.id ?? null));
|
||||
let pieceScroller = $state<HTMLElement>();
|
||||
let railScroller = $state<HTMLElement>();
|
||||
@@ -74,7 +76,9 @@
|
||||
);
|
||||
let currentActualSeconds = $derived(
|
||||
(data.live.current?.actualDurationSeconds ?? 0) +
|
||||
(data.live.current ? Math.max(0, Math.floor((now - loadedAt) / 1000)) : 0)
|
||||
(data.live.current && data.live.totalTimer.runningSinceUtc
|
||||
? Math.max(0, Math.floor((now - loadedAt) / 1000))
|
||||
: 0)
|
||||
);
|
||||
let viewedActualSeconds = $derived(
|
||||
viewingLiveCurrent ? currentActualSeconds : (viewedUsage?.actualDurationSeconds ?? 0)
|
||||
@@ -91,17 +95,34 @@
|
||||
let hasActiveSectionTimer = $derived(
|
||||
Boolean(
|
||||
data.live.sectionTimer &&
|
||||
data.live.sectionTimer.plannedDurationSeconds > 0 &&
|
||||
data.live.current &&
|
||||
data.live.sectionTimer.sectionId === data.live.current.sectionId
|
||||
)
|
||||
);
|
||||
let runningOffsetSeconds = $derived(
|
||||
data.live.totalTimer.runningSinceUtc ? Math.max(0, Math.floor((now - loadedAt) / 1000)) : 0
|
||||
);
|
||||
let totalElapsedSeconds = $derived(data.live.totalTimer.elapsedSeconds + runningOffsetSeconds);
|
||||
let remainingSeconds = $derived(
|
||||
Math.max(
|
||||
0,
|
||||
data.live.remainingEstimate.seconds -
|
||||
Math.min(runningOffsetSeconds, data.live.remainingEstimate.activeCountdownSeconds)
|
||||
)
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const nextCurrentId = data.live.current?.id ?? null;
|
||||
if (nextCurrentId !== currentId || data.live.liveRevision !== loadedRevision) {
|
||||
if (
|
||||
nextCurrentId !== currentId ||
|
||||
data.live.liveRevision !== loadedRevision ||
|
||||
data.live.calculatedAtUtc !== calculatedAtUtc
|
||||
) {
|
||||
const changedPiece = nextCurrentId !== currentId;
|
||||
currentId = nextCurrentId;
|
||||
loadedRevision = data.live.liveRevision;
|
||||
calculatedAtUtc = data.live.calculatedAtUtc;
|
||||
loadedAt = Date.now();
|
||||
if (changedPiece) {
|
||||
viewedUsageId = nextCurrentId;
|
||||
@@ -130,6 +151,9 @@
|
||||
if (typeof settings.promptSize === 'number') promptSize = settings.promptSize;
|
||||
if (typeof settings.showRail === 'boolean') showRail = settings.showRail;
|
||||
if (typeof settings.showTimes === 'boolean') showTimes = settings.showTimes;
|
||||
if (settings.timerDisplay === 'elapsed' || settings.timerDisplay === 'remaining') {
|
||||
timerDisplay = settings.timerDisplay;
|
||||
}
|
||||
if (settings.scrollPositions && typeof settings.scrollPositions === 'object') {
|
||||
scrollPositions = Object.fromEntries(
|
||||
Object.entries(settings.scrollPositions).filter((entry): entry is [string, number] =>
|
||||
@@ -182,6 +206,7 @@
|
||||
promptSize,
|
||||
showRail,
|
||||
showTimes,
|
||||
timerDisplay,
|
||||
scrollPositions
|
||||
})
|
||||
);
|
||||
@@ -296,6 +321,11 @@
|
||||
persistSettings();
|
||||
}
|
||||
|
||||
function toggleTimerDisplay() {
|
||||
timerDisplay = timerDisplay === 'elapsed' ? 'remaining' : 'elapsed';
|
||||
persistSettings();
|
||||
}
|
||||
|
||||
function formatCountdown(totalSeconds: number) {
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
@@ -367,7 +397,7 @@
|
||||
<header class="live-command-header" inert={mobileRailOpen}>
|
||||
<div class="live-context-row">
|
||||
<div class="live-context-summary">
|
||||
<span class="live-pulse" aria-hidden="true"></span>
|
||||
<span class:paused={data.live.isPaused} class="live-pulse" aria-hidden="true"></span>
|
||||
<button
|
||||
type="button"
|
||||
class:expanded={expandedContext === 'section'}
|
||||
@@ -425,10 +455,27 @@
|
||||
<input type="hidden" name="usageId" value="" />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="text-button" type="submit">Präsentation leeren</button>
|
||||
<button class="text-button" type="submit" disabled={Boolean(data.live.endedAtUtc)}>Präsentation leeren</button>
|
||||
</form>{/if}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="total-countdown"
|
||||
type="button"
|
||||
onclick={toggleTimerDisplay}
|
||||
aria-label={timerDisplay === 'elapsed'
|
||||
? 'Gelaufene Gesamtzeit, zu Restzeit wechseln'
|
||||
: 'Geschätzte Restzeit, zu gelaufener Zeit wechseln'}
|
||||
>
|
||||
<span>{timerDisplay === 'elapsed' ? 'Gelaufen' : 'Restzeit'}</span>
|
||||
<strong>{formatCountdown(timerDisplay === 'elapsed' ? totalElapsedSeconds : remainingSeconds)}</strong>
|
||||
{#if timerDisplay === 'remaining' && data.live.remainingEstimate.unplannedSectionCount}
|
||||
<small title="Verbleibende Abschnitte ohne geplante Abschnittsdauer"
|
||||
>+ {data.live.remainingEstimate.unplannedSectionCount} ungeplant</small
|
||||
>
|
||||
{/if}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class:warning={hasActiveSectionTimer &&
|
||||
sectionRemainingSeconds <= 120 &&
|
||||
@@ -509,8 +556,96 @@
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
<strong>Sendung</strong>
|
||||
{#if needsResume}<form
|
||||
<strong>Sendungssteuerung</strong>
|
||||
<small>Status: {data.live.show.status}</small>
|
||||
{#if !data.live.startedAtUtc}
|
||||
<form method="POST" action={liveAction('setStatus')} use:enhance={enhanceCommand}>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="status" value={data.live.show.status === 'ready' ? 'draft' : 'ready'} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="text-button" type="submit">
|
||||
{data.live.show.status === 'ready' ? 'Als Entwurf markieren' : 'Als bereit markieren'}
|
||||
</button>
|
||||
</form>
|
||||
<form method="POST" action={liveAction('start')} use:enhance={enhanceCommand}>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="secondary-button" type="submit"><AppIcon name="play" /> Sendung starten</button>
|
||||
</form>
|
||||
{:else if data.live.endedAtUtc}
|
||||
<form method="POST" action={liveAction('resumeEnded')} use:enhance={enhanceCommand}>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="secondary-button" type="submit"><AppIcon name="play" /> Lauf fortsetzen</button>
|
||||
</form>
|
||||
<form
|
||||
method="POST"
|
||||
action={liveAction('startNewRun')}
|
||||
use:enhance={enhanceCommand}
|
||||
onsubmit={(event) => {
|
||||
if (!confirm('Neuen Durchlauf starten? Fortschritt und Zeitmessungen werden zurückgesetzt.')) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="text-button" type="submit"><AppIcon name="arrow-rotate-left" /> Neuer Durchlauf</button>
|
||||
</form>
|
||||
{:else}
|
||||
<form method="POST" action={liveAction(data.live.isPaused ? 'resume' : 'pause')} use:enhance={enhanceCommand}>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="secondary-button" type="submit">
|
||||
<AppIcon name={data.live.isPaused ? 'play' : 'pause'} />
|
||||
{data.live.isPaused ? 'Fortsetzen' : 'Pausieren'}
|
||||
</button>
|
||||
</form>
|
||||
{#if data.live.sectionTimer && data.live.sectionTimer.plannedDurationSeconds > 0}<form
|
||||
method="POST"
|
||||
action={liveAction('resetSectionTimer')}
|
||||
use:enhance={enhanceCommand}
|
||||
onsubmit={(event) => {
|
||||
if (!confirm('Abschnittstimer wirklich zurücksetzen?')) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="text-button" type="submit"><AppIcon name="arrow-rotate-left" /> Abschnittszeit zurücksetzen</button>
|
||||
</form>{/if}
|
||||
<form
|
||||
method="POST"
|
||||
action={liveAction('resetTotalTimer')}
|
||||
use:enhance={enhanceCommand}
|
||||
onsubmit={(event) => {
|
||||
if (!confirm('Gesamtzeit wirklich zurücksetzen?')) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="text-button" type="submit"><AppIcon name="timer" /> Gesamtzeit zurücksetzen</button>
|
||||
</form>
|
||||
<form
|
||||
method="POST"
|
||||
action={liveAction('end')}
|
||||
use:enhance={enhanceCommand}
|
||||
onsubmit={(event) => {
|
||||
if (!confirm('Sendung wirklich beenden? Offene Pieces bleiben offen.')) event.preventDefault();
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="text-button danger-text" type="submit"><AppIcon name="stop" /> Sendung beenden</button>
|
||||
</form>
|
||||
{/if}
|
||||
{#if needsResume && !data.live.endedAtUtc}<form
|
||||
method="POST"
|
||||
action={liveAction('start')}
|
||||
use:enhance={enhanceCommand}
|
||||
@@ -560,6 +695,11 @@
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div class="live-markdown">{@html viewedUsage.moderationHtml}</div>
|
||||
</section>{/if}
|
||||
{#if viewedUsage.interactionHtml}<section class="live-script-section">
|
||||
<p class="eyebrow">Interaktion</p>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
<div class="live-markdown">{@html viewedUsage.interactionHtml}</div>
|
||||
</section>{/if}
|
||||
{#if viewedUsage.transitionHtml}<section class="live-script-section transition-card">
|
||||
<span>Überleitung</span>
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||||
@@ -613,7 +753,7 @@
|
||||
</div>
|
||||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||||
{/if}
|
||||
{#if !viewedUsage.introHtml && !viewedUsage.moderationHtml && !viewedUsage.transitionHtml}
|
||||
{#if !viewedUsage.introHtml && !viewedUsage.moderationHtml && !viewedUsage.interactionHtml && !viewedUsage.transitionHtml}
|
||||
<p class="live-empty-copy">Für dieses Piece sind keine Moderationstexte hinterlegt.</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -673,6 +813,7 @@
|
||||
</div>
|
||||
<button
|
||||
bind:this={railCloseButton}
|
||||
class="dialog-close"
|
||||
type="button"
|
||||
aria-label="Ablauf schließen"
|
||||
onclick={() => closeMobileRail()}><AppIcon name="xmark" /></button
|
||||
@@ -783,7 +924,7 @@
|
||||
<input type="hidden" name="usageId" value={usage.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button type="submit" aria-label={`${usage.title} präsentieren`}
|
||||
<button type="submit" disabled={Boolean(data.live.endedAtUtc)} aria-label={`${usage.title} präsentieren`}
|
||||
><AppIcon name="display" /></button
|
||||
>
|
||||
</form>
|
||||
@@ -892,7 +1033,7 @@
|
||||
<input type="hidden" name="usageId" value={viewedUsage.id} />
|
||||
<input type="hidden" name="revision" value={data.live.liveRevision} />
|
||||
<input type="hidden" name="commandId" value={data.commandId} />
|
||||
<button class="secondary-button" type="submit">
|
||||
<button class="secondary-button" type="submit" disabled={Boolean(data.live.endedAtUtc)}>
|
||||
{data.live.presentationUsageId === viewedUsage.id
|
||||
? 'Erneut präsentieren'
|
||||
: 'Präsentieren'}
|
||||
@@ -905,7 +1046,7 @@
|
||||
<button
|
||||
class="secondary-button"
|
||||
type="submit"
|
||||
disabled={!viewingLiveCurrent}
|
||||
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
|
||||
title={!viewingLiveCurrent
|
||||
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
|
||||
: undefined}>Überspringen</button
|
||||
@@ -918,7 +1059,7 @@
|
||||
<button
|
||||
class="primary-button"
|
||||
type="submit"
|
||||
disabled={!viewingLiveCurrent}
|
||||
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
|
||||
title={!viewingLiveCurrent
|
||||
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
|
||||
: undefined}
|
||||
|
||||
Reference in new issue
Block a user