summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-08-28 16:11:46 +0200
committerChristophe Besson <cbesson@gmail.com>2026-08-28 16:11:46 +0200
commit4ef6ca817e36e207f57dd810727f7225e7d26eff (patch)
tree535de76925d84a03c5f3a5e86ff9e0393c6227f5 /packages/meshbay-hub/src/meshbay_hub/static/photos-app.js
parenteba2e6b14484c124f3c87ff95cd7ee640833e5d3 (diff)
downloadmeshbay-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.js34
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} />
`}
`;
}