mirror of
https://git.shipoclu.com/moon/plspace.git
synced 2026-08-13 02:42:30 +00:00
101 lines
3.3 KiB
Svelte
101 lines
3.3 KiB
Svelte
<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}
|