appview-less bluesky client
1<script lang="ts">
2 import { isBlob } from '@atcute/lexicons/interfaces';
3 import PhotoSwipeGallery, { type GalleryItem } from './PhotoSwipeGallery.svelte';
4 import { blob, img } from '$lib/cdn';
5 import { type Did } from '@atcute/lexicons';
6 import { resolveDidDoc } from '$lib/at/client.svelte';
7 import type { AppBskyEmbedMedia } from '$lib/at/types';
8
9 interface Props {
10 did: Did;
11 embed: AppBskyEmbedMedia;
12 }
13
14 let { did, embed }: Props = $props();
15
16 let videoPds = $state<string | undefined>();
17
18 $effect(() => {
19 if (embed.$type === 'app.bsky.embed.video' && isBlob(embed.video)) {
20 resolveDidDoc(did).then((didDoc) => {
21 if (didDoc.ok) videoPds = didDoc.value.pds;
22 });
23 }
24 });
25</script>
26
27<!-- svelte-ignore a11y_no_static_element_interactions -->
28<div oncontextmenu={(e) => e.stopPropagation()}>
29 {#if embed.$type === 'app.bsky.embed.images'}
30 {@const _images = embed.images.flatMap((img) =>
31 isBlob(img.image) ? [{ ...img, image: img.image }] : []
32 )}
33 {@const images = _images.map((i): GalleryItem => {
34 const size = i.aspectRatio;
35 const cid = i.image.ref.$link;
36 return {
37 ...size,
38 src: img('feed_fullsize', did, cid),
39 thumbnail: {
40 src: img('feed_thumbnail', did, cid),
41 ...size
42 },
43 alt: i.alt
44 };
45 })}
46 {#if images.length > 0}
47 <PhotoSwipeGallery {images} />
48 {/if}
49 {:else if embed.$type === 'app.bsky.embed.video'}
50 {#if isBlob(embed.video)}
51 {@const ratio = embed.aspectRatio}
52 <div
53 class="relative w-full overflow-hidden rounded-sm bg-black/5"
54 style:aspect-ratio={ratio ? `${ratio.width} / ${ratio.height}` : '16 / 9'}
55 >
56 {#if videoPds}
57 <!-- svelte-ignore a11y_media_has_caption -->
58 <video
59 class="absolute inset-0 h-full w-full"
60 src={blob(videoPds, did, embed.video.ref.$link)}
61 controls
62 playsinline
63 loop
64 ></video>
65 {/if}
66 </div>
67 {/if}
68 {/if}
69</div>