document.addEventListener('alpine:init', () => { Alpine.data('pkgList', (i18n) => ({ state: 'loading', // 'loading' | 'loaded' | 'error' i18n: i18n, packages: [], filter: '', get filtered() { if (!this.filter) return this.packages; const q = this.filter.toLowerCase(); return this.packages.filter(p => p.name.toLowerCase().includes(q)); }, async init() { try { const res = await fetch('https://packages.danix.xyz/PACKAGES.TXT'); if (!res.ok) throw new Error('HTTP ' + res.status); const text = await res.text(); this.packages = parsePkgTxt(text); this.state = 'loaded'; } catch (e) { console.error('pkg-list fetch error:', e); this.state = 'error'; } } })); }); function parsePkgTxt(text) { const packages = []; const nameRe = /^PACKAGE NAME:\s+(.+\.txz)\s*$/m; const locRe = /^PACKAGE LOCATION:\s+(.+)\s*$/m; // Split on blank lines between blocks const blocks = text.split(/\n{2,}/); for (const block of blocks) { const nameMatch = block.match(nameRe); const locMatch = block.match(locRe); if (!nameMatch || !locMatch) continue; const filename = nameMatch[1].trim(); const location = locMatch[1].trim(); // e.g. ./desktop/waybar // Parse name and version from filename: name-version-arch-build_tag.txz // Strategy: version segment starts with a digit after a hyphen const parts = filename.replace(/\.txz$/, '').split('-'); let nameEnd = 1; for (let i = 1; i < parts.length; i++) { if (/^\d/.test(parts[i])) { nameEnd = i; break; } } const name = parts.slice(0, nameEnd).join('-'); const version = parts[nameEnd] || ''; // Derive folder URL: ./desktop/waybar -> https://packages.danix.xyz/desktop/waybar/ const folder = location.replace(/^\.\//, ''); const url = 'https://packages.danix.xyz/' + folder + '/'; const label = folder; // e.g. desktop/waybar packages.push({ name, version, url, label }); } // Sort alphabetically by name packages.sort((a, b) => a.name.localeCompare(b.name)); return packages; }