From c0ff5074f5ddd3178c448f34801372641cd8c606 Mon Sep 17 00:00:00 2001 From: dawn <90008@gaze.systems> Date: Fri, 2 Jan 2026 21:29:30 +0300 Subject: [PATCH] implement photo gallery and lightbox --- deno.lock | 5 + package.json | 1 + src/components/BskyPost.svelte | 87 +++++++++-- src/components/PhotoSwipeGallery.svelte | 199 ++++++++++++++++++++++++ src/components/PostComposer.svelte | 5 +- 5 files changed, 276 insertions(+), 21 deletions(-) create mode 100644 src/components/PhotoSwipeGallery.svelte diff --git a/deno.lock b/deno.lock index 1031b1b..96f8b4c 100644 --- a/deno.lock +++ b/deno.lock @@ -32,6 +32,7 @@ "npm:globals@17": "17.0.0", "npm:hash-wasm@^4.12.0": "4.12.0", "npm:lru-cache@^11.2.4": "11.2.4", + "npm:photoswipe@^5.4.4": "5.4.4", "npm:prettier-plugin-svelte@^3.4.1": "3.4.1_prettier@3.7.4_svelte@5.46.1__acorn@8.15.0", "npm:prettier-plugin-tailwindcss@~0.7.2": "0.7.2_prettier@3.7.4_prettier-plugin-svelte@3.4.1__prettier@3.7.4__svelte@5.46.1___acorn@8.15.0_svelte@5.46.1__acorn@8.15.0", "npm:prettier@^3.7.4": "3.7.4", @@ -1486,6 +1487,9 @@ "path-key@3.1.1": { "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, + "photoswipe@5.4.4": { + "integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==" + }, "picocolors@1.1.1": { "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" }, @@ -1869,6 +1873,7 @@ "npm:globals@17", "npm:hash-wasm@^4.12.0", "npm:lru-cache@^11.2.4", + "npm:photoswipe@^5.4.4", "npm:prettier-plugin-svelte@^3.4.1", "npm:prettier-plugin-tailwindcss@~0.7.2", "npm:prettier@^3.7.4", diff --git a/package.json b/package.json index 20f9927..725d3ae 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "async-cache-dedupe": "^3.4.0", "hash-wasm": "^4.12.0", "lru-cache": "^11.2.4", + "photoswipe": "^5.4.4", "svelte-device-info": "^1.0.6", "svelte-infinite": "^0.5.1", "svelte-portal": "^2.2.1" diff --git a/src/components/BskyPost.svelte b/src/components/BskyPost.svelte index 5b379dc..34ed02e 100644 --- a/src/components/BskyPost.svelte +++ b/src/components/BskyPost.svelte @@ -43,6 +43,7 @@ import { getRelativeTime } from '$lib/date'; import { likeSource, repostSource, toCanonicalUri } from '$lib'; import ProfileInfo from './ProfileInfo.svelte'; + import PhotoSwipeGallery, { type GalleryItem } from './PhotoSwipeGallery.svelte'; interface Props { client: AtpClient; @@ -95,16 +96,15 @@ if (!p.ok) return; profile = p.value; profiles.set(did, profile); - // console.log(profile.description); }); - const postId = $derived(`timeline-post-${aturi}-${quoteDepth}`); + const postId = $derived( + `timeline-post-${did.replace(/[^a-zA-Z0-9]/g, '_')}-${rkey}-${quoteDepth}` + ); const isPulsing = derived(pulsingPostId, (pulsingPostId) => pulsingPostId === postId); - // todo: this fucking sucks const scrollToAndPulse = (targetUri: ResourceUri) => { const targetId = `timeline-post-${targetUri}-0`; - // console.log(`Scrolling to ${targetId}`); const element = document.getElementById(targetId); if (!element) return; @@ -116,7 +116,6 @@ generateColorForDid(expect(parseCanonicalResourceUri(targetUri)).repo) ); pulsingPostId.set(targetId); - // Clear pulse after animation setTimeout(() => pulsingPostId.set(null), 1200); }, 400); }; @@ -212,7 +211,6 @@ {/snippet} - {#snippet profilePopout()}
e.stopPropagation()}> {#if embed.$type === 'app.bsky.embed.images'} - - {#each embed.images as image (image.image)} - {#if isBlob(image.image)} - {image.alt} - {/if} - {/each} + {@const _images = embed.images.flatMap((img) => + isBlob(img.image) ? [{ ...img, image: img.image }] : [] + )} + {@const images = _images.map((i): GalleryItem => { + const sizeFactor = 200; + const size = { + width: (i.aspectRatio?.width ?? 4) * sizeFactor, + height: (i.aspectRatio?.height ?? 3) * sizeFactor + }; + return { + ...size, + src: img('feed_fullsize', did, i.image.ref.$link), + thumbnail: { + src: img('feed_thumbnail', did, i.image.ref.$link), + ...size + } + }; + })} + {:else if embed.$type === 'app.bsky.embed.video'} {#if isBlob(embed.video)} {#await didDoc then didDoc} @@ -385,7 +392,6 @@ {@render embedMedia(embed.media)}
{/if} - {/snippet} {#snippet postControls(post: PostWithUri)} @@ -536,4 +542,51 @@ :global(.post-dropdown) { @apply flex min-w-54 flex-col gap-1 rounded-sm border-2 p-1 shadow-2xl backdrop-blur-xl backdrop-brightness-60; } + + .image-grid { + display: grid; + gap: 2px; + border-radius: 0.375rem; + overflow: hidden; + max-height: 500px; + } + + /* 1 image: full width */ + .image-grid.count-1 { + grid-template-columns: 1fr; + } + + /* 2 images: side by side */ + .image-grid.count-2 { + grid-template-columns: repeat(2, 1fr); + } + + /* 3 images: first spans left, two stack on right */ + .image-grid.count-3 { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + } + .image-grid.count-3 a:first-child { + grid-row: 1 / 3; + } + + /* 4+ images: 2x2 grid */ + .image-grid.count-4, + .image-grid.count-5 { + grid-template-columns: repeat(2, 1fr); + grid-template-rows: repeat(2, 1fr); + } + + .image-item { + width: 100%; + height: 100%; + object-fit: cover; + display: block; + cursor: pointer; + transition: opacity 0.2s; + } + + .image-item:hover { + opacity: 0.9; + } diff --git a/src/components/PhotoSwipeGallery.svelte b/src/components/PhotoSwipeGallery.svelte new file mode 100644 index 0000000..25066d2 --- /dev/null +++ b/src/components/PhotoSwipeGallery.svelte @@ -0,0 +1,199 @@ + + + + + + + diff --git a/src/components/PostComposer.svelte b/src/components/PostComposer.svelte index 0c3a22b..9353d19 100644 --- a/src/components/PostComposer.svelte +++ b/src/components/PostComposer.svelte @@ -174,10 +174,7 @@ {_state.text.length} / 300