diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-28 16:11:46 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-28 16:11:46 +0200 |
| commit | 4ef6ca817e36e207f57dd810727f7225e7d26eff (patch) | |
| tree | 535de76925d84a03c5f3a5e86ff9e0393c6227f5 /packages/meshbay-hub/src/meshbay_hub/static/photos-app.js | |
| parent | eba2e6b14484c124f3c87ff95cd7ee640833e5d3 (diff) | |
| download | meshbay-4ef6ca817e36e207f57dd810727f7225e7d26eff.tar.gz | |
feat(hub): search downloads, photos view, pre-connect optimization
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 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/photos-app.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/photos-app.js | 34 |
1 files changed, 20 insertions, 14 deletions
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` <div class="photo-album-card" onClick=${onOpen}> <${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" /> <div class="photo-album-info"> <div class="photo-album-title">${albumTitle(album.dir)}</div> <div class="photo-album-sub"> ${year}${year ? ' · ' : ''}${t('photo.n_photos', { n: album.photos.length })} </div> + ${cover.groupName && html` + <div class="photo-card-group">${cover.groupName}</div> + `} </div> </div> `; @@ -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` <div class="photo-tile" onClick=${onOpen}> <${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" /> </div> `; @@ -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`<span class="photo-album-heading-year">${year}</span>`} </div> </div> - <button class="photo-icon-btn" onClick=${zip} title=${t('photo.zip_album')}> - <${Icon} name="archive" cls="photo-icon-btn-icon" /></button> + ${!readOnly && html`<button class="photo-icon-btn" onClick=${zip} title=${t('photo.zip_album')}> + <${Icon} name="archive" cls="photo-icon-btn-icon" /></button>`} </div> <div class="photo-grid"> ${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` <div class="photo-toolbar"> - <div class="tb-search"> + ${!hideFilter && html`<div class="tb-search"> <${Icon} name="search" /> <input type="text" placeholder="${t('group.filter')}" value=${filter} onInput=${(e) => setFilter(e.target.value)} /> - </div> + </div>`} </div> ${filteredAlbums.length === 0 && html` <p class="page-message">${needle ? t('group.empty_filter') : t('photo.empty')}</p> @@ -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} /> `} `; } |