120 lines
4.8 KiB
Svelte
120 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import type { PresentationContent } from '#lib/server/presentation.js';
|
|
|
|
let {
|
|
content,
|
|
imageSrc = ''
|
|
}: {
|
|
content: PresentationContent & {
|
|
sources?: { label: string; url: string }[];
|
|
images?: { id: string; altText: string }[];
|
|
};
|
|
imageSrc?: string;
|
|
} = $props();
|
|
let frame = $state<HTMLIFrameElement>();
|
|
let playerUnavailable = $state(false);
|
|
let imageUnavailable = $state(false);
|
|
|
|
onMount(() => {
|
|
const receive = (event: MessageEvent) => {
|
|
if (content.provider !== 'tiktok' || event.origin !== 'https://www.tiktok.com') return;
|
|
if (event.source !== frame?.contentWindow || typeof event.data !== 'object' || !event.data)
|
|
return;
|
|
const message = event.data as { type?: string; value?: unknown };
|
|
if (message.type === 'onPlayerReady' && content.source?.timecodeSeconds) {
|
|
frame.contentWindow?.postMessage(
|
|
{
|
|
type: 'seekTo',
|
|
value: content.source.timecodeSeconds,
|
|
'x-tiktok-player': true
|
|
},
|
|
'https://www.tiktok.com'
|
|
);
|
|
}
|
|
if (message.type === 'onPlayerError') playerUnavailable = true;
|
|
};
|
|
addEventListener('message', receive);
|
|
return () => removeEventListener('message', receive);
|
|
});
|
|
</script>
|
|
|
|
{#if content.mode === 'text'}
|
|
<article class="output-content output-text">
|
|
{#if content.topics.length}<p class="output-topics">{content.topics.join(' · ')}</p>{/if}
|
|
<h1>{content.title}</h1>
|
|
<!-- HTML is produced exclusively by the server-side allowlist sanitizer. -->
|
|
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
<div class="output-copy">{@html content.html}</div>
|
|
{#if content.images?.length}<div class="output-images">
|
|
{#each content.images as image (image.id)}
|
|
<img src={`${imageSrc}/${image.id}`} alt={image.altText} />
|
|
{/each}
|
|
</div>{/if}
|
|
{#if content.sources?.length}<footer class="output-sources">
|
|
<span>Quellen</span>
|
|
{#each content.sources as source, index (`${index}-${source.url}`)}
|
|
<!-- The URL is a server-validated external HTTP(S) source. -->
|
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
|
<a href={source.url} target="_blank" rel="noopener noreferrer">{source.label}</a>
|
|
{/each}
|
|
</footer>{/if}
|
|
</article>
|
|
{:else if content.mode === 'image'}
|
|
<article class="output-content output-image-only">
|
|
{#if imageSrc}<img src={imageSrc} alt={content.images?.[0]?.altText ?? content.title} />{/if}
|
|
</article>
|
|
{:else if content.mode === 'link'}
|
|
<article class="output-content output-link-card">
|
|
{#if imageSrc && !imageUnavailable}<div class="output-link-image">
|
|
<img src={imageSrc} alt="" onerror={() => (imageUnavailable = true)} />
|
|
</div>{/if}
|
|
<div class="output-link-copy">
|
|
{#if content.topics.length}<p class="output-topics">{content.topics.join(' · ')}</p>{/if}
|
|
<p class="output-kicker">{content.siteName || content.source?.label || 'Webseite'}</p>
|
|
<h1>{content.previewTitle || content.title}</h1>
|
|
{#if content.description}<p>{content.description}</p>{/if}
|
|
<!-- The URL is a server-validated external HTTP(S) source. -->
|
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
|
{#if content.source}<a href={content.source.url} target="_blank" rel="noopener noreferrer"
|
|
>Original öffnen</a
|
|
>{/if}
|
|
</div>
|
|
</article>
|
|
{:else}
|
|
<article class:portrait={content.provider === 'tiktok'} class="output-content output-video">
|
|
<header>
|
|
{#if content.topics.length}<p class="output-topics">{content.topics.join(' · ')}</p>{/if}
|
|
<p class="output-kicker">{content.siteName || content.source?.label || 'Video'}</p>
|
|
<h1>{content.previewTitle || content.title}</h1>
|
|
</header>
|
|
{#if content.embedUrl && !playerUnavailable}
|
|
<div class="output-player">
|
|
<iframe
|
|
bind:this={frame}
|
|
title={content.previewTitle || content.title}
|
|
src={content.embedUrl}
|
|
allow="encrypted-media; fullscreen; picture-in-picture"
|
|
allowfullscreen
|
|
onerror={() => (playerUnavailable = true)}
|
|
></iframe>
|
|
</div>
|
|
{:else}
|
|
<section class="output-fallback">
|
|
<strong>Video kann nicht eingebettet werden</strong>
|
|
<p>Der Anbieter oder diese URL ist für die eingebettete Wiedergabe nicht verfügbar.</p>
|
|
<!-- The URL is a server-validated external HTTP(S) source. -->
|
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
|
{#if content.source}<a href={content.source.url} target="_blank" rel="noopener noreferrer"
|
|
>Original öffnen</a
|
|
>{/if}
|
|
</section>
|
|
{/if}
|
|
{#if content.source}<footer class="output-video-source">
|
|
<!-- The URL is a server-validated external HTTP(S) source. -->
|
|
<!-- eslint-disable-next-line svelte/no-navigation-without-resolve -->
|
|
<a href={content.source.url} target="_blank" rel="noopener noreferrer">Original öffnen</a>
|
|
</footer>{/if}
|
|
</article>
|
|
{/if}
|