PageSourceSearch

https://nobaraproject.org/script.js

js nobaraproject.org collected 2026-10-03 19:27:57 UTC 6,964 bytes, 218 lines download raw bytes

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.