Initial curated chimera source from a8ae5b1db07fb49926c94585e2718e5130330974
This commit is contained in:
commit
c737a37f71
531 files changed
+119270
No files matched your search
@@ -0,0 +1,199 @@
|
||||
// ── 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) => `
|
||||
<button class="dest-picker-option" data-idx="${i}">
|
||||
<span class="dest-opt-label">${escHtml(p.label)}</span>
|
||||
<span class="dest-opt-path">${escHtml(p.path)}</span>
|
||||
</button>
|
||||
`).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 '<span style="color:var(--text-dim)">—</span>';
|
||||
return firmware_labels.split(',').filter(Boolean)
|
||||
.map(l => `<span class="fw-pill fw-pill-patched" title="Backporked for firmware ${escHtml(l)}">🔧 ${escHtml(l)}</span>`).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 = `<button class="fwbtn${state.firmwareFilter === 'all' ? ' active' : ''}" data-fw="all">All</button>` +
|
||||
labels.map(l => `<button class="fwbtn${state.firmwareFilter === l ? ' active' : ''}" data-fw="${escHtml(l)}">${escHtml(l)}</button>`).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();
|
||||
};
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user