Files
plspace/src/components/blog/Attachments.svelte
T

101 lines
3.3 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
/**
* Media on a status.
*
* Sensitive media is blurred rather than hidden so the layout doesn't jump
* when it's revealed, and the reveal is per-attachment because a single post
* can mix flagged and unflagged media.
*/
import type { CustomEmoji, MediaAttachment } from '$lib/api/types'
import EmojiText from '../common/EmojiText.svelte'
interface Props {
attachments: MediaAttachment[]
emojis?: CustomEmoji[]
sensitive?: boolean
}
let { attachments, emojis, sensitive = false }: Props = $props()
let revealed = $state<Record<string, boolean>>({})
function isRevealed(id: string): boolean {
return !sensitive || revealed[id] === true
}
function toggle(id: string): void {
revealed = { ...revealed, [id]: !revealed[id] }
}
/**
* Ask the browser to decode the first real frame while it loads metadata.
* The tiny media-fragment seek also makes the video's intrinsic dimensions
* available before playback on browsers that otherwise leave it at 300×150.
*/
function videoPreviewUrl(url: string): string {
const fragment = url.indexOf('#')
const base = fragment >= 0 ? url.slice(0, fragment) : url
return `${base}#t=0.001`
}
</script>
{#if attachments.length > 0}
<ul class="attachment-list">
{#each attachments as media (media.id)}
<li
class="attachment"
data-type={media.type}
data-sensitive={sensitive ? 'true' : 'false'}
data-revealed={isRevealed(media.id) ? 'true' : 'false'}
>
<figure class="attachment-figure">
{#if media.type === 'video' || media.type === 'gifv'}
<video
class="attachment-media"
src={videoPreviewUrl(media.url)}
poster={media.preview_url ?? undefined}
controls
playsinline
loop={media.type === 'gifv'}
preload="metadata"
>
<!-- Remote media carries no caption track; declared so the
requirement is explicit rather than merely unmet. -->
<track kind="captions" />
</video>
{:else if media.type === 'audio'}
<audio class="attachment-media attachment-media--audio" src={media.url} controls preload="none"
></audio>
{:else if media.type === 'image'}
<a href={media.url} target="_blank" rel="noopener noreferrer">
<img
class="attachment-media"
src={media.preview_url ?? media.url}
alt={media.description ?? ''}
loading="lazy"
decoding="async"
/>
</a>
{:else}
<a class="attachment-media attachment-media--file" href={media.url} target="_blank" rel="noopener noreferrer">
Attachment
</a>
{/if}
{#if media.description}
<figcaption class="attachment-caption">
<EmojiText text={media.description} {emojis} />
</figcaption>
{/if}
</figure>
{#if sensitive}
<button type="button" class="button button--small attachment-reveal" onclick={() => toggle(media.id)}>
{isRevealed(media.id) ? 'Hide' : 'Show'} sensitive media
</button>
{/if}
</li>
{/each}
</ul>
{/if}