1// Dark mode, navigation drawer, lightbox, download dialog and reactive gradient 2(function () { 3 const body = document.body; 4 const themeToggle = document.querySelector('.theme-toggle'); 5 const navMenuButton = document.querySelector('.nav-menu-button'); 6 const navDrawer = document.querySelector('.nav-drawer'); 7 const navDrawerClose = document.querySelector('.nav-drawer__close'); 8 const navDrawerBackdrop = document.querySelector('.nav-drawer__backdrop'); 9 10 const lightbox = document.querySelector('.lightbox'); 11 const lightboxImg = document.querySelector('.lightbox__img'); 12 const lightboxBackdrop = document.querySelector('.lightbox__backdrop'); 13 14 const downloadDialog = document.querySelector('.download-dialog'); 15 const downloadDialogBackdrop = document.querySelector('.download-dialog__backdrop'); 16 const downloadDialogIso = document.querySelector('.download-dialog__iso-value[data-field="iso"]'); 17 const downloadDialogSha = document.querySelector('.download-dialog__checksum[data-field="sha"]'); 18 const downloadDialogCopy = document.querySelector('.download-dialog__copy'); 19 const downloadDialogConfirm = document.querySelector('.download-dialog__confirm'); 20 21 // Theme: default is dark. Only apply light if stored explicitly. 22 const storedTheme = window.localStorage.getItem('nobara-theme'); 23 24 if (storedTheme === 'light') { 25 body.classList.remove('dark'); 26 } else { 27 // storedTheme is "dark" OR null/anything else => default to dark 28 body.classList.add('dark'); 29 } 30 31 function toggleTheme() { 32 const isDark = body.classList.toggle('dark'); 33 window.localStorage.setItem('nobara-theme', isDark ? 'dark' : 'light'); 34 } 35 36 if (themeToggle) { 37 themeToggle.addEventListener('click', toggleTheme); 38 } 39 40 41 // Nav drawer 42 function openDrawer() { 43 if (!navDrawer || !navDrawerBackdrop) return; 44 navDrawer.classList.add('open'); 45 navDrawerBackdrop.classList.add('open'); 46 } 47 48 function closeDrawer() { 49 if (!navDrawer || !navDrawerBackdrop) return; 50 navDrawer.classList.remove('open'); 51 navDrawerBackdrop.classList.remove('open'); 52 } 53 54 if (navMenuButton) { 55 navMenuButton.addEventListener('click', openDrawer); 56 } 57 58 if (navDrawerClose) { 59 navDrawerClose.addEventListener('click', closeDrawer); 60 } 61 62 if (navDrawerBackdrop) { 63 navDrawerBackdrop.addEventListener('click', closeDrawer); 64 } 65 66 document.querySelectorAll('.nav-drawer__link').forEach((link) => { 67 link.addEventListener('click', closeDrawer); 68 }); 69 70 // Lightbox for edition images 71 function openLightbox(src, alt) { 72 if (!lightbox || !lightboxImg) return; 73 lightboxImg.src = src; 74 lightboxImg.alt = alt || ''; 75 lightbox.classList.add('open'); 76 lightbox.setAttribute('aria-hidden', 'false'); 77 } 78 79 function closeLightbox() { 80 if (!lightbox || !lightboxImg) return; 81 lightbox.classList.remove('open'); 82 lightbox.setAttribute('aria-hidden', 'true'); 83 } 84 85 if (lightboxBackdrop) { 86 lightboxBackdrop.addEventListener('click', closeLightbox); 87 } 88 89 if (lightbox) { 90 lightbox.addEventListener('click', (event) => { 91 if (event.target === lightbox) { 92 closeLightbox(); 93 } 94 }); 95 } 96 97 document.querySelectorAll('.lightbox-trigger').forEach((img) => { 98 img.addEventListener('click', () => openLightbox(img.src, img.alt)); 99 }); 100 101 102 async function ensureShaForButton(btn) { 103 // If already present, return it 104 const existing = btn.getAttribute('data-sha'); 105 if (existing && existing.trim()) return existing.trim(); 106 107 const isoName = btn.getAttribute('data-iso'); 108 const isoUrl = btn.getAttribute('data-url'); 109 if (!isoName || !isoUrl) throw new Error('Missing data-iso or data-url'); 110 111 // Same folder + "<iso>.sha256sum" 112 const shaUrl = new URL(`${isoName}.sha256sum`, isoUrl).toString(); 113 114 const res = await fetch(shaUrl, { cache: 'no-store' }); 115 if (!res.ok) throw new Error(`Failed to fetch checksum (${res.status})`); 116 117 const text = await res.text(); 118 const match = text.match(/\b[a-f0-9]{64}\b/i); 119 if (!match) throw new Error('No SHA256 found in checksum file'); 120 121 const sha = match[0].toLowerCase(); 122 btn.setAttribute('data-sha', sha); // cache it on the element 123 return sha; 124 } 125 126 // Download dialog with checksum 127 let pendingDownloadUrl = null; 128 129 function openDownloadDialog(iso, sha, url) { 130 if (!downloadDialog) return; 131 pendingDownloadUrl = url; 132 if (downloadDialogIso) downloadDialogIso.textContent = iso || ''; 133 if (downloadDialogSha) downloadDialogSha.textContent = sha || ''; 134 135 // Remove transform from body so dialog positions relative to viewport 136 body.style.transform = 'none'; 137 138 downloadDialog.classList.add('open'); 139 downloadDialog.setAttribute('aria-hidden', 'false'); 140 } 141 142 function closeDownloadDialog() { 143 if (!downloadDialog) return; 144 downloadDialog.classList.remove('open'); 145 downloadDialog.setAttribute('aria-hidden', 'true'); 146 pendingDownloadUrl = null; 147 148 // Restore transform on body 149 body.style.transform = ''; 150 } 151 152 document.querySelectorAll('.download-btn').forEach((btn) => { 153 btn.addEventListener('click', async (event) => { 154 event.preventDefault(); 155 156 const iso = btn.getAttribute('data-iso'); 157 const url = btn.getAttribute('data-url'); 158 159 // Open dialog immediately with a placeholder while we fetch 160 openDownloadDialog(iso, 'Loadingâ¦', url); 161
162 try { 163 const sha = await ensureShaForButton(btn); 164 if (downloadDialogSha) downloadDialogSha.textContent = sha; 165 } catch (err) { 166 if (downloadDialogSha) downloadDialogSha.textContent = 'Unavailable'; 167 console.warn(err); 168 } 169 }); 170 }); 171 172 if (downloadDialogBackdrop) { 173 downloadDialogBackdrop.addEventListener('click', closeDownloadDialog); 174 } 175 176 if (downloadDialogCopy && downloadDialogSha) { 177 downloadDialogCopy.addEventListener('click', async () => { 178 const text = (downloadDialogSha.textContent || '').trim(); 179 180 // Only copy if it looks like a SHA256 181 if (!/^[a-f0-9]{64}$/i.test(text)) return; 182 183 try { 184 await navigator.clipboard.writeText(text); 185 } catch { 186 // fallback (optional) 187 const tmp = document.createElement('textarea'); 188 tmp.value = text; 189 document.body.appendChild(tmp); 190 tmp.select(); 191 document.execCommand('copy'); 192 tmp.remove(); 193 } 194 }); 195 } 196 197 if (downloadDialogConfirm) { 198 downloadDialogConfirm.addEventListener('click', () => { 199 if (pendingDownloadUrl) { 200 window.open(pendingDownloadUrl, '_blank', 'noopener'); 201 } 202 closeDownloadDialog(); 203 }); 204 } 205 206 // Close on Escape 207 document.addEventListener('keydown', (event) => { 208 if (event.key === 'Escape') { 209 closeLightbox(); 210 closeDownloadDialog(); 211 closeDrawer(); 212 } 213 }); 214 215 const rootEl = document.documentElement; 216 rootEl.style.setProperty('--bg-grad-x', '50%'); 217 rootEl.style.setProperty('--bg-grad-y', '0%'); 218})();
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.