1256 lines
47 KiB
Svelte
1256 lines
47 KiB
Svelte
<script lang="ts">
|
||
import { enhance, type SubmitFunction } from '$app/forms';
|
||
import { resolve } from '$app/paths';
|
||
import { onMount, tick, untrack } from 'svelte';
|
||
import AppIcon from '#lib/components/AppIcon.svelte';
|
||
import ThemeToggle from '#lib/components/ThemeToggle.svelte';
|
||
import ShareManagement from '#lib/components/ShareManagement.svelte';
|
||
import { formatClock, formatDuration } from '#lib/format.js';
|
||
import type { PageProps } from './$types';
|
||
|
||
let { data, form }: PageProps = $props();
|
||
let promptSize = $state(1.55);
|
||
let showRail = $state(true);
|
||
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('');
|
||
let now = $state(Date.now());
|
||
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>();
|
||
let liveRail = $state<HTMLElement>();
|
||
let mobileRunButton = $state<HTMLButtonElement>();
|
||
let railCloseButton = $state<HTMLButtonElement>();
|
||
let liveMenuTrigger = $state<HTMLButtonElement>();
|
||
let liveMenuPopover = $state<HTMLElement>();
|
||
let completionDialog = $state<HTMLDialogElement>();
|
||
let scrollPositions: Record<string, number> = {};
|
||
let outputWindow: Window | null = null;
|
||
let allUsages = $derived(data.live.sections.flatMap((section) => section.usages));
|
||
let viewedUsage = $derived(
|
||
allUsages.find((usage) => usage.id === viewedUsageId) ?? data.live.current
|
||
);
|
||
let currentPosition = $derived(
|
||
data.live.current ? allUsages.findIndex((usage) => usage.id === data.live.current?.id) + 1 : 0
|
||
);
|
||
let viewedPosition = $derived(
|
||
viewedUsage ? allUsages.findIndex((usage) => usage.id === viewedUsage?.id) + 1 : 0
|
||
);
|
||
let previousUsage = $derived(viewedPosition > 1 ? allUsages[viewedPosition - 2] : null);
|
||
let viewingLiveCurrent = $derived(viewedUsage?.id === data.live.current?.id);
|
||
let runExpanded = $derived(isMobile ? mobileRailOpen : showRail);
|
||
let currentSection = $derived(
|
||
data.live.sections.find((section) => section.id === data.live.current?.sectionId) ?? null
|
||
);
|
||
let viewedSection = $derived(
|
||
data.live.sections.find((section) => section.id === viewedUsage?.sectionId) ?? null
|
||
);
|
||
let presentedUsage = $derived(
|
||
allUsages.find((usage) => usage.id === data.live.presentationUsageId) ?? null
|
||
);
|
||
let progress = $derived(
|
||
data.live.totalCount ? Math.round((data.live.completedCount / data.live.totalCount) * 100) : 0
|
||
);
|
||
let needsResume = $derived(
|
||
Boolean(data.live.startedAtUtc && !data.live.current && data.live.next)
|
||
);
|
||
let hasOtherOpenUsages = $derived(
|
||
allUsages.some((usage) => usage.status === 'open' && usage.id !== data.live.current?.id)
|
||
);
|
||
let sessionComplete = $derived(
|
||
Boolean(
|
||
data.live.startedAtUtc &&
|
||
!data.live.current &&
|
||
data.live.totalCount &&
|
||
data.live.completedCount === data.live.totalCount
|
||
)
|
||
);
|
||
let currentSectionTitle = $derived(
|
||
currentSection?.name ?? (sessionComplete ? 'Sendungsende' : 'Live-Regie')
|
||
);
|
||
let nextTitle = $derived(
|
||
data.live.next?.title ?? (sessionComplete ? 'Sendungsende erreicht' : 'Kein weiteres Piece')
|
||
);
|
||
let currentActualSeconds = $derived(
|
||
(data.live.current?.actualDurationSeconds ?? 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)
|
||
);
|
||
let sectionElapsedSeconds = $derived(
|
||
(data.live.sectionTimer?.elapsedSeconds ?? 0) +
|
||
(data.live.sectionTimer?.runningSinceUtc
|
||
? Math.max(0, Math.floor((now - loadedAt) / 1000))
|
||
: 0)
|
||
);
|
||
let sectionRemainingSeconds = $derived(
|
||
(data.live.sectionTimer?.plannedDurationSeconds ?? 0) - sectionElapsedSeconds
|
||
);
|
||
let hasActiveSectionTimer = $derived(
|
||
Boolean(
|
||
data.live.sectionTimer &&
|
||
data.live.sectionTimer.plannedDurationSeconds > 0 &&
|
||
data.live.current &&
|
||
data.live.sectionTimer.sectionId === data.live.current.sectionId
|
||
)
|
||
);
|
||
|
||
async function copyTwitchTags() {
|
||
if (!viewedSection?.twitchTags.length) return;
|
||
try {
|
||
await navigator.clipboard.writeText(viewedSection.twitchTags.join(', '));
|
||
popupMessage = 'Twitch-Tags kopiert.';
|
||
} catch {
|
||
popupMessage = 'Kopieren wurde vom Browser blockiert.';
|
||
}
|
||
}
|
||
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 ||
|
||
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;
|
||
void tick().then(() => {
|
||
if (pieceScroller)
|
||
pieceScroller.scrollTop = nextCurrentId ? (scrollPositions[nextCurrentId] ?? 0) : 0;
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
$effect(() => {
|
||
const activeId = data.live.current?.id;
|
||
if (!activeId) return;
|
||
void tick().then(() => {
|
||
railScroller
|
||
?.querySelector<HTMLElement>('[data-current="true"]')
|
||
?.scrollIntoView({ block: 'nearest' });
|
||
});
|
||
});
|
||
|
||
$effect(() => {
|
||
if (!data.completion && completionDialog?.open) completionDialog.close();
|
||
});
|
||
|
||
onMount(() => {
|
||
const key = `sendewerk-live-${data.live.show.id}`;
|
||
try {
|
||
const settings = JSON.parse(localStorage.getItem(key) || '{}');
|
||
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] =>
|
||
Number.isFinite(entry[1])
|
||
)
|
||
);
|
||
} else if (currentId && typeof settings.scrollY === 'number') {
|
||
scrollPositions[currentId] = settings.scrollY;
|
||
}
|
||
requestAnimationFrame(() => {
|
||
if (pieceScroller && currentId) pieceScroller.scrollTop = scrollPositions[currentId] ?? 0;
|
||
});
|
||
} catch {
|
||
localStorage.removeItem(key);
|
||
}
|
||
const mobileBreakpoint = matchMedia('(max-width: 46rem)');
|
||
isMobile = mobileBreakpoint.matches;
|
||
const handleBreakpointChange = (event: MediaQueryListEvent) => {
|
||
isMobile = event.matches;
|
||
if (!event.matches) void closeMobileRail(false);
|
||
};
|
||
const handleDocumentClick = (event: MouseEvent) => {
|
||
const target = event.target;
|
||
if (!(target instanceof Node)) return;
|
||
if (menuOpen && !liveMenuPopover?.contains(target) && !liveMenuTrigger?.contains(target)) {
|
||
void closeLiveMenu();
|
||
}
|
||
if (
|
||
expandedContext &&
|
||
!(target instanceof Element && target.closest('.live-context-title'))
|
||
) {
|
||
expandedContext = null;
|
||
}
|
||
};
|
||
mobileBreakpoint.addEventListener('change', handleBreakpointChange);
|
||
document.addEventListener('click', handleDocumentClick);
|
||
const interval = setInterval(() => (now = Date.now()), 1000);
|
||
return () => {
|
||
mobileBreakpoint.removeEventListener('change', handleBreakpointChange);
|
||
document.removeEventListener('click', handleDocumentClick);
|
||
clearInterval(interval);
|
||
};
|
||
});
|
||
|
||
function persistSettings() {
|
||
if (pieceScroller && viewedUsageId) scrollPositions[viewedUsageId] = pieceScroller.scrollTop;
|
||
localStorage.setItem(
|
||
`sendewerk-live-${data.live.show.id}`,
|
||
JSON.stringify({
|
||
promptSize,
|
||
showRail,
|
||
showTimes,
|
||
timerDisplay,
|
||
scrollPositions
|
||
})
|
||
);
|
||
}
|
||
|
||
function changePromptSize(delta: number) {
|
||
promptSize = Math.min(2.4, Math.max(1.1, promptSize + delta));
|
||
persistSettings();
|
||
}
|
||
|
||
function toggleDesktopRail() {
|
||
showRail = !showRail;
|
||
persistSettings();
|
||
}
|
||
|
||
function toggleRunPanel() {
|
||
void closeLiveMenu(false);
|
||
if (isMobile) {
|
||
if (mobileRailOpen) void closeMobileRail();
|
||
else void openMobileRail();
|
||
return;
|
||
}
|
||
toggleDesktopRail();
|
||
}
|
||
|
||
function viewUsage(usageId: string) {
|
||
if (pieceScroller && viewedUsageId) scrollPositions[viewedUsageId] = pieceScroller.scrollTop;
|
||
viewedUsageId = usageId;
|
||
void tick().then(() => {
|
||
if (pieceScroller) pieceScroller.scrollTop = scrollPositions[usageId] ?? 0;
|
||
});
|
||
void closeMobileRail();
|
||
}
|
||
|
||
function viewPreviousUsage() {
|
||
if (previousUsage) viewUsage(previousUsage.id);
|
||
}
|
||
|
||
async function openMobileRail() {
|
||
await closeLiveMenu(false);
|
||
mobileRailOpen = true;
|
||
await tick();
|
||
railCloseButton?.focus();
|
||
}
|
||
|
||
async function closeMobileRail(restoreFocus = true) {
|
||
if (!mobileRailOpen) return;
|
||
mobileRailOpen = false;
|
||
await tick();
|
||
if (restoreFocus) mobileRunButton?.focus();
|
||
}
|
||
|
||
function handleWindowKeydown(event: KeyboardEvent) {
|
||
if (event.key !== 'Escape') return;
|
||
if (menuOpen) {
|
||
event.preventDefault();
|
||
void closeLiveMenu();
|
||
} else if (expandedContext) {
|
||
event.preventDefault();
|
||
expandedContext = null;
|
||
} else if (mobileRailOpen) {
|
||
event.preventDefault();
|
||
void closeMobileRail();
|
||
}
|
||
}
|
||
|
||
async function toggleLiveMenu() {
|
||
if (menuOpen) {
|
||
await closeLiveMenu();
|
||
return;
|
||
}
|
||
menuOpen = true;
|
||
expandedContext = null;
|
||
await tick();
|
||
liveMenuPopover
|
||
?.querySelector<HTMLElement>('button:not(:disabled), a[href]')
|
||
?.focus({ preventScroll: true });
|
||
}
|
||
|
||
async function closeLiveMenu(restoreFocus = true) {
|
||
if (!menuOpen) return;
|
||
menuOpen = false;
|
||
await tick();
|
||
if (restoreFocus) liveMenuTrigger?.focus({ preventScroll: true });
|
||
}
|
||
|
||
function toggleContext(name: 'section' | 'next') {
|
||
expandedContext = expandedContext === name ? null : name;
|
||
}
|
||
|
||
function trapRailFocus(event: KeyboardEvent) {
|
||
if (!mobileRailOpen || event.key !== 'Tab' || !liveRail) return;
|
||
const focusable = Array.from(
|
||
liveRail.querySelectorAll<HTMLElement>(
|
||
'button:not(:disabled), summary, a[href], input:not(:disabled), [tabindex]:not([tabindex="-1"])'
|
||
)
|
||
).filter((element) => element.offsetParent !== null);
|
||
const first = focusable[0];
|
||
const last = focusable.at(-1);
|
||
if (!first || !last) return;
|
||
if (event.shiftKey && document.activeElement === first) {
|
||
event.preventDefault();
|
||
last.focus();
|
||
} else if (!event.shiftKey && document.activeElement === last) {
|
||
event.preventDefault();
|
||
first.focus();
|
||
}
|
||
}
|
||
|
||
function toggleTimes() {
|
||
showTimes = !showTimes;
|
||
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);
|
||
const seconds = Math.floor(totalSeconds % 60);
|
||
return hours > 0
|
||
? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
|
||
: `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||
}
|
||
|
||
function statusLabel(status: string) {
|
||
if (status === 'current') return 'Aktuell';
|
||
if (status === 'done') return 'Erledigt';
|
||
if (status === 'skipped') return 'Übersprungen';
|
||
return 'Kommend';
|
||
}
|
||
|
||
function liveAction(name: string) {
|
||
return `?show=${encodeURIComponent(data.live.show.id)}&/${name}`;
|
||
}
|
||
|
||
const enhanceCommand: SubmitFunction = ({ formElement, formData, submitter }) => {
|
||
void closeLiveMenu();
|
||
const submittedFromRail = Boolean(formElement.closest('.live-rail'));
|
||
const keepRailOpen = formElement.dataset.keepRailOpen === 'true';
|
||
const command = formElement.elements.namedItem('commandId');
|
||
if (command instanceof HTMLInputElement) {
|
||
command.value = crypto.randomUUID();
|
||
formData.set('commandId', command.value);
|
||
}
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = true;
|
||
return async ({ result, update }) => {
|
||
await update({ reset: false, invalidateAll: true });
|
||
if (submittedFromRail && (result.type !== 'success' || !keepRailOpen)) {
|
||
await closeMobileRail();
|
||
}
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = false;
|
||
};
|
||
};
|
||
|
||
const enhanceOutput: SubmitFunction = ({ submitter }) => {
|
||
void closeLiveMenu();
|
||
popupMessage = '';
|
||
outputWindow = window.open('about:blank', 'heinzlive-output');
|
||
if (!outputWindow) popupMessage = 'Popup blockiert. Bitte Popups erlauben und erneut öffnen.';
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = true;
|
||
return async ({ result, update }) => {
|
||
if (result.type === 'success') {
|
||
const response = result.data as { outputUrl: string };
|
||
if (outputWindow && !outputWindow.closed) {
|
||
outputWindow.location.href = response.outputUrl;
|
||
popupMessage = 'Präsentationsfenster verbunden.';
|
||
} else {
|
||
popupMessage = `Präsentationsfenster konnte nicht geöffnet werden: ${response.outputUrl}`;
|
||
}
|
||
} else {
|
||
outputWindow?.close();
|
||
}
|
||
await update({ reset: false, invalidateAll: false });
|
||
if (submitter instanceof HTMLButtonElement) submitter.disabled = false;
|
||
};
|
||
};
|
||
</script>
|
||
|
||
<svelte:window onkeydown={handleWindowKeydown} />
|
||
|
||
<svelte:head><title>Live | {data.live.show.title} | Sendewerk</title></svelte:head>
|
||
|
||
<div class:without-rail={!showRail} class="live-page live-controller">
|
||
<header class="live-command-header" inert={mobileRailOpen}>
|
||
<div class="live-context-row">
|
||
<div class="live-context-summary">
|
||
<span class:paused={data.live.isPaused} class="live-pulse" aria-hidden="true"></span>
|
||
<button
|
||
type="button"
|
||
class:expanded={expandedContext === 'section'}
|
||
class="live-context-title section"
|
||
aria-label={`Aktueller Abschnitt: ${currentSectionTitle}`}
|
||
aria-expanded={expandedContext === 'section'}
|
||
onclick={() => toggleContext('section')}
|
||
>
|
||
<span>{currentSectionTitle}</span>
|
||
</button>
|
||
<div class="live-next-inline">
|
||
<small>Danach:</small>
|
||
<button
|
||
type="button"
|
||
class:expanded={expandedContext === 'next'}
|
||
class="live-context-title next"
|
||
aria-label={`Danach: ${nextTitle}`}
|
||
aria-expanded={expandedContext === 'next'}
|
||
onclick={() => toggleContext('next')}
|
||
>
|
||
<span>{nextTitle}</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<button
|
||
bind:this={liveMenuTrigger}
|
||
class="live-menu-trigger"
|
||
type="button"
|
||
aria-haspopup="dialog"
|
||
aria-controls="live-menu-popover"
|
||
aria-expanded={menuOpen}
|
||
onclick={toggleLiveMenu}
|
||
>
|
||
<AppIcon name="bars" />
|
||
<span>Menü</span>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="live-status-row">
|
||
<div class:active={Boolean(presentedUsage)} class="live-output-control">
|
||
<div class="live-output-state" aria-live="polite">
|
||
<span class="live-output-icon" aria-hidden="true"><AppIcon name="display" /></span>
|
||
<div>
|
||
<small>Ausgabe</small>
|
||
<strong title={presentedUsage?.title ?? undefined}
|
||
>{presentedUsage?.title ?? 'Leer'}</strong
|
||
>
|
||
</div>
|
||
</div>
|
||
{#if data.live.presentationUsageId}<form
|
||
method="POST"
|
||
action={liveAction('present')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<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" 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 class="timer-label"
|
||
><AppIcon name="timer" /> {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="section-timer-control">
|
||
<div
|
||
class:warning={hasActiveSectionTimer &&
|
||
sectionRemainingSeconds <= 120 &&
|
||
sectionRemainingSeconds >= 0}
|
||
class:overrun={hasActiveSectionTimer && sectionRemainingSeconds < 0}
|
||
class="section-countdown"
|
||
aria-live="off"
|
||
>
|
||
<span class="timer-label"><AppIcon name="timer" /> Abschnitt</span>
|
||
<strong>
|
||
{#if hasActiveSectionTimer}
|
||
{sectionRemainingSeconds < 0
|
||
? `+${formatCountdown(Math.abs(sectionRemainingSeconds))}`
|
||
: formatCountdown(sectionRemainingSeconds)}
|
||
{:else}Kein Countdown{/if}
|
||
</strong>
|
||
</div>
|
||
{#if data.live.startedAtUtc && !data.live.endedAtUtc}<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:paused={data.live.isPaused}
|
||
class="live-timer-toggle"
|
||
type="submit"
|
||
aria-label={data.live.isPaused ? 'Sendung fortsetzen' : 'Sendung pausieren'}
|
||
title={data.live.isPaused ? 'Sendung fortsetzen' : 'Sendung pausieren'}
|
||
>
|
||
<AppIcon name={data.live.isPaused ? 'play' : 'pause'} />
|
||
</button>
|
||
</form>{/if}
|
||
</div>
|
||
|
||
<button
|
||
bind:this={mobileRunButton}
|
||
class:active={runExpanded}
|
||
class="live-run-button"
|
||
type="button"
|
||
aria-controls="live-run-sheet"
|
||
aria-expanded={runExpanded}
|
||
onclick={toggleRunPanel}
|
||
>
|
||
<AppIcon name="list" />
|
||
<span>Ablauf · {currentPosition || data.live.completedCount}/{data.live.totalCount}</span>
|
||
<AppIcon name="chevron-right" />
|
||
</button>
|
||
</div>
|
||
</header>
|
||
|
||
{#if menuOpen}<div
|
||
bind:this={liveMenuPopover}
|
||
class="live-menu-popover"
|
||
id="live-menu-popover"
|
||
role="dialog"
|
||
aria-label="Regiemenü"
|
||
inert={mobileRailOpen}
|
||
>
|
||
<section class="live-menu-show">
|
||
<small>Sendung</small>
|
||
<strong>{data.live.show.title}</strong>
|
||
</section>
|
||
<section>
|
||
<strong>Ansicht</strong>
|
||
<div class="font-control" aria-label="Prompter-Schriftgröße">
|
||
<button
|
||
type="button"
|
||
onclick={() => changePromptSize(-0.1)}
|
||
aria-label="Schrift verkleinern">A−</button
|
||
>
|
||
<button
|
||
type="button"
|
||
onclick={() => changePromptSize(0.1)}
|
||
aria-label="Schrift vergrößern">A+</button
|
||
>
|
||
</div>
|
||
<button type="button" class="text-button" aria-pressed={!showTimes} onclick={toggleTimes}>
|
||
{showTimes ? 'Zeiten ausblenden' : 'Zeiten einblenden'}
|
||
</button>
|
||
<ThemeToggle />
|
||
</section>
|
||
<section>
|
||
<strong>Ausgabekanal</strong>
|
||
{#if data.canManageShares}<ShareManagement
|
||
shows={[{ ...data.live.show, startsAtUtc: null }]}
|
||
shares={data.shares}
|
||
fixedShowId={data.live.show.id}
|
||
{form}
|
||
/>{/if}
|
||
<form method="POST" action={liveAction('createOutput')} use:enhance={enhanceOutput}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<button class="secondary-button" type="submit">Ausgabe verbinden und öffnen</button>
|
||
</form>
|
||
<form method="POST" action={liveAction('rotateOutput')} use:enhance={enhanceOutput}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<button class="text-button" type="submit">Schlüssel erneuern</button>
|
||
</form>
|
||
<form method="POST" action={liveAction('clearOutput')} use:enhance={enhanceCommand}>
|
||
<button class="text-button danger-text" type="submit">Ausgabekanal trennen</button>
|
||
</form>
|
||
</section>
|
||
<section>
|
||
<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}
|
||
{#if data.live.show.status === 'completed'}
|
||
<p class="quiet-copy">Diese Sendung ist historisch abgeschlossen und unveränderlich.</p>
|
||
<a class="secondary-button" href={resolve('/sendungen/[id]', { id: data.live.show.id })}
|
||
>Historische Sendung öffnen</a
|
||
>
|
||
{:else}
|
||
<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>
|
||
<button
|
||
class="primary-button"
|
||
type="button"
|
||
onclick={() => completionDialog?.showModal()}
|
||
>
|
||
<AppIcon name="box-archive" /> Sendung abschließen
|
||
</button>
|
||
{/if}
|
||
{: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}
|
||
>
|
||
<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">Live-Stand wiederaufnehmen</button>
|
||
</form>{/if}
|
||
<a href={resolve('/sendungen/[id]', { id: data.live.show.id })}
|
||
>Liveansicht verlassen · Zur Planung</a
|
||
>
|
||
</section>
|
||
</div>{/if}
|
||
|
||
<div class="live-messages">
|
||
{#if form?.error}<div class="live-message form-message error" role="alert">
|
||
{form.error}
|
||
</div>{/if}
|
||
{#if popupMessage}<div class="live-message form-message" role="status">{popupMessage}</div>{/if}
|
||
</div>
|
||
|
||
<dialog bind:this={completionDialog} class="completion-dialog">
|
||
<form method="POST" action={liveAction('finalize')} use:enhance={enhanceCommand}>
|
||
<header>
|
||
<p class="eyebrow">Unveränderliche Historie</p>
|
||
<h2>Sendung abschließen</h2>
|
||
</header>
|
||
<p>
|
||
Der aktuelle Ablauf wird dauerhaft eingefroren. Übersprungene Pieces zählen nicht als
|
||
gesendet.
|
||
</p>
|
||
{#if data.completion}
|
||
<div class="completion-counts">
|
||
<span><strong>{data.completion.doneCount}</strong> gesendet</span><span
|
||
><strong>{data.completion.skippedCount}</strong> übersprungen</span
|
||
><span><strong>{data.completion.openCount}</strong> offen</span>
|
||
</div>
|
||
<fieldset>
|
||
<legend>Originale bewusst in den Fundus-Archivbereich legen</legend>
|
||
{#each data.completion.candidates as candidate (candidate.pieceId)}<label
|
||
class="check-row"
|
||
><input type="checkbox" name="archivePieceIds" value={candidate.pieceId} /><span
|
||
><strong>{candidate.title}</strong>{#if candidate.usageCount > 1}<small
|
||
>{candidate.usageCount} gesendete Verwendungen</small
|
||
>{/if}</span
|
||
></label
|
||
>{/each}
|
||
{#if !data.completion.candidates.length}<p class="quiet-copy">
|
||
Keine archivierbaren Originale vorhanden.
|
||
</p>{/if}
|
||
</fieldset>
|
||
{/if}
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<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} />
|
||
<footer>
|
||
<button class="text-button" type="button" onclick={() => completionDialog?.close()}
|
||
>Abbrechen</button
|
||
><button class="primary-button" type="submit">Historischen Stand abschließen</button>
|
||
</footer>
|
||
</form>
|
||
</dialog>
|
||
|
||
<div class="live-workspace">
|
||
{#if viewedUsage}<article
|
||
class="live-current"
|
||
style={`--prompt-size: ${promptSize}rem`}
|
||
aria-labelledby="current-title"
|
||
inert={mobileRailOpen}
|
||
>
|
||
<div class="live-current-scroll" bind:this={pieceScroller} onscroll={persistSettings}>
|
||
<div class="live-piece-heading">
|
||
<h1 id="current-title">{viewedUsage.title}</h1>
|
||
{#if showTimes}<strong class="live-piece-time">
|
||
{formatClock(viewedActualSeconds)} / {formatClock(viewedUsage.durationSeconds)}
|
||
</strong>{/if}
|
||
</div>
|
||
{#if viewedSection?.twitchTags.length}<button
|
||
class="text-button live-twitch-tags"
|
||
type="button"
|
||
onclick={copyTwitchTags}
|
||
>
|
||
<AppIcon name="copy" />
|
||
{viewedSection.twitchTags.join(' · ')}
|
||
</button>{/if}
|
||
|
||
{#if viewedUsage.introHtml}<section class="live-script-section live-intro">
|
||
<span>Einleitung</span>
|
||
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||
<div class="prompter-copy">{@html viewedUsage.introHtml}</div>
|
||
</section>{/if}
|
||
{#if viewedUsage.moderationHtml}<section class="live-script-section moderation-card">
|
||
<span>Moderationsstichpunkte</span>
|
||
<!-- 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 -->
|
||
<div class="live-markdown">{@html viewedUsage.transitionHtml}</div>
|
||
</section>{/if}
|
||
|
||
{#if viewedUsage.researchHtml || viewedUsage.sources.length || viewedUsage.tools.length || viewedUsage.ownNotes}
|
||
<!-- External source and tool URLs are server-validated private snapshots. -->
|
||
<!-- eslint-disable svelte/no-navigation-without-resolve -->
|
||
<div class="live-support-actions" aria-label="Private Zusatzinformationen">
|
||
{#if viewedUsage.researchHtml}<details class="research-details">
|
||
<summary>Recherche</summary>
|
||
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
||
<div class="live-markdown">{@html viewedUsage.researchHtml}</div>
|
||
</details>{/if}
|
||
{#if viewedUsage.sources.length}<details class="live-materials">
|
||
<summary>Quellen · {viewedUsage.sources.length}</summary>
|
||
<ul>
|
||
{#each viewedUsage.sources as source (source.id)}<li>
|
||
<!-- The URL is a server-validated private HTTP(S) snapshot. -->
|
||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||
<a href={source.url} target="_blank" rel="noopener noreferrer"
|
||
>{source.label}</a
|
||
>
|
||
{#if source.timecodeSeconds !== null}<small
|
||
>ab {formatClock(source.timecodeSeconds)}</small
|
||
>{/if}
|
||
</li>{/each}
|
||
</ul>
|
||
</details>{/if}
|
||
{#if viewedUsage.tools.length}<details class="live-materials">
|
||
<summary>Werkzeuge · {viewedUsage.tools.length}</summary>
|
||
<ul>
|
||
{#each viewedUsage.tools as tool (tool.id)}<li>
|
||
{#if tool.workUrl || tool.startUrl}<!-- The URL is a server-validated private HTTP(S) snapshot. -->
|
||
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
||
<a
|
||
href={tool.workUrl || tool.startUrl}
|
||
target="_blank"
|
||
rel="noopener noreferrer">{tool.name}</a
|
||
>
|
||
{:else}<span>{tool.name}</span><small>Kein Link hinterlegt</small>{/if}
|
||
{#if tool.note}<small>{tool.note}</small>{/if}
|
||
</li>{/each}
|
||
</ul>
|
||
</details>{/if}
|
||
{#if viewedUsage.ownNotes}<details class="live-materials">
|
||
<summary>Eigene Notizen</summary>
|
||
<p>{viewedUsage.ownNotes}</p>
|
||
</details>{/if}
|
||
</div>
|
||
<!-- eslint-enable svelte/no-navigation-without-resolve -->
|
||
{/if}
|
||
{#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>
|
||
</article>{:else}<section class="live-current live-empty" inert={mobileRailOpen}>
|
||
<div>
|
||
<p class="eyebrow">{sessionComplete ? 'Ablauf abgeschlossen' : 'Live-Stand'}</p>
|
||
<h1>
|
||
{sessionComplete
|
||
? 'Alle Pieces bearbeitet'
|
||
: data.live.totalCount
|
||
? 'Bereit für die Sendung'
|
||
: 'Noch kein Piece eingeplant'}
|
||
</h1>
|
||
<p>
|
||
{sessionComplete
|
||
? 'Der Live-Ablauf ist am Ende. Die Sendung wird hier nicht automatisch abgeschlossen.'
|
||
: data.live.totalCount
|
||
? 'Starte oder setze den gespeicherten Live-Stand fort.'
|
||
: 'In der Planung kann zuerst ein sendefertiges Piece hinzugefügt werden.'}
|
||
</p>
|
||
{#if !data.live.startedAtUtc || needsResume}<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="primary-button" type="submit">
|
||
{data.live.startedAtUtc ? 'Live-Stand wiederaufnehmen' : 'Sendung starten'}
|
||
</button>
|
||
</form>{/if}
|
||
</div>
|
||
</section>{/if}
|
||
|
||
<button
|
||
class:visible={mobileRailOpen}
|
||
class="live-rail-backdrop"
|
||
type="button"
|
||
aria-label="Ablauf schließen"
|
||
onclick={() => closeMobileRail()}
|
||
></button>
|
||
<aside
|
||
bind:this={liveRail}
|
||
class:open={mobileRailOpen}
|
||
class="live-rail"
|
||
id="live-run-sheet"
|
||
role={mobileRailOpen ? 'dialog' : undefined}
|
||
aria-modal={mobileRailOpen ? 'true' : undefined}
|
||
aria-labelledby="live-run-title"
|
||
onkeydown={trapRailFocus}
|
||
>
|
||
<header>
|
||
<div>
|
||
<strong id="live-run-title">Ablauf</strong>
|
||
<span>{data.live.completedCount}/{data.live.totalCount} bearbeitet · {progress}%</span>
|
||
</div>
|
||
<button
|
||
bind:this={railCloseButton}
|
||
class="dialog-close"
|
||
type="button"
|
||
aria-label="Ablauf schließen"
|
||
onclick={() => closeMobileRail()}><AppIcon name="xmark" /></button
|
||
>
|
||
</header>
|
||
<div class="progress-track"><span style={`width:${progress}%`}></span></div>
|
||
<div class="live-run-scroll" bind:this={railScroller}>
|
||
{#each data.live.sections as section, sectionIndex (section.id)}
|
||
<details
|
||
class:active={section.id === data.live.current?.sectionId}
|
||
class="rail-section"
|
||
open={section.id === data.live.current?.sectionId ||
|
||
(!data.live.current &&
|
||
sectionIndex ===
|
||
data.live.sections.findIndex((candidate) => candidate.usages.length))}
|
||
>
|
||
<summary>
|
||
<span>{String(sectionIndex + 1).padStart(2, '0')}</span>
|
||
<div>
|
||
<strong>{section.name}</strong>
|
||
<small
|
||
>{section.usages.length} Pieces{#if showTimes}
|
||
· {formatDuration(section.durationSeconds)}{/if}</small
|
||
>
|
||
</div>
|
||
</summary>
|
||
<div class="rail-section-items">
|
||
{#each section.usages as usage, usageIndex (usage.id)}
|
||
<div
|
||
class:current={usage.id === data.live.current?.id}
|
||
class:selected={usage.id === viewedUsage?.id}
|
||
class:presented={usage.id === data.live.presentationUsageId}
|
||
class:done={usage.status === 'done'}
|
||
class:skipped={usage.status === 'skipped'}
|
||
class="live-run-item"
|
||
data-current={usage.id === data.live.current?.id}
|
||
>
|
||
<button
|
||
class="live-run-select"
|
||
type="button"
|
||
disabled={!data.live.startedAtUtc || usage.id === viewedUsage?.id}
|
||
aria-label={`${usage.title} in der Regie auswählen`}
|
||
onclick={() => viewUsage(usage.id)}
|
||
>
|
||
<strong>{usage.title}</strong>
|
||
<small
|
||
>{statusLabel(usage.status)}{usage.id === data.live.presentationUsageId
|
||
? ' · Ausgegeben'
|
||
: ''}</small
|
||
>
|
||
</button>
|
||
<div class="live-run-actions">
|
||
{#if usage.status === 'done' || usage.status === 'skipped'}<form
|
||
method="POST"
|
||
action={liveAction('resumeUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<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
|
||
class="text-button"
|
||
type="submit"
|
||
disabled={data.live.isPaused || Boolean(data.live.endedAtUtc)}
|
||
>Wiederaufnehmen</button
|
||
>
|
||
</form>{/if}
|
||
{#if usage.status === 'open'}<details class="live-run-order">
|
||
<summary aria-label={`${usage.title} sortieren`}
|
||
><AppIcon name="ellipsis" /></summary
|
||
>
|
||
<div>
|
||
<form
|
||
data-keep-rail-open="true"
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input
|
||
type="hidden"
|
||
name="showRevision"
|
||
value={data.live.show.revision}
|
||
/>
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value={section.id} />
|
||
<input type="hidden" name="position" value={usageIndex - 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={usageIndex === 0 ||
|
||
section.usages[usageIndex - 1]?.status !== 'open'}
|
||
aria-label={`${usage.title} nach oben`}
|
||
><AppIcon name="arrow-up" /></button
|
||
>
|
||
</form>
|
||
<form
|
||
data-keep-rail-open="true"
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input
|
||
type="hidden"
|
||
name="showRevision"
|
||
value={data.live.show.revision}
|
||
/>
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value={section.id} />
|
||
<input type="hidden" name="position" value={usageIndex + 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={usageIndex === section.usages.length - 1 ||
|
||
section.usages[usageIndex + 1]?.status !== 'open'}
|
||
aria-label={`${usage.title} nach unten`}
|
||
><AppIcon name="arrow-down" /></button
|
||
>
|
||
</form>
|
||
</div>
|
||
</details>{/if}
|
||
<form method="POST" action={liveAction('present')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<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"
|
||
disabled={Boolean(data.live.endedAtUtc)}
|
||
aria-label={`${usage.title} präsentieren`}
|
||
><AppIcon name="display" /></button
|
||
>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
{#if !section.usages.length}<p>Keine Pieces in diesem Abschnitt.</p>{/if}
|
||
</div>
|
||
</details>
|
||
{/each}
|
||
|
||
<details class="live-reserve">
|
||
<summary><span>Reserve</span><strong>{data.live.reserve.length}</strong></summary>
|
||
<div>
|
||
{#each data.live.reserve as usage, reserveIndex (usage.id)}
|
||
<div class="live-reserve-item">
|
||
<div>
|
||
<strong>{usage.title}</strong>{#if showTimes}<small
|
||
>{formatClock(usage.durationSeconds)}</small
|
||
>{/if}
|
||
</div>
|
||
<div class="live-reserve-actions">
|
||
<details class="live-run-order">
|
||
<summary aria-label={`${usage.title} in Reserve sortieren`}
|
||
><AppIcon name="ellipsis" /></summary
|
||
>
|
||
<div>
|
||
<form
|
||
data-keep-rail-open="true"
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value="" />
|
||
<input type="hidden" name="position" value={reserveIndex - 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={reserveIndex === 0}
|
||
aria-label={`${usage.title} in Reserve nach oben`}
|
||
><AppIcon name="arrow-up" /></button
|
||
>
|
||
</form>
|
||
<form
|
||
data-keep-rail-open="true"
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input type="hidden" name="sectionId" value="" />
|
||
<input type="hidden" name="position" value={reserveIndex + 1} />
|
||
<button
|
||
type="submit"
|
||
disabled={reserveIndex === data.live.reserve.length - 1}
|
||
aria-label={`${usage.title} in Reserve nach unten`}
|
||
><AppIcon name="arrow-down" /></button
|
||
>
|
||
</form>
|
||
</div>
|
||
</details>
|
||
{#if data.live.current || data.live.sections[0]}<form
|
||
method="POST"
|
||
action={liveAction('moveUsage')}
|
||
use:enhance={enhanceCommand}
|
||
>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<input type="hidden" name="showRevision" value={data.live.show.revision} />
|
||
<input type="hidden" name="usageId" value={usage.id} />
|
||
<input
|
||
type="hidden"
|
||
name="sectionId"
|
||
value={data.live.current?.sectionId ?? data.live.sections[0]?.id}
|
||
/>
|
||
<input
|
||
type="hidden"
|
||
name="position"
|
||
value={(data.live.current?.position ?? -1) + 1}
|
||
/>
|
||
<button class="text-button" type="submit">Als Nächstes</button>
|
||
</form>{/if}
|
||
</div>
|
||
</div>
|
||
{/each}
|
||
{#if !data.live.reserve.length}<p>Keine Pieces in Reserve.</p>{/if}
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
|
||
<footer class="live-action-bar" inert={mobileRailOpen}>
|
||
<div class="live-footer-actions">
|
||
{#if data.live.current && viewedUsage}<button
|
||
class="secondary-button"
|
||
type="button"
|
||
disabled={!previousUsage}
|
||
onclick={viewPreviousUsage}>Zurück</button
|
||
>
|
||
<form method="POST" action={liveAction('present')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<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" disabled={Boolean(data.live.endedAtUtc)}>
|
||
{data.live.presentationUsageId === viewedUsage.id
|
||
? 'Erneut präsentieren'
|
||
: 'Präsentieren'}
|
||
</button>
|
||
</form>
|
||
{#if (viewedUsage.status === 'done' || viewedUsage.status === 'skipped') && !viewingLiveCurrent}
|
||
<form method="POST" action={liveAction('resumeUsage')} use:enhance={enhanceCommand}>
|
||
<input type="hidden" name="showId" value={data.live.show.id} />
|
||
<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="primary-button"
|
||
type="submit"
|
||
disabled={data.live.isPaused || Boolean(data.live.endedAtUtc)}>Wiederaufnehmen</button
|
||
>
|
||
</form>
|
||
{:else}
|
||
<form method="POST" action={liveAction('skip')} 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"
|
||
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
|
||
title={!viewingLiveCurrent
|
||
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
|
||
: undefined}>Überspringen</button
|
||
>
|
||
</form>
|
||
<form method="POST" action={liveAction('complete')} 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="primary-button"
|
||
type="submit"
|
||
disabled={!viewingLiveCurrent || data.live.isPaused || Boolean(data.live.endedAtUtc)}
|
||
title={!viewingLiveCurrent
|
||
? 'Zuerst im Ablauf zum aktuellen Live-Piece zurückkehren'
|
||
: undefined}
|
||
>
|
||
{data.live.next
|
||
? 'Erledigen & weiter'
|
||
: hasOtherOpenUsages
|
||
? 'Erledigen'
|
||
: 'Erledigen & Sendungsende'}
|
||
</button>
|
||
</form>
|
||
{/if}
|
||
{:else if sessionComplete}<strong class="live-finished-label">Ablauf abgeschlossen</strong
|
||
>{/if}
|
||
</div>
|
||
</footer>
|
||
</div>
|