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,481 @@
|
||||
// ── Games page ────────────────────────────────────────────────────────────────
|
||||
async function loadGames() {
|
||||
try {
|
||||
const library = await window.pork.listLibraryTitles({ limit: 1000 });
|
||||
state.games = window.chimeraLibraryModel.items(library);
|
||||
window.pork.shadowMountStatus?.().then(result => {
|
||||
const byGame = new Map();
|
||||
for (const entry of (result.model?.entries || [])) {
|
||||
if (!entry.titleId) continue;
|
||||
const current = byGame.get(entry.titleId) || [];
|
||||
current.push(entry);
|
||||
byGame.set(entry.titleId, current);
|
||||
}
|
||||
state.games = state.games.map(game => {
|
||||
const matches = byGame.get(game.game_id) || [];
|
||||
if (!matches.length) return game;
|
||||
return { ...game, shadow_mount: {
|
||||
state: matches.some(entry => entry.state === 'installed') ? 'installed' : matches[0].state,
|
||||
sources: matches.length,
|
||||
experimental: matches.some(entry => entry.format?.support === 'experimental'),
|
||||
formats: [...new Set(matches.map(entry => entry.format?.id).filter(Boolean))],
|
||||
} };
|
||||
});
|
||||
renderGames();
|
||||
}).catch(() => {});
|
||||
// Refresh hash summaries (fire-and-forget so badges update when ready)
|
||||
window.pork.hashGameSummary().then(summaries => {
|
||||
_populateHashMaps(summaries);
|
||||
renderGames();
|
||||
}).catch(() => {});
|
||||
renderGames();
|
||||
await refreshFirmwareFilters();
|
||||
await refreshConvQueuePanel();
|
||||
const conversionQueue = $('conv-queue-panel');
|
||||
const gamesPage = $('page-games');
|
||||
if (conversionQueue && gamesPage) gamesPage.append(conversionQueue);
|
||||
await refreshGameCatalogHealth();
|
||||
} catch (e) {
|
||||
setStatus(e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshGameCatalogHealth() {
|
||||
if (!window.pork.gameCatalogHealth || !$('game-catalog-health')) return;
|
||||
try {
|
||||
const health = await window.pork.gameCatalogHealth();
|
||||
$('catalog-health-complete').textContent = `${Number(health.complete || 0)} / ${Number(health.total || 0)}`;
|
||||
$('catalog-health-missing').textContent = String(Number(health.missing_metadata || 0));
|
||||
$('catalog-health-artwork').textContent = String(Number(health.missing_artwork || 0));
|
||||
const scan = health.scan || {};
|
||||
$('catalog-health-scan').textContent = scan.running ? 'Scanning…' : (scan.lastError ? 'Scan issue' : 'Automatic');
|
||||
$('catalog-health-scan-detail').textContent = scan.lastError
|
||||
? String(scan.lastError)
|
||||
: (scan.lastCompleted
|
||||
? window.tf('games.health.lastScan', 'last scan {date}', { date: new Date(scan.lastCompleted).toLocaleString(window.i18n?.current || 'en') })
|
||||
: window.t('games.health.interval', 'source check every 15 min'));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
window.pork.on('local:scan:started', refreshGameCatalogHealth);
|
||||
window.pork.on('local:scan:complete', () => refreshGameCatalogHealth());
|
||||
window.pork.on('prospero:queue:done', () => refreshGameCatalogHealth());
|
||||
|
||||
function gamePlatform(game) {
|
||||
const explicit = String(game?.platform || '').toLowerCase();
|
||||
if (explicit === 'ps4' || explicit === 'ps5') return explicit;
|
||||
const identity = [
|
||||
game?.game_id,
|
||||
game?.content_id,
|
||||
game?.prospero_content_id,
|
||||
].filter(Boolean).join(' ').toUpperCase();
|
||||
if (/CUSA\d{5}/.test(identity)) return 'ps4';
|
||||
if (/PPSA\d{5}/.test(identity)) return 'ps5';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
function versionedArtworkUrl(url, fetchedAt) {
|
||||
const source = String(url || '');
|
||||
if (!source.startsWith('/pork-cache/')) return source;
|
||||
const revision = Number(fetchedAt || 0);
|
||||
return revision > 0 ? `${source}?v=${Math.trunc(revision)}` : source;
|
||||
}
|
||||
|
||||
function disconnectedDriveLabels(game) {
|
||||
const labels = [];
|
||||
for (const copy of Array.isArray(game?.localCopies) ? game.localCopies : []) {
|
||||
const device = copy.device || copy.storageDevice || copy.volume?.device || {};
|
||||
const state = String(device.availabilityState || copy.volume?.availabilityState || copy.availabilityState || '').toLowerCase();
|
||||
if (!['unavailable', 'disconnected', 'offline', 'missing'].includes(state)) continue;
|
||||
labels.push(device.friendlyLabel || device.displayName || device.label || device.stableFingerprint || copy.volume?.stableVolumeKey || 'external drive');
|
||||
}
|
||||
return [...new Set(labels.map(String))];
|
||||
}
|
||||
|
||||
function renderGamePlatformFilters() {
|
||||
const counts = { all: state.games.length, ps4: 0, ps5: 0, unknown: 0 };
|
||||
for (const game of state.games) counts[gamePlatform(game)] += 1;
|
||||
for (const platform of Object.keys(counts)) {
|
||||
const count = $(`games-platform-count-${platform}`);
|
||||
if (count) count.textContent = String(counts[platform]);
|
||||
}
|
||||
document.querySelectorAll('[data-game-platform]').forEach(button => {
|
||||
const platform = button.dataset.gamePlatform;
|
||||
const active = platform === state.platformFilter;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
if (platform === 'unknown') button.hidden = counts.unknown === 0 && !active;
|
||||
});
|
||||
}
|
||||
|
||||
function getFilteredGames() {
|
||||
return state.games.filter(g => {
|
||||
const q = state.search.toLowerCase();
|
||||
if (q && !(g.title || '').toLowerCase().includes(q) && !(g.game_id || '').toLowerCase().includes(q) &&
|
||||
!(g.prospero_name || '').toLowerCase().includes(q)) return false;
|
||||
if (state.platformFilter !== 'all' && gamePlatform(g) !== state.platformFilter) return false;
|
||||
// Status and firmware are independent filter dimensions — compute status as a
|
||||
// boolean so both can compose (previously the status branches returned early,
|
||||
// silently disabling the firmware filter).
|
||||
let statusOk = true;
|
||||
if (state.filter === 'installed') statusOk = window.chimeraLibraryModel.isInstalled(g);
|
||||
else if (state.filter === 'backed_up') statusOk = window.chimeraLibraryModel.hasLocalCopy(g);
|
||||
else if (state.filter === 'not_backed') statusOk = window.chimeraLibraryModel.needsProtection(g);
|
||||
if (!statusOk) return false;
|
||||
if (state.firmwareFilter !== 'all') {
|
||||
const labels = (g.firmware_labels || '').split(',').filter(Boolean);
|
||||
if (!labels.includes(state.firmwareFilter)) return false;
|
||||
}
|
||||
return true;
|
||||
}).sort((a, b) => {
|
||||
const sortValue = game => state.sortCol === 'title'
|
||||
? (game.prospero_name || game.title || game.game_id || '')
|
||||
: (game[state.sortCol] ?? '');
|
||||
let av = sortValue(a), bv = sortValue(b);
|
||||
if (typeof av === 'string') av = av.toLowerCase();
|
||||
if (typeof bv === 'string') bv = bv.toLowerCase();
|
||||
if (av === bv) return 0;
|
||||
return state.sortDir === 'asc' ? (av > bv ? 1 : -1) : (av < bv ? 1 : -1);
|
||||
});
|
||||
}
|
||||
|
||||
function renderGames() {
|
||||
renderGamePlatformFilters();
|
||||
const allGames = getFilteredGames();
|
||||
const list = allGames.slice(0, state.gameRenderLimit);
|
||||
const tbody = $('games-tbody');
|
||||
const grid = $('games-grid');
|
||||
const tableWrap = $('games-table-wrap');
|
||||
const empty = $('games-empty');
|
||||
const emptyDetail = $('games-empty-detail');
|
||||
const emptyScan = $('btn-games-empty-scan');
|
||||
const emptyReset = $('btn-games-empty-reset');
|
||||
const loadMore = $('btn-games-load-more');
|
||||
const resultCount = $('games-result-count');
|
||||
const filtersActive = Boolean(
|
||||
state.games.length && (
|
||||
state.search ||
|
||||
state.filter !== 'all' ||
|
||||
state.platformFilter !== 'all' ||
|
||||
state.firmwareFilter !== 'all'
|
||||
)
|
||||
);
|
||||
|
||||
tbody.innerHTML = '';
|
||||
grid.innerHTML = '';
|
||||
empty.hidden = allGames.length !== 0;
|
||||
emptyDetail.textContent = filtersActive
|
||||
? window.t('games.empty.filtered', 'No games match the selected platform and filters.')
|
||||
: window.t('games.empty.detail', 'Connect the PS5 or scan the configured source folders.');
|
||||
emptyScan.hidden = filtersActive;
|
||||
emptyReset.hidden = !filtersActive;
|
||||
grid.hidden = !allGames.length || state.gameView !== 'grid';
|
||||
tableWrap.hidden = !allGames.length || state.gameView !== 'table';
|
||||
loadMore.hidden = list.length >= allGames.length;
|
||||
resultCount.textContent = allGames.length
|
||||
? window.tf('games.resultCount', '{visible} of {total}', { visible: list.length, total: allGames.length })
|
||||
: '';
|
||||
document.querySelectorAll('[data-game-view]').forEach(button => {
|
||||
const active = button.dataset.gameView === state.gameView;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-pressed', String(active));
|
||||
});
|
||||
|
||||
for (const g of list) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'game-row';
|
||||
tr.dataset.id = g.game_id;
|
||||
// Compact status cell: readable tags instead of cryptic symbols
|
||||
const patchesOnly = g.backup_count === 0 && g.backpork_count > 0;
|
||||
const _hs = _gameHashSummary.get(g.game_id);
|
||||
const hashTag = _hs
|
||||
? _hs.community_mismatch
|
||||
? `<span class="s-tag s-hash-fail" title="⚠ Community hash check FAILED — files may be modified or contain brick code">⚠ Hash Fail</span>`
|
||||
: _hs.community_matches > 0
|
||||
? `<span class="s-tag s-hash-ok" title="Community verified: ${_hs.community_matches}/${_hs.hash_count} files match known-good hashes">✓ Verified</span>`
|
||||
: `<span class="s-tag s-hash-local" title="${_hs.hash_count} file(s) hashed — not yet in community list">⧭ Hashed</span>`
|
||||
: '';
|
||||
const fwLabels = (g.firmware_labels || '').split(',').filter(Boolean);
|
||||
const patchedTag = fwLabels.length
|
||||
? `<span class="s-tag s-patched" title="Backporked for: ${escHtml(fwLabels.join(', '))}">🔧 ${escHtml(fwLabels.join(', '))}</span>`
|
||||
: '';
|
||||
const shadowTag = g.shadow_mount
|
||||
? `<span class="s-tag ${g.shadow_mount.state === 'installed' ? 's-backed' : 's-warn'}" title="ShadowMount: ${escHtml(g.shadow_mount.sources)} source(s) · ${escHtml((g.shadow_mount.formats || []).join(', ') || 'unknown format')}${g.shadow_mount.experimental ? ' · experimental format' : ''}">◉ Mount ${escHtml(g.shadow_mount.state)}</span>`
|
||||
: '';
|
||||
const metadataTag = `<span class="s-tag ${g.metadata?.source === 'manual' ? 's-patched' : 's-num'}" title="Metadata: ${escHtml(g.metadata?.source || 'unknown')} · ${escHtml(window.t('games.metadataConfidence', 'confidence'))} ${escHtml(g.metadata?.confidence || 'unknown')}">Meta ${escHtml(g.metadata?.source || '?')}</span>`;
|
||||
const variantTag = g.regional_variants?.length
|
||||
? `<span class="s-tag s-num" title="${escHtml(g.regional_variants.map(item => `${item.titleId}${item.region ? ` (${item.region})` : ''}`).join(' · '))}">${g.regional_variants.length} ${escHtml(g.regional_variants.length === 1 ? window.t('games.region', 'region') : window.t('games.regions', 'regions'))}</span>`
|
||||
: '';
|
||||
const platform = gamePlatform(g);
|
||||
const platformLabel = platform === 'unknown'
|
||||
? window.t('games.platform.unknown', 'Unknown')
|
||||
: platform.toUpperCase();
|
||||
const platformTag = `<span class="s-tag s-platform-${platform}">${escHtml(platformLabel)}</span>`;
|
||||
const requiredDrives = disconnectedDriveLabels(g);
|
||||
const requiredDriveTag = requiredDrives.length
|
||||
? `<span class="s-tag s-warn s-required-drive" title="${escHtml(window.t('games.requiredDrive.title', 'Reconnect the named drive to access this copy'))}">${escHtml(window.t('games.requiredDrive', 'Needs drive'))}: ${escHtml(requiredDrives.join(', '))}</span>`
|
||||
: '';
|
||||
const statusHtml = `<div class="game-status-col">
|
||||
${platformTag}
|
||||
${requiredDriveTag}
|
||||
${window.chimeraLibraryModel.isInstalled(g) ? `<span class="s-tag s-installed" title="${escHtml(window.t('games.status.installedTitle', 'Currently installed on the PS5'))}">${escHtml(window.t('games.status.installed', 'Installed'))}</span>` : ''}
|
||||
${g.backup_count > 0 ? `<span class="s-tag s-num" title="${g.backup_count} ${escHtml(window.t('games.status.localBackups', 'local backup(s)'))}">${g.backup_count} bkp</span>` : ''}
|
||||
${window.chimeraLibraryModel.hasLocalCopy(g) ? `<span class="s-tag s-backed" title="${escHtml(window.t('games.status.backedUpTitle', 'Local copy available'))}">✓</span>` : ''}
|
||||
${patchesOnly ? `<span class="s-tag s-warn" title="${escHtml(window.t('games.status.patchesOnlyTitle', 'Only Backpork patches were found; no local game backup exists'))}">${escHtml(window.t('games.status.patchesOnly', 'Patches only'))}</span>` : ''}
|
||||
${patchedTag}
|
||||
${shadowTag}
|
||||
${hashTag}
|
||||
${metadataTag}
|
||||
${variantTag}
|
||||
</div>`;
|
||||
const displayTitle = g.prospero_name || g.title || g.game_id;
|
||||
const metadataLine = [
|
||||
g.prospero_publisher,
|
||||
g.prospero_region,
|
||||
g.content_id || g.prospero_content_id,
|
||||
].filter(Boolean).join(' · ');
|
||||
const coverUrl = g.prospero_cover_url || '';
|
||||
const renderedCoverUrl = versionedArtworkUrl(coverUrl, g.cover_fetched_at);
|
||||
const iconFallbackAttribute = g.prospero_icon_url
|
||||
? ` data-fallback-src="${escHtml(g.prospero_icon_url)}"`
|
||||
: '';
|
||||
const coverPlaceholder = `<div class="game-card-cover-fallback" hidden aria-hidden="true"><span>${escHtml(platformLabel)}</span><strong>${escHtml(displayTitle)}</strong><small>${escHtml(g.game_id)}</small></div>`;
|
||||
const cover = renderedCoverUrl
|
||||
? `<div class="game-card-cover-case has-native-cover">
|
||||
<img src="${escHtml(renderedCoverUrl)}" class="game-card-cover" alt="${escHtml(displayTitle)}" loading="lazy" decoding="async" data-image-error="fallback"${iconFallbackAttribute}>
|
||||
${coverPlaceholder}
|
||||
</div>`
|
||||
: g.prospero_icon_url
|
||||
? `<img src="${escHtml(g.prospero_banner_url || g.prospero_icon_url)}" class="game-card-cover-backdrop" alt="" aria-hidden="true" loading="lazy" decoding="async">
|
||||
<div class="game-card-cover-case">
|
||||
<span class="game-card-cover-platform">${escHtml(platformLabel)}</span>
|
||||
<img src="${escHtml(g.prospero_icon_url)}" class="game-card-cover" alt="${escHtml(displayTitle)}" loading="lazy" decoding="async" data-image-error="fallback">
|
||||
${coverPlaceholder}
|
||||
<div class="game-card-cover-caption"><strong>${escHtml(displayTitle)}</strong><small>${escHtml(g.game_id)}</small></div>
|
||||
</div>`
|
||||
: `<div class="game-card-cover-fallback is-visible" aria-label="${escHtml(window.t?.('games.coverMissing', 'Cover unavailable') || 'Cover unavailable')}"><span>${escHtml(platformLabel)}</span><strong>${escHtml(displayTitle)}</strong><small>${escHtml(g.game_id || '?')}</small></div>`;
|
||||
const metadataSynced = Boolean(g.prospero_name && g.prospero_icon_url && g.prospero_cover_url);
|
||||
const metadataState = metadataSynced
|
||||
? window.t('games.metadata.synced', 'Metadata: synced')
|
||||
: window.t('games.metadata.enriching', 'Metadata: enriching');
|
||||
const card = document.createElement('article');
|
||||
card.className = 'game-card';
|
||||
card.dataset.id = g.game_id;
|
||||
card.dataset.platform = platform;
|
||||
card.style.setProperty('--card-hue', String(titleHue(g.game_id)));
|
||||
card.tabIndex = 0;
|
||||
card.innerHTML = `
|
||||
<div class="game-card-art">${cover}
|
||||
${metadataSynced ? '' : `<span class="game-card-sync pending" title="${escHtml(metadataState)}">${escHtml(metadataState)}</span>`}
|
||||
<div class="game-card-actions">
|
||||
${patchesOnly ? '' : `<button class="btn btn-sm btn-accent" data-action="install" data-id="${escHtml(g.game_id)}" data-i18n="games.install">Plan install</button>`}
|
||||
<button class="btn btn-sm" data-action="metadata" data-id="${escHtml(g.game_id)}" data-i18n="games.metadata">Metadata</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="game-card-body">
|
||||
<div class="game-card-heading">
|
||||
<strong title="${escHtml(displayTitle)}">${escHtml(displayTitle)}</strong>
|
||||
</div>
|
||||
<span class="game-card-meta">${escHtml([g.game_id, metadataLine].filter(Boolean).join(' · '))}</span>
|
||||
${statusHtml}
|
||||
</div>`;
|
||||
const nativeCover = card.querySelector('.game-card-cover-case.has-native-cover .game-card-cover');
|
||||
if (nativeCover) {
|
||||
const applyNaturalCoverRatio = () => {
|
||||
const width = Number(nativeCover.naturalWidth || 0);
|
||||
const height = Number(nativeCover.naturalHeight || 0);
|
||||
const ratio = height > 0 ? width / height : 0;
|
||||
if (width >= 160 && height >= 240 && ratio >= 0.52 && ratio <= 0.86) {
|
||||
card.style.setProperty('--game-cover-ratio', `${width} / ${height}`);
|
||||
}
|
||||
};
|
||||
if (nativeCover.complete) applyNaturalCoverRatio();
|
||||
else nativeCover.addEventListener('load', applyNaturalCoverRatio, { once: true });
|
||||
}
|
||||
grid.appendChild(card);
|
||||
if (patchesOnly) tr.classList.add('game-row-patches-only');
|
||||
tr.innerHTML = `
|
||||
<td>
|
||||
<div class="game-title-cell">
|
||||
${renderedCoverUrl || g.prospero_icon_url
|
||||
? `<img src="${escHtml(renderedCoverUrl || g.prospero_icon_url)}" class="game-row-icon${renderedCoverUrl ? ' is-portrait' : ''}" alt="${escHtml(window.t('games.coverFor', 'Cover for'))} ${escHtml(g.prospero_name || g.title || g.game_id)}" loading="lazy" decoding="async" data-image-error="fallback"${renderedCoverUrl && g.prospero_icon_url ? ` data-fallback-src="${escHtml(g.prospero_icon_url)}"` : ''}>`
|
||||
: '<div class="game-row-icon game-row-icon-empty"></div>'}
|
||||
${renderedCoverUrl || g.prospero_icon_url ? '<div class="game-row-icon game-row-icon-empty" hidden></div>' : ''}
|
||||
<span class="game-title-copy">
|
||||
<strong>${escHtml(g.prospero_name || g.title || g.game_id)}</strong>
|
||||
<small>${escHtml([
|
||||
g.prospero_publisher,
|
||||
g.prospero_region,
|
||||
g.content_id || g.prospero_content_id,
|
||||
].filter(Boolean).join(' · ') || window.t('games.metadataQueued', 'Metadata is being enriched…'))}</small>
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td><code>${escHtml(g.game_id)}</code></td>
|
||||
<td>${escHtml(g.version || g.prospero_version || '—')}</td>
|
||||
<td>${g.backup_size ? fmt(g.backup_size) : (g.size ? fmt(g.size) : (g.prospero_size || '—'))}</td>
|
||||
<td>${renderFwPills(g.firmware_labels)}</td>
|
||||
<td>${statusHtml}</td>
|
||||
<td class="game-actions-cell">
|
||||
${patchesOnly
|
||||
? `<span class="patches-only-hint" title="${escHtml(window.t('games.noBackupDetail', 'No game backup was found; only patch folders exist. Create or download a base-game backup first.'))}">⚠ ${escHtml(window.t('games.noBackup', 'No game backup'))}</span>`
|
||||
: `<button class="btn btn-sm btn-teal" data-action="install" data-id="${escHtml(g.game_id)}" title="${escHtml(window.t('games.installQueueTitle', 'Add to the PS5 installation queue'))}">
|
||||
⬆ ${escHtml(window.t('games.install', 'Plan install'))}
|
||||
</button>
|
||||
<button class="btn btn-sm" data-action="convert" data-id="${escHtml(g.game_id)}" title="${escHtml(window.t('games.convertTitle', 'Convert the game and optionally upload it to the PS5'))}">
|
||||
⚙ ${escHtml(window.t('games.convert', 'Convert'))}
|
||||
</button>`
|
||||
}
|
||||
<button class="btn btn-sm" data-action="metadata" data-id="${escHtml(g.game_id)}" title="${escHtml(window.t('games.metadataEditTitle', 'Correct the title, artwork and description manually'))}">Metadata</button>
|
||||
</td>`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
|
||||
// Click delegation: action buttons take priority; otherwise the card/row opens detail.
|
||||
const handleLibraryClick = async e => {
|
||||
const btn = e.target.closest('[data-action]');
|
||||
if (btn) {
|
||||
try {
|
||||
if (btn.dataset.action === 'install') {
|
||||
await openInstallModal(btn.dataset.id, 'pfs');
|
||||
} else if (btn.dataset.action === 'convert') {
|
||||
const s = await window.pork.getSettings();
|
||||
const mode = (s.gameConversionMode === 'pfs' || !s.gameConversionMode)
|
||||
? (window.pork.isWebMode ? 'ffpfsc' : 'ffpkg')
|
||||
: s.gameConversionMode;
|
||||
await openInstallModal(btn.dataset.id, mode);
|
||||
} else if (btn.dataset.action === 'metadata') {
|
||||
await editGameMetadata(btn.dataset.id);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message, 'error');
|
||||
showToast(error.message, 'error', 9000);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const item = e.target.closest('[data-id].game-row, [data-id].game-card');
|
||||
if (item) openModal(item.dataset.id);
|
||||
};
|
||||
tbody.onclick = handleLibraryClick;
|
||||
grid.onclick = handleLibraryClick;
|
||||
grid.onkeydown = e => {
|
||||
if ((e.key === 'Enter' || e.key === ' ') && !e.target.closest('button')) {
|
||||
e.preventDefault();
|
||||
const card = e.target.closest('.game-card[data-id]');
|
||||
if (card) openModal(card.dataset.id);
|
||||
}
|
||||
};
|
||||
window.i18n?.apply(grid);
|
||||
window.i18n?.apply(tbody);
|
||||
}
|
||||
|
||||
async function editGameMetadata(gameId) {
|
||||
const game = state.games.find(item => item.game_id === gameId);
|
||||
if (!game) return;
|
||||
const hasOverride = Boolean(game.metadata?.overriddenFields?.length);
|
||||
if (hasOverride && await showExactConfirmation('Clear manual metadata and return to online and local metadata management?', 'CLEAR METADATA')) {
|
||||
await window.pork.updateGameMetadata(gameId, {});
|
||||
await loadGames();
|
||||
setStatus(`Handmatige metadata voor ${gameId} gewist.`, 'ok');
|
||||
return;
|
||||
}
|
||||
const ask = (label, value = '') => showPrompt(`${label} voor ${gameId}:`, String(value || ''));
|
||||
const title = await ask('Volledige titel', game.prospero_name || game.title);
|
||||
if (title == null) return;
|
||||
const publisher = await ask('Uitgever', game.prospero_publisher);
|
||||
if (publisher == null) return;
|
||||
const region = await ask('Regio', game.prospero_region);
|
||||
if (region == null) return;
|
||||
const iconUrl = await ask(window.t?.('games.metadata.iconUrl', 'HTTPS image URL') || 'HTTPS image URL', game.prospero_icon_url);
|
||||
if (iconUrl == null) return;
|
||||
if (iconUrl && !/^(https:\/\/|\/api\/local-artwork\/)/i.test(iconUrl)) {
|
||||
throw new Error(window.t?.('games.metadata.invalidImage', 'Use an HTTPS URL or a local Chimera image.') || 'Use an HTTPS URL or a local Chimera image.');
|
||||
}
|
||||
const coverUrl = await ask(window.t?.('games.metadata.coverUrl', 'HTTPS portrait cover URL') || 'HTTPS portrait cover URL', game.prospero_cover_url);
|
||||
if (coverUrl == null) return;
|
||||
if (coverUrl && !/^(https:\/\/|\/api\/local-artwork\/)/i.test(coverUrl)) {
|
||||
throw new Error(window.t?.('games.metadata.invalidImage', 'Use an HTTPS URL or a local Chimera image.') || 'Use an HTTPS URL or a local Chimera image.');
|
||||
}
|
||||
const description = await ask(window.t?.('games.metadata.description', 'Description') || 'Description', game.prospero_description);
|
||||
if (description == null) return;
|
||||
await window.pork.updateGameMetadata(gameId, { title, publisher, region, iconUrl, coverUrl, description });
|
||||
await loadGames();
|
||||
setStatus(`Metadata voor ${gameId} handmatig vastgezet.`, 'success');
|
||||
}
|
||||
|
||||
// Sorting
|
||||
document.querySelectorAll('#games-table th[data-col]').forEach(th => {
|
||||
th.addEventListener('click', () => {
|
||||
if (state.sortCol === th.dataset.col) state.sortDir = state.sortDir === 'asc' ? 'desc' : 'asc';
|
||||
else { state.sortCol = th.dataset.col; state.sortDir = 'asc'; }
|
||||
localStorage.setItem('chimera.games.sortCol', state.sortCol);
|
||||
localStorage.setItem('chimera.games.sortDir', state.sortDir);
|
||||
renderGames();
|
||||
});
|
||||
});
|
||||
|
||||
// Filter buttons
|
||||
document.querySelectorAll('.fbtn').forEach(btn =>
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.fbtn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
state.filter = btn.dataset.filter;
|
||||
state.gameRenderLimit = 160;
|
||||
localStorage.setItem('chimera.games.filter', state.filter);
|
||||
renderGames();
|
||||
})
|
||||
);
|
||||
|
||||
document.querySelectorAll('[data-game-platform]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
state.platformFilter = ['ps4', 'ps5', 'unknown'].includes(button.dataset.gamePlatform)
|
||||
? button.dataset.gamePlatform
|
||||
: 'all';
|
||||
state.gameRenderLimit = 160;
|
||||
localStorage.setItem('chimera.games.platform', state.platformFilter);
|
||||
renderGames();
|
||||
});
|
||||
});
|
||||
|
||||
// Search
|
||||
$('games-search').addEventListener('input', e => {
|
||||
state.search = e.target.value;
|
||||
state.gameRenderLimit = 160;
|
||||
renderGames();
|
||||
});
|
||||
|
||||
document.querySelectorAll('[data-game-view]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
state.gameView = button.dataset.gameView === 'table' ? 'table' : 'grid';
|
||||
localStorage.setItem('chimera.games.view', state.gameView);
|
||||
renderGames();
|
||||
});
|
||||
});
|
||||
|
||||
$('btn-games-load-more')?.addEventListener('click', () => {
|
||||
state.gameRenderLimit += 160;
|
||||
renderGames();
|
||||
});
|
||||
|
||||
$('btn-games-empty-scan')?.addEventListener('click', () => $('btn-rescan-game-sources')?.click());
|
||||
$('btn-games-empty-reset')?.addEventListener('click', () => {
|
||||
state.search = '';
|
||||
state.filter = 'all';
|
||||
state.platformFilter = 'all';
|
||||
state.firmwareFilter = 'all';
|
||||
state.gameRenderLimit = 160;
|
||||
$('games-search').value = '';
|
||||
localStorage.setItem('chimera.games.filter', 'all');
|
||||
localStorage.setItem('chimera.games.platform', 'all');
|
||||
document.querySelectorAll('.fwbtn').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.fw === 'all');
|
||||
});
|
||||
renderGames();
|
||||
});
|
||||
|
||||
document.querySelectorAll('.fbtn').forEach(button => {
|
||||
button.classList.toggle('active', button.dataset.filter === state.filter);
|
||||
});
|
||||
|
||||
window.addEventListener('chimera:language-changed', () => renderGames());
|
||||
|
||||
Reference in new issue
Block a user