diff --git a/search-app/live-search.js b/search-app/live-search.js index 9eacb7d..fd9fb3c 100644 --- a/search-app/live-search.js +++ b/search-app/live-search.js @@ -742,6 +742,263 @@ console.log('[live-search] Immich live search with map view loaded'); + // --- Enable pinch-zoom on mobile --- + // Immich ships a viewport meta that disables user scaling. Keep rewriting + // it so any future re-render doesn't clobber our override. + (function enablePinchZoom() { + const desired = 'width=device-width, initial-scale=1, maximum-scale=5, user-scalable=yes'; + function apply() { + let meta = document.querySelector('meta[name="viewport"]'); + if (!meta) { + meta = document.createElement('meta'); + meta.name = 'viewport'; + document.head.appendChild(meta); + } + if (meta.content !== desired) meta.content = desired; + } + apply(); + new MutationObserver(apply).observe(document.head, { + childList: true, subtree: true, + attributes: true, attributeFilter: ['content'] + }); + })(); + + // --- /explore page enhancements --- + // 1. Iframe the photo-locations heatmap at the top of the page. + // 2. Replace People + Places sections (truncated + "view more") with + // horizontally-scrollable strips showing the full list. + (function setupExploreEnhancements() { + const HEATMAP_URL = 'https://heatmap.jeffemmett.com/'; + const PEOPLE_CACHE_MS = 5 * 60 * 1000; + const PLACES_CACHE_MS = 5 * 60 * 1000; + const peopleCache = { data: null, ts: 0 }; + const placesCache = { data: null, ts: 0 }; + + // Inject styles once + const st = document.createElement('style'); + st.textContent = ` + .ls-heatmap-banner { + margin: 0 0 16px 0; border-radius: 10px; overflow: hidden; + background: #16213e; box-shadow: 0 4px 14px rgba(0,0,0,0.25); + } + .ls-heatmap-header { + display: flex; justify-content: space-between; align-items: center; + padding: 8px 14px; color: #fff; font-size: 14px; font-weight: 600; + background: rgba(0,0,0,0.25); + } + .ls-heatmap-header button { + background: rgba(255,255,255,0.15); color: #fff; border: none; + border-radius: 6px; padding: 4px 10px; cursor: pointer; font-size: 12px; + } + .ls-heatmap-header button:hover { background: rgba(255,255,255,0.25); } + .ls-heatmap-frame { + width: 100%; height: 360px; border: none; display: block; + } + .ls-heatmap-banner.collapsed .ls-heatmap-frame { display: none; } + + .ls-strip-wrap { margin: 0 0 20px 0; } + .ls-strip-wrap h3 { + font-size: 15px; font-weight: 600; margin: 0 0 8px 0; + } + .ls-strip { + display: flex; gap: 10px; overflow-x: auto; overflow-y: hidden; + padding-bottom: 8px; scroll-snap-type: x proximity; + -webkit-overflow-scrolling: touch; + } + .ls-strip::-webkit-scrollbar { height: 8px; } + .ls-strip::-webkit-scrollbar-thumb { + background: rgba(127,127,127,0.4); border-radius: 4px; + } + .ls-strip-item { + flex: 0 0 auto; text-align: center; cursor: pointer; + scroll-snap-align: start; + } + .ls-strip-item img { + display: block; border-radius: 50%; object-fit: cover; + background: #333; + } + .ls-strip-item.ls-place img { + border-radius: 10px; + } + .ls-strip-item .ls-strip-label { + font-size: 12px; margin-top: 6px; max-width: 100px; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + } + .ls-strip-people .ls-strip-item img { width: 90px; height: 90px; } + .ls-strip-places .ls-strip-item img { width: 140px; height: 100px; } + .ls-strip-empty { + font-size: 13px; opacity: 0.6; padding: 12px; + } + `; + document.head.appendChild(st); + + async function fetchPeople() { + const now = Date.now(); + if (peopleCache.data && (now - peopleCache.ts) < PEOPLE_CACHE_MS) return peopleCache.data; + const r = await fetch('/api/people?size=1000&withHidden=false', { + headers: getAuthHeaders(), credentials: 'include' + }); + if (!r.ok) return []; + const j = await r.json(); + const people = (j.people || []) + .filter(p => p.name) // only named people — matches Immich's Explore filter + .sort((a, b) => (a.name || '').localeCompare(b.name || '')); + peopleCache.data = people; + peopleCache.ts = now; + return people; + } + + async function fetchPlaces() { + const now = Date.now(); + if (placesCache.data && (now - placesCache.ts) < PLACES_CACHE_MS) return placesCache.data; + const r = await fetch('/api/search/cities', { + headers: getAuthHeaders(), credentials: 'include' + }); + if (!r.ok) return []; + const j = await r.json(); + const places = (Array.isArray(j) ? j : []) + .filter(a => a && a.exifInfo && a.exifInfo.city) + .sort((a, b) => a.exifInfo.city.localeCompare(b.exifInfo.city)); + placesCache.data = places; + placesCache.ts = now; + return places; + } + + function buildStrip(className, items, makeItem) { + const strip = document.createElement('div'); + strip.className = 'ls-strip ' + className; + if (!items.length) { + strip.innerHTML = '