// ── Accent color picker ──────────────────────────────────────────────────────── $('s-accent-color').addEventListener('input', async e => { const hex = e.target.value; applyAccent(hex); $('s-accent-hex').value = hex.toUpperCase(); await window.pork.setSettings({ accentColor: hex }); }); // Hex text field — let user type a hex value directly $('s-accent-hex').addEventListener('input', e => { const raw = e.target.value.trim(); const hex = raw.startsWith('#') ? raw : '#' + raw; if (/^#[0-9a-fA-F]{6}$/.test(hex)) { applyAccent(hex); window.pork.setSettings({ accentColor: hex }); } }); $('btn-reset-accent').addEventListener('click', async () => { const def = '#CBBAFF'; applyAccent(def); await window.pork.setSettings({ accentColor: def }); setStatus('Accent color reset to default', 'ok'); }); // Confirm dialog helper function showConfirm(message) { return new Promise(resolve => { $('confirm-message').textContent = message; openAccessibleDialog($('confirm-overlay'), $('confirm-ok')); function cleanup(result) { closeAccessibleDialog($('confirm-overlay')); $('confirm-ok').removeEventListener('click', onOk); $('confirm-cancel').removeEventListener('click', onCancel); $('confirm-overlay').removeEventListener('keydown', onKey); resolve(result); } const onOk = () => cleanup(true); const onCancel = () => cleanup(false); const onKey = event => { if (event.key === 'Escape') onCancel(); }; $('confirm-ok').addEventListener('click', onOk); $('confirm-cancel').addEventListener('click', onCancel); $('confirm-overlay').addEventListener('keydown', onKey); }); } // Prompt dialog helper function showPrompt(message, defaultValue = '', options = {}) { return new Promise(resolve => { $('prompt-message').textContent = message; $('prompt-input').value = defaultValue; $('prompt-dialog').classList.toggle('prompt-dialog-danger', options.tone === 'danger'); $('prompt-ok').className = `btn ${options.tone === 'danger' ? 'btn-danger' : 'btn-accent'}`; $('prompt-ok').textContent = options.confirmLabel || (typeof window.t === 'function' ? window.t('common.ok', 'OK') : 'OK'); $('prompt-cancel').textContent = typeof window.t === 'function' ? window.t('common.cancel', 'Cancel') : 'Cancel'; openAccessibleDialog($('prompt-overlay'), $('prompt-input')); function cleanup(result) { closeAccessibleDialog($('prompt-overlay')); $('prompt-ok').removeEventListener('click', onOk); $('prompt-cancel').removeEventListener('click', onCancel); $('prompt-input').removeEventListener('keydown', onKey); resolve(result); } function onOk() { cleanup($('prompt-input').value); } function onCancel() { cleanup(null); } function onKey(e) { if (e.key === 'Enter') onOk(); else if (e.key === 'Escape') onCancel(); } $('prompt-ok').addEventListener('click', onOk); $('prompt-cancel').addEventListener('click', onCancel); $('prompt-input').addEventListener('keydown', onKey); }); } async function showExactConfirmation(message, phrase) { const isDutch = window.i18n?.current === 'nl'; const englishPhrases = { VERWIJDER: 'DELETE', 'VERWIJDER VAN PS5': 'DELETE FROM PS5', UITSCHAKELEN: 'DISABLE', INTREKKEN: 'REVOKE', HERSTEL: 'RESTORE', 'PAS ETAHEN TOE': 'APPLY ETAHEN', 'WEK PS5': 'WAKE PS5' }; // i18n: server confirmation tokens const visiblePhrase = isDutch ? phrase : (englishPhrases[phrase] || phrase); const instruction = isDutch ? `Typ exact ${visiblePhrase} om door te gaan:` : `Type ${visiblePhrase} exactly to continue:`; const entered = await showPrompt(`${message}\n\n${instruction}`, '', { tone: 'danger', confirmLabel: isDutch ? 'Bevestigen' : 'Confirm', }); if (entered === visiblePhrase) return phrase; if (entered !== null) showToast(isDutch ? 'Actie geannuleerd: de bevestiging kwam niet exact overeen.' : 'Action cancelled: the confirmation phrase did not match exactly.', 'info'); return null; } // Destination picker for upload and conversion function showDestinationPicker(paths) { return new Promise(resolve => { const list = $('dest-picker-list'); list.innerHTML = paths.map((p, i) => ` `).join(''); openAccessibleDialog($('dest-picker-overlay'), list.querySelector('.dest-picker-option')); let resolved = false; function cleanup(result) { if (resolved) return; resolved = true; closeAccessibleDialog($('dest-picker-overlay')); // #dest-picker-cancel is a persistent element; remove our handler so it // doesn't accumulate across opens when the user picks an option instead. $('dest-picker-cancel').removeEventListener('click', onCancel); $('dest-picker-overlay').removeEventListener('keydown', onKey); resolve(result); } const onCancel = () => cleanup(null); const onKey = event => { if (event.key === 'Escape') onCancel(); }; // Option buttons live in list.innerHTML, which is re-rendered each open, so // their listeners die with the old DOM — no cleanup needed for those. list.querySelectorAll('.dest-picker-option').forEach((btn, i) => { btn.addEventListener('click', () => cleanup(paths[i]), { once: true }); }); $('dest-picker-cancel').addEventListener('click', onCancel); $('dest-picker-overlay').addEventListener('keydown', onKey); }); } // Ensure a game has an ftp_path set (prompting if needed). Returns the path or null if cancelled. async function ensureGameFtpPath(gameId) { const g = state.currentGame?.game_id === gameId ? state.currentGame : (await window.pork.listGames({})).find(x => x.game_id === gameId); if (g?.ftp_path) return g.ftp_path; // No stored path — pick one let basePath = null; if (remoteGamePaths.length === 0) { // No locations configured — use default, but don't store (user hasn't set up locations) return `/mnt/sandbox/pfsmnt/${gameId}-app0`; } else if (remoteGamePaths.length === 1) { basePath = remoteGamePaths[0].path; } else { const chosen = await showDestinationPicker(remoteGamePaths); if (!chosen) return null; basePath = chosen.path; } const ftp_path = basePath.replace(/\/?$/, '/') + gameId + '-app0'; await window.pork.updateGame({ game_id: gameId, fields: { ftp_path } }); if (state.currentGame?.game_id === gameId) { state.currentGame = { ...state.currentGame, ftp_path }; renderManageInstallLocation(state.currentGame); } return ftp_path; } $('btn-refresh-all-meta').addEventListener('click', async () => { const ok = await showConfirm( 'This will clear all cached Prospero metadata and re-fetch every game from scratch. ' + 'Background fetches will queue automatically. Continue?' ); if (!ok) return; $('btn-refresh-all-meta').disabled = true; setStatus('Queueing metadata refresh for all games…'); try { const { queued } = await window.pork.prosperoRefreshAll(); setStatus(`Refreshing metadata for ${queued} game(s) in background…`, 'ok'); } catch (e) { setStatus(`Refresh failed: ${e.message}`, 'error'); } finally { $('btn-refresh-all-meta').disabled = false; } }); // Firmware helpers function renderFwPills(firmware_labels) { if (!firmware_labels) return '—'; return firmware_labels.split(',').filter(Boolean) .map(l => `🔧 ${escHtml(l)}`).join(''); } async function refreshFirmwareFilters() { const labels = await window.pork.firmwareLabels(); const row = $('firmware-filter-row'); const cont = $('firmware-filter-btns'); if (!labels.length) { row.hidden = true; return; } row.hidden = false; cont.innerHTML = `` + labels.map(l => ``).join(''); cont.onclick = e => { const btn = e.target.closest('.fwbtn'); if (!btn) return; state.firmwareFilter = btn.dataset.fw; cont.querySelectorAll('.fwbtn').forEach(b => b.classList.toggle('active', b.dataset.fw === state.firmwareFilter)); renderGames(); }; }