// Lightweight multi-page handler for GD fedl (function(){ function qs(id){return document.getElementById(id)} const page = document.body.dataset.page; let cachedItems = null; let cachedLevelMeta = null; function read(key, fallback){ try{const v = localStorage.getItem(key); return v?JSON.parse(v):fallback} catch(e){return fallback} } function write(key, val){localStorage.setItem(key,JSON.stringify(val))} function parseData(txt){ return txt.split(/\r?\n/).map(l=>l.trim()).filter(Boolean).map((l, index)=>{ const parts = l.split('|').map(p=>p.trim()); return { id:`legacy-${index + 1}`, level:parts[0]||'Unknown', position:parts[1]||'', title:parts[2]||'Untitled', url:parts[3]||'' }; }); } function parseLevelMeta(txt){ const map = {}; txt.split(/\r?\n/).map(l=>l.trim()).filter(Boolean).forEach(l=>{ if(l.startsWith('//')) return; const parts = l.split('|').map(p=>p.trim()); const title = parts[0] || ''; if(!title) return; map[title] = { levelId: parts[1] || 'unknown', percent: parts[2] || '100' }; }); return map; } function invalidateItems(){cachedItems = null} function loadItems(){ if(cachedItems) return Promise.resolve(cachedItems); return fetch('/api/levels', {cache:'no-store'}).then(r=>{ if(!r.ok) throw new Error('API unavailable'); return r.json(); }).then(data=>{ cachedItems = Array.isArray(data.items) ? data.items : []; return cachedItems; }).catch(()=>{ return fetch('data.txt').then(r=>r.text()).then(txt=>{ cachedItems = parseData(txt); return cachedItems; }); }); } function requestJson(url, options){ return fetch(url, Object.assign({ cache:'no-store', headers:{'Content-Type':'application/json'} }, options || {})).then(async r=>{ const data = await r.json().catch(()=>({})); if(!r.ok) throw new Error(data.error || 'Request failed.'); return data; }); } function loadLevelMeta(){ if(cachedLevelMeta) return Promise.resolve(cachedLevelMeta); return fetch('level-ids.txt').then(r=>r.text()).then(txt=>{ cachedLevelMeta = parseLevelMeta(txt); return cachedLevelMeta; }).catch(()=>{ cachedLevelMeta = {}; return cachedLevelMeta; }); } function fetchLevelIdFromApi(title){ const url = `https://gdbrowser.com/api/search/${encodeURIComponent(title)}?diff=-2&demonFilter=5&count=10`; return fetch(url).then(r=>r.json()).then(results=>{ if(!Array.isArray(results) || !results.length) return null; const exact = results.find(item=>String(item.name||'').toLowerCase() === String(title||'').toLowerCase()); const match = exact || results[0]; if(!match || !match.id) return null; return String(match.id); }).catch(()=>null); } function extractYouTubeID(url){ const m = String(url || '').match(/(?:v=|\/embed\/|youtu\.be\/)([A-Za-z0-9_-]{6,})/); return m ? m[1] : ''; } function openVideo(url){ if(!url) return; const id = extractYouTubeID(url); if(!id){ window.open(url,'_blank'); return; } let modal = document.querySelector('.video-modal'); if(!modal){ modal = document.createElement('div'); modal.className = 'video-modal'; const inner = document.createElement('div'); inner.className = 'inner'; const close = document.createElement('button'); close.textContent = 'Close'; close.className = 'btn'; close.style.float = 'right'; close.onclick = ()=>modal.remove(); inner.appendChild(close); const iframe = document.createElement('iframe'); iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture'; iframe.allowFullscreen = true; inner.appendChild(iframe); modal.appendChild(inner); document.body.appendChild(modal); } modal.querySelector('iframe').src = `https://www.youtube.com/embed/${id}`; modal.style.display = 'flex'; } if(page==='roulette'){ const spinBtn = qs('roulette-spin'); const statusEl = qs('roulette-status'); const titleEl = qs('roulette-title'); const rankEl = qs('roulette-rank'); const idEl = qs('roulette-level-id'); const noteEl = qs('roulette-note'); const openEl = qs('roulette-open'); function showPick(item, meta){ statusEl.textContent = 'Your demon is:'; titleEl.textContent = item.title; rankEl.textContent = `Rank: #${item.position}`; idEl.textContent = `Level ID: ${meta.levelId || 'unknown'}`; noteEl.textContent = meta.source === 'api' ? 'Level ID was looked up from the Geometry Dash community API.' : 'Level ID came from your local level-ids.txt file.'; if(item.url){ openEl.hidden = false; openEl.href = item.url; }else{ openEl.hidden = true; } } spinBtn.addEventListener('click', ()=>{ statusEl.textContent = 'Spinning...'; titleEl.textContent = 'Choosing a demon'; rankEl.textContent = 'Rank: -'; idEl.textContent = 'Level ID: -'; noteEl.textContent = 'Checking your local file and API if needed.'; openEl.hidden = true; Promise.all([loadItems(), loadLevelMeta()]).then(([items, metaMap])=>{ if(!items.length){ statusEl.textContent = 'No demons found.'; titleEl.textContent = 'Add demons from the manage page'; idEl.textContent = 'Level ID: -'; noteEl.textContent = 'No list data was found.'; return; } const item = items[Math.floor(Math.random()*items.length)]; const localMeta = metaMap[item.title] || {levelId:'unknown', percent:'100'}; if(localMeta.levelId && localMeta.levelId !== 'unknown'){ window.setTimeout(()=>showPick(item, {levelId: localMeta.levelId, percent: localMeta.percent, source: 'file'}), 350); return; } fetchLevelIdFromApi(item.title).then(levelId=>{ const meta = { levelId: levelId || 'unknown', percent: localMeta.percent || '100', source: levelId ? 'api' : 'file' }; window.setTimeout(()=>showPick(item, meta), 350); }); }).catch(err=>{ statusEl.textContent = 'Could not load the list.'; titleEl.textContent = 'Run the Node server'; rankEl.textContent = 'Rank: -'; idEl.textContent = 'Level ID: -'; noteEl.textContent = 'The local file or API lookup failed.'; console.error(err); }); }); } if(page==='players'){ const form = qs('player-form'); const nameIn = qs('player-name'); const list = qs('players'); let players = read('gd_players',[]); function render(){ list.innerHTML=''; players.forEach((p,idx)=>{ const li=document.createElement('li'); li.innerHTML=`${escapeHtml(p.name)} (${p.id})`; const actions=document.createElement('div'); actions.className='actions'; const edit=document.createElement('button'); edit.textContent='Edit'; edit.onclick=()=>{const nv=prompt('Edit name',p.name); if(nv){players[idx].name=nv; write('gd_players',players); render()}}; const del=document.createElement('button'); del.textContent='Delete'; del.onclick=()=>{if(confirm('Delete player?')){players.splice(idx,1); write('gd_players',players); render()}}; actions.appendChild(edit); actions.appendChild(del); li.appendChild(actions); list.appendChild(li); }); } form.addEventListener('submit',e=>{ e.preventDefault(); const name=nameIn.value.trim(); if(!name) return; players.push({id:Date.now().toString(36),name}); write('gd_players',players); nameIn.value=''; render(); }); render(); } if(page==='lists'){ const levelsEl = qs('levels'); const listArea = qs('list-area'); function loadData(){ loadItems().then(items=>{ setupLevels(items); }).catch(err=>{ listArea.innerHTML='Failed to load list data. Start the Node server and try again.'; console.error(err); }); } function computeCategories(items){ const max = items.reduce((m,it)=>Math.max(m, Number(it.position)||0), 0); const cats = ['Full List']; for(let i=1;i<=max;i+=10){ const start = i; const end = Math.min(i+9, max); cats.push(`Top ${start}-${end}`); } return cats; } function setupLevels(items){ const categories = computeCategories(items); levelsEl.innerHTML=''; const filterSelect = qs('level-filter'); filterSelect.innerHTML = ''; categories.forEach(cat=>{ const li = document.createElement('li'); const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = cat; btn.className = 'level-link'; btn.addEventListener('click', ()=> { selectLevel(cat, items, btn); filterSelect.value = cat; }); li.appendChild(btn); levelsEl.appendChild(li); const opt = document.createElement('option'); opt.value = cat; opt.textContent = cat; filterSelect.appendChild(opt); }); qs('search').addEventListener('input', ()=> renderTable(items)); filterSelect.addEventListener('change', ()=> renderTable(items)); selectLevel('Full List', items, levelsEl.querySelector('.level-link')); } function selectLevel(level, items, linkEl){ levelsEl.querySelectorAll('.level-link').forEach(a=>a.classList.remove('active')); if(linkEl) linkEl.classList.add('active'); qs('level-filter').value = level; renderTable(items); } function renderTable(items){ const q = (qs('search') && qs('search').value || '').toLowerCase(); const levelFilter = (qs('level-filter') && qs('level-filter').value) || 'all'; const filtered = items.filter(it=>{ if(levelFilter && levelFilter!=='all' && levelFilter!=='Full List'){ const m = levelFilter.match(/Top\s*(\d+)-(\d+)/i); if(m){ const s = Number(m[1]); const e = Number(m[2]); const pos = Number(it.position)||0; if(pos < s || pos > e) return false; } } if(!q) return true; return (it.title||'').toLowerCase().includes(q) || (it.level||'').toLowerCase().includes(q); }).sort((a,b)=> (Number(a.position)||0)-(Number(b.position)||0)); const tbody = qs('list-area'); tbody.innerHTML=''; filtered.forEach(it=>{ const tr = document.createElement('tr'); const tdNum = document.createElement('td'); tdNum.textContent = it.position; const tdTitle = document.createElement('td'); tdTitle.textContent = it.title; const tdAct = document.createElement('td'); const a = document.createElement('a'); a.textContent='Open'; a.href='#'; a.className='btn'; a.addEventListener('click', (e)=>{e.preventDefault(); openVideo(it.url)}); tdAct.appendChild(a); tr.appendChild(tdNum); tr.appendChild(tdTitle); tr.appendChild(tdAct); tbody.appendChild(tr); }); } loadData(); } if(page==='manage'){ const form = qs('level-form'); const statusEl = qs('manage-status'); const listEl = qs('manage-list'); const searchEl = qs('manage-search'); const formTitleEl = qs('manage-form-title'); const submitEl = qs('level-submit'); const resetEl = qs('level-reset'); const idEl = qs('level-id'); const categoryEl = qs('level-category'); const positionEl = qs('level-position'); const titleEl = qs('level-title'); const urlEl = qs('level-url'); let items = []; function setStatus(message, isError){ statusEl.textContent = message; statusEl.style.color = isError ? '#ffb4b4' : ''; } function resetForm(){ form.reset(); idEl.value = ''; categoryEl.value = 'new'; formTitleEl.textContent = 'Add Entry'; submitEl.textContent = 'Save Entry'; setStatus(`Loaded ${items.length} entr${items.length===1?'y':'ies'} from the server.`, false); } function fillForm(item){ idEl.value = item.id || ''; categoryEl.value = item.level || 'new'; positionEl.value = item.position || ''; titleEl.value = item.title || ''; urlEl.value = item.url || ''; formTitleEl.textContent = `Edit #${item.position}`; submitEl.textContent = 'Update Entry'; window.scrollTo({top:0, behavior:'smooth'}); } function renderItems(){ const q = (searchEl.value || '').trim().toLowerCase(); const visible = items.filter(item=>{ if(!q) return true; return String(item.title||'').toLowerCase().includes(q) || String(item.level||'').toLowerCase().includes(q); }).sort((a,b)=>Number(a.position)-Number(b.position)); listEl.innerHTML = ''; if(!visible.length){ const tr = document.createElement('tr'); const td = document.createElement('td'); td.colSpan = 5; td.className = 'muted'; td.textContent = 'No entries match the current search.'; tr.appendChild(td); listEl.appendChild(tr); return; } visible.forEach(item=>{ const tr = document.createElement('tr'); const posTd = document.createElement('td'); posTd.textContent = item.position; const catTd = document.createElement('td'); catTd.textContent = item.level; const titleTd = document.createElement('td'); titleTd.textContent = item.title; const linkTd = document.createElement('td'); if(item.url){ const link = document.createElement('a'); link.href = item.url; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.className = 'table-link'; link.textContent = 'Open video'; linkTd.appendChild(link); }else{ linkTd.textContent = '-'; } const actionsTd = document.createElement('td'); const actions = document.createElement('div'); actions.className = 'table-actions'; const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.className = 'btn'; editBtn.textContent = 'Edit'; editBtn.addEventListener('click', ()=>fillForm(item)); const deleteBtn = document.createElement('button'); deleteBtn.type = 'button'; deleteBtn.className = 'btn ghost-btn'; deleteBtn.textContent = 'Delete'; deleteBtn.addEventListener('click', ()=>{ if(!confirm(`Delete "${item.title}"?`)) return; setStatus(`Deleting "${item.title}"...`, false); requestJson(`/api/levels/${encodeURIComponent(item.id)}`, {method:'DELETE'}) .then(data=>{ items = Array.isArray(data.items) ? data.items : []; invalidateItems(); renderItems(); resetForm(); }) .catch(error=>setStatus(error.message, true)); }); actions.appendChild(editBtn); actions.appendChild(deleteBtn); actionsTd.appendChild(actions); tr.appendChild(posTd); tr.appendChild(catTd); tr.appendChild(titleTd); tr.appendChild(linkTd); tr.appendChild(actionsTd); listEl.appendChild(tr); }); } function loadManageItems(){ setStatus('Loading current entries from the server.', false); loadItems().then(data=>{ items = Array.isArray(data) ? [...data] : []; renderItems(); resetForm(); }).catch(error=>setStatus(error.message || 'Could not load entries.', true)); } form.addEventListener('submit', e=>{ e.preventDefault(); const payload = { level: categoryEl.value.trim() || 'new', position: positionEl.value.trim(), title: titleEl.value.trim(), url: urlEl.value.trim() }; const id = idEl.value.trim(); const isEdit = Boolean(id); setStatus(isEdit ? `Updating "${payload.title}"...` : `Creating "${payload.title}"...`, false); requestJson(isEdit ? `/api/levels/${encodeURIComponent(id)}` : '/api/levels', { method: isEdit ? 'PUT' : 'POST', body: JSON.stringify(payload) }).then(data=>{ items = Array.isArray(data.items) ? data.items : items; invalidateItems(); renderItems(); resetForm(); }).catch(error=>setStatus(error.message, true)); }); resetEl.addEventListener('click', resetForm); searchEl.addEventListener('input', renderItems); loadManageItems(); } function escapeHtml(s){return String(s).replace(/[&<>"']/g, c=>({"&":"&","<":"<",">":">","\"":""","'":"'"})[c])} })();