From 4ef6ca817e36e207f57dd810727f7225e7d26eff Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 28 Aug 2026 16:11:46 +0200 Subject: feat(hub): search downloads, photos view, pre-connect optimization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Downloads from search: FilesPanel accepts a getTransport callback for cross-group file downloads. Select/Download are available in search; delete and zip are hidden (readOnly). Each download connects to the entry's group via the pool on demand. Photos view: PhotosApp reused in search with the same pattern as Videos/Music — synthetic root, per-entry transport refs, group badges on album cards. Lightbox uses per-entry transport for full-resolution image fetches. Zip download hidden in search context. Pre-connecting: connections to all groups start as soon as indexing finishes, regardless of the active view. Thumbnails cache across pool evictions, so they're ready when the user switches views. Co-Authored-By: Claude Opus 4.6 --- .../src/meshbay_hub/static/photos-app.js | 34 +++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'packages/meshbay-hub/src/meshbay_hub/static/photos-app.js') diff --git a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js index 26785e7..d63b384 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js @@ -79,22 +79,23 @@ function albumYearLabel(photos) { // ── landing grid: one card per album (directory containing images) ───────── function AlbumCard({ album, transportRef, gekRef, onOpen }) { - // A photo whose own thumbnail is already ready, over blindly photos[0] — - // that specific file's enrichment may not have finished yet even though - // a sibling's has (same fallback video-app.js's PosterGrid already uses - // picking a show's representative episode). const cover = album.photos.find((p) => p.thumb_hash) || album.photos[0]; + const tRef = cover._tRef || transportRef; + const gRef = cover._gRef || gekRef; const year = albumYearLabel(album.photos); return html`
<${MediaThumb} thumbHash=${cover.thumb_hash} alt=${albumTitle(album.dir)} - cls="photo-album-cover" transportRef=${transportRef} gekRef=${gekRef} + cls="photo-album-cover" transportRef=${tRef} gekRef=${gRef} emptyIcon="image" />
${albumTitle(album.dir)}
${year}${year ? ' · ' : ''}${t('photo.n_photos', { n: album.photos.length })}
+ ${cover.groupName && html` +
${cover.groupName}
+ `}
`; @@ -116,10 +117,12 @@ function AlbumLanding({ albums, transportRef, gekRef, onOpen }) { // ── open album: grid of its own photos ────────────────────────────────────── function PhotoTile({ entry, transportRef, gekRef, onOpen }) { + const tRef = entry._tRef || transportRef; + const gRef = entry._gRef || gekRef; return html`
<${MediaThumb} thumbHash=${entry.thumb_hash} alt=${entry.name} - cls="photo-tile-thumb" transportRef=${transportRef} gekRef=${gekRef} + cls="photo-tile-thumb" transportRef=${tRef} gekRef=${gRef} emptyIcon="image" />
`; @@ -142,6 +145,8 @@ const ZOOM_MAX = 400; function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav }) { const entry = photos[index]; + const tRef = entry._tRef || transportRef; + const gRef = entry._gRef || gekRef; const [blobUrl, setBlobUrl] = useState(() => _fullBlobCache.get(entry.id) || null); const [loading, setLoading] = useState(!_fullBlobCache.has(entry.id)); // null = "fit to window" (the default, object-fit: contain); a number is @@ -160,12 +165,12 @@ function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav }) { setLoading(true); let cancelled = false; (async () => { - const transport = transportRef.current; + const transport = tRef.current; if (!transport || !transport.connected) { setLoading(false); return; } try { const totalChunks = Math.ceil(entry.size / CHUNK_SIZE); const chunks = await pipelinedDownload( - transport, gekRef.current, entry.id, totalChunks); + transport, gRef.current, entry.id, totalChunks); if (cancelled) return; const url = URL.createObjectURL(new Blob(chunks)); _fullBlobCache.set(entry.id, url); @@ -268,7 +273,7 @@ function Lightbox({ photos, index, transportRef, gekRef, onClose, onNav }) { `; } -function AlbumView({ album, entries, transportRef, gekRef, setError, onBack }) { +function AlbumView({ album, entries, transportRef, gekRef, setError, onBack, readOnly }) { const [lightboxIndex, setLightboxIndex] = useState(null); const zip = useCallback(async () => { @@ -296,8 +301,8 @@ function AlbumView({ album, entries, transportRef, gekRef, setError, onBack }) { ${year && html`${year}`} - + ${!readOnly && html``}
${album.photos.map((e, i) => html` @@ -319,6 +324,7 @@ function AlbumView({ album, entries, transportRef, gekRef, setError, onBack }) { function PhotosApp({ groupId, transportRef, gekRef, status, entries, photoRoots, setError, + hideFilter, readOnly, }) { const [openDir, setOpenDir] = useState(null); const [filter, setFilter] = useState(''); @@ -346,11 +352,11 @@ function PhotosApp({ `} ${status === 'connected' && photoRoots && photoRoots.length > 0 && !openAlbum && html`
- `}
${filteredAlbums.length === 0 && html`

${needle ? t('group.empty_filter') : t('photo.empty')}

@@ -362,7 +368,7 @@ function PhotosApp({ ${status === 'connected' && openAlbum && html` <${AlbumView} album=${openAlbum} entries=${entries} transportRef=${transportRef} gekRef=${gekRef} setError=${setError} - onBack=${() => setOpenDir(null)} /> + onBack=${() => setOpenDir(null)} readOnly=${readOnly} /> `} `; } -- cgit v1.2.3