From 5e9d8a4f7228e3393eb14139d9c4c231ed566d8f Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Fri, 19 Jun 2026 13:50:25 -0500 Subject: [PATCH] feat: open blobs in EOG by default --- .changeset/kind-apples-kick.md | 5 + TODO.md | 4 +- src/lib/atproto/blobs.svelte.ts | 35 +++++ src/lib/atproto/blobs.test.ts | 19 +++ src/lib/components/CollectionBrowser.svelte | 10 +- src/lib/components/EyeOfGnome.svelte | 114 ++++++++++++++- src/lib/components/Gedit.svelte | 154 ++++++++++++++++++-- src/routes/+layout.svelte | 2 +- 8 files changed, 327 insertions(+), 16 deletions(-) create mode 100644 .changeset/kind-apples-kick.md diff --git a/.changeset/kind-apples-kick.md b/.changeset/kind-apples-kick.md new file mode 100644 index 0000000..fff658d --- /dev/null +++ b/.changeset/kind-apples-kick.md @@ -0,0 +1,5 @@ +--- +'intrepid-ibex': minor +--- + +open blobs in eog by default diff --git a/TODO.md b/TODO.md index e6f8f9d..8af07e4 100644 --- a/TODO.md +++ b/TODO.md @@ -43,12 +43,12 @@ Every new surface should feel like a GNOME 2 app that happens to speak `at://`. selection. - Add collection schema tabs and links into System Monitor for the selected repo or collection. -- [ ] **gedit** +- [x] **gedit** - Read-only JSON viewer for records. - Preserve copy, wrapping, syntax highlighting, and native GTK-style window behavior. - Add record tabs for JSON, schema, backlinks, and info. - Show AT URI, CID, raw PDS link, external app links, and read-only verification status. -- [ ] **Eye of GNOME (eog)** +- [x] **Eye of GNOME (eog)** - List public repo blob CIDs via `com.atproto.sync.listBlobs`. - Open from Nautilus when a record contains embedded images or media blobs. - Preview image/video blobs, with `app.bsky.feed.post` embedded images as the first diff --git a/src/lib/atproto/blobs.svelte.ts b/src/lib/atproto/blobs.svelte.ts index 62f6c5d..a17a7f8 100644 --- a/src/lib/atproto/blobs.svelte.ts +++ b/src/lib/atproto/blobs.svelte.ts @@ -168,6 +168,9 @@ export function rawBlobUrl(identity: AccountIdentity, cid: string) { } export function firstBlobReference(value: unknown, sourceUri: string | null): BlobReference | null { + const postImage = firstPostImageBlobReference(value, sourceUri); + if (postImage) return postImage; + return blobReferences(value, sourceUri)[0] ?? null; } @@ -210,4 +213,36 @@ function collectBlobReferences(value: unknown, sourceUri: string | null, path: s return references; } +function firstPostImageBlobReference(value: unknown, sourceUri: string | null): BlobReference | null { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return null; + + const record = value as Record; + if (record.$type !== 'app.bsky.feed.post') return null; + + const embed = record.embed; + if (typeof embed !== 'object' || embed === null || Array.isArray(embed)) return null; + + const embedRecord = embed as Record; + if (embedRecord.$type !== 'app.bsky.embed.images' || !Array.isArray(embedRecord.images)) return null; + + for (const [index, image] of embedRecord.images.entries()) { + if (typeof image !== 'object' || image === null || Array.isArray(image)) continue; + + const imageRecord = image as Record; + const blob = imageRecord.image; + if (typeof blob !== 'object' || blob === null || Array.isArray(blob)) continue; + if (!isBlobObject(blob)) continue; + + return { + cid: blob.ref.$link, + sourceUri, + mimeType: typeof blob.mimeType === 'string' ? blob.mimeType : null, + size: typeof blob.size === 'number' ? blob.size : null, + path: `embed.images.[${index}].image` + }; + } + + return null; +} + export const repoBlobs = new RepoBlobState(); diff --git a/src/lib/atproto/blobs.test.ts b/src/lib/atproto/blobs.test.ts index 5be6d59..23b2cfe 100644 --- a/src/lib/atproto/blobs.test.ts +++ b/src/lib/atproto/blobs.test.ts @@ -25,6 +25,25 @@ describe('ATProto blob helpers', () => { }); }); + it('prioritizes app.bsky.feed.post embedded images over other blob fields', () => { + const blob = firstBlobReference( + { + $type: 'app.bsky.feed.post', + cover: { ref: { $link: 'bafkcover' }, mimeType: 'image/png', size: 222 }, + embed: { + $type: 'app.bsky.embed.images', + images: [ + { alt: 'first post image', image: { ref: { $link: 'bafkpostimage' }, mimeType: 'image/jpeg', size: 111 } } + ] + } + }, + 'at://did:plc:abc123/app.bsky.feed.post/post1' + ); + + expect(blob?.cid).toBe('bafkpostimage'); + expect(blob?.path).toBe('embed.images.[0].image'); + }); + it('returns null when a record has no blob ref', () => { expect(firstBlobReference({ text: 'plain record' }, 'at://did:plc:abc123/app.bsky.feed.post/post2')).toBeNull(); }); diff --git a/src/lib/components/CollectionBrowser.svelte b/src/lib/components/CollectionBrowser.svelte index 3753253..731f175 100644 --- a/src/lib/components/CollectionBrowser.svelte +++ b/src/lib/components/CollectionBrowser.svelte @@ -5,8 +5,9 @@ import { onMount } from 'svelte'; import { repoSession } from '$lib/atproto/session.svelte'; import { accountSetup } from '$lib/atproto/setup.svelte'; + import { firstBlobReference, isRenderableBlob, repoBlobs } from '$lib/atproto/blobs.svelte'; import { repoBrowser } from '$lib/atproto/repo.svelte'; - import { collectionPath, identityPath, recordPath } from '$lib/atproto/routes'; + import { blobPath, collectionPath, identityPath, recordPath } from '$lib/atproto/routes'; import { lexiconPath } from '$lib/atproto/lexicon'; import { isRecordValue } from '$lib/atproto/types'; import type { CollectionSummary, RepoRecordSummary } from '$lib/atproto/types'; @@ -67,6 +68,13 @@ function openRecord(record: RepoRecordSummary) { repoBrowser.selectedRecord = record; if (identity) { + const firstMediaBlob = firstBlobReference(record.value, record.uri); + if (firstMediaBlob && isRenderableBlob(firstMediaBlob)) { + repoBlobs.openMedia(identity, { ...firstMediaBlob, sourceIcon: record.icon }); + navigate(blobPath(identity.did, firstMediaBlob.cid)); + return; + } + navigate(recordPath({ did: identity.did, collection: record.collection, rkey: record.rkey })); } } diff --git a/src/lib/components/EyeOfGnome.svelte b/src/lib/components/EyeOfGnome.svelte index dcc7799..63c51ca 100644 --- a/src/lib/components/EyeOfGnome.svelte +++ b/src/lib/components/EyeOfGnome.svelte @@ -1,7 +1,13 @@ @@ -136,6 +163,35 @@ {/if} +
+ {#if selectedBlob} +
+ CID + {selectedBlob.cid} +
+
+ MIME + {selectedBlob.mimeType ?? 'Unknown'} +
+
+ Source path + {selectedBlob.path ?? 'Repository blob'} +
+
+ Raw PDS URL + {#if selectedBlob.sourceUri} + + {/if} + +
+ {:else} +
+ Repository + {identity?.did ?? 'No repo loaded'} +
+ {/if} +
+