1import { db, liveStorage } from './firebase.js'; 2 3// Resolve storage URLs (gs:// or submissions/) to download URLs (w/ fallback to live db bucket if local storage fails) 4export function getResolvedAssetUrl(fileUrl) { 5 if (!fileUrl) return Promise.resolve(""); 6 7 const isStorage = fileUrl.startsWith("submissions/") || fileUrl.startsWith("/submissions/") || fileUrl.startsWith("gs://"); 8 if (isStorage) { 9 let cleanPath = fileUrl; 10 if (cleanPath.startsWith("/")) { 11 cleanPath = cleanPath.substring(1); 12 } 13 14 let primaryPromise; 15 if (cleanPath.startsWith("gs://")) { 16 try { 17 primaryPromise = firebase.storage().refFromURL(cleanPath).getDownloadURL(); 18 } catch (err) { 19 primaryPromise = Promise.reject(err); 20 } 21 } else { 22 try { 23 primaryPromise = firebase.storage().ref(cleanPath).getDownloadURL(); 24 } catch (err) { 25 primaryPromise = Promise.reject(err); 26 } 27 } 28 29 return primaryPromise.catch((err) => { 30 console.warn("Primary storage resolution failed, attempting fallback to live storage:", cleanPath, err); 31 if (liveStorage) { 32 if (cleanPath.startsWith("gs://")) { 33 try { 34 return liveStorage.refFromURL(cleanPath).getDownloadURL(); 35 } catch (fallbackErr) { 36 return Promise.reject(fallbackErr); 37 } 38 } else { 39 try { 40 return liveStorage.ref(cleanPath).getDownloadURL(); 41 } catch (fallbackErr) { 42 return Promise.reject(fallbackErr); 43 } 44 } 45 } 46 return Promise.reject(err); 47 }); 48 } 49 return Promise.resolve(fileUrl); 50} 51 52// Find all images with data-storage-url and load them 53export function resolveAllStorageImages() { 54 const imgs = document.querySelectorAll("img[data-storage-url]"); 55 imgs.forEach((img, index) => { 56 const path = img.getAttribute("data-storage-url"); 57 if (!path) return; 58 59 // Lazy-loading check: if it is inside the canvas, only load if canvas is high zoom 60 const inCanvas = img.closest("#gallery-canvas"); 61 if (inCanvas) { 62 const canvas = document.getElementById("gallery-canvas"); 63 if (canvas && !canvas.classList.contains("canvas-lod-high")) { 64 return; 65 } 66 } 67 68 img.removeAttribute("data-storage-url"); 69 70 setTimeout(() => { 71 getResolvedAssetUrl(path).then((url) => { 72 const tempImg = new Image(); 73 tempImg.onload = () => { 74 img.src = url; 75 img.classList.add("loaded"); 76 }; 77 tempImg.onerror = (e) => { 78 console.warn("[Storage] Pre-loader failed to load image, setting src directly:", url, e); 79 img.src = url; 80 }; 81 tempImg.src = url; 82 }).catch((err) => { 83 console.error("Failed to resolve storage URL", path, err); 84 img.classList.add("hidden"); 85 }); 86 }, index * 80); 87 }); 88} 89 90// Find all links with data-storage-href and resolve them 91export function resolveAllStorageLinks() { 92 const links = document.querySelectorAll("a[data-storage-href]"); 93 links.forEach((link) => { 94 const path = link.getAttribute("data-storage-href"); 95 if (!path) return; 96 97 const inCanvas = link.closest("#gallery-canvas"); 98 if (inCanvas) { 99 const canvas = document.getElementById("gallery-canvas"); 100 if (canvas && !canvas.classList.contains("canvas-lod-high")) { 101 return; 102 } 103 } 104 105 link.classList.add("opacity-50", "pointer-events-none"); 106 link.href = "#"; 107 link.removeAttribute("data-storage-href"); 108 getResolvedAssetUrl(path).then((url) => { 109 link.href = url; 110 link.classList.remove("opacity-50", "pointer-events-none"); 111 }).catch((err) => { 112 console.error("Failed to resolve storage URL for link", path, err); 113 }); 114 }); 115} 116 117// Find all audio/video elements with data-storage-url and set their src 118export function resolveAllStorageVideos() { 119 const vids = document.querySelectorAll("video[data-storage-url], audio[data-storage-url]"); 120 vids.forEach((vid) => { 121 const path = vid.getAttribute("data-storage-url"); 122 if (!path) return; 123 124 const inCanvas = vid.closest("#gallery-canvas"); 125 if (inCanvas) { 126 const canvas = document.getElementById("gallery-canvas"); 127 if (canvas && !canvas.classList.contains("canvas-lod-high")) { 128 return; 129 } 130 } 131 132 vid.removeAttribute("data-storage-url"); 133 getResolvedAssetUrl(path).then((url) => { 134 // Append #t=0.5 to show first frame (thumbnail) for video elements 135 const finalUrl = vid.tagName.toLowerCase() === 'video' ? (url + (url.includes('#') ? '' : '#t=0.5')) : url; 136 vid.src = finalUrl; 137 }).catch((err) => { 138 console.error("Failed to resolve storage URL for video/audio", path, err); 139 }); 140 }); 141} 142 143// Extract YouTube video ID from URL 144export function getYouTubeId(url) { 145 if (!url) return null; 146 const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/; 147 const match = url.match(regExp); 148 return (match && match[2].length === 11) ? match[2] : null; 149} 150 151// Extract Vimeo video ID from URL 152export function getVimeoId(url) { 153 if (!url) return null; 154 const regExp = /vimeo\.com\/(?:channels\/(?:\w+\/)?|groups\/([^\/]*)\/videos\/|album\/(\d+)\/video\/|video\/|)(\d+)(?:$|\/|\?)/; 155 const match = url.match(regExp); 156 return match ? match[3] : null; 157} 158 159// Compile player HTML wrapper for external or direct video attachments 160export function getEmbeddablePlayerHtml(url, fileName) { 161 if (!url) return ""; 162 const urlLower = url.toLowerCase(); 163 164 // Check if YouTube 165 const ytId = getYouTubeId(url); 166 if (ytId) { 167 return ` 168 <div class="relative pb-[56.25%] h-0 overflow-hidden rounded-xl border border-white/10 bg-black/60 mx-auto w-full max-w-2xl shadow-lg my-2"> 169 <iframe class="absolute top-0 left-0 w-full h-full rounded-lg" src="https://www.youtube.com/embed/${ytId}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe> 170 </div> 171 <p class="text-[10px] text-slate-500 px-3 py-1.5 text-center">${fileName || 'Embedded YouTube Video'}</p> 172 `; 173 } 174 175 // Check if Vimeo 176 const vimeoId = getVimeoId(url); 177 if (vimeoId) { 178 return ` 179 <div class="relative pb-[56.25%] h-0 overflow-hidden rounded-xl border border-white/10 bg-black/60 mx-auto w-full max-w-2xl shadow-lg my-2"> 180 <iframe class="absolute top-0 left-0 w-full h-full rounded-lg" src="https://player.vimeo.com/video/${vimeoId}" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe> 181 </div> 182 <p class="text-[10px] text-slate-500 px-3 py-1.5 text-center">${fileName || 'Embedded Vimeo Video'}</p> 183 `; 184 } 185 186 // Check direct video extension 187 const fn = (fileName || "").toLowerCase(); 188 const isVidFile = /\.(mp4|mov|webm|avi)(\?|$)/.test(fn) || /\.(mp4|mov|webm|avi)(\?|$)/.test(urlLower); 189 190 if (isVidFile) { 191 const isStorage = url.startsWith("submissions/") || url.startsWith("gs://"); 192 if (isStorage) { 193 return ` 194 <video class="w-full max-h-80 object-contain bg-black/60 rounded-xl border border-white/10 mx-auto block shadow-lg my-2" controls data-storage-url="${url}"></video> 195 <p class="text-[10px] text-slate-500 px-3 py-1.5 text-center">${fileName || 'Video attachment'}</p> 196 `; 197 } else { 198 return ` 199 <video class="w-full max-h-80 object-contain bg-black/60 rounded-xl border border-white/10 mx-auto block shadow-lg my-2" controls src="${url}"></video> 200 <p class="text-[10px] text-slate-500 px-3 py-1.5 text-center">${fileName || 'Video link'}</p> 201 `; 202 } 203 } 204 205 // External generic/social link pages 206 if (url.startsWith("http://") || url.startsWith("https://")) { 207 return ` 208 <div class="flex flex-col items-center justify-center gap-3 py-8 px-4 bg-slate-950/40 rounded-xl border border-white/10 text-center max-w-md mx-auto my-2"> 209 <div class="w-12 h-12 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-400 border border-blue-500/20 shadow-md"> 210 <i class="fa-solid fa-link text-lg"></i> 211 </div> 212 <div> 213 <div class="text-xs text-white font-semibold mb-1 truncate max-w-[280px]">${fileName || 'External Video Link'}</div> 214 <a href="${url}" target="_blank" rel="noopener noreferrer" class="text-[10px] text-cyan-400 hover:underline flex items-center justify-center gap-1 font-bold"> 215 Open External Video Link <i class="fa-solid fa-up-right-from-square text-[8px]"></i> 216 </a> 217 </div> 218 </div> 219 `; 220 } 221 return ""; 222}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.