PageSourceSearch

https://imagine-aru-2036.web.app/src/core/storage.js

js imagine-aru-2036.web.app collected 2026-10-03 12:26:35 UTC 9,649 bytes, 222 lines download raw bytes

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.