mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-19 13:21:46 +00:00
feat(photos): add 1h disk cache for remote thumbnails and keep tabs mounted
Closes #686 - Add trekPhotoCache service: SHA1-keyed disk cache under uploads/photos/trek/, 1h TTL, in-flight dedup map to prevent stampedes on concurrent requests - Add migration 108: trek_photo_cache_meta table - Hook cache into streamPhoto for Immich/Synology thumbnail path; originals bypass cache - Add fetchImmichThumbnailBytes / fetchSynologyThumbnailBytes returning Buffer instead of piping, used by the cache layer - Add scheduler entry (every 2h + startup sweep) to evict expired disk files and DB rows via sweepExpired() - Client: convert journey tab conditional-mount to hidden-toggle so img elements stay in DOM across tab switches, preventing redundant thumbnail requests on rapid tab changes - Expose invalidateSize() on JourneyMapHandle; call it on map tab activation to fix Leaflet rendering in previously-hidden container
This commit is contained in:
@@ -14,6 +14,7 @@ export interface MapMarkerItem {
|
||||
export interface JourneyMapHandle {
|
||||
highlightMarker: (id: string | null) => void
|
||||
focusMarker: (id: string) => void
|
||||
invalidateSize: () => void
|
||||
}
|
||||
|
||||
interface MapEntry {
|
||||
@@ -151,7 +152,11 @@ const JourneyMap = forwardRef<JourneyMapHandle, Props>(function JourneyMap(
|
||||
}
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(ref, () => ({ highlightMarker, focusMarker }), [])
|
||||
const invalidateSize = useCallback(() => {
|
||||
try { mapRef.current?.invalidateSize() } catch { /* map not yet initialized */ }
|
||||
}, [])
|
||||
|
||||
useImperativeHandle(ref, () => ({ highlightMarker, focusMarker, invalidateSize }), [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
|
||||
@@ -164,6 +164,12 @@ export default function JourneyDetailPage() {
|
||||
setActiveLocationId(id)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (view === 'map') {
|
||||
requestAnimationFrame(() => fullMapRef.current?.invalidateSize())
|
||||
}
|
||||
}, [view])
|
||||
|
||||
const mapEntries = useMemo(
|
||||
() => (current?.entries || []).filter(e => e.location_lat && e.location_lng),
|
||||
[current?.entries]
|
||||
@@ -413,8 +419,8 @@ export default function JourneyDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Timeline (desktop only — mobile uses fullscreen combined view above) */}
|
||||
{!isMobile && view === 'timeline' && (
|
||||
<div className="flex flex-col gap-6 pb-24 md:pb-6">
|
||||
{!isMobile && (
|
||||
<div className={`flex flex-col gap-6 pb-24 md:pb-6${view === 'timeline' ? '' : ' hidden'}`}>
|
||||
{sortedDates.length === 0 && (
|
||||
<div className="text-center py-16">
|
||||
<div className="w-16 h-16 rounded-full bg-zinc-100 dark:bg-zinc-800 flex items-center justify-center mx-auto mb-4">
|
||||
@@ -469,7 +475,7 @@ export default function JourneyDetailPage() {
|
||||
)}
|
||||
|
||||
{/* Gallery View */}
|
||||
{view === 'gallery' && (
|
||||
<div className={view === 'gallery' ? '' : 'hidden'}>
|
||||
<GalleryView
|
||||
entries={current.entries}
|
||||
journeyId={current.id}
|
||||
@@ -478,17 +484,21 @@ export default function JourneyDetailPage() {
|
||||
onPhotoClick={(photos, idx) => setLightbox({ photos: photos.map(p => ({ id: p.id, src: photoUrl(p, 'original'), caption: p.caption, provider: p.provider, asset_id: p.asset_id, owner_id: p.owner_id })), index: idx })}
|
||||
onRefresh={() => loadJourney(Number(id))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Full Map View (desktop only — mobile uses combined view) */}
|
||||
{!isMobile && view === 'map' && <div className="pb-24 md:pb-6"><MapView
|
||||
entries={current.entries}
|
||||
mapEntries={mapEntries}
|
||||
sortedDates={sortedDates}
|
||||
activeLocationId={activeLocationId}
|
||||
fullMapRef={fullMapRef}
|
||||
onLocationClick={handleLocationClick}
|
||||
/></div>}
|
||||
{!isMobile && (
|
||||
<div className={`pb-24 md:pb-6${view === 'map' ? '' : ' hidden'}`}>
|
||||
<MapView
|
||||
entries={current.entries}
|
||||
mapEntries={mapEntries}
|
||||
sortedDates={sortedDates}
|
||||
activeLocationId={activeLocationId}
|
||||
fullMapRef={fullMapRef}
|
||||
onLocationClick={handleLocationClick}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right sidebar — hidden on mobile */}
|
||||
|
||||
Reference in New Issue
Block a user