From 5e0981fb71c5168f66911c45e589cb96f1247e01 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Fri, 13 Feb 2026 01:10:17 +0100 Subject: [PATCH] grain photo gallery --- .../CreateGrainGalleryCardModal.svelte | 62 ++++ .../PhotoGalleryCard/PhotoGalleryCard.svelte | 29 +- .../cards/media/PhotoGalleryCard/helpers.ts | 78 +++++ src/lib/cards/media/PhotoGalleryCard/index.ts | 81 ++---- .../image-viewer/ImageViewerModal.svelte | 40 +++ .../image-viewer/ImageViewerProvider.svelte | 23 ++ .../image-viewer/imageViewer.svelte.ts | 14 + src/lib/website/EditableWebsite.svelte | 275 +----------------- src/lib/website/Website.svelte | 2 + 9 files changed, 260 insertions(+), 344 deletions(-) create mode 100644 src/lib/cards/media/PhotoGalleryCard/CreateGrainGalleryCardModal.svelte create mode 100644 src/lib/cards/media/PhotoGalleryCard/helpers.ts create mode 100644 src/lib/components/image-viewer/ImageViewerModal.svelte create mode 100644 src/lib/components/image-viewer/ImageViewerProvider.svelte create mode 100644 src/lib/components/image-viewer/imageViewer.svelte.ts diff --git a/src/lib/cards/media/PhotoGalleryCard/CreateGrainGalleryCardModal.svelte b/src/lib/cards/media/PhotoGalleryCard/CreateGrainGalleryCardModal.svelte new file mode 100644 index 0000000..0a7c61c --- /dev/null +++ b/src/lib/cards/media/PhotoGalleryCard/CreateGrainGalleryCardModal.svelte @@ -0,0 +1,62 @@ + + + +
{ + if (await checkUrl()) oncreate(); + }} + class="flex flex-col gap-2" + > + Enter a grain.social gallery URL + + + {#if errorMessage} + {errorMessage} + {/if} + +
+ + +
+
+
diff --git a/src/lib/cards/media/PhotoGalleryCard/PhotoGalleryCard.svelte b/src/lib/cards/media/PhotoGalleryCard/PhotoGalleryCard.svelte index 43514ee..feddca6 100644 --- a/src/lib/cards/media/PhotoGalleryCard/PhotoGalleryCard.svelte +++ b/src/lib/cards/media/PhotoGalleryCard/PhotoGalleryCard.svelte @@ -1,16 +1,12 @@ + + + +{#if open} + +
{ + if (e.key === 'Escape') close(); + }} + role="dialog" + aria-modal="true" + > + + e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + /> +
+{/if} diff --git a/src/lib/components/image-viewer/ImageViewerProvider.svelte b/src/lib/components/image-viewer/ImageViewerProvider.svelte new file mode 100644 index 0000000..2b62f3e --- /dev/null +++ b/src/lib/components/image-viewer/ImageViewerProvider.svelte @@ -0,0 +1,23 @@ + + + diff --git a/src/lib/components/image-viewer/imageViewer.svelte.ts b/src/lib/components/image-viewer/imageViewer.svelte.ts new file mode 100644 index 0000000..ecb4edd --- /dev/null +++ b/src/lib/components/image-viewer/imageViewer.svelte.ts @@ -0,0 +1,14 @@ +// Global state for fullscreen image viewer +let openFn: ((src: string) => void) | null = null; + +export function registerImageViewer(fn: (src: string) => void) { + openFn = fn; +} + +export function unregisterImageViewer() { + openFn = null; +} + +export function openImageViewer(src: string) { + openFn?.(src); +} diff --git a/src/lib/website/EditableWebsite.svelte b/src/lib/website/EditableWebsite.svelte index 7d3af0c..a811cdd 100644 --- a/src/lib/website/EditableWebsite.svelte +++ b/src/lib/website/EditableWebsite.svelte @@ -34,6 +34,7 @@ import { launchConfetti } from '@foxui/visual'; import Controls from './Controls.svelte'; import CardCommand from '$lib/components/card-command/CardCommand.svelte'; + import ImageViewerProvider from '$lib/components/image-viewer/ImageViewerProvider.svelte'; import { SvelteMap } from 'svelte/reactivity'; import { fixCollisions, @@ -244,279 +245,6 @@ } } - function addAllCardTypes() { - const groupOrder = ['Core', 'Social', 'Media', 'Content', 'Visual', 'Utilities', 'Games']; - const grouped = new SvelteMap(); - - for (const def of AllCardDefinitions) { - if (!def.name) continue; - const group = def.groups?.[0] ?? 'Other'; - if (!grouped.has(group)) grouped.set(group, []); - grouped.get(group)!.push(def); - } - - // Sort groups by predefined order, unknowns at end - const sortedGroups = [...grouped.keys()].sort((a, b) => { - const ai = groupOrder.indexOf(a); - const bi = groupOrder.indexOf(b); - return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi); - }); - - // Sample data for cards that would otherwise render empty - const sampleData: Record> = { - text: { text: 'The quick brown fox jumps over the lazy dog. This is a sample text card.' }, - link: { - href: 'https://bsky.app', - title: 'Bluesky', - domain: 'bsky.app', - description: 'Social networking that gives you choice', - hasFetched: true - }, - image: { - image: 'https://images.unsplash.com/photo-1506744038136-46273834b3fb?w=600', - alt: 'Mountain landscape' - }, - button: { text: 'Visit Bluesky', href: 'https://bsky.app' }, - bigsocial: { platform: 'bluesky', href: 'https://bsky.app', color: '0085ff' }, - blueskyPost: { - uri: 'at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.post/3jt64kgkbbs2y', - href: 'https://bsky.app/profile/bsky.app/post/3jt64kgkbbs2y' - }, - blueskyProfile: { - handle: 'bsky.app', - displayName: 'Bluesky', - avatar: - 'https://cdn.bsky.app/img/avatar/plain/did:plc:z72i7hdynmk6r22z27h6tvur/bafkreihagr2cmvl2jt4mgx3sppwe2it3fwolkrbtjrhcnwjk4pcnbaq53m@jpeg' - }, - blueskyMedia: {}, - latestPost: {}, - youtubeVideo: { - youtubeId: 'dQw4w9WgXcQ', - poster: 'https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg', - href: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', - showInline: true - }, - 'spotify-list-embed': { - spotifyType: 'album', - spotifyId: '4aawyAB9vmqN3uQ7FjRGTy', - href: 'https://open.spotify.com/album/4aawyAB9vmqN3uQ7FjRGTy' - }, - latestLivestream: {}, - livestreamEmbed: { - href: 'https://stream.place/', - embed: 'https://stream.place/embed/' - }, - mapLocation: { lat: 48.8584, lon: 2.2945, zoom: 13, name: 'Eiffel Tower, Paris' }, - gif: { url: 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.mp4', alt: 'Cat typing' }, - event: { - uri: 'at://did:plc:257wekqxg4hyapkq6k47igmp/community.lexicon.calendar.event/3mcsoqzy7gm2q' - }, - guestbook: { label: 'Guestbook' }, - githubProfile: { user: 'sveltejs', href: 'https://github.com/sveltejs' }, - photoGallery: { - galleryUri: 'at://did:plc:tas6hj2xjrqben5653v5kohk/social.grain.gallery/3mclhsljs6h2w' - }, - atprotocollections: {}, - publicationList: {}, - recentPopfeedReviews: {}, - recentTealFMPlays: {}, - statusphere: { emoji: '✨' }, - vcard: {}, - 'fluid-text': { text: 'Hello World' }, - draw: { strokesJson: '[]', viewBox: '', strokeWidth: 1, locked: true }, - clock: {}, - countdown: { targetDate: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() }, - timer: {}, - 'dino-game': {}, - tetris: {}, - updatedBlentos: {} - }; - - // Labels for cards that support canHaveLabel - const sampleLabels: Record = { - image: 'Mountain Landscape', - mapLocation: 'Eiffel Tower', - gif: 'Cat Typing', - bigsocial: 'Bluesky', - guestbook: 'Guestbook', - statusphere: 'My Status', - recentPopfeedReviews: 'My Reviews', - recentTealFMPlays: 'Recently Played', - clock: 'Local Time', - countdown: 'Launch Day', - timer: 'Timer', - 'dino-game': 'Dino Game', - tetris: 'Tetris', - blueskyMedia: 'Bluesky Media' - }; - - const newItems: Item[] = []; - let cursorY = 0; - let mobileCursorY = 0; - - for (const group of sortedGroups) { - const defs = grouped.get(group)!; - - // Add a section heading for the group - const heading = createEmptyCard(data.page); - heading.cardType = 'section'; - heading.cardData = { text: group, verticalAlign: 'bottom', textSize: 1 }; - heading.w = COLUMNS; - heading.h = 1; - heading.x = 0; - heading.y = cursorY; - heading.mobileW = COLUMNS; - heading.mobileH = 2; - heading.mobileX = 0; - heading.mobileY = mobileCursorY; - newItems.push(heading); - cursorY += 1; - mobileCursorY += 2; - - // Place cards in rows - let rowX = 0; - let rowMaxH = 0; - let mobileRowX = 0; - let mobileRowMaxH = 0; - - for (const def of defs) { - if (def.type === 'section' || def.type === 'embed') continue; - - const item = createEmptyCard(data.page); - item.cardType = def.type; - item.cardData = {}; - def.createNew?.(item); - - // Merge in sample data (without overwriting createNew defaults) - const extra = sampleData[def.type]; - if (extra) { - item.cardData = { ...item.cardData, ...extra }; - } - - // Set item-level color for cards that need it - if (def.type === 'button') { - item.color = 'transparent'; - } - - // Add label if card supports it - const label = sampleLabels[def.type]; - if (label && def.canHaveLabel) { - item.cardData.label = label; - } - - // Desktop layout - if (rowX + item.w > COLUMNS) { - cursorY += rowMaxH; - rowX = 0; - rowMaxH = 0; - } - item.x = rowX; - item.y = cursorY; - rowX += item.w; - rowMaxH = Math.max(rowMaxH, item.h); - - // Mobile layout - if (mobileRowX + item.mobileW > COLUMNS) { - mobileCursorY += mobileRowMaxH; - mobileRowX = 0; - mobileRowMaxH = 0; - } - item.mobileX = mobileRowX; - item.mobileY = mobileCursorY; - mobileRowX += item.mobileW; - mobileRowMaxH = Math.max(mobileRowMaxH, item.mobileH); - - newItems.push(item); - } - - // Move cursor past last row - cursorY += rowMaxH; - mobileCursorY += mobileRowMaxH; - } - - items = newItems; - onLayoutChanged(); - } - - let copyInput = $state(''); - let isCopying = $state(false); - - async function copyPageFrom() { - const input = copyInput.trim(); - if (!input) return; - - isCopying = true; - try { - // Parse "handle" or "handle/page" - const parts = input.split('/'); - const handle = parts[0]; - const pageName = parts[1] || 'self'; - - const did = await resolveHandle({ handle: handle as `${string}.${string}` }); - if (!did) throw new Error('Could not resolve handle'); - - const records = await listRecords({ did, collection: 'app.blento.card' }); - const targetPage = 'blento.' + pageName; - - const copiedCards: Item[] = records - .map((r) => ({ ...r.value }) as Item) - .filter((card) => { - // v0/v1 cards without page field belong to blento.self - if (!card.page) return targetPage === 'blento.self'; - return card.page === targetPage; - }) - .map((card) => { - // Apply v0→v1 migration (coords were halved in old format) - if (!card.version) { - card.x *= 2; - card.y *= 2; - card.h *= 2; - card.w *= 2; - card.mobileX *= 2; - card.mobileY *= 2; - card.mobileH *= 2; - card.mobileW *= 2; - card.version = 1; - } - - // Convert blob refs to CDN URLs using source DID - if (card.cardData) { - for (const key of Object.keys(card.cardData)) { - const val = card.cardData[key]; - if (val && typeof val === 'object' && val.$type === 'blob') { - const url = getCDNImageBlobUrl({ did, blob: val }); - if (url) card.cardData[key] = url; - } - } - } - - // Regenerate ID and assign to current page - card.id = TID.now(); - card.page = data.page; - return card; - }); - - if (copiedCards.length === 0) { - toast.error('No cards found on that page'); - return; - } - - fixAllCollisions(copiedCards, false); - fixAllCollisions(copiedCards, true); - compactItems(copiedCards, false); - compactItems(copiedCards, true); - - items = copiedCards; - onLayoutChanged(); - toast.success(`Copied ${copiedCards.length} cards from ${handle}`); - } catch (e) { - console.error('Failed to copy page:', e); - toast.error('Failed to copy page'); - } finally { - isCopying = false; - } - } - let linkValue = $state(''); function addLink(url: string, specificCardDef?: CardDefinition) { @@ -769,6 +497,7 @@ + { diff --git a/src/lib/website/Website.svelte b/src/lib/website/Website.svelte index 86567d3..365c948 100644 --- a/src/lib/website/Website.svelte +++ b/src/lib/website/Website.svelte @@ -18,6 +18,7 @@ import Head from './Head.svelte'; import type { Did, Handle } from '@atcute/lexicons'; import QRModalProvider from '$lib/components/qr/QRModalProvider.svelte'; + import ImageViewerProvider from '$lib/components/image-viewer/ImageViewerProvider.svelte'; import EmptyState from './EmptyState.svelte'; import FloatingEditButton from './FloatingEditButton.svelte'; import { user } from '$lib/atproto'; @@ -67,6 +68,7 @@ +
{#if !getHideProfileSection(data)} -- 2.51.2