411 lines
22 KiB
JavaScript
411 lines
22 KiB
JavaScript
/* RustProxy Package Registry — dependency-free interactive client */
|
|
(() => {
|
|
'use strict';
|
|
|
|
const el = (id) => document.getElementById(id);
|
|
const CHANNEL_LABELS = {linux:'Linux',suse:'SUSE / openSUSE',opnsense:'OPNsense',freebsd:'FreeBSD'};
|
|
const INSTALL_NAMES = {deb:'Debian / Ubuntu',rpm:'RHEL / Rocky / Alma / Fedora',suse:'SUSE / openSUSE / Leap',opnsense:'OPNsense',freebsd:'FreeBSD native'};
|
|
const SECTION_LABELS = {overview:'Overview',packages:'Packages & versions',install:'Installation',repository:'Repositories'};
|
|
const esc = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
|
|
const collator = new Intl.Collator('en', {numeric:true,sensitivity:'base'});
|
|
const formatNumber = new Intl.NumberFormat('en-GB');
|
|
const site = ['linux','opnsense','freebsd'].includes(document.body.dataset.site) ? document.body.dataset.site : 'linux';
|
|
const state = {
|
|
site, catalog:null, section:'overview', installing:site === 'linux' ? 'deb' : site,
|
|
channel:site === 'linux' ? 'all' : site,
|
|
format:'all', search:'', sort:'date', rows:25, refreshing:false,
|
|
};
|
|
|
|
const formatDate = (value) => {
|
|
if (!value) return '—';
|
|
const time = new Date(value);
|
|
if (Number.isNaN(time.valueOf())) return '—';
|
|
return new Intl.DateTimeFormat('en-GB', {year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}).format(time);
|
|
};
|
|
const formatShortDate = (value) => {
|
|
if (!value) return '—';
|
|
const time = new Date(value);
|
|
if (Number.isNaN(time.valueOf())) return '—';
|
|
return new Intl.DateTimeFormat('en-GB', {day:'2-digit',month:'short',year:'numeric'}).format(time);
|
|
};
|
|
const formatSize = (value) => {
|
|
const size = Number(value);
|
|
if (!Number.isFinite(size) || size < 0) return '—';
|
|
if (size < 1024) return `${formatNumber.format(size)} B`;
|
|
if (size < 1024 ** 2) return `${(size / 1024).toFixed(1)} KB`;
|
|
return `${(size / 1024 ** 2).toFixed(1)} MB`;
|
|
};
|
|
const safeLink = (url) => {
|
|
try {
|
|
const resolved = new URL(url, window.location.href);
|
|
if (['http:','https:'].includes(resolved.protocol)) return resolved.href;
|
|
} catch (_) { /* invalid URL */ }
|
|
return '#';
|
|
};
|
|
const origin = (url) => typeof url === 'string' ? url.replace(/\/$/, '') : '';
|
|
const dateLabel = (kind) => kind === 'build' ? 'Build timestamp recorded by the builder' : 'File modification time (older builds without a build timestamp)';
|
|
const setText = (id, value) => {const node = el(id);if (node) node.textContent = value;};
|
|
const versionText = (b) => b?.app_version ? `v${b.app_version}` : '—';
|
|
|
|
let toastTimer;
|
|
function toast(message) {
|
|
const node = el('toast');
|
|
node.textContent = message;
|
|
node.hidden = false;
|
|
window.clearTimeout(toastTimer);
|
|
toastTimer = window.setTimeout(() => {node.hidden = true;}, 2700);
|
|
}
|
|
function setTheme(theme) {
|
|
const target = ['dark','light','cream'].includes(theme) ? theme : 'dark';
|
|
document.documentElement.dataset.theme = target;
|
|
setText('theme-name', {dark:'Dark',light:'Light',cream:'Cream'}[target]);
|
|
try {localStorage.setItem('rustproxy-repo-theme', target);} catch (_) { /* optional */ }
|
|
}
|
|
function navigate(section) {
|
|
const next = SECTION_LABELS[section] ? section : 'overview';
|
|
state.section = next;
|
|
document.querySelectorAll('.view').forEach((view) => {view.hidden = view.id !== `view-${next}`;});
|
|
document.querySelectorAll('[data-section]').forEach((btn) => {
|
|
const active = btn.dataset.section === next;
|
|
btn.classList.toggle('active', active);
|
|
if (active) btn.setAttribute('aria-current','page'); else btn.removeAttribute('aria-current');
|
|
});
|
|
setText('crumb-section', SECTION_LABELS[next]);
|
|
if (window.location.hash !== `#${next}`) history.replaceState(null, '', `${location.pathname}${location.search}#${next}`);
|
|
window.scrollTo({top:0,behavior:'instant'});
|
|
}
|
|
|
|
function buildCard(build, channel) {
|
|
const live = !!build?.published;
|
|
const commit = build?.source_commit ? build.source_commit.slice(0,9) : '—';
|
|
const subtitle = channel === 'linux' ? 'APT / DNF · amd64 + x86_64' : channel === 'suse'
|
|
? 'RPM / Zypper · x86_64' : channel === 'opnsense'
|
|
? `PKG · ${build?.opnsense_series || 'FreeBSD'}` : `PKG · ${build?.freebsd_abi || 'Native'}`;
|
|
const version = build?.package_version || (live ? build?.app_version : '') || '—';
|
|
return `<article class="build-card${live ? '' : ' off'}">
|
|
<div class="build-card-top"><span>${esc(CHANNEL_LABELS[channel].toUpperCase())} / ${['linux','suse'].includes(channel) ? 'SIGNED' : 'PKG'}</span><span class="pill ${live ? 'live' : 'missing'}">${live ? 'PUBLISHED' : 'NOT BUILT'}</span></div>
|
|
<div class="build-card-version">${esc(version)}</div><div class="build-card-sub">${esc(subtitle)}</div>
|
|
<div class="build-card-details"><div><small>BUILD DATE</small><strong title="${esc(dateLabel(build?.date_source))}">${esc(formatDate(build?.built_at))}</strong></div><div><small>COMMIT</small><strong class="commit">${esc(commit)}</strong></div></div>
|
|
</article>`;
|
|
}
|
|
|
|
function formatBadge(pkg) {
|
|
return `<span class="format-label${pkg.format === 'PKG' ? ' pkg' : ''}">${esc(pkg.format)}</span>`;
|
|
}
|
|
function tableRow(pkg, compact = false) {
|
|
const download = `<a class="download-link" href="${esc(safeLink(pkg.download_url))}" target="_blank" rel="noopener noreferrer" download title="Download ${esc(pkg.name)} ${esc(pkg.version)}">${compact ? 'Download' : 'Download file'} <span>↗</span></a>`;
|
|
const first = `<span class="artifact-name" title="${esc(pkg.name)}">${esc(pkg.name)}</span><span class="artifact-arch">${esc(pkg.architecture)}</span>`;
|
|
const format = `${formatBadge(pkg)}<span class="channel-tag ${pkg.format === 'PKG' ? 'pkg' : ''}">${esc(CHANNEL_LABELS[pkg.channel] || pkg.channel)}</span>`;
|
|
const date = `<span class="date-cell" title="${esc(dateLabel(pkg.date_source))}">${esc(formatDate(pkg.built_at))}</span>`;
|
|
if (compact) return `<tr><td>${first}</td><td>${format}</td><td><span class="version-code">${esc(pkg.version)}</span></td><td>${date}</td><td>${download}</td></tr>`;
|
|
return `<tr><td>${first}</td><td>${format}</td><td><span class="version-code">${esc(pkg.version)}</span></td><td>${date}</td><td><span class="date-cell">${esc(formatSize(pkg.size_bytes))}</span></td><td>${download}</td></tr>`;
|
|
}
|
|
|
|
function renderOverview(data) {
|
|
const main = data.builds?.[state.site];
|
|
const heroVersion = versionText(main);
|
|
setText('hero-version', heroVersion);
|
|
setText('hero-channel', state.site.toUpperCase());
|
|
setText('hero-date', formatDate(main?.built_at));
|
|
setText('hero-commit', main?.source_commit ? main.source_commit.slice(0,12) : '—');
|
|
el('hero-date').title = dateLabel(main?.date_source);
|
|
setText('stat-packages', formatNumber.format(data.summary?.packages_count || 0));
|
|
setText('stat-channels', `${data.summary?.channels_published || 0} / 4`);
|
|
setText('stat-date', formatShortDate(data.summary?.latest_build_at));
|
|
setText('stat-version', heroVersion);
|
|
setText('stat-release', main?.package_version ? `Package: ${main.package_version}` : 'No release published yet');
|
|
setText('stat-date-sub', 'Latest publication date');
|
|
setText('pipeline-updated', formatDate(data.generated_at));
|
|
const ready = !!data.ready;
|
|
el('hero-status').innerHTML = `<span class="pulse-dot"></span> ${ready ? 'REPOSITORY ONLINE' : 'WAITING FOR BUILD'}`;
|
|
el('hero-status').classList.toggle('waiting', !ready);
|
|
el('sidebar-status').classList.toggle('waiting', !ready);
|
|
el('sidebar-status').querySelector('span').textContent = ready ? 'REPOSITORY ONLINE' : 'WAITING FOR DATA';
|
|
const side = el('side-channels');
|
|
side.innerHTML = ['linux','suse','opnsense','freebsd'].map((channel) => `<span class="side-channel${data.builds[channel]?.published ? '' : ' disabled'}"><i class="tiny-dot"></i>${esc(CHANNEL_LABELS[channel])}<small>${channel === 'linux' ? 'APT / DNF' : channel === 'suse' ? 'RPM / Zypper' : 'PKG'}</small></span>`).join('');
|
|
el('build-grid').innerHTML = ['linux','suse','opnsense','freebsd'].map((channel) => buildCard(data.builds[channel],channel)).join('');
|
|
const recent = data.packages.filter((pkg) => state.site === 'linux' || pkg.channel === state.site).slice(0,6);
|
|
el('recent-rows').innerHTML = recent.length ? recent.map((pkg) => tableRow(pkg,true)).join('') : '<tr><td colspan="5" class="empty-table">No packages published yet. The catalog will populate after the first build.</td></tr>';
|
|
}
|
|
|
|
function filteredPackages() {
|
|
const arr = state.catalog?.packages || [];
|
|
const search = state.search.trim().toLocaleLowerCase('en-GB');
|
|
const filtered = arr.filter((pkg) => {
|
|
if (state.channel !== 'all' && pkg.channel !== state.channel) return false;
|
|
if (state.format !== 'all' && pkg.format !== state.format) return false;
|
|
if (!search) return true;
|
|
return [pkg.name,pkg.version,pkg.channel,pkg.format,pkg.architecture].some((field) => String(field ?? '').toLocaleLowerCase('en-GB').includes(search));
|
|
});
|
|
if (state.sort === 'name') filtered.sort((a,b) => collator.compare(a.name,b.name) || collator.compare(b.version,a.version));
|
|
if (state.sort === 'version') filtered.sort((a,b) => collator.compare(b.version,a.version) || collator.compare(a.name,b.name));
|
|
if (state.sort === 'size') filtered.sort((a,b) => b.size_bytes - a.size_bytes);
|
|
if (state.sort === 'date') filtered.sort((a,b) => String(b.built_at).localeCompare(String(a.built_at)) || collator.compare(b.version,a.version));
|
|
return filtered;
|
|
}
|
|
function renderPackages() {
|
|
const data = state.catalog;
|
|
if (!data) return;
|
|
const all = filteredPackages();
|
|
const shown = all.slice(0,state.rows);
|
|
setText('packages-total', `${formatNumber.format(data.summary.packages_count)} packages`);
|
|
setText('filter-count', `${formatNumber.format(all.length)} ${all.length === 1 ? 'result' : 'results'}`);
|
|
setText('result-range', all.length ? `Showing ${shown.length} of ${all.length}` : 'No matching packages');
|
|
el('catalog-rows').innerHTML = shown.length ? shown.map((pkg) => tableRow(pkg)).join('') : '<tr><td colspan="6" class="empty-table">No packages match these filters. Adjust your search or check back after a new release.</td></tr>';
|
|
el('show-more').hidden = shown.length >= all.length;
|
|
}
|
|
|
|
function instructions(kind, data) {
|
|
const urls = data.repository_urls || {};
|
|
const linux = origin(urls.linux);
|
|
const suse = origin(urls.suse || `${linux}/suse`);
|
|
const opnsense = origin(urls.opnsense);
|
|
const freebsd = origin(urls.freebsd);
|
|
const distribution = data.apt_distribution || 'stable';
|
|
const component = data.apt_component || 'main';
|
|
if (kind === 'deb') {
|
|
return {
|
|
description:'Add the signed APT repository, update the package list, and install RustProxy.',
|
|
signature:'GPG SIGNED',
|
|
note:'Supported: Debian / Ubuntu · amd64 architecture.',
|
|
reference:`${linux}/apt/dists/${encodeURIComponent(distribution)}/InRelease`,
|
|
agent:'sudo apt install rustproxy-docker-agent',
|
|
code:[
|
|
`curl -fsSL ${linux}/repo-key.gpg \\`,
|
|
' | sudo gpg --dearmor -o /usr/share/keyrings/rustproxy.gpg',
|
|
'',
|
|
`echo "deb [signed-by=/usr/share/keyrings/rustproxy.gpg] ${linux}/apt ${distribution} ${component}" \\`,
|
|
' | sudo tee /etc/apt/sources.list.d/rustproxy.list',
|
|
'',
|
|
'sudo apt update',
|
|
'sudo apt install rustproxy',
|
|
].join('\n'),
|
|
};
|
|
}
|
|
if (kind === 'rpm') {
|
|
return {
|
|
description:'Add the signed DNF/YUM repository and install the RustProxy server.',
|
|
signature:'GPG SIGNED',
|
|
note:'Supported: RHEL / Rocky / Alma / Fedora · x86_64 architecture.',
|
|
reference:`${linux}/rpm/x86_64/repodata/repomd.xml`,
|
|
agent:'sudo dnf install rustproxy-docker-agent',
|
|
code:[
|
|
`sudo rpm --import ${linux}/repo-key.gpg`,
|
|
'',
|
|
"sudo tee /etc/yum.repos.d/rustproxy.repo > /dev/null <<'REPO'",
|
|
'[rustproxy]',
|
|
'name=RustProxy Repository',
|
|
`baseurl=${linux}/rpm/$basearch`,
|
|
'enabled=1',
|
|
'gpgcheck=1',
|
|
'repo_gpgcheck=1',
|
|
`gpgkey=${linux}/repo-key.gpg`,
|
|
'REPO',
|
|
'',
|
|
'sudo dnf install rustproxy',
|
|
].join('\n'),
|
|
};
|
|
}
|
|
if (kind === 'suse') {
|
|
return {
|
|
description:'Add the signed RPM repository with Zypper on SUSE Linux Enterprise, openSUSE Leap or Tumbleweed.',
|
|
signature:'GPG SIGNED',
|
|
note:'x86_64. Minimum glibc: 2.34; intended for SLES 15 SP6+, SLES 16+, Leap 15.6+, and Tumbleweed. Check Python >= 3.10 dependencies.',
|
|
reference:`${suse}/x86_64/repodata/repomd.xml`,
|
|
agent:'sudo zypper install rustproxy-docker-agent',
|
|
code:[
|
|
`sudo rpm --import ${linux}/repo-key.gpg`,
|
|
`sudo zypper addrepo --refresh --gpgcheck ${suse}/x86_64/ rustproxy-suse`,
|
|
'sudo zypper refresh rustproxy-suse',
|
|
'sudo zypper install rustproxy',
|
|
].join('\n'),
|
|
};
|
|
}
|
|
if (kind === 'opnsense') {
|
|
return {
|
|
description:'Fetch the pkg repository configuration and install the os-rustproxy plugin.',
|
|
signature:'UNSIGNED PKG',
|
|
note:'The pkg repository index is unsigned. Only use a trusted HTTPS host.',
|
|
reference:`${opnsense}/RustProxy.conf`,
|
|
agent:null,
|
|
code:[
|
|
'mkdir -p /usr/local/etc/pkg/repos',
|
|
`fetch -o /usr/local/etc/pkg/repos/RustProxy.conf ${opnsense}/RustProxy.conf`,
|
|
'pkg update -f',
|
|
'pkg install os-rustproxy',
|
|
].join('\n'),
|
|
};
|
|
}
|
|
return {
|
|
description:'Install the native rustproxy package and enable its rc.d service.',
|
|
signature:'UNSIGNED PKG',
|
|
note:'The FreeBSD pkg index is unsigned. Only use a trusted HTTPS source.',
|
|
reference:`${freebsd}/RustProxy-FreeBSD.conf`,
|
|
agent:null,
|
|
code:[
|
|
'mkdir -p /usr/local/etc/pkg/repos',
|
|
`fetch -o /usr/local/etc/pkg/repos/RustProxy-FreeBSD.conf ${freebsd}/RustProxy-FreeBSD.conf`,
|
|
'pkg update -f',
|
|
'pkg install rustproxy',
|
|
'sysrc rustproxy_enable=YES',
|
|
'service rustproxy start',
|
|
].join('\n'),
|
|
};
|
|
}
|
|
|
|
function renderInstall() {
|
|
if (!state.catalog) return;
|
|
const kind = state.installing;
|
|
const instructionsData = instructions(kind,state.catalog);
|
|
document.querySelectorAll('[data-install]').forEach((node) => {
|
|
const active = node.dataset.install === kind;
|
|
node.classList.toggle('active',active);
|
|
node.setAttribute('aria-selected', String(active));
|
|
});
|
|
setText('install-tab-title', INSTALL_NAMES[kind]);
|
|
setText('install-description', instructionsData.description);
|
|
setText('install-signature', instructionsData.signature);
|
|
el('install-signature').classList.toggle('live', instructionsData.signature === 'GPG SIGNED');
|
|
setText('install-command', instructionsData.code);
|
|
setText('install-note', instructionsData.note);
|
|
// Docker discovery agent is only built for Linux DEB/RPM.
|
|
const hasDockerAgent = (kind === 'deb' || kind === 'rpm' || kind === 'suse') && !!instructionsData.agent;
|
|
el('agent-card').hidden = !hasDockerAgent;
|
|
el('install-extra-grid').classList.toggle('single-card', !hasDockerAgent);
|
|
setText('trust-index', hasDockerAgent ? '02' : '01');
|
|
setText('agent-command', hasDockerAgent ? instructionsData.agent : '');
|
|
el('install-reference').href = safeLink(instructionsData.reference);
|
|
}
|
|
|
|
function endpointsCard(name, label, url, links, signed) {
|
|
return `<div class="endpoint-card">
|
|
<div class="endpoint-card-top"><h2>${esc(name)}</h2><span class="pill${signed ? ' live' : ''}">${esc(label)}</span></div>
|
|
<a href="${esc(safeLink(url))}" target="_blank" rel="noopener noreferrer">${esc(url)} ↗</a>
|
|
<ul>${links.map((link) => `<li><a href="${esc(safeLink(link.url))}" target="_blank" rel="noopener noreferrer"><span>${esc(link.name)}</span><span>↗</span></a></li>`).join('')}</ul>
|
|
</div>`;
|
|
}
|
|
function renderRepositories() {
|
|
const data = state.catalog;
|
|
if (!data) return;
|
|
const url = data.repository_urls;
|
|
const distribution = encodeURIComponent(data.apt_distribution || 'stable');
|
|
el('endpoints-grid').innerHTML = [
|
|
endpointsCard('Linux','GPG SIGNED',url.linux,[
|
|
{name:'GPG public key',url:`${url.linux}/repo-key.gpg`},
|
|
{name:'APT InRelease',url:`${url.linux}/apt/dists/${distribution}/InRelease`},
|
|
{name:'RPM repomd.xml',url:`${url.linux}/rpm/x86_64/repodata/repomd.xml`},
|
|
{name:'Build info',url:`${url.linux}/LINUX-BUILD-INFO.txt`},
|
|
],true),
|
|
endpointsCard('SUSE / openSUSE','GPG SIGNED',url.suse,[
|
|
{name:'GPG public key',url:`${url.linux}/repo-key.gpg`},
|
|
{name:'RPM repomd.xml',url:`${url.suse}/x86_64/repodata/repomd.xml`},
|
|
{name:'Metadata signature',url:`${url.suse}/x86_64/repodata/repomd.xml.asc`},
|
|
{name:'Build info',url:`${url.linux}/SUSE-BUILD-INFO.txt`},
|
|
],true),
|
|
endpointsCard('OPNsense','UNSIGNED PKG',url.opnsense,[
|
|
{name:'RustProxy.conf',url:`${url.opnsense}/RustProxy.conf`},
|
|
{name:'meta.conf',url:`${url.opnsense}/meta.conf`},
|
|
{name:'packagesite.pkg',url:`${url.opnsense}/packagesite.pkg`},
|
|
{name:'Build info',url:`${url.opnsense}/BUILD-INFO.txt`},
|
|
],false),
|
|
endpointsCard('FreeBSD','UNSIGNED PKG',url.freebsd,[
|
|
{name:'RustProxy-FreeBSD.conf',url:`${url.freebsd}/RustProxy-FreeBSD.conf`},
|
|
{name:'meta.conf',url:`${url.freebsd}/meta.conf`},
|
|
{name:'packagesite.pkg',url:`${url.freebsd}/packagesite.pkg`},
|
|
{name:'Build info',url:`${url.freebsd}/BUILD-INFO.txt`},
|
|
],false),
|
|
].join('');
|
|
el('metadata-table').innerHTML = ['linux','suse','opnsense','freebsd'].map((channel) => {
|
|
const b = data.builds[channel] || {};
|
|
const rows = [
|
|
['APPLICATION VERSION',b.app_version || '—','latest-value'],
|
|
['PACKAGE VERSION',b.package_version || '—',''],
|
|
['BUILD DATE',formatDate(b.built_at),''],
|
|
['SOURCE COMMIT',b.source_commit || '—',''],
|
|
['PACKAGING REVISION',b.packaging_revision || '—',''],
|
|
];
|
|
return `<div class="meta-block"><h3>${esc(CHANNEL_LABELS[channel])}</h3>${rows.map(([label,value,klass]) => `<div class="meta-row"><small>${esc(label)}</small><strong class="${klass}"${label === 'BUILD DATE' ? ` title="${esc(dateLabel(b.date_source))}"` : ''}>${esc(value)}</strong></div>`).join('')}</div>`;
|
|
}).join('');
|
|
}
|
|
|
|
function renderAll(data) {
|
|
state.catalog = data;
|
|
renderOverview(data);
|
|
renderPackages();
|
|
renderInstall();
|
|
renderRepositories();
|
|
setText('last-sync', formatDate(data.generated_at));
|
|
el('last-sync').innerHTML = `<i></i> ${esc(formatDate(data.generated_at))}`;
|
|
}
|
|
|
|
async function refresh() {
|
|
if (state.refreshing) return;
|
|
state.refreshing = true;
|
|
el('refresh').classList.add('busy');
|
|
el('refresh').disabled = true;
|
|
try {
|
|
const response = await fetch('/api/v1/packages', {cache:'no-store',headers:{'Accept':'application/json'}});
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const data = await response.json();
|
|
if (!Array.isArray(data.packages) || !data.builds || !data.summary) throw new Error('Invalid catalog');
|
|
renderAll(data);
|
|
el('error-banner').hidden = true;
|
|
} catch (err) {
|
|
el('error-banner').hidden = false;
|
|
setText('last-sync','Load failed');
|
|
console.error('RustProxy repository:',err);
|
|
} finally {
|
|
state.refreshing = false;
|
|
el('refresh').classList.remove('busy');
|
|
el('refresh').disabled = false;
|
|
}
|
|
}
|
|
|
|
async function copyText(text) {
|
|
try {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
await navigator.clipboard.writeText(text);
|
|
} else {
|
|
const input = document.createElement('textarea');
|
|
input.value = text;
|
|
input.setAttribute('readonly','');
|
|
input.className = 'clipboard-helper';
|
|
document.body.appendChild(input);
|
|
input.focus();input.select();
|
|
const copied = document.execCommand('copy');
|
|
input.remove();
|
|
if (!copied) throw new Error('Clipboard unsupported');
|
|
}
|
|
toast('Copied to clipboard');
|
|
} catch (_) { toast('Copy failed. Select and copy the command manually.'); }
|
|
}
|
|
|
|
function init() {
|
|
setText('year',new Date().getFullYear());
|
|
try {setTheme(localStorage.getItem('rustproxy-repo-theme') || 'dark');} catch (_) {setTheme('dark');}
|
|
document.querySelectorAll('[data-section]').forEach((button) => button.addEventListener('click',() => navigate(button.dataset.section)));
|
|
document.querySelectorAll('[data-go-section]').forEach((button) => button.addEventListener('click',() => navigate(button.dataset.goSection)));
|
|
document.querySelectorAll('[data-install]').forEach((button) => button.addEventListener('click',() => {state.installing = button.dataset.install;renderInstall();}));
|
|
document.querySelectorAll('[data-copy-target]').forEach((button) => button.addEventListener('click',() => copyText(el(button.dataset.copyTarget)?.textContent || '')));
|
|
el('theme-toggle').addEventListener('click',() => {
|
|
const themes = ['dark','light','cream'];
|
|
const i = themes.indexOf(document.documentElement.dataset.theme);
|
|
setTheme(themes[(i+1)%themes.length]);
|
|
});
|
|
el('refresh').addEventListener('click',() => {refresh();});
|
|
el('retry-inline').addEventListener('click',() => {refresh();});
|
|
el('search-input').addEventListener('input',(event) => {state.search = event.target.value;state.rows = 25;renderPackages();});
|
|
el('filter-channel').value = state.channel;
|
|
el('filter-channel').addEventListener('change',(event) => {state.channel = event.target.value;state.rows = 25;renderPackages();});
|
|
el('filter-format').addEventListener('change',(event) => {state.format = event.target.value;state.rows = 25;renderPackages();});
|
|
el('filter-sort').addEventListener('change',(event) => {state.sort = event.target.value;state.rows = 25;renderPackages();});
|
|
el('show-more').addEventListener('click',() => {state.rows += 25;renderPackages();});
|
|
window.addEventListener('hashchange',() => navigate(window.location.hash.slice(1)));
|
|
navigate(window.location.hash.slice(1) || 'overview');
|
|
refresh();
|
|
window.setInterval(() => {if (!document.hidden) refresh();}, 60000);
|
|
document.addEventListener('visibilitychange',() => {if (!document.hidden) refresh();});
|
|
}
|
|
init();
|
|
})();
|